Compare commits
217 Commits
post-layou
...
fix-hover-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30239b3cc6 | ||
|
|
180421fe5a | ||
|
|
1a13995b8f | ||
|
|
b6379a9177 | ||
|
|
2cb041504b | ||
|
|
4d8dc79d7e | ||
|
|
474c806331 | ||
|
|
2db6ccd803 | ||
|
|
2d6a227258 | ||
|
|
a3ce933b04 | ||
|
|
816c48b7d6 | ||
|
|
42ac9880c6 | ||
|
|
65318cb6ac | ||
|
|
71557f3eb3 | ||
|
|
a588f674db | ||
|
|
50dd38bd02 | ||
|
|
caa156ab13 | ||
|
|
a82f4857f4 | ||
|
|
0de8672044 | ||
|
|
cc8e3c2286 | ||
|
|
347f68887f | ||
|
|
a475d8640f | ||
|
|
991c9ec441 | ||
|
|
250df707bf | ||
|
|
ba6b319046 | ||
|
|
bd94a0e921 | ||
|
|
40bbd0031d | ||
|
|
946f4a312a | ||
|
|
af06063d31 | ||
|
|
5c4f3c0cea | ||
|
|
c6826a61a0 | ||
|
|
fa2c92d190 | ||
|
|
20b10fdca9 | ||
|
|
4f40d3c801 | ||
|
|
b716035d02 | ||
|
|
94bc216bbd | ||
|
|
95d5ea7edc | ||
|
|
aff858bd00 | ||
|
|
583d85cf66 | ||
|
|
36586b77ec | ||
|
|
587788b9a0 | ||
|
|
6f36527bc6 | ||
|
|
aa34e306f7 | ||
|
|
e5d971f4c7 | ||
|
|
38c3a93f0c | ||
|
|
f930969411 | ||
|
|
266bb62813 | ||
|
|
6e897d9969 | ||
|
|
d90b052162 | ||
|
|
49a53e7654 | ||
|
|
3220986fc9 | ||
|
|
9fcda5a5ac | ||
|
|
5e43290aa1 | ||
|
|
f895d66d1c | ||
|
|
7bf16f263e | ||
|
|
d3745a3931 | ||
|
|
0c939e5dfc | ||
|
|
b9151b9506 | ||
|
|
2679457b02 | ||
|
|
45e2c01773 | ||
|
|
fd9823898f | ||
|
|
d5aba2795b | ||
|
|
92b2e5608b | ||
|
|
c58d72ea2b | ||
|
|
58a5a1eb8f | ||
|
|
cd640a87a9 | ||
|
|
c97ecc7326 | ||
|
|
48f0f387f8 | ||
|
|
2ec910f772 | ||
|
|
8a73bc4c7d | ||
|
|
8f5d7db875 | ||
|
|
389d26d974 | ||
|
|
e580e2ff0a | ||
|
|
3d9503a454 | ||
|
|
5c7cec9f85 | ||
|
|
b028231aea | ||
|
|
f7d2cb1818 | ||
|
|
78dcd72790 | ||
|
|
d51a0b60be | ||
|
|
8178d347b6 | ||
|
|
33ecb424af | ||
|
|
91b97387b6 | ||
|
|
0a40a21c74 | ||
|
|
b14d576349 | ||
|
|
db0eaca2e5 | ||
|
|
80db468720 | ||
|
|
0d2ad67b27 | ||
|
|
7065d6c46d | ||
|
|
6c714c13b3 | ||
|
|
c54d6aff6c | ||
|
|
48a6fb9e84 | ||
|
|
e9f400a8bd | ||
|
|
fc101c1fb3 | ||
|
|
4616d66e1d | ||
|
|
3ef8a9910d | ||
|
|
1e44bac418 | ||
|
|
aad7761038 | ||
|
|
0422d43798 | ||
|
|
b00b65b330 | ||
|
|
fddb778e5f | ||
|
|
77974a4367 | ||
|
|
0037f0b2fd | ||
|
|
37f6a706cc | ||
|
|
f4bafd5899 | ||
|
|
b3d3a00a14 | ||
|
|
0a5df7d597 | ||
|
|
4d1585b917 | ||
|
|
99559f3975 | ||
|
|
5783497c21 | ||
|
|
e27c2fc946 | ||
|
|
f17d0b5729 | ||
|
|
ca251babcd | ||
|
|
c33efe8cd0 | ||
|
|
2b56c43f2d | ||
|
|
bd137b01ad | ||
|
|
4e1e26b696 | ||
|
|
12b12ba17a | ||
|
|
8acd4d122e | ||
|
|
251218954d | ||
|
|
3ca6f7572f | ||
|
|
b91d6da6b6 | ||
|
|
a041e07c99 | ||
|
|
6d9b8cc595 | ||
|
|
4c781b6455 | ||
|
|
8aa5319210 | ||
|
|
f19378135a | ||
|
|
7804be0286 | ||
|
|
98ffdca32e | ||
|
|
cd4d2f7900 | ||
|
|
43a845cbbf | ||
|
|
3cbc18895a | ||
|
|
f82b2741cd | ||
|
|
9ad1862f2f | ||
|
|
1c361ac579 | ||
|
|
bea36918f4 | ||
|
|
43e8fdbe82 | ||
|
|
5df1318e75 | ||
|
|
577b244b03 | ||
|
|
2e0d18ee76 | ||
|
|
c5a23faf7c | ||
|
|
86f81c4db3 | ||
|
|
07501d9cfa | ||
|
|
07c7778cff | ||
|
|
aa6926e57a | ||
|
|
ae577c9d5c | ||
|
|
f4f72a1136 | ||
|
|
4310b0b8de | ||
|
|
a161a7d0c9 | ||
|
|
ab6b9196e1 | ||
|
|
ef551cedef | ||
|
|
9ef83a2557 | ||
|
|
32fdff0285 | ||
|
|
4094562321 | ||
|
|
6869b62af3 | ||
|
|
21a7421ee0 | ||
|
|
96dcc385dd | ||
|
|
e6766e102e | ||
|
|
694e18417e | ||
|
|
94426c4393 | ||
|
|
bf1bcd027c | ||
|
|
23132b5ab1 | ||
|
|
a8d5864524 | ||
|
|
ea322e1d1c | ||
|
|
e1ae0d46da | ||
|
|
bdc2558eac | ||
|
|
a41fb29e01 | ||
|
|
aa319ccfd0 | ||
|
|
f01763a1fa | ||
|
|
2dffc5f6e1 | ||
|
|
ed791c4fc1 | ||
|
|
7c6b34cb73 | ||
|
|
3921259b6c | ||
|
|
e93dca5ec3 | ||
|
|
c6626627c2 | ||
|
|
db86f4006e | ||
|
|
41372a96ed | ||
|
|
f62baeda64 | ||
|
|
6e6ae0ef21 | ||
|
|
a3300aed31 | ||
|
|
cbbc8cad84 | ||
|
|
8e52cf1495 | ||
|
|
65a1938e52 | ||
|
|
855acb948c | ||
|
|
54f82eb166 | ||
|
|
ac59b9b02f | ||
|
|
8f7a26f397 | ||
|
|
181f556269 | ||
|
|
a02bdd0a9f | ||
|
|
6876ea44ac | ||
|
|
16849f48e6 | ||
|
|
7a6d01e113 | ||
|
|
b14fbd4ddc | ||
|
|
b47aff4c14 | ||
|
|
7ac055627e | ||
|
|
db0455beb0 | ||
|
|
017b2db630 | ||
|
|
75eac4783b | ||
|
|
7956a9a547 | ||
|
|
c357e37dde | ||
|
|
c3176392c6 | ||
|
|
e9b95fde68 | ||
|
|
e73e93f333 | ||
|
|
a2144faf9c | ||
|
|
d744aa896f | ||
|
|
2294d99046 | ||
|
|
ecd9b93cb1 | ||
|
|
fecb5a82f1 | ||
|
|
33f713a8ab | ||
|
|
798c9a7d8b | ||
|
|
98fff014da | ||
|
|
ea51536e0f | ||
|
|
a1899bac4e | ||
|
|
04fc0dde1a | ||
|
|
b800fe96d2 | ||
|
|
21d2b5fe50 | ||
|
|
d13a731cd6 | ||
|
|
ede9600ab4 |
38
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
38
.github/ISSUE_TEMPLATE/0_feature_request.yml
vendored
@@ -2,23 +2,23 @@ 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:
|
||||
- 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: |
|
||||
If applicable, add mockups / screenshots to help present your vision of the feature
|
||||
description: Drag images into the text input below
|
||||
validations:
|
||||
required: false
|
||||
- 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
|
||||
|
||||
82
.github/ISSUE_TEMPLATE/1_language_support.yml
vendored
82
.github/ISSUE_TEMPLATE/1_language_support.yml
vendored
@@ -2,46 +2,46 @@ name: Language Support
|
||||
description: Request language support
|
||||
title: "<name_of_language> support"
|
||||
labels:
|
||||
[
|
||||
"admin read",
|
||||
"triage",
|
||||
"enhancement",
|
||||
"language",
|
||||
"unsupported language",
|
||||
"potential plugin",
|
||||
]
|
||||
[
|
||||
"admin read",
|
||||
"triage",
|
||||
"enhancement",
|
||||
"language",
|
||||
"unsupported language",
|
||||
"potential extension",
|
||||
]
|
||||
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:
|
||||
- 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: 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
|
||||
- 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
|
||||
|
||||
66
.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
66
.github/ISSUE_TEMPLATE/2_bug_report.yml
vendored
@@ -2,37 +2,37 @@ 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:
|
||||
- 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
|
||||
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
|
||||
- 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
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/config.yml
vendored
21
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,10 +1,13 @@
|
||||
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: Positive Feedback
|
||||
url: https://github.com/zed-industries/zed/discussions/5397
|
||||
about: A central location for kind words about Zed
|
||||
- name: Theme Request
|
||||
url: https://github.com/zed-industries/extensions/issues/new/choose
|
||||
about: Request a theme in the extensions repository
|
||||
- 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: Positive Feedback
|
||||
url: https://github.com/zed-industries/zed/discussions/5397
|
||||
about: A central location for kind words about Zed
|
||||
|
||||
20
.github/actions/check_style/action.yml
vendored
20
.github/actions/check_style/action.yml
vendored
@@ -2,14 +2,14 @@ name: "Check formatting"
|
||||
description: "Checks code formatting use cargo fmt"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: cargo fmt
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo fmt --all -- --check
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: cargo fmt
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Find modified migrations
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
|
||||
. ./script/squawk
|
||||
- name: Find modified migrations
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
|
||||
. ./script/squawk
|
||||
|
||||
32
.github/actions/run_tests/action.yml
vendored
32
.github/actions/run_tests/action.yml
vendored
@@ -2,22 +2,22 @@ name: "Run tests"
|
||||
description: "Runs the tests"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -7,3 +7,5 @@ Release Notes:
|
||||
**or**
|
||||
|
||||
- N/A
|
||||
|
||||
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
|
||||
|
||||
394
.github/workflows/ci.yml
vendored
394
.github/workflows/ci.yml
vendored
@@ -1,204 +1,240 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check formatting and spelling
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
style:
|
||||
name: Check formatting and spelling
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
- name: Remove untracked files
|
||||
run: git clean -df
|
||||
|
||||
- name: Check spelling
|
||||
run: |
|
||||
if ! which typos > /dev/null; then
|
||||
cargo install typos-cli
|
||||
fi
|
||||
typos
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
- name: Check spelling
|
||||
run: |
|
||||
if ! which typos > /dev/null; then
|
||||
cargo install typos-cli
|
||||
fi
|
||||
typos
|
||||
|
||||
- name: Ensure fresh merge
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ];
|
||||
then
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> $GITHUB_ENV
|
||||
else
|
||||
git checkout -B temp
|
||||
git merge -q origin/$GITHUB_BASE_REF -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
- uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: "crates/rpc/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
|
||||
- name: Ensure fresh merge
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
if [ -z "$GITHUB_BASE_REF" ];
|
||||
then
|
||||
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> $GITHUB_ENV
|
||||
else
|
||||
git checkout -B temp
|
||||
git merge -q origin/$GITHUB_BASE_REF -m "merge main into temp"
|
||||
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
macos_tests:
|
||||
name: (macOS) Run Clippy and tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- uses: bufbuild/buf-setup-action@v1
|
||||
with:
|
||||
version: v1.29.0
|
||||
- uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
input: "crates/rpc/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
|
||||
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
macos_tests:
|
||||
name: (macOS) Run Clippy and tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
# todo!(linux): Actually run the tests
|
||||
linux_tests:
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Build other binaries and features
|
||||
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
|
||||
|
||||
- name: Restore from cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-
|
||||
# todo!(linux): Actually run the tests
|
||||
linux_tests:
|
||||
name: (Linux) Run Clippy and tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
- name: Restore from cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
- name: configure linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/linux
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: [macos_tests, linux_tests]
|
||||
- name: cargo clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
# todo!(windows): Actually run the tests
|
||||
windows_tests:
|
||||
name: (Windows) Run Clippy and tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Restore from cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
# todo!(windows): Actually run clippy
|
||||
#- name: cargo clippy
|
||||
# shell: bash -euxo pipefail {0}
|
||||
# run: script/clippy
|
||||
|
||||
- name: Build Zed
|
||||
run: cargo build -p zed
|
||||
|
||||
bundle:
|
||||
name: Bundle macOS app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: [macos_tests]
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
50
.github/workflows/danger.yml
vendored
50
.github/workflows/danger.yml
vendored
@@ -1,35 +1,35 @@
|
||||
name: Danger
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- edited
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
danger:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "script/danger/pnpm-lock.yaml"
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "script/danger/pnpm-lock.yaml"
|
||||
|
||||
- run: pnpm install --dir script/danger
|
||||
- run: pnpm install --dir script/danger
|
||||
|
||||
- name: Run Danger
|
||||
run: pnpm run --dir script/danger danger ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
- name: Run Danger
|
||||
run: pnpm run --dir script/danger danger ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
40
.github/workflows/deploy_collab.yml
vendored
40
.github/workflows/deploy_collab.yml
vendored
@@ -45,8 +45,18 @@ jobs:
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install cargo nextest
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --package collab --no-fail-fast
|
||||
|
||||
publish:
|
||||
name: Publish collab server image
|
||||
@@ -63,9 +73,6 @@ jobs:
|
||||
- name: Sign into DigitalOcean docker registry
|
||||
run: doctl registry login
|
||||
|
||||
- name: Prune Docker system
|
||||
run: docker system prune --filter 'until=720h' -f
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -78,6 +85,9 @@ jobs:
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
|
||||
- name: Prune Docker system
|
||||
run: docker system prune --filter 'until=72h' -f
|
||||
|
||||
deploy:
|
||||
name: Deploy new server image
|
||||
needs:
|
||||
@@ -90,22 +100,26 @@ jobs:
|
||||
- name: Sign into Kubernetes
|
||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }}
|
||||
|
||||
- name: Determine namespace
|
||||
- name: Start rollout
|
||||
run: |
|
||||
set -eu
|
||||
if [[ $GITHUB_REF_NAME = "collab-production" ]]; then
|
||||
echo "Deploying collab:$GITHUB_SHA to production"
|
||||
echo "KUBE_NAMESPACE=production" >> $GITHUB_ENV
|
||||
export ZED_KUBE_NAMESPACE=production
|
||||
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
|
||||
echo "Deploying collab:$GITHUB_SHA to staging"
|
||||
echo "KUBE_NAMESPACE=staging" >> $GITHUB_ENV
|
||||
export ZED_KUBE_NAMESPACE=staging
|
||||
else
|
||||
echo "cowardly refusing to deploy from an unknown branch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Start rollout
|
||||
run: kubectl -n "$KUBE_NAMESPACE" set image deployment/collab collab=registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
echo "Deploying collab:$GITHUB_SHA to $ZED_KUBE_NAMESPACE"
|
||||
|
||||
- name: Wait for rollout to finish
|
||||
run: kubectl -n "$KUBE_NAMESPACE" rollout status deployment/collab
|
||||
source script/lib/deploy-helpers.sh
|
||||
export_vars_for_environment $ZED_KUBE_NAMESPACE
|
||||
|
||||
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
|
||||
export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
|
||||
|
||||
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
|
||||
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/collab --watch
|
||||
echo "deployed collab.template.yml to ${ZED_KUBE_NAMESPACE}"
|
||||
|
||||
52
.github/workflows/randomized_tests.yml
vendored
52
.github/workflows/randomized_tests.yml
vendored
@@ -3,35 +3,35 @@ name: Randomized Tests
|
||||
concurrency: randomized-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- randomized-tests-runner
|
||||
# schedule:
|
||||
# - cron: '0 * * * *'
|
||||
push:
|
||||
branches:
|
||||
- randomized-tests-runner
|
||||
# schedule:
|
||||
# - cron: '0 * * * *'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ZED_SERVER_URL: https://zed.dev
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ZED_SERVER_URL: https://zed.dev
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Run randomized tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- randomized-tests
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
tests:
|
||||
name: Run randomized tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- randomized-tests
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run randomized tests
|
||||
run: script/randomized-test-ci
|
||||
- name: Run randomized tests
|
||||
run: script/randomized-test-ci
|
||||
|
||||
162
.github/workflows/release_nightly.yml
vendored
162
.github/workflows/release_nightly.yml
vendored
@@ -1,98 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every day at 7:00am UTC (Roughly before EU workday and after US workday)
|
||||
- cron: "0 7 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
schedule:
|
||||
# Fire every day at 7:00am UTC (Roughly before EU workday and after US workday)
|
||||
- cron: "0 7 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check formatting and Clippy lints
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
style:
|
||||
name: Check formatting and Clippy lints
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
- name: Run style checks
|
||||
uses: ./.github/actions/check_style
|
||||
|
||||
- name: Run clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
tests:
|
||||
name: Run tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Run clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clippy
|
||||
tests:
|
||||
name: Run tests
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: style
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
bundle:
|
||||
name: Bundle app
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
steps:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 */12 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
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 5393 --github-token ${{ secrets.GITHUB_TOKEN }} --prod
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
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 5393 --github-token ${{ secrets.GITHUB_TOKEN }} --prod
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 15 * * *"
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 15 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
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 6952 --github-token ${{ secrets.GITHUB_TOKEN }} --prod --query-day-interval 7
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
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 6952 --github-token ${{ secrets.GITHUB_TOKEN }} --prod --query-day-interval 7
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,12 +5,8 @@
|
||||
.DS_Store
|
||||
/plugins/bin
|
||||
/script/node_modules
|
||||
/styles/node_modules
|
||||
/styles/src/types/zed.ts
|
||||
/crates/theme/schemas/theme.json
|
||||
/crates/collab/static/styles.css
|
||||
/crates/collab/.admins.json
|
||||
/vendor/bin
|
||||
/assets/*licenses.md
|
||||
**/venv
|
||||
.build
|
||||
@@ -25,3 +21,4 @@ DerivedData/
|
||||
**/*.db
|
||||
.pytest_cache
|
||||
.venv
|
||||
.blob_store
|
||||
|
||||
2
.mailmap
2
.mailmap
@@ -11,6 +11,8 @@
|
||||
|
||||
Antonio Scandurra <me@as-cii.com>
|
||||
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de>
|
||||
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
|
||||
Conrad Irwin <conrad@zed.dev>
|
||||
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
|
||||
Greg Morenz <greg-morenz@droid.cafe>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"JSON": {
|
||||
"tab_size": 4
|
||||
"languages": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"TOML": {
|
||||
"formatter": "prettier",
|
||||
"format_on_save": "off"
|
||||
},
|
||||
"YAML": {
|
||||
"formatter": "prettier"
|
||||
}
|
||||
},
|
||||
"formatter": "auto"
|
||||
}
|
||||
|
||||
1616
Cargo.lock
generated
1616
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
46
Cargo.toml
46
Cargo.toml
@@ -22,6 +22,7 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/editor",
|
||||
"crates/extension",
|
||||
"crates/extensions_ui",
|
||||
"crates/feature_flags",
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
@@ -62,6 +63,8 @@ members = [
|
||||
"crates/rich_text",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/settings",
|
||||
@@ -113,6 +116,7 @@ db = { path = "crates/db" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
extensions_ui = { path = "crates/extensions_ui" }
|
||||
feature_flags = { path = "crates/feature_flags" }
|
||||
feedback = { path = "crates/feedback" }
|
||||
file_finder = { path = "crates/file_finder" }
|
||||
@@ -151,6 +155,8 @@ release_channel = { path = "crates/release_channel" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -177,7 +183,11 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
|
||||
anyhow = "1.0.57"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-tar = "0.4.2"
|
||||
async-trait = "0.1"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
|
||||
blade-rwh = { package = "raw-window-handle", version = "0.5" }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ctor = "0.2.6"
|
||||
derive_more = "0.99.17"
|
||||
@@ -187,14 +197,13 @@ git2 = { version = "0.15", default-features = false }
|
||||
globset = "0.4"
|
||||
indoc = "1"
|
||||
# We explicitly disable a http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"static-curl",
|
||||
"text-decoding",
|
||||
] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
|
||||
lazy_static = "1.4.0"
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = "2.1.1"
|
||||
parking_lot = "0.11.1"
|
||||
profiling = "1"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
prost = "0.8"
|
||||
@@ -205,13 +214,11 @@ regex = "1.5"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.1", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
] }
|
||||
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
|
||||
serde_repr = "0.1"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
@@ -220,17 +227,18 @@ sysinfo = "0.29.10"
|
||||
tempfile = "3.9.0"
|
||||
thiserror = "1.0.29"
|
||||
tiktoken-rs = "0.5.7"
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
toml = "0.5"
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known", "formatting"] }
|
||||
toml = "0.8"
|
||||
tree-sitter = { version = "0.20", features = ["wasm"] }
|
||||
tree-sitter-astro = { git = "https://github.com/virchau13/tree-sitter-astro.git", rev = "e924787e12e8a03194f36a113290ac11d6dc10f3" }
|
||||
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
|
||||
tree-sitter-beancount = { git = "https://github.com/polarmutex/tree-sitter-beancount", rev = "da1bf8c6eb0ae7a97588affde7227630bcd678b6" }
|
||||
tree-sitter-c = "0.20.1"
|
||||
tree-sitter-clojure = { git = "https://github.com/prcastro/tree-sitter-clojure", branch = "update-ts"}
|
||||
tree-sitter-clojure = { git = "https://github.com/prcastro/tree-sitter-clojure", branch = "update-ts" }
|
||||
tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" }
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
|
||||
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
tree-sitter-dockerfile = { git = "https://github.com/camdencheek/tree-sitter-dockerfile", rev = "33e22c33bcdbfc33d42806ee84cfd0b1248cc392" }
|
||||
tree-sitter-dart = { git = "https://github.com/agent3bood/tree-sitter-dart", rev = "48934e3bf757a9b78f17bdfaa3e2b4284656fdc7" }
|
||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
|
||||
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
@@ -241,7 +249,7 @@ tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
|
||||
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "cf98de23e4285b8e6bcb57b050ef2326e2cc284b" }
|
||||
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" }
|
||||
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
|
||||
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
|
||||
tree-sitter-html = "0.19.0"
|
||||
@@ -249,21 +257,21 @@ tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", re
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
|
||||
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
|
||||
tree-sitter-php = "0.21.1"
|
||||
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
|
||||
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
|
||||
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
|
||||
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
|
||||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
|
||||
tree-sitter-ruby = "0.20.0"
|
||||
tree-sitter-rust = "0.20.3"
|
||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca" }
|
||||
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
|
||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" }
|
||||
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" }
|
||||
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
|
||||
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
|
||||
@@ -271,10 +279,14 @@ unindent = "0.1.7"
|
||||
url = "2.2"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
wasmtime = "16"
|
||||
which = "6.0.0"
|
||||
sys-locale = "0.3.1"
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "1d8975319c2d5de1bf710e7e21db25b0eee4bc66" }
|
||||
wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "v16.0.0" }
|
||||
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
|
||||
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "e4fcda0d5259d0acf902aee6de7d2501f2bd6629" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
@@ -11,6 +11,7 @@ ARG GITHUB_SHA
|
||||
ENV GITHUB_SHA=$GITHUB_SHA
|
||||
RUN --mount=type=cache,target=./script/node_modules \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=./target \
|
||||
cargo build --release --package collab --bin collab
|
||||
|
||||
|
||||
3
Procfile
3
Procfile
@@ -1,2 +1,3 @@
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||
collab: RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run --package=collab serve
|
||||
livekit: livekit-server --dev
|
||||
blob_store: MINIO_ROOT_USER=the-blob-store-access-key MINIO_ROOT_PASSWORD=the-blob-store-secret-key minio server .blob_store
|
||||
|
||||
@@ -21,7 +21,8 @@ brew install zed
|
||||
|
||||
## Developing Zed
|
||||
|
||||
- [Building Zed](./docs/src/developing_zed__building_zed.md)
|
||||
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
|
||||
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
|
||||
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
"bmp": "image",
|
||||
"c": "code",
|
||||
"cc": "code",
|
||||
"cjs": "code",
|
||||
"conf": "settings",
|
||||
"cpp": "code",
|
||||
"css": "css",
|
||||
"csv": "storage",
|
||||
"cts": "typescript",
|
||||
"dat": "storage",
|
||||
"db": "storage",
|
||||
"dbf": "storage",
|
||||
@@ -73,6 +75,7 @@
|
||||
"ldf": "storage",
|
||||
"lock": "lock",
|
||||
"log": "log",
|
||||
"lua": "lua",
|
||||
"m4a": "audio",
|
||||
"m4v": "video",
|
||||
"md": "document",
|
||||
@@ -80,12 +83,14 @@
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"mkv": "video",
|
||||
"mjs": "code",
|
||||
"mka": "audio",
|
||||
"ml": "ocaml",
|
||||
"mli": "ocaml",
|
||||
"mov": "video",
|
||||
"mp3": "audio",
|
||||
"mp4": "video",
|
||||
"mts": "typescript",
|
||||
"myd": "storage",
|
||||
"myi": "storage",
|
||||
"odp": "document",
|
||||
@@ -206,8 +211,11 @@
|
||||
"log": {
|
||||
"icon": "icons/file_icons/info.svg"
|
||||
},
|
||||
"lua": {
|
||||
"icon": "icons/file_icons/lua.svg"
|
||||
},
|
||||
"ocaml": {
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
"icon": "icons/file_icons/ocaml.svg"
|
||||
},
|
||||
"phoenix": {
|
||||
"icon": "icons/file_icons/phoenix.svg"
|
||||
|
||||
10
assets/icons/file_icons/lua.svg
Normal file
10
assets/icons/file_icons/lua.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_34_7)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2625 7.00157C12.2625 4.09565 9.90677 1.73994 7.00085 1.73994C4.09494 1.73994 1.73923 4.09565 1.73923 7.00157C1.73923 9.90749 4.09494 12.2632 7.00085 12.2632C9.90677 12.2632 12.2625 9.90749 12.2625 7.00157ZM10.7213 4.82205C10.7213 3.97095 10.0313 3.281 9.18023 3.281C8.32913 3.281 7.63917 3.97095 7.63917 4.82205C7.63917 5.67315 8.32913 6.3631 9.18023 6.3631C10.0313 6.3631 10.7213 5.67315 10.7213 4.82205ZM11.9704 11.3383L12.2849 11.6123C12.4856 11.3819 12.6732 11.1354 12.8424 10.8799L12.4947 10.6496C12.3355 10.8899 12.1591 11.1216 11.9704 11.3383ZM3.09019 12.8218C2.83557 12.6512 2.59021 12.4623 2.36091 12.2602L2.63665 11.9473C2.85224 12.1372 3.08295 12.3149 3.3224 12.4754L3.09019 12.8218ZM13.2533 9.09685L13.6488 9.22944C13.5516 9.51944 13.4341 9.806 13.2995 10.0812L12.9248 9.89792C13.0514 9.63912 13.1619 9.3696 13.2533 9.09685ZM11.3601 11.9516L11.6357 12.2647C11.4062 12.4667 11.1607 12.6555 10.9061 12.8259L10.6741 12.4793C10.9135 12.3191 11.1443 12.1415 11.3601 11.9516ZM9.92464 12.9125L10.1093 13.2865C9.83502 13.4219 9.54901 13.5406 9.25919 13.6391L9.12488 13.2442C9.39754 13.1515 9.66662 13.0399 9.92464 12.9125ZM0.698663 10.0754C0.564769 9.80041 0.447705 9.51376 0.350717 9.22346L0.746321 9.09128C0.837577 9.36442 0.947715 9.63411 1.0737 9.89286L0.698663 10.0754ZM4.7358 13.6379C4.44725 13.539 4.16148 13.4197 3.88645 13.2833L4.07178 12.9096C4.33065 13.038 4.59955 13.1503 4.871 13.2433L4.7358 13.6379ZM7.50907 0.425028L7.54103 0.00914471C7.84635 0.0326099 8.15299 0.0767322 8.45243 0.140276L8.36583 0.548303C8.08435 0.488557 7.79609 0.447081 7.50907 0.425028ZM8.28868 13.469L8.37042 13.878C8.07006 13.9381 7.76308 13.9787 7.45798 13.999L7.4304 13.5828C7.71737 13.5638 8.00614 13.5255 8.28868 13.469ZM6.56475 13.5831L6.5374 13.9993C6.23179 13.9792 5.92469 13.9385 5.62462 13.8783L5.70667 13.4693C5.98876 13.5259 6.27745 13.5642 6.56475 13.5831ZM1.71207 11.6074C1.51131 11.3768 1.32385 11.1303 1.15489 10.8748L1.5028 10.6447C1.66168 10.885 1.83795 11.1167 2.0267 11.3336L1.71207 11.6074ZM1.54706 3.2931L1.20185 3.05901C1.3739 2.80528 1.56421 2.56096 1.76748 2.33281L2.07891 2.61029C1.8878 2.82479 1.70885 3.05452 1.54706 3.2931ZM13.5829 7.4008L13.9993 7.42603C13.9807 7.73163 13.9418 8.03889 13.8834 8.33925L13.474 8.25969C13.5289 7.97721 13.5655 7.68824 13.5829 7.4008ZM2.69596 2.00373L2.42386 1.68762C2.65562 1.48813 2.90314 1.30202 3.15957 1.13448L3.38773 1.48365C3.14661 1.64119 2.91386 1.81619 2.69596 2.00373ZM4.14194 1.05867L3.96141 0.682649C4.23716 0.550267 4.52444 0.434775 4.8153 0.339358L4.9453 0.735689C4.67165 0.825447 4.40135 0.934116 4.14194 1.05867ZM6.64336 0.415732C6.35624 0.431589 6.06708 0.466692 5.78392 0.520066L5.70665 0.110177C6.00769 0.0534275 6.3151 0.0161137 6.62038 -0.00073251L6.64336 0.415732ZM9.33849 0.390492C9.62584 0.492485 9.91028 0.614901 10.1839 0.754324L9.99453 1.12595C9.73698 0.994735 9.46932 0.879534 9.19896 0.783565L9.33849 0.390492ZM0.116692 8.3334C0.0582983 8.03277 0.0192821 7.72556 0.000734408 7.42027L0.417082 7.39499C0.434525 7.68212 0.471213 7.97108 0.526144 8.25385L0.116692 8.3334ZM13.8694 5.59362C13.9308 5.89342 13.9729 6.20022 13.9945 6.50553L13.5785 6.53503C13.5581 6.24787 13.5185 5.95929 13.4607 5.67733L13.8694 5.59362ZM0.421578 6.52929L0.00551996 6.49964C0.0272978 6.19421 0.0697176 5.88734 0.131602 5.58753L0.540095 5.67184C0.48192 5.95369 0.442046 6.24218 0.421578 6.52929ZM13.268 3.85795C13.4053 4.1315 13.5257 4.41681 13.6258 4.70597L13.2317 4.84251C13.1375 4.57059 13.0243 4.30226 12.8952 4.045L13.268 3.85795ZM0.770731 4.83746L0.376902 4.70007C0.477381 4.41205 0.598242 4.12695 0.736138 3.85268L1.10878 4.04006C0.979007 4.29819 0.86526 4.56647 0.770731 4.83746ZM10.7213 1.73979C10.7213 0.888612 11.4113 0.198592 12.2625 0.198592C13.1137 0.198592 13.8037 0.888612 13.8037 1.73979C13.8037 2.59098 13.1137 3.281 12.2625 3.281C11.4113 3.281 10.7213 2.59098 10.7213 1.73979Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_34_7">
|
||||
<rect width="14" height="14" fill="white" transform="matrix(-1 0 0 1 14 0)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
assets/icons/play.svg
Normal file
1
assets/icons/play.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.24182 2.32181C3.3919 2.23132 3.5784 2.22601 3.73338 2.30781L12.7334 7.05781C12.8974 7.14436 13 7.31457 13 7.5C13 7.68543 12.8974 7.85564 12.7334 7.94219L3.73338 12.6922C3.5784 12.774 3.3919 12.7687 3.24182 12.6782C3.09175 12.5877 3 12.4252 3 12.25V2.75C3 2.57476 3.09175 2.4123 3.24182 2.32181ZM4 3.57925V11.4207L11.4288 7.5L4 3.57925Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 518 B |
561
assets/keymaps/default-linux.json
Normal file
561
assets/keymaps/default-linux.json
Normal file
@@ -0,0 +1,561 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"ctrl-up": "menu::SelectFirst",
|
||||
"ctrl-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"shift-f10": "menu::ShowContextMenu",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"ctrl-o": "workspace::Open",
|
||||
"ctrl-=": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-+": "zed::IncreaseBufferFontSize",
|
||||
"ctrl--": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-0": "zed::ResetBufferFontSize",
|
||||
"ctrl-,": "zed::OpenSettings",
|
||||
"ctrl-q": "zed::Quit",
|
||||
"ctrl-h": "zed::Hide",
|
||||
"alt-ctrl-h": "zed::HideOthers",
|
||||
"ctrl-m": "zed::Minimize",
|
||||
"f11": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"backspace": "editor::Backspace",
|
||||
"shift-backspace": "editor::Backspace",
|
||||
"ctrl-h": "editor::Backspace",
|
||||
"delete": "editor::Delete",
|
||||
"ctrl-d": "editor::Delete",
|
||||
"tab": "editor::Tab",
|
||||
"shift-tab": "editor::TabPrev",
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-t": "editor::Transpose",
|
||||
"ctrl-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-h": "editor::DeleteToPreviousWordStart",
|
||||
"alt-d": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::PageUp",
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
"ctrl-down": "editor::MoveToEndOfParagraph",
|
||||
"pagedown": "editor::PageDown",
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-shift-l": "editor::NextScreen", // todo!(linux): What is this
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
"alt-f": "editor::MoveToNextWordEnd",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-=end": "editor::MoveToEnd",
|
||||
"shift-up": "editor::SelectUp",
|
||||
"ctrl-shift-p": "editor::SelectUp",
|
||||
"shift-down": "editor::SelectDown",
|
||||
"ctrl-shift-n": "editor::SelectDown",
|
||||
"shift-left": "editor::SelectLeft",
|
||||
"ctrl-shift-b": "editor::SelectLeft",
|
||||
"shift-right": "editor::SelectRight",
|
||||
"ctrl-shift-f": "editor::SelectRight",
|
||||
"alt-shift-left": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"alt-shift-right": "editor::SelectToNextWordEnd",
|
||||
"alt-shift-f": "editor::SelectToNextWordEnd",
|
||||
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
|
||||
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"ctrl-l": "editor::SelectLine",
|
||||
"ctrl-shift-i": "editor::Format",
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-e": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"alt-\\": "copilot::Suggest",
|
||||
"alt-]": "copilot::NextSuggestion",
|
||||
"alt-[": "copilot::PreviousSuggestion",
|
||||
"ctrl->": "assistant::QuoteSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height",
|
||||
"bindings": {
|
||||
"ctrl-enter": "editor::Newline",
|
||||
"shift-enter": "editor::Newline",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-tab": "search::CycleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace > Editor",
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar > Editor",
|
||||
"bindings": {
|
||||
"up": "search::PreviousHistoryQuery",
|
||||
"down": "search::NextHistoryQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"ctrl-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-{": "pane::ActivatePrevItem",
|
||||
"ctrl-}": "pane::ActivateNextItem",
|
||||
"ctrl-alt-left": "pane::ActivatePrevItem",
|
||||
"ctrl-alt-right": "pane::ActivateNextItem",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-alt-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k u": "pane::CloseCleanItems",
|
||||
"ctrl-k ctrl-w": "pane::CloseAllItems",
|
||||
"ctrl-f": "project_search::ToggleFocus",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"ctrl-alt-c": "search::ToggleCaseSensitive",
|
||||
"ctrl-alt-w": "search::ToggleWholeWord",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"ctrl-alt-f": "project_search::ToggleFilters",
|
||||
"ctrl-alt-g": "search::ActivateRegexMode",
|
||||
"ctrl-alt-s": "search::ActivateSemanticMode",
|
||||
"ctrl-alt-x": "search::ActivateTextMode"
|
||||
}
|
||||
},
|
||||
// Bindings from VS Code
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-shift-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-d": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-shift-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"ctrl-u": "editor::UndoSelection",
|
||||
"ctrl-shift-u": "editor::RedoSelection",
|
||||
"f8": "editor::GoToDiagnostic",
|
||||
"shift-f8": "editor::GoToPrevDiagnostic",
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-alt-[": "editor::Fold",
|
||||
"ctrl-alt-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"ctrl-alt-r": "editor::RevealInFinder",
|
||||
"ctrl-alt-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-shift-o": "outline::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-1": ["pane::ActivateItem", 0],
|
||||
"ctrl-2": ["pane::ActivateItem", 1],
|
||||
"ctrl-3": ["pane::ActivateItem", 2],
|
||||
"ctrl-4": ["pane::ActivateItem", 3],
|
||||
"ctrl-5": ["pane::ActivateItem", 4],
|
||||
"ctrl-6": ["pane::ActivateItem", 5],
|
||||
"ctrl-7": ["pane::ActivateItem", 6],
|
||||
"ctrl-8": ["pane::ActivateItem", 7],
|
||||
"ctrl-9": ["pane::ActivateItem", 8],
|
||||
"ctrl-0": "pane::ActivateLastItem",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-_": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-alt-o": "projects::OpenRecent",
|
||||
"ctrl-alt-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-1": ["workspace::ActivatePane", 0],
|
||||
"ctrl-2": ["workspace::ActivatePane", 1],
|
||||
"ctrl-3": ["workspace::ActivatePane", 2],
|
||||
"ctrl-4": ["workspace::ActivatePane", 3],
|
||||
"ctrl-5": ["workspace::ActivatePane", 4],
|
||||
"ctrl-6": ["workspace::ActivatePane", 5],
|
||||
"ctrl-7": ["workspace::ActivatePane", 6],
|
||||
"ctrl-8": ["workspace::ActivatePane", 7],
|
||||
"ctrl-9": ["workspace::ActivatePane", 8],
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-r": "workspace::ToggleRightDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||
"ctrl-k ctrl-s": "zed::OpenKeymap",
|
||||
"ctrl-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-?": "assistant::ToggleFocus",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-k ctrl-right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-k ctrl-up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-k ctrl-down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
// todo!(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-alt-up": "editor::MoveLineUp",
|
||||
"ctrl-alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
|
||||
}
|
||||
},
|
||||
// Bindings from Atom
|
||||
// todo!(linux) make sure these match linux bindings or remove above comment?
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-k up": "pane::SplitUp",
|
||||
"ctrl-k down": "pane::SplitDown",
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
// Bindings that should be unified with bindings for more general actions
|
||||
{
|
||||
"context": "Editor && renaming",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmRename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_code_actions",
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCodeAction"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && (showing_code_actions || showing_completions)",
|
||||
"bindings": {
|
||||
"up": "editor::ContextMenuPrev",
|
||||
"ctrl-p": "editor::ContextMenuPrev",
|
||||
"down": "editor::ContextMenuNext",
|
||||
"ctrl-n": "editor::ContextMenuNext",
|
||||
"pageup": "editor::ContextMenuFirst",
|
||||
"pagedown": "editor::ContextMenuLast"
|
||||
}
|
||||
},
|
||||
// Custom bindings
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
|
||||
// TODO: Move this to a dock open action
|
||||
"ctrl-alt-c": "collab_panel::ToggleFocus",
|
||||
"ctrl-alt-i": "zed::DebugElements",
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"alt-enter": "editor::OpenExcerpts",
|
||||
"ctrl-f8": "editor::GoToHunk",
|
||||
"ctrl-shift-f8": "editor::GoToPrevHunk",
|
||||
"ctrl-enter": "assistant::InlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"ctrl-enter": "project_search::SearchInNew"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"ctrl-alt-n": "project_panel::NewDirectory",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"ctrl-alt-c": "project_panel::CopyPath",
|
||||
"ctrl-alt-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"ctrl-alt-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(CollabPanel && editing) > Editor",
|
||||
"bindings": {
|
||||
"space": "collab_panel::InsertSpace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal",
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChannelModal > Picker > Editor",
|
||||
"bindings": {
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ChatPanel > MessageEditor",
|
||||
"bindings": {
|
||||
"escape": "chat_panel::CloseReplyPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-k": "terminal::Clear",
|
||||
// Some nice conveniences
|
||||
"ctrl-backspace": ["terminal::SendText", "\u0015"],
|
||||
"ctrl-right": ["terminal::SendText", "\u0005"],
|
||||
"ctrl-left": ["terminal::SendText", "\u0001"],
|
||||
// Terminal.app compatibility
|
||||
"alt-left": ["terminal::SendText", "\u001bb"],
|
||||
"alt-right": ["terminal::SendText", "\u001bf"],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": ["terminal::SendKeystroke", "up"],
|
||||
"pageup": ["terminal::SendKeystroke", "pageup"],
|
||||
"down": ["terminal::SendKeystroke", "down"],
|
||||
"pagedown": ["terminal::SendKeystroke", "pagedown"],
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -423,7 +423,9 @@
|
||||
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -529,7 +531,8 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete",
|
||||
"delete": "project_panel::Delete",
|
||||
"cmd-backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
23
assets/keymaps/storybook.json
Normal file
23
assets/keymaps/storybook.json
Normal file
@@ -0,0 +1,23 @@
|
||||
[
|
||||
// Standard macOS bindings
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::ShowContextMenu",
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-q": "storybook::Quit"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -101,8 +101,14 @@
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-i": "pane::GoForward",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"v": "vim::ToggleVisual",
|
||||
"shift-v": "vim::ToggleVisualLine",
|
||||
"ctrl-v": "vim::ToggleVisualBlock",
|
||||
@@ -117,12 +123,15 @@
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp",
|
||||
// "g" commands
|
||||
"g e": "vim::PreviousWordEnd",
|
||||
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g t": "pane::ActivateNextItem",
|
||||
"g shift-t": "pane::ActivatePrevItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToTypeDefinition",
|
||||
"g x": "editor::OpenUrl",
|
||||
"g n": "vim::SelectNext",
|
||||
"g shift-n": "vim::SelectPrevious",
|
||||
"g >": [
|
||||
@@ -232,36 +241,123 @@
|
||||
}
|
||||
],
|
||||
// Count support
|
||||
"1": ["vim::Number", 1],
|
||||
"2": ["vim::Number", 2],
|
||||
"3": ["vim::Number", 3],
|
||||
"4": ["vim::Number", 4],
|
||||
"5": ["vim::Number", 5],
|
||||
"6": ["vim::Number", 6],
|
||||
"7": ["vim::Number", 7],
|
||||
"8": ["vim::Number", 8],
|
||||
"9": ["vim::Number", 9],
|
||||
"1": [
|
||||
"vim::Number",
|
||||
1
|
||||
],
|
||||
"2": [
|
||||
"vim::Number",
|
||||
2
|
||||
],
|
||||
"3": [
|
||||
"vim::Number",
|
||||
3
|
||||
],
|
||||
"4": [
|
||||
"vim::Number",
|
||||
4
|
||||
],
|
||||
"5": [
|
||||
"vim::Number",
|
||||
5
|
||||
],
|
||||
"6": [
|
||||
"vim::Number",
|
||||
6
|
||||
],
|
||||
"7": [
|
||||
"vim::Number",
|
||||
7
|
||||
],
|
||||
"8": [
|
||||
"vim::Number",
|
||||
8
|
||||
],
|
||||
"9": [
|
||||
"vim::Number",
|
||||
9
|
||||
],
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
|
||||
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
|
||||
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
|
||||
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
|
||||
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
|
||||
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-w left": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w right": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w up": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w down": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w ctrl-h": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w ctrl-l": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w ctrl-k": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-j": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-left": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-right": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-up": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-down": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-h": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-l": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-k": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-j": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
@@ -283,8 +379,14 @@
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
|
||||
"ctrl-w n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"-": "pane::RevealInProjectPanel"
|
||||
}
|
||||
},
|
||||
@@ -300,12 +402,21 @@
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": ["vim::PushOperator", "Delete"],
|
||||
"d": [
|
||||
"vim::PushOperator",
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"y": ["vim::PushOperator", "Yank"],
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
@@ -336,7 +447,10 @@
|
||||
],
|
||||
"*": "vim::MoveToNext",
|
||||
"#": "vim::MoveToPrev",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"s": "vim::Substitute",
|
||||
"shift-s": "vim::SubstituteLine",
|
||||
"> >": "editor::Indent",
|
||||
@@ -348,7 +462,10 @@
|
||||
{
|
||||
"context": "Editor && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
"0": [
|
||||
"vim::Number",
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -451,10 +568,22 @@
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"r": ["vim::PushOperator", "Replace"],
|
||||
"ctrl-c": ["vim::SwitchMode", "Normal"],
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"ctrl-c": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
">": "editor::Indent",
|
||||
"<": "editor::Outdent",
|
||||
"i": [
|
||||
@@ -485,7 +614,9 @@
|
||||
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
|
||||
"ctrl-x ctrl-c": "copilot::Suggest", // zed specific
|
||||
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
|
||||
"ctrl-x ctrl-z": "editor::Cancel"
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -493,8 +624,14 @@
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"ctrl-[": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -503,5 +640,27 @@
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
// netrw compatibility
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"%": "project_panel::NewFile",
|
||||
"/": "project_panel::NewSearchInDirectory",
|
||||
"d": "project_panel::NewDirectory",
|
||||
"enter": "project_panel::Open",
|
||||
"escape": "project_panel::ToggleFocus",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"o": "project_panel::Open",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
"t": "project_panel::Open",
|
||||
"v": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFinder"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -104,8 +104,10 @@
|
||||
"show_whitespaces": "selection",
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone muted by default
|
||||
"mute_on_join": false
|
||||
// Join calls with the microphone live by default
|
||||
"mute_on_join": false,
|
||||
// Share your project when you are the first to join a channel
|
||||
"share_on_join": true
|
||||
},
|
||||
// Toolbar related settings
|
||||
"toolbar": {
|
||||
@@ -138,6 +140,14 @@
|
||||
// Whether to show diagnostic indicators in the scrollbar.
|
||||
"diagnostics": true
|
||||
},
|
||||
"gutter": {
|
||||
// Whether to show line numbers in the gutter.
|
||||
"line_numbers": true,
|
||||
// Whether to show code action buttons in the gutter.
|
||||
"code_actions": true,
|
||||
// Whether to show fold buttons in the gutter.
|
||||
"folds": true
|
||||
},
|
||||
// The number of lines to keep above/below the cursor when scrolling.
|
||||
"vertical_scroll_margin": 3,
|
||||
"relative_line_numbers": false,
|
||||
@@ -329,7 +339,9 @@
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [".env"]
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@@ -438,7 +450,12 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
@@ -449,7 +466,10 @@
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono",
|
||||
// ---
|
||||
// Sets the maximum number of lines in the terminal's scrollback buffer.
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
// "max_scroll_history_lines": 10000,
|
||||
},
|
||||
// Difference settings for semantic_index
|
||||
"semantic_index": {
|
||||
@@ -480,6 +500,7 @@
|
||||
"deno": {
|
||||
"enable": false
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
@@ -488,11 +509,18 @@
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Gleam": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
"hard_tabs": true,
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"JavaScript": {
|
||||
@@ -534,14 +562,18 @@
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// "rust-analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// // These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// "check": {
|
||||
// "command": "clippy" // rust-analyzer.check.command (default: "check")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"use_system_clipboard": "always"
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
19
assets/settings/initial_tasks.json
Normal file
19
assets/settings/initial_tasks.json
Normal file
@@ -0,0 +1,19 @@
|
||||
// Static tasks configuration.
|
||||
//
|
||||
// Example:
|
||||
[
|
||||
{
|
||||
"label": "Example task",
|
||||
"command": "bash",
|
||||
// rest of the parameters are optional
|
||||
"args": ["-c", "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done"],
|
||||
// Env overrides for the command, will be appended to the terminal's environment from the settings.
|
||||
"env": { "foo": "bar" },
|
||||
// Current working directory to spawn the command into, defaults to current project root.
|
||||
//"cwd": "/path/to/working/directory",
|
||||
// Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
|
||||
"use_new_terminal": false,
|
||||
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
|
||||
"allow_concurrent_runs": false
|
||||
}
|
||||
]
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#21242bff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f7f7f84c",
|
||||
"scrollbar.thumb.background": "#f7f7f84c",
|
||||
"scrollbar.thumb.hover_background": "#252931ff",
|
||||
"scrollbar.thumb.border": "#252931ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#221f26ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#efecf44c",
|
||||
"scrollbar.thumb.background": "#efecf44c",
|
||||
"scrollbar.thumb.hover_background": "#332f38ff",
|
||||
"scrollbar.thumb.border": "#332f38ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -430,7 +430,7 @@
|
||||
"panel.background": "#e6e3ebff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#19171c4c",
|
||||
"scrollbar.thumb.background": "#19171c4c",
|
||||
"scrollbar.thumb.hover_background": "#cbc8d1ff",
|
||||
"scrollbar.thumb.border": "#cbc8d1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -814,7 +814,7 @@
|
||||
"panel.background": "#262622ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fefbec4c",
|
||||
"scrollbar.thumb.background": "#fefbec4c",
|
||||
"scrollbar.thumb.hover_background": "#3b3933ff",
|
||||
"scrollbar.thumb.border": "#3b3933ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1198,7 +1198,7 @@
|
||||
"panel.background": "#eeebd7ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#20201d4c",
|
||||
"scrollbar.thumb.background": "#20201d4c",
|
||||
"scrollbar.thumb.hover_background": "#d7d3beff",
|
||||
"scrollbar.thumb.border": "#d7d3beff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1582,7 +1582,7 @@
|
||||
"panel.background": "#2c2b23ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f4f3ec4c",
|
||||
"scrollbar.thumb.background": "#f4f3ec4c",
|
||||
"scrollbar.thumb.hover_background": "#3c3b31ff",
|
||||
"scrollbar.thumb.border": "#3c3b31ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1966,7 +1966,7 @@
|
||||
"panel.background": "#ebeae3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#22221b4c",
|
||||
"scrollbar.thumb.background": "#22221b4c",
|
||||
"scrollbar.thumb.hover_background": "#d0cfc5ff",
|
||||
"scrollbar.thumb.border": "#d0cfc5ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -2350,7 +2350,7 @@
|
||||
"panel.background": "#27211eff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f0eeed4c",
|
||||
"scrollbar.thumb.background": "#f0eeed4c",
|
||||
"scrollbar.thumb.hover_background": "#3b3431ff",
|
||||
"scrollbar.thumb.border": "#3b3431ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -2734,7 +2734,7 @@
|
||||
"panel.background": "#e9e6e4ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b19184c",
|
||||
"scrollbar.thumb.background": "#1b19184c",
|
||||
"scrollbar.thumb.hover_background": "#d6d1cfff",
|
||||
"scrollbar.thumb.border": "#d6d1cfff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3118,7 +3118,7 @@
|
||||
"panel.background": "#252025ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f7f3f74c",
|
||||
"scrollbar.thumb.background": "#f7f3f74c",
|
||||
"scrollbar.thumb.hover_background": "#393239ff",
|
||||
"scrollbar.thumb.border": "#393239ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3502,7 +3502,7 @@
|
||||
"panel.background": "#e0d5e0ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b181b4c",
|
||||
"scrollbar.thumb.background": "#1b181b4c",
|
||||
"scrollbar.thumb.hover_background": "#ccbdccff",
|
||||
"scrollbar.thumb.border": "#ccbdccff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -3886,7 +3886,7 @@
|
||||
"panel.background": "#1c2529ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#ebf8ff4c",
|
||||
"scrollbar.thumb.background": "#ebf8ff4c",
|
||||
"scrollbar.thumb.hover_background": "#2c3b42ff",
|
||||
"scrollbar.thumb.border": "#2c3b42ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -4270,7 +4270,7 @@
|
||||
"panel.background": "#cdeaf9ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#161b1d4c",
|
||||
"scrollbar.thumb.background": "#161b1d4c",
|
||||
"scrollbar.thumb.hover_background": "#b0d3e5ff",
|
||||
"scrollbar.thumb.border": "#b0d3e5ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -4654,7 +4654,7 @@
|
||||
"panel.background": "#252020ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f4ecec4c",
|
||||
"scrollbar.thumb.background": "#f4ecec4c",
|
||||
"scrollbar.thumb.hover_background": "#352f2fff",
|
||||
"scrollbar.thumb.border": "#352f2fff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5038,7 +5038,7 @@
|
||||
"panel.background": "#ebe3e3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1b18184c",
|
||||
"scrollbar.thumb.background": "#1b18184c",
|
||||
"scrollbar.thumb.hover_background": "#cfc7c7ff",
|
||||
"scrollbar.thumb.border": "#cfc7c7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5422,7 +5422,7 @@
|
||||
"panel.background": "#1f2621ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#ecf4ee4c",
|
||||
"scrollbar.thumb.background": "#ecf4ee4c",
|
||||
"scrollbar.thumb.hover_background": "#2f3832ff",
|
||||
"scrollbar.thumb.border": "#2f3832ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -5806,7 +5806,7 @@
|
||||
"panel.background": "#e3ebe6ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#171c194c",
|
||||
"scrollbar.thumb.background": "#171c194c",
|
||||
"scrollbar.thumb.hover_background": "#c8d1cbff",
|
||||
"scrollbar.thumb.border": "#c8d1cbff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6190,7 +6190,7 @@
|
||||
"panel.background": "#1f231fff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f3faf34c",
|
||||
"scrollbar.thumb.background": "#f3faf34c",
|
||||
"scrollbar.thumb.hover_background": "#333b33ff",
|
||||
"scrollbar.thumb.border": "#333b33ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6574,7 +6574,7 @@
|
||||
"panel.background": "#daeedaff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#1315134c",
|
||||
"scrollbar.thumb.background": "#1315134c",
|
||||
"scrollbar.thumb.hover_background": "#bed7beff",
|
||||
"scrollbar.thumb.border": "#bed7beff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -6958,7 +6958,7 @@
|
||||
"panel.background": "#262f51ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f5f7ff4c",
|
||||
"scrollbar.thumb.background": "#f5f7ff4c",
|
||||
"scrollbar.thumb.hover_background": "#363f62ff",
|
||||
"scrollbar.thumb.border": "#363f62ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -7342,7 +7342,7 @@
|
||||
"panel.background": "#e5e8f5ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2026464c",
|
||||
"scrollbar.thumb.background": "#2026464c",
|
||||
"scrollbar.thumb.hover_background": "#ccd0e1ff",
|
||||
"scrollbar.thumb.border": "#ccd0e1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#1f2127ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#bfbdb64c",
|
||||
"scrollbar.thumb.background": "#bfbdb64c",
|
||||
"scrollbar.thumb.hover_background": "#2d2f34ff",
|
||||
"scrollbar.thumb.border": "#2d2f34ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -415,7 +415,7 @@
|
||||
"panel.background": "#ececedff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#5c61664c",
|
||||
"scrollbar.thumb.background": "#5c61664c",
|
||||
"scrollbar.thumb.hover_background": "#dfe0e1ff",
|
||||
"scrollbar.thumb.border": "#dfe0e1ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -784,7 +784,7 @@
|
||||
"panel.background": "#353944ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#cccac24c",
|
||||
"scrollbar.thumb.background": "#cccac24c",
|
||||
"scrollbar.thumb.hover_background": "#43464fff",
|
||||
"scrollbar.thumb.border": "#43464fff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#3a3735ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -420,7 +420,7 @@
|
||||
"panel.background": "#393634ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -794,7 +794,7 @@
|
||||
"panel.background": "#3b3735ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.background": "#fbf1c74c",
|
||||
"scrollbar.thumb.hover_background": "#494340ff",
|
||||
"scrollbar.thumb.border": "#494340ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1168,7 +1168,7 @@
|
||||
"panel.background": "#ecddb4ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1542,7 +1542,7 @@
|
||||
"panel.background": "#ecddb5ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -1916,7 +1916,7 @@
|
||||
"panel.background": "#ecdcb3ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.background": "#2828284c",
|
||||
"scrollbar.thumb.hover_background": "#ddcca7ff",
|
||||
"scrollbar.thumb.border": "#ddcca7ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#2f343eff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#c8ccd44c",
|
||||
"scrollbar.thumb.background": "#c8ccd44c",
|
||||
"scrollbar.thumb.hover_background": "#363c46ff",
|
||||
"scrollbar.thumb.border": "#363c46ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -420,7 +420,7 @@
|
||||
"panel.background": "#ebebecff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#383a414c",
|
||||
"scrollbar.thumb.background": "#383a414c",
|
||||
"scrollbar.thumb.hover_background": "#dfdfe0ff",
|
||||
"scrollbar.thumb.border": "#dfdfe0ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#1c1b2aff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#232132ff",
|
||||
"scrollbar.thumb.border": "#232132ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -425,7 +425,7 @@
|
||||
"panel.background": "#fef9f2ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#5752794c",
|
||||
"scrollbar.thumb.background": "#5752794c",
|
||||
"scrollbar.thumb.hover_background": "#e5e0dfff",
|
||||
"scrollbar.thumb.border": "#e5e0dfff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -804,7 +804,7 @@
|
||||
"panel.background": "#28253cff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.background": "#e0def44c",
|
||||
"scrollbar.thumb.hover_background": "#322f48ff",
|
||||
"scrollbar.thumb.border": "#322f48ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#2b3038ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fdf4c14c",
|
||||
"scrollbar.thumb.background": "#fdf4c14c",
|
||||
"scrollbar.thumb.hover_background": "#313741ff",
|
||||
"scrollbar.thumb.border": "#313741ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#04313bff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#fdf6e34c",
|
||||
"scrollbar.thumb.background": "#fdf6e34c",
|
||||
"scrollbar.thumb.hover_background": "#053541ff",
|
||||
"scrollbar.thumb.border": "#053541ff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
@@ -415,7 +415,7 @@
|
||||
"panel.background": "#f3eddaff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#002a354c",
|
||||
"scrollbar.thumb.background": "#002a354c",
|
||||
"scrollbar.thumb.hover_background": "#dcdacbff",
|
||||
"scrollbar.thumb.border": "#dcdacbff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"panel.background": "#231f16ff",
|
||||
"panel.focused_border": null,
|
||||
"pane.focused_border": null,
|
||||
"scrollbar_thumb.background": "#f8f5de4c",
|
||||
"scrollbar.thumb.background": "#f8f5de4c",
|
||||
"scrollbar.thumb.hover_background": "#29251bff",
|
||||
"scrollbar.thumb.border": "#29251bff",
|
||||
"scrollbar.track.background": "#00000000",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
pub mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
@@ -122,16 +122,13 @@ impl AssistantPanel {
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let (api_url, model_name) = cx
|
||||
.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.unwrap();
|
||||
let (api_url, model_name) = cx.update(|cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
(
|
||||
settings.openai_api_url.clone(),
|
||||
settings.default_open_ai_model.full_name().to_string(),
|
||||
)
|
||||
})?;
|
||||
let completion_provider = OpenAiCompletionProvider::new(
|
||||
api_url,
|
||||
model_name,
|
||||
@@ -365,7 +362,7 @@ impl AssistantPanel {
|
||||
move |cx: &mut BlockContext| {
|
||||
measurements.set(BlockMeasurements {
|
||||
anchor_x: cx.anchor_x,
|
||||
gutter_width: cx.gutter_width,
|
||||
gutter_width: cx.gutter_dimensions.width,
|
||||
});
|
||||
inline_assistant.clone().into_any_element()
|
||||
}
|
||||
|
||||
@@ -13,10 +13,12 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
markdown_preview.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Result};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, ViewContext, VisualContext, WindowContext,
|
||||
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
@@ -26,13 +28,24 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::http::{HttpClient, ZedHttpClient};
|
||||
use util::{
|
||||
http::{HttpClient, ZedHttpClient},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
actions!(
|
||||
auto_update,
|
||||
[
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
ViewReleaseNotesLocally
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -96,6 +109,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
|
||||
|
||||
impl Global for GlobalAutoUpdate {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReleaseNotesBody {
|
||||
title: String,
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
@@ -105,6 +124,10 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
|
||||
workspace.register_action(|_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -165,6 +188,71 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
|
||||
None
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let client = client::Client::global(cx).http_client();
|
||||
let url = client.zed_url(&format!(
|
||||
"/api/release_notes/{}/{}",
|
||||
release_channel.dev_name(),
|
||||
version
|
||||
));
|
||||
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
let Some(mut response) = response.log_err() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await.ok();
|
||||
|
||||
let body: serde_json::Result<ReleaseNotesBody> =
|
||||
serde_json::from_slice(body.as_slice());
|
||||
|
||||
if let Ok(body) = body {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx
|
||||
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
editor,
|
||||
workspace_handle,
|
||||
Some(tab_description),
|
||||
cx,
|
||||
);
|
||||
workspace.add_item(Box::new(view.clone()), cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version;
|
||||
|
||||
@@ -84,7 +84,6 @@ pub struct ActiveCall {
|
||||
),
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
pending_channel_id: Option<u64>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
@@ -98,7 +97,6 @@ impl ActiveCall {
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
pending_channel_id: None,
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
|
||||
@@ -113,10 +111,6 @@ impl ActiveCall {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
pub fn pending_channel_id(&self) -> Option<u64> {
|
||||
self.pending_channel_id
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
@@ -345,13 +339,11 @@ impl ActiveCall {
|
||||
channel_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
let mut leave = None;
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
let (room, _) = self.room.take().unwrap();
|
||||
leave = room.update(cx, |room, cx| Some(room.leave(cx)));
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,21 +353,14 @@ impl ActiveCall {
|
||||
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
self.pending_channel_id = Some(channel_id);
|
||||
let join = self._join_debouncer.spawn(cx, move |cx| async move {
|
||||
if let Some(task) = leave {
|
||||
task.await?
|
||||
}
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_channel_id.take();
|
||||
this.set_room(room.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
|
||||
@@ -7,6 +7,7 @@ use settings::Settings;
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
pub mute_on_join: bool,
|
||||
pub share_on_join: bool,
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
@@ -16,6 +17,11 @@ pub struct CallSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
pub mute_on_join: Option<bool>,
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: true
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
|
||||
@@ -49,7 +49,6 @@ pub struct RemoteParticipant {
|
||||
pub participant_index: ParticipantIndex,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
pub in_call: bool,
|
||||
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ pub struct Room {
|
||||
id: u64,
|
||||
channel_id: Option<u64>,
|
||||
live_kit: Option<LiveKitRoom>,
|
||||
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
status: RoomStatus,
|
||||
shared_projects: HashSet<WeakModel<Project>>,
|
||||
joined_projects: HashSet<WeakModel<Project>>,
|
||||
@@ -113,18 +112,91 @@ impl Room {
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
|
||||
let room = live_kit_client::Room::new();
|
||||
let mut status = room.status();
|
||||
// Consume the initial status of the room.
|
||||
let _ = status.try_recv();
|
||||
let _maintain_room = cx.spawn(|this, mut cx| async move {
|
||||
while let Some(status) = status.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
if status == live_kit_client::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _handle_updates = cx.spawn({
|
||||
let room = room.clone();
|
||||
move |this, mut cx| async move {
|
||||
let mut updates = room.updates();
|
||||
while let Some(update) = updates.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.live_kit_room_updated(update, cx).log_err()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connect = room.connect(&connection_info.server_url, &connection_info.token);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.can_use_microphone() {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(LiveKitRoom {
|
||||
room,
|
||||
screen_track: LocalTrack::None,
|
||||
microphone_track: LocalTrack::None,
|
||||
next_publish_id: 0,
|
||||
muted_by_user: Self::mute_on_join(cx),
|
||||
deafened: false,
|
||||
speaking: false,
|
||||
_maintain_room,
|
||||
_handle_updates,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let maintain_connection = cx.spawn({
|
||||
let client = client.clone();
|
||||
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
|
||||
});
|
||||
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
|
||||
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
|
||||
|
||||
let mut this = Self {
|
||||
Self {
|
||||
id,
|
||||
channel_id,
|
||||
live_kit: None,
|
||||
live_kit_connection_info,
|
||||
live_kit: live_kit_room,
|
||||
status: RoomStatus::Online,
|
||||
shared_projects: Default::default(),
|
||||
joined_projects: Default::default(),
|
||||
@@ -148,11 +220,7 @@ impl Room {
|
||||
maintain_connection: Some(maintain_connection),
|
||||
room_update_completed_tx,
|
||||
room_update_completed_rx,
|
||||
};
|
||||
if this.live_kit_connection_info.is_some() {
|
||||
this.join_call(cx).detach_and_log_err(cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
@@ -211,7 +279,7 @@ impl Room {
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Model<Self>> {
|
||||
Self::from_join_response(
|
||||
client.request(proto::JoinChannel2 { channel_id }).await?,
|
||||
client.request(proto::JoinChannel { channel_id }).await?,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
@@ -256,7 +324,7 @@ impl Room {
|
||||
}
|
||||
|
||||
pub fn mute_on_join(cx: &AppContext) -> bool {
|
||||
CallSettings::get_global(cx).mute_on_join
|
||||
CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some()
|
||||
}
|
||||
|
||||
fn from_join_response(
|
||||
@@ -306,9 +374,7 @@ impl Room {
|
||||
}
|
||||
|
||||
log::info!("leaving room");
|
||||
if self.live_kit.is_some() {
|
||||
Audio::play_sound(Sound::Leave, cx);
|
||||
}
|
||||
Audio::play_sound(Sound::Leave, cx);
|
||||
|
||||
self.clear_state(cx);
|
||||
|
||||
@@ -527,24 +593,6 @@ impl Room {
|
||||
&self.remote_participants
|
||||
}
|
||||
|
||||
pub fn call_participants(&self, cx: &AppContext) -> Vec<Arc<User>> {
|
||||
self.remote_participants()
|
||||
.values()
|
||||
.filter_map(|participant| {
|
||||
if participant.in_call {
|
||||
Some(participant.user.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.chain(if self.in_call() {
|
||||
self.user_store.read(cx).current_user()
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn remote_participant_for_peer_id(&self, peer_id: PeerId) -> Option<&RemoteParticipant> {
|
||||
self.remote_participants
|
||||
.values()
|
||||
@@ -569,6 +617,10 @@ impl Room {
|
||||
self.local_participant.role == proto::ChannelRole::Admin
|
||||
}
|
||||
|
||||
pub fn local_participant_is_guest(&self) -> bool {
|
||||
self.local_participant.role == proto::ChannelRole::Guest
|
||||
}
|
||||
|
||||
pub fn set_participant_role(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
@@ -776,7 +828,6 @@ impl Room {
|
||||
}
|
||||
|
||||
let role = participant.role();
|
||||
let in_call = participant.in_call;
|
||||
let location = ParticipantLocation::from_proto(participant.location)
|
||||
.unwrap_or(ParticipantLocation::External);
|
||||
if let Some(remote_participant) =
|
||||
@@ -787,15 +838,9 @@ impl Room {
|
||||
remote_participant.participant_index = participant_index;
|
||||
if location != remote_participant.location
|
||||
|| role != remote_participant.role
|
||||
|| in_call != remote_participant.in_call
|
||||
{
|
||||
if in_call && !remote_participant.in_call {
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
}
|
||||
remote_participant.location = location;
|
||||
remote_participant.role = role;
|
||||
remote_participant.in_call = participant.in_call;
|
||||
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
@@ -812,15 +857,12 @@ impl Room {
|
||||
role,
|
||||
muted: true,
|
||||
speaking: false,
|
||||
in_call: participant.in_call,
|
||||
video_tracks: Default::default(),
|
||||
audio_tracks: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
if participant.in_call {
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
}
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
|
||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||
let video_tracks =
|
||||
@@ -1009,6 +1051,15 @@ impl Room {
|
||||
}
|
||||
|
||||
RoomUpdate::SubscribedToRemoteAudioTrack(track, publication) => {
|
||||
if let Some(live_kit) = &self.live_kit {
|
||||
if live_kit.deafened {
|
||||
track.stop();
|
||||
cx.foreground_executor()
|
||||
.spawn(publication.set_enabled(false))
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = track.publisher_id().parse()?;
|
||||
let track_id = track.sid().to_string();
|
||||
let participant = self
|
||||
@@ -1155,7 +1206,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn share_project(
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
@@ -1257,19 +1308,18 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_muted(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(true, |live_kit| match &live_kit.microphone_track {
|
||||
LocalTrack::None => true,
|
||||
LocalTrack::Pending { .. } => true,
|
||||
LocalTrack::Published { track_publication } => track_publication.is_muted(),
|
||||
})
|
||||
pub fn is_sharing_mic(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
!matches!(live_kit.microphone_track, LocalTrack::None)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_only(&self) -> bool {
|
||||
!(self.local_participant().role == proto::ChannelRole::Member
|
||||
|| self.local_participant().role == proto::ChannelRole::Admin)
|
||||
pub fn is_muted(&self) -> bool {
|
||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||
matches!(live_kit.microphone_track, LocalTrack::None)
|
||||
|| live_kit.muted_by_user
|
||||
|| live_kit.deafened
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
@@ -1278,8 +1328,24 @@ impl Room {
|
||||
.map_or(false, |live_kit| live_kit.speaking)
|
||||
}
|
||||
|
||||
pub fn in_call(&self) -> bool {
|
||||
self.live_kit.is_some()
|
||||
pub fn is_deafened(&self) -> Option<bool> {
|
||||
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
|
||||
}
|
||||
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_share_projects(&self) -> bool {
|
||||
use proto::ChannelRole::*;
|
||||
match self.local_participant.role {
|
||||
Admin | Member => true,
|
||||
Guest | Banned | Talker => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -1332,8 +1398,12 @@ impl Room {
|
||||
Ok(publication) => {
|
||||
if canceled {
|
||||
live_kit.room.unpublish_track(publication);
|
||||
live_kit.microphone_track = LocalTrack::None;
|
||||
} else {
|
||||
if live_kit.muted_by_user || live_kit.deafened {
|
||||
cx.background_executor()
|
||||
.spawn(publication.set_mute(true))
|
||||
.detach();
|
||||
}
|
||||
live_kit.microphone_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
};
|
||||
@@ -1437,140 +1507,50 @@ impl Room {
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let muted = !self.is_muted();
|
||||
if let Some(task) = self.set_mute(muted, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
// When unmuting, undeafen if the user was deafened before.
|
||||
let was_deafened = live_kit.deafened;
|
||||
if live_kit.muted_by_user
|
||||
|| live_kit.deafened
|
||||
|| matches!(live_kit.microphone_track, LocalTrack::None)
|
||||
{
|
||||
live_kit.muted_by_user = false;
|
||||
live_kit.deafened = false;
|
||||
} else {
|
||||
live_kit.muted_by_user = true;
|
||||
}
|
||||
let muted = live_kit.muted_by_user;
|
||||
let should_undeafen = was_deafened && !live_kit.deafened;
|
||||
|
||||
if let Some(task) = self.set_mute(muted, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_undeafen {
|
||||
if let Some(task) = self.set_deafened(false, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_call(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
if self.live_kit.is_some() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
// When deafening, mute the microphone if it was not already muted.
|
||||
// When un-deafening, unmute the microphone, unless it was explicitly muted.
|
||||
let deafened = !live_kit.deafened;
|
||||
live_kit.deafened = deafened;
|
||||
let should_change_mute = !live_kit.muted_by_user;
|
||||
|
||||
let room = live_kit_client::Room::new();
|
||||
let mut status = room.status();
|
||||
// Consume the initial status of the room.
|
||||
let _ = status.try_recv();
|
||||
let _maintain_room = cx.spawn(|this, mut cx| async move {
|
||||
while let Some(status) = status.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
if let Some(task) = self.set_deafened(deafened, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if status == live_kit_client::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
|
||||
.ok();
|
||||
break;
|
||||
if should_change_mute {
|
||||
if let Some(task) = self.set_mute(deafened, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _handle_updates = cx.spawn({
|
||||
let room = room.clone();
|
||||
move |this, mut cx| async move {
|
||||
let mut updates = room.updates();
|
||||
while let Some(update) = updates.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.live_kit_room_updated(update, cx).log_err()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.live_kit = Some(LiveKitRoom {
|
||||
room: room.clone(),
|
||||
screen_track: LocalTrack::None,
|
||||
microphone_track: LocalTrack::None,
|
||||
next_publish_id: 0,
|
||||
speaking: false,
|
||||
_maintain_room,
|
||||
_handle_updates,
|
||||
});
|
||||
|
||||
cx.spawn({
|
||||
let client = self.client.clone();
|
||||
let share_microphone = !self.read_only() && !Self::mute_on_join(cx);
|
||||
let connection_info = self.live_kit_connection_info.clone();
|
||||
let channel_id = self.channel_id;
|
||||
|
||||
move |this, mut cx| async move {
|
||||
let connection_info = if let Some(connection_info) = connection_info {
|
||||
connection_info.clone()
|
||||
} else if let Some(channel_id) = channel_id {
|
||||
if let Some(connection_info) = client
|
||||
.request(proto::JoinChannelCall { channel_id })
|
||||
.await?
|
||||
.live_kit_connection_info
|
||||
{
|
||||
connection_info
|
||||
} else {
|
||||
return Err(anyhow!("failed to get connection info from server"));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"tried to connect to livekit without connection info"
|
||||
));
|
||||
};
|
||||
room.connect(&connection_info.server_url, &connection_info.token)
|
||||
.await?;
|
||||
|
||||
let track_updates = this.update(&mut cx, |this, cx| {
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
let Some(live_kit) = this.live_kit.as_mut() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut track_updates = Vec::new();
|
||||
for participant in this.remote_participants.values() {
|
||||
for publication in live_kit
|
||||
.room
|
||||
.remote_audio_track_publications(&participant.user.id.to_string())
|
||||
{
|
||||
track_updates.push(publication.set_enabled(true));
|
||||
}
|
||||
|
||||
for track in participant.audio_tracks.values() {
|
||||
track.start();
|
||||
}
|
||||
}
|
||||
track_updates
|
||||
})?;
|
||||
|
||||
if share_microphone {
|
||||
this.update(&mut cx, |this, cx| this.share_microphone(cx))?
|
||||
.await?
|
||||
};
|
||||
|
||||
for result in futures::future::join_all(track_updates).await {
|
||||
result?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn leave_call(&mut self, cx: &mut ModelContext<Self>) {
|
||||
Audio::play_sound(Sound::Leave, cx);
|
||||
if let Some(channel_id) = self.channel_id() {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor()
|
||||
.spawn(client.request(proto::LeaveChannelCall { channel_id }))
|
||||
.detach_and_log_err(cx);
|
||||
self.live_kit.take();
|
||||
self.live_kit_connection_info.take();
|
||||
cx.notify();
|
||||
} else {
|
||||
self.leave(cx).detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1601,6 +1581,40 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_deafened(
|
||||
&mut self,
|
||||
deafened: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
|
||||
let mut track_updates = Vec::new();
|
||||
for participant in self.remote_participants.values() {
|
||||
for publication in live_kit
|
||||
.room
|
||||
.remote_audio_track_publications(&participant.user.id.to_string())
|
||||
{
|
||||
track_updates.push(publication.set_enabled(!deafened));
|
||||
}
|
||||
|
||||
for track in participant.audio_tracks.values() {
|
||||
if deafened {
|
||||
track.stop();
|
||||
} else {
|
||||
track.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
for result in futures::future::join_all(track_updates).await {
|
||||
result?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn set_mute(
|
||||
&mut self,
|
||||
should_mute: bool,
|
||||
@@ -1645,6 +1659,9 @@ struct LiveKitRoom {
|
||||
room: Arc<live_kit_client::Room>,
|
||||
screen_track: LocalTrack,
|
||||
microphone_track: LocalTrack,
|
||||
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.
|
||||
muted_by_user: bool,
|
||||
deafened: bool,
|
||||
speaking: bool,
|
||||
next_publish_id: usize,
|
||||
_maintain_room: Task<()>,
|
||||
|
||||
@@ -120,7 +120,8 @@ impl ChannelMembership {
|
||||
proto::ChannelRole::Admin => 0,
|
||||
proto::ChannelRole::Member => 1,
|
||||
proto::ChannelRole::Banned => 2,
|
||||
proto::ChannelRole::Guest => 3,
|
||||
proto::ChannelRole::Talker => 3,
|
||||
proto::ChannelRole::Guest => 4,
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
@@ -348,6 +349,21 @@ impl ChannelStore {
|
||||
.is_some_and(|state| state.has_new_messages())
|
||||
}
|
||||
|
||||
pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
|
||||
self.channel_states.get(&channel_id).and_then(|state| {
|
||||
if let Some(last_message_id) = state.latest_chat_message {
|
||||
if state
|
||||
.last_acknowledged_message_id()
|
||||
.is_some_and(|id| id < last_message_id)
|
||||
{
|
||||
return state.last_acknowledged_message_id();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
@@ -1152,6 +1168,10 @@ impl ChannelState {
|
||||
})
|
||||
}
|
||||
|
||||
fn last_acknowledged_message_id(&self) -> Option<u64> {
|
||||
self.observed_chat_message
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||
|
||||
use super::*;
|
||||
use client::{test::FakeServer, Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext, Context, Model, TestAppContext};
|
||||
use rpc::proto::{self};
|
||||
use settings::SettingsStore;
|
||||
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
release_channel::init("0.0.0", cx);
|
||||
client::init_settings(cx);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
client::init(&client, cx);
|
||||
|
||||
@@ -10,10 +10,11 @@ path = "src/client.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -51,6 +52,7 @@ uuid.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
|
||||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let client = Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
http,
|
||||
state: Default::default(),
|
||||
|
||||
@@ -1455,6 +1460,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeServer;
|
||||
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{BackgroundExecutor, Context, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use settings::SettingsStore;
|
||||
@@ -1465,7 +1471,13 @@ mod tests {
|
||||
async fn test_reconnection(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
let mut status = client.status();
|
||||
assert!(matches!(
|
||||
@@ -1500,7 +1512,13 @@ mod tests {
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut status = client.status();
|
||||
|
||||
// Time out when client tries to connect.
|
||||
@@ -1573,7 +1591,13 @@ mod tests {
|
||||
init_test(cx);
|
||||
let auth_count = Arc::new(Mutex::new(0));
|
||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
client.override_authenticate({
|
||||
let auth_count = auth_count.clone();
|
||||
let dropped_auth_count = dropped_auth_count.clone();
|
||||
@@ -1621,7 +1645,13 @@ mod tests {
|
||||
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
@@ -1675,7 +1705,13 @@ mod tests {
|
||||
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
@@ -1704,7 +1740,13 @@ mod tests {
|
||||
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let user_id = 5;
|
||||
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(FakeSystemClock::default()),
|
||||
FakeHttpClient::with_404_response(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
|
||||
@@ -2,6 +2,7 @@ mod event_coalescer;
|
||||
|
||||
use crate::TelemetrySettings;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -24,6 +25,7 @@ use util::TryFutureExt;
|
||||
use self::event_coalescer::EventCoalescer;
|
||||
|
||||
pub struct Telemetry {
|
||||
clock: Arc<dyn SystemClock>,
|
||||
http_client: Arc<ZedHttpClient>,
|
||||
executor: BackgroundExecutor,
|
||||
state: Arc<Mutex<TelemetryState>>,
|
||||
@@ -156,7 +158,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
client: Arc<ZedHttpClient>,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<Self> {
|
||||
let release_channel =
|
||||
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
|
||||
|
||||
@@ -205,6 +211,7 @@ impl Telemetry {
|
||||
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
clock,
|
||||
http_client: client,
|
||||
executor: cx.background_executor().clone(),
|
||||
state,
|
||||
@@ -317,7 +324,8 @@ impl Telemetry {
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -333,7 +341,8 @@ impl Telemetry {
|
||||
suggestion_id,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -349,7 +358,8 @@ impl Telemetry {
|
||||
conversation_id,
|
||||
kind,
|
||||
model,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -365,7 +375,8 @@ impl Telemetry {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -375,7 +386,8 @@ impl Telemetry {
|
||||
let event = Event::Cpu {
|
||||
usage_as_percentage,
|
||||
core_count,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -389,24 +401,18 @@ impl Telemetry {
|
||||
let event = Event::Memory {
|
||||
memory_in_bytes,
|
||||
virtual_memory_in_bytes,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) {
|
||||
self.report_app_event_with_date_time(operation, Utc::now());
|
||||
}
|
||||
|
||||
fn report_app_event_with_date_time(
|
||||
self: &Arc<Self>,
|
||||
operation: String,
|
||||
date_time: DateTime<Utc>,
|
||||
) -> Event {
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App {
|
||||
operation,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event.clone());
|
||||
@@ -418,7 +424,8 @@ impl Telemetry {
|
||||
let event = Event::Setting {
|
||||
setting,
|
||||
value,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -433,7 +440,8 @@ impl Telemetry {
|
||||
let event = Event::Edit {
|
||||
duration: end.timestamp_millis() - start.timestamp_millis(),
|
||||
environment,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event);
|
||||
@@ -444,7 +452,8 @@ impl Telemetry {
|
||||
let event = Event::Action {
|
||||
source,
|
||||
action,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
|
||||
milliseconds_since_first_event: self
|
||||
.milliseconds_since_first_event(self.clock.utc_now()),
|
||||
};
|
||||
|
||||
self.report_event(event)
|
||||
@@ -590,29 +599,32 @@ impl Telemetry {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -627,9 +639,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -644,9 +656,9 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -661,10 +673,10 @@ mod tests {
|
||||
Some(first_date_time)
|
||||
);
|
||||
|
||||
date_time += chrono::Duration::milliseconds(100);
|
||||
clock.advance(chrono::Duration::milliseconds(100));
|
||||
|
||||
// Adding a 4th event should cause a flush
|
||||
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
@@ -680,22 +692,24 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let clock = Arc::new(FakeSystemClock::new(
|
||||
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
|
||||
));
|
||||
let http = FakeHttpClient::with_200_response();
|
||||
let installation_id = Some("installation_id".to_string());
|
||||
let session_id = "session_id".to_string();
|
||||
|
||||
cx.update(|cx| {
|
||||
let telemetry = Telemetry::new(http, cx);
|
||||
let telemetry = Telemetry::new(clock.clone(), http, cx);
|
||||
telemetry.state.lock().max_queue_size = 4;
|
||||
telemetry.start(installation_id, session_id, cx);
|
||||
|
||||
assert!(is_empty_state(&telemetry));
|
||||
|
||||
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
|
||||
let first_date_time = clock.utc_now();
|
||||
let operation = "test".to_string();
|
||||
|
||||
let event =
|
||||
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
Event::App {
|
||||
|
||||
@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
|
||||
path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["dep:parking_lot"]
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
mod system_clock;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
fmt, iter,
|
||||
};
|
||||
|
||||
pub use system_clock::*;
|
||||
|
||||
/// A unique identifier for each distributed node.
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
|
||||
pub type Seq = u32;
|
||||
|
||||
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
/// used to determine the ordering of events in the editor.
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||
pub struct Lamport {
|
||||
pub replica_id: ReplicaId,
|
||||
pub value: Seq,
|
||||
}
|
||||
|
||||
/// A vector clock
|
||||
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
|
||||
#[derive(Clone, Default, Hash, Eq, PartialEq)]
|
||||
pub struct Global(SmallVec<[u32; 8]>);
|
||||
|
||||
|
||||
59
crates/clock/src/system_clock.rs
Normal file
59
crates/clock/src/system_clock.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
pub trait SystemClock: Send + Sync {
|
||||
/// Returns the current date and time in UTC.
|
||||
fn utc_now(&self) -> DateTime<Utc>;
|
||||
}
|
||||
|
||||
pub struct RealSystemClock;
|
||||
|
||||
impl SystemClock for RealSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
Utc::now()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClockState {
|
||||
now: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeSystemClock {
|
||||
// Use an unfair lock to ensure tests are deterministic.
|
||||
state: parking_lot::Mutex<FakeSystemClockState>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for FakeSystemClock {
|
||||
fn default() -> Self {
|
||||
Self::new(Utc::now())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeSystemClock {
|
||||
pub fn new(now: DateTime<Utc>) -> Self {
|
||||
let state = FakeSystemClockState { now };
|
||||
|
||||
Self {
|
||||
state: parking_lot::Mutex::new(state),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_now(&self, now: DateTime<Utc>) {
|
||||
self.state.lock().now = now;
|
||||
}
|
||||
|
||||
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
|
||||
pub fn advance(&self, duration: chrono::Duration) {
|
||||
self.state.lock().now += duration;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl SystemClock for FakeSystemClock {
|
||||
fn utc_now(&self) -> DateTime<Utc> {
|
||||
self.state.lock().now
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ ZED_ENVIRONMENT = "development"
|
||||
LIVE_KIT_SERVER = "http://localhost:7880"
|
||||
LIVE_KIT_KEY = "devkey"
|
||||
LIVE_KIT_SECRET = "secret"
|
||||
BLOB_STORE_ACCESS_KEY = "the-blob-store-access-key"
|
||||
BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
|
||||
BLOB_STORE_BUCKET = "the-extensions-bucket"
|
||||
BLOB_STORE_URL = "http://127.0.0.1:9000"
|
||||
BLOB_STORE_REGION = "the-region"
|
||||
|
||||
# RUST_LOG=info
|
||||
# LOG_JSON=true
|
||||
|
||||
@@ -17,6 +17,8 @@ required-features = ["seed-support"]
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-tungstenite = "0.16"
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||
base64 = "0.13"
|
||||
@@ -41,6 +43,7 @@ reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
rpc.workspace = true
|
||||
scrypt = "0.7"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -61,7 +64,6 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
release_channel.workspace = true
|
||||
async-trait.workspace = true
|
||||
audio.workspace = true
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
@@ -86,6 +88,7 @@ node_runtime.workspace = true
|
||||
notifications = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -105,6 +105,31 @@ spec:
|
||||
secretKeyRef:
|
||||
name: livekit
|
||||
key: secret
|
||||
- name: BLOB_STORE_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: access_key
|
||||
- name: BLOB_STORE_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: secret_key
|
||||
- name: BLOB_STORE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: url
|
||||
- name: BLOB_STORE_REGION
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: region
|
||||
- name: BLOB_STORE_BUCKET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: blob-store
|
||||
key: bucket
|
||||
- name: INVITE_LINK_PREFIX
|
||||
value: ${INVITE_LINK_PREFIX}
|
||||
- name: RUST_BACKTRACE
|
||||
|
||||
@@ -353,3 +353,25 @@ CREATE TABLE contributors (
|
||||
signed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE extensions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
external_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
latest_version TEXT NOT NULL,
|
||||
total_download_count INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE extension_versions (
|
||||
extension_id INTEGER REFERENCES extensions(id),
|
||||
version TEXT NOT NULL,
|
||||
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
authors TEXT NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
download_count INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (extension_id, version)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id");
|
||||
CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Add migration script here
|
||||
ALTER TABLE rooms DROP COLUMN enviroment;
|
||||
ALTER TABLE rooms DROP COLUMN environment;
|
||||
ALTER TABLE room_participants DROP COLUMN in_call;
|
||||
22
crates/collab/migrations/20240214102900_add_extensions.sql
Normal file
22
crates/collab/migrations/20240214102900_add_extensions.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS extensions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
external_id TEXT NOT NULL,
|
||||
latest_version TEXT NOT NULL,
|
||||
total_download_count BIGINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS extension_versions (
|
||||
extension_id INTEGER REFERENCES extensions(id),
|
||||
version TEXT NOT NULL,
|
||||
published_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
authors TEXT NOT NULL,
|
||||
repository TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
download_count BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(extension_id, version)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id");
|
||||
CREATE INDEX "trigram_index_extensions_name" ON "extensions" USING GIN(name gin_trgm_ops);
|
||||
CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
|
||||
@@ -1,3 +1,5 @@
|
||||
mod extensions;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{ContributorSelector, User, UserId},
|
||||
@@ -20,6 +22,8 @@ use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
pub use extensions::fetch_extensions_from_blob_store_periodically;
|
||||
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
@@ -28,6 +32,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route("/contributors", get(get_contributors).post(add_contributor))
|
||||
.route("/contributor", get(check_is_contributor))
|
||||
.merge(extensions::router())
|
||||
.layer(
|
||||
ServiceBuilder::new()
|
||||
.layer(Extension(state))
|
||||
|
||||
237
crates/collab/src/api/extensions.rs
Normal file
237
crates/collab/src/api/extensions.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
use crate::{
|
||||
db::{ExtensionMetadata, NewExtensionVersion},
|
||||
executor::Executor,
|
||||
AppState, Error, Result,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use aws_sdk_s3::presigning::PresigningConfig;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
response::Redirect,
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use hyper::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::PrimitiveDateTime;
|
||||
use util::ResultExt;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/extensions", get(get_extensions))
|
||||
.route(
|
||||
"/extensions/:extension_id/:version/download",
|
||||
get(download_extension),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetExtensionsParams {
|
||||
filter: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DownloadExtensionParams {
|
||||
extension_id: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GetExtensionsResponse {
|
||||
pub data: Vec<ExtensionMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ExtensionManifest {
|
||||
name: String,
|
||||
version: String,
|
||||
description: Option<String>,
|
||||
authors: Vec<String>,
|
||||
repository: String,
|
||||
}
|
||||
|
||||
async fn get_extensions(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetExtensionsParams>,
|
||||
) -> Result<Json<GetExtensionsResponse>> {
|
||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
|
||||
Ok(Json(GetExtensionsResponse { data: extensions }))
|
||||
}
|
||||
|
||||
async fn download_extension(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Path(params): Path<DownloadExtensionParams>,
|
||||
) -> Result<Redirect> {
|
||||
let Some((blob_store_client, bucket)) = app
|
||||
.blob_store_client
|
||||
.clone()
|
||||
.zip(app.config.blob_store_bucket.clone())
|
||||
else {
|
||||
Err(Error::Http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let DownloadExtensionParams {
|
||||
extension_id,
|
||||
version,
|
||||
} = params;
|
||||
|
||||
let version_exists = app
|
||||
.db
|
||||
.record_extension_download(&extension_id, &version)
|
||||
.await?;
|
||||
|
||||
if !version_exists {
|
||||
Err(Error::Http(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown extension version".into(),
|
||||
))?;
|
||||
}
|
||||
|
||||
let url = blob_store_client
|
||||
.get_object()
|
||||
.bucket(bucket)
|
||||
.key(format!(
|
||||
"extensions/{extension_id}/{version}/archive.tar.gz"
|
||||
))
|
||||
.presigned(PresigningConfig::expires_in(EXTENSION_DOWNLOAD_URL_LIFETIME).unwrap())
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to create presigned extension download url {e}"))?;
|
||||
|
||||
Ok(Redirect::temporary(url.uri()))
|
||||
}
|
||||
|
||||
const EXTENSION_FETCH_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
||||
const EXTENSION_DOWNLOAD_URL_LIFETIME: Duration = Duration::from_secs(3 * 60);
|
||||
|
||||
pub fn fetch_extensions_from_blob_store_periodically(app_state: Arc<AppState>, executor: Executor) {
|
||||
let Some(blob_store_client) = app_state.blob_store_client.clone() else {
|
||||
log::info!("no blob store client");
|
||||
return;
|
||||
};
|
||||
let Some(blob_store_bucket) = app_state.config.blob_store_bucket.clone() else {
|
||||
log::info!("no blob store bucket");
|
||||
return;
|
||||
};
|
||||
|
||||
executor.spawn_detached({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
fetch_extensions_from_blob_store(
|
||||
&blob_store_client,
|
||||
&blob_store_bucket,
|
||||
&app_state,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
executor.sleep(EXTENSION_FETCH_INTERVAL).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn fetch_extensions_from_blob_store(
|
||||
blob_store_client: &aws_sdk_s3::Client,
|
||||
blob_store_bucket: &String,
|
||||
app_state: &Arc<AppState>,
|
||||
) -> anyhow::Result<()> {
|
||||
let list = blob_store_client
|
||||
.list_objects()
|
||||
.bucket(blob_store_bucket)
|
||||
.prefix("extensions/")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let objects = list
|
||||
.contents
|
||||
.ok_or_else(|| anyhow!("missing bucket contents"))?;
|
||||
|
||||
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
|
||||
for object in &objects {
|
||||
let Some(key) = object.key.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
let mut parts = key.split('/');
|
||||
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
|
||||
continue;
|
||||
};
|
||||
let Some(extension_id) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
published_versions
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(version);
|
||||
}
|
||||
|
||||
let known_versions = app_state.db.get_known_extension_versions().await?;
|
||||
|
||||
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
|
||||
let empty = Vec::new();
|
||||
for (extension_id, published_versions) in published_versions {
|
||||
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
|
||||
|
||||
for published_version in published_versions {
|
||||
if known_versions
|
||||
.binary_search_by_key(&published_version, String::as_str)
|
||||
.is_err()
|
||||
{
|
||||
let object = blob_store_client
|
||||
.get_object()
|
||||
.bucket(blob_store_bucket)
|
||||
.key(format!(
|
||||
"extensions/{extension_id}/{published_version}/manifest.json"
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
let manifest_bytes = object
|
||||
.body
|
||||
.collect()
|
||||
.await
|
||||
.map(|data| data.into_bytes())
|
||||
.with_context(|| format!("failed to download manifest for extension {extension_id} version {published_version}"))?
|
||||
.to_vec();
|
||||
let manifest = serde_json::from_slice::<ExtensionManifest>(&manifest_bytes)
|
||||
.with_context(|| format!("invalid manifest for extension {extension_id} version {published_version}: {}", String::from_utf8_lossy(&manifest_bytes)))?;
|
||||
|
||||
let published_at = object.last_modified.ok_or_else(|| anyhow!("missing last modified timestamp for extension {extension_id} version {published_version}"))?;
|
||||
let published_at =
|
||||
time::OffsetDateTime::from_unix_timestamp_nanos(published_at.as_nanos())?;
|
||||
let published_at = PrimitiveDateTime::new(published_at.date(), published_at.time());
|
||||
|
||||
let version = semver::Version::parse(&manifest.version).with_context(|| {
|
||||
format!(
|
||||
"invalid version for extension {extension_id} version {published_version}"
|
||||
)
|
||||
})?;
|
||||
|
||||
new_versions
|
||||
.entry(extension_id)
|
||||
.or_default()
|
||||
.push(NewExtensionVersion {
|
||||
name: manifest.name,
|
||||
version,
|
||||
description: manifest.description.unwrap_or_default(),
|
||||
authors: manifest.authors,
|
||||
repository: manifest.repository,
|
||||
published_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app_state
|
||||
.db
|
||||
.insert_extension_versions(&new_versions)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -18,8 +18,8 @@ struct GitHubUser {
|
||||
async fn main() {
|
||||
load_dotenv().expect("failed to load .env.toml file");
|
||||
|
||||
let mut admin_logins =
|
||||
load_admins("./.admins.default.json").expect("failed to load default admins file");
|
||||
let mut admin_logins = load_admins("crates/collab/.admins.default.json")
|
||||
.expect("failed to load default admins file");
|
||||
if let Ok(other_admins) = load_admins("./.admins.json") {
|
||||
admin_logins.extend(other_admins);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestDb;
|
||||
|
||||
mod ids;
|
||||
mod queries;
|
||||
mod tables;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
use crate::{executor::Executor, Error, Result};
|
||||
use anyhow::anyhow;
|
||||
@@ -25,7 +21,7 @@ use sea_orm::{
|
||||
FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
|
||||
TransactionTrait,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{ser::Error as _, Deserialize, Serialize, Serializer};
|
||||
use sqlx::{
|
||||
migrate::{Migrate, Migration, MigrationSource},
|
||||
Connection,
|
||||
@@ -40,13 +36,17 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
pub use tables::*;
|
||||
use time::{format_description::well_known::iso8601, PrimitiveDateTime};
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestDb;
|
||||
|
||||
pub use ids::*;
|
||||
pub use queries::contributors::ContributorSelector;
|
||||
pub use sea_orm::ConnectOptions;
|
||||
pub use tables::user::Model as User;
|
||||
pub use tables::*;
|
||||
|
||||
/// Database gives you a handle that lets you access the database.
|
||||
/// It handles pooling internally.
|
||||
@@ -717,3 +717,43 @@ pub struct WorktreeSettingsFile {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub struct NewExtensionVersion {
|
||||
pub name: String,
|
||||
pub version: semver::Version,
|
||||
pub description: String,
|
||||
pub authors: Vec<String>,
|
||||
pub repository: String,
|
||||
pub published_at: PrimitiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq)]
|
||||
pub struct ExtensionMetadata {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub authors: Vec<String>,
|
||||
pub description: String,
|
||||
pub repository: String,
|
||||
#[serde(serialize_with = "serialize_iso8601")]
|
||||
pub published_at: PrimitiveDateTime,
|
||||
pub download_count: u64,
|
||||
}
|
||||
|
||||
pub fn serialize_iso8601<S: Serializer>(
|
||||
datetime: &PrimitiveDateTime,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
const SERDE_CONFIG: iso8601::EncodedConfig = iso8601::Config::DEFAULT
|
||||
.set_year_is_six_digits(false)
|
||||
.set_time_precision(iso8601::TimePrecision::Second {
|
||||
decimal_digits: None,
|
||||
})
|
||||
.encode();
|
||||
|
||||
datetime
|
||||
.assume_utc()
|
||||
.format(&time::format_description::well_known::Iso8601::<SERDE_CONFIG>)
|
||||
.map_err(S::Error::custom)?
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ id_type!(SignupId);
|
||||
id_type!(UserId);
|
||||
id_type!(ChannelBufferCollaboratorId);
|
||||
id_type!(FlagId);
|
||||
id_type!(ExtensionId);
|
||||
id_type!(NotificationId);
|
||||
id_type!(NotificationKindId);
|
||||
|
||||
@@ -99,8 +100,12 @@ pub enum ChannelRole {
|
||||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
/// Talker can read, but not write.
|
||||
/// They can use microphones and the channel chat
|
||||
#[sea_orm(string_value = "talker")]
|
||||
Talker,
|
||||
/// Guest can read, but not write.
|
||||
/// (thought they can use the channel chat)
|
||||
/// They can not use microphones but can use the chat.
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
/// Banned may not read.
|
||||
@@ -113,8 +118,9 @@ impl ChannelRole {
|
||||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Admin => matches!(other, Member | Banned | Talker | Guest),
|
||||
Member => matches!(other, Banned | Talker | Guest),
|
||||
Talker => matches!(other, Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
@@ -133,7 +139,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Guest | Talker => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -143,7 +149,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Guest | Talker | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,16 +157,16 @@ impl ChannelRole {
|
||||
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Guest => true,
|
||||
Guest | Talker => true,
|
||||
Admin | Member | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role can share screen/microphone/projects into rooms.
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -170,7 +176,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Talker | Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +184,7 @@ impl ChannelRole {
|
||||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member | Guest => true,
|
||||
Admin | Member | Guest | Talker => true,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
@@ -187,7 +193,7 @@ impl ChannelRole {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Banned | Guest => false,
|
||||
Banned | Guest | Talker => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
|
||||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Talker => ChannelRole::Talker,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
@@ -208,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
|
||||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Talker => proto::ChannelRole::Talker,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod buffers;
|
||||
pub mod channels;
|
||||
pub mod contacts;
|
||||
pub mod contributors;
|
||||
pub mod extensions;
|
||||
pub mod messages;
|
||||
pub mod notifications;
|
||||
pub mod projects;
|
||||
|
||||
@@ -97,59 +97,12 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn set_in_channel_call(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
in_call: bool,
|
||||
) -> Result<(proto::Room, ChannelRole)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let role = self.channel_role_for_user(&channel, user_id, &*tx).await?;
|
||||
if role.is_none() || role == Some(ChannelRole::Banned) {
|
||||
Err(ErrorCode::Forbidden.anyhow())?
|
||||
}
|
||||
let role = role.unwrap();
|
||||
|
||||
let Some(room) = room::Entity::find()
|
||||
.filter(room::Column::ChannelId.eq(channel_id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
else {
|
||||
Err(anyhow!("no room exists"))?
|
||||
};
|
||||
|
||||
let result = room_participant::Entity::update_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(room_participant::Column::RoomId.eq(room.id))
|
||||
.add(room_participant::Column::UserId.eq(user_id)),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
in_call: ActiveValue::Set(in_call),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected != 1 {
|
||||
Err(anyhow!("not in channel"))?
|
||||
}
|
||||
|
||||
let room = self.get_room(room.id, &*tx).await?;
|
||||
Ok((room, role))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds a user to the specified channel.
|
||||
pub async fn join_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
autojoin: bool,
|
||||
connection: ConnectionId,
|
||||
environment: &str,
|
||||
) -> Result<(JoinRoom, Option<MembershipUpdated>, ChannelRole)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
@@ -209,10 +162,10 @@ impl Database {
|
||||
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
let room_id = self
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, environment, &*tx)
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*tx)
|
||||
.await?;
|
||||
|
||||
self.join_channel_room_internal(room_id, user_id, autojoin, connection, role, &*tx)
|
||||
self.join_channel_room_internal(room_id, user_id, connection, role, &*tx)
|
||||
.await
|
||||
.map(|jr| (jr, accept_invite_result, role))
|
||||
})
|
||||
@@ -842,6 +795,7 @@ impl Database {
|
||||
match role {
|
||||
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Talker)
|
||||
| Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| None => Err(anyhow!(
|
||||
@@ -860,7 +814,10 @@ impl Database {
|
||||
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match channel_role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
||||
Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker)
|
||||
| None => Err(anyhow!(
|
||||
"user is not a channel member or channel does not exist"
|
||||
))?,
|
||||
}
|
||||
@@ -875,9 +832,10 @@ impl Database {
|
||||
) -> Result<ChannelRole> {
|
||||
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
|
||||
Ok(role.unwrap())
|
||||
}
|
||||
Some(ChannelRole::Admin)
|
||||
| Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
||||
"user is not a channel participant or channel does not exist"
|
||||
))?,
|
||||
@@ -979,7 +937,6 @@ impl Database {
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
live_kit_room: &str,
|
||||
environment: &str,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<RoomId> {
|
||||
let room = room::Entity::find()
|
||||
@@ -988,19 +945,11 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let room_id = if let Some(room) = room {
|
||||
if let Some(env) = room.environment {
|
||||
if &env != environment {
|
||||
Err(ErrorCode::WrongReleaseChannel
|
||||
.with_tag("required", &env)
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
room.id
|
||||
} else {
|
||||
let result = room::Entity::insert(room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel_id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
environment: ActiveValue::Set(Some(environment.to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
|
||||
206
crates/collab/src/db/queries/extensions.rs
Normal file
206
crates/collab/src/db/queries/extensions.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use super::*;
|
||||
|
||||
impl Database {
|
||||
pub async fn get_extensions(
|
||||
&self,
|
||||
filter: Option<&str>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<ExtensionMetadata>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut condition = Condition::all();
|
||||
if let Some(filter) = filter {
|
||||
let fuzzy_name_filter = Self::fuzzy_like_string(filter);
|
||||
condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter));
|
||||
}
|
||||
|
||||
let extensions = extension::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(extension::Column::TotalDownloadCount)
|
||||
.order_by_asc(extension::Column::Name)
|
||||
.limit(Some(limit as u64))
|
||||
.filter(
|
||||
extension::Column::LatestVersion
|
||||
.into_expr()
|
||||
.eq(extension_version::Column::Version.into_expr()),
|
||||
)
|
||||
.inner_join(extension_version::Entity)
|
||||
.select_also(extension_version::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(extensions
|
||||
.into_iter()
|
||||
.filter_map(|(extension, latest_version)| {
|
||||
let version = latest_version?;
|
||||
Some(ExtensionMetadata {
|
||||
id: extension.external_id,
|
||||
name: extension.name,
|
||||
version: version.version,
|
||||
authors: version
|
||||
.authors
|
||||
.split(',')
|
||||
.map(|author| author.trim().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
description: version.description,
|
||||
repository: version.repository,
|
||||
published_at: version.published_at,
|
||||
download_count: extension.total_download_count as u64,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_known_extension_versions<'a>(&self) -> Result<HashMap<String, Vec<String>>> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut extension_external_ids_by_id = HashMap::default();
|
||||
|
||||
let mut rows = extension::Entity::find().stream(&*tx).await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
extension_external_ids_by_id.insert(row.id, row.external_id);
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut known_versions_by_extension_id: HashMap<String, Vec<String>> =
|
||||
HashMap::default();
|
||||
let mut rows = extension_version::Entity::find().stream(&*tx).await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
|
||||
let Some(extension_id) = extension_external_ids_by_id.get(&row.extension_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let versions = known_versions_by_extension_id
|
||||
.entry(extension_id.clone())
|
||||
.or_default();
|
||||
if let Err(ix) = versions.binary_search(&row.version) {
|
||||
versions.insert(ix, row.version);
|
||||
}
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
Ok(known_versions_by_extension_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn insert_extension_versions(
|
||||
&self,
|
||||
versions_by_extension_id: &HashMap<&str, Vec<NewExtensionVersion>>,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
for (external_id, versions) in versions_by_extension_id {
|
||||
if versions.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let latest_version = versions
|
||||
.iter()
|
||||
.max_by_key(|version| &version.version)
|
||||
.unwrap();
|
||||
|
||||
let insert = extension::Entity::insert(extension::ActiveModel {
|
||||
name: ActiveValue::Set(latest_version.name.clone()),
|
||||
external_id: ActiveValue::Set(external_id.to_string()),
|
||||
id: ActiveValue::NotSet,
|
||||
latest_version: ActiveValue::Set(latest_version.version.to_string()),
|
||||
total_download_count: ActiveValue::NotSet,
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([extension::Column::ExternalId])
|
||||
.update_column(extension::Column::ExternalId)
|
||||
.to_owned(),
|
||||
);
|
||||
|
||||
let extension = if tx.support_returning() {
|
||||
insert.exec_with_returning(&*tx).await?
|
||||
} else {
|
||||
// Sqlite
|
||||
insert.exec_without_returning(&*tx).await?;
|
||||
extension::Entity::find()
|
||||
.filter(extension::Column::ExternalId.eq(*external_id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("failed to insert extension"))?
|
||||
};
|
||||
|
||||
extension_version::Entity::insert_many(versions.iter().map(|version| {
|
||||
extension_version::ActiveModel {
|
||||
extension_id: ActiveValue::Set(extension.id),
|
||||
published_at: ActiveValue::Set(version.published_at),
|
||||
version: ActiveValue::Set(version.version.to_string()),
|
||||
authors: ActiveValue::Set(version.authors.join(", ")),
|
||||
repository: ActiveValue::Set(version.repository.clone()),
|
||||
description: ActiveValue::Set(version.description.clone()),
|
||||
download_count: ActiveValue::NotSet,
|
||||
}
|
||||
}))
|
||||
.on_conflict(OnConflict::new().do_nothing().to_owned())
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
|
||||
if let Ok(db_version) = semver::Version::parse(&extension.latest_version) {
|
||||
if db_version >= latest_version.version {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut extension = extension.into_active_model();
|
||||
extension.latest_version = ActiveValue::Set(latest_version.version.to_string());
|
||||
extension.name = ActiveValue::set(latest_version.name.clone());
|
||||
extension::Entity::update(extension).exec(&*tx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn record_extension_download(&self, extension: &str, version: &str) -> Result<bool> {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryId {
|
||||
Id,
|
||||
}
|
||||
|
||||
let extension_id: Option<ExtensionId> = extension::Entity::find()
|
||||
.filter(extension::Column::ExternalId.eq(extension))
|
||||
.select_only()
|
||||
.column(extension::Column::Id)
|
||||
.into_values::<_, QueryId>()
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
let Some(extension_id) = extension_id else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
extension_version::Entity::update_many()
|
||||
.col_expr(
|
||||
extension_version::Column::DownloadCount,
|
||||
extension_version::Column::DownloadCount.into_expr().add(1),
|
||||
)
|
||||
.filter(
|
||||
extension_version::Column::ExtensionId
|
||||
.eq(extension_id)
|
||||
.and(extension_version::Column::Version.eq(version)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
extension::Entity::update_many()
|
||||
.col_expr(
|
||||
extension::Column::TotalDownloadCount,
|
||||
extension::Column::TotalDownloadCount.into_expr().add(1),
|
||||
)
|
||||
.filter(extension::Column::Id.eq(extension_id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ impl Database {
|
||||
if !participant
|
||||
.role
|
||||
.unwrap_or(ChannelRole::Member)
|
||||
.can_publish_to_rooms()
|
||||
.can_edit_projects()
|
||||
{
|
||||
return Err(anyhow!("guests cannot share projects"))?;
|
||||
}
|
||||
|
||||
@@ -110,12 +110,10 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
release_channel: &str,
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
environment: ActiveValue::set(Some(release_channel.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@@ -135,7 +133,6 @@ impl Database {
|
||||
))),
|
||||
participant_index: ActiveValue::set(Some(0)),
|
||||
role: ActiveValue::set(Some(ChannelRole::Admin)),
|
||||
in_call: ActiveValue::set(true),
|
||||
|
||||
id: ActiveValue::NotSet,
|
||||
location_kind: ActiveValue::NotSet,
|
||||
@@ -172,7 +169,7 @@ impl Database {
|
||||
|
||||
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||
ChannelRole::Guest => ChannelRole::Guest,
|
||||
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
|
||||
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||
};
|
||||
|
||||
@@ -188,7 +185,6 @@ impl Database {
|
||||
))),
|
||||
initial_project_id: ActiveValue::set(initial_project_id),
|
||||
role: ActiveValue::set(Some(called_user_role)),
|
||||
in_call: ActiveValue::set(true),
|
||||
|
||||
id: ActiveValue::NotSet,
|
||||
answering_connection_id: ActiveValue::NotSet,
|
||||
@@ -304,31 +300,21 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
environment: &str,
|
||||
) -> Result<RoomGuard<JoinRoom>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelIdAndEnvironment {
|
||||
enum QueryChannelId {
|
||||
ChannelId,
|
||||
Environment,
|
||||
}
|
||||
|
||||
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
|
||||
room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.column(room::Column::Environment)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelIdAndEnvironment>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if let Some(release_channel) = release_channel {
|
||||
if &release_channel != environment {
|
||||
Err(anyhow!("must join using the {} release", release_channel))?;
|
||||
}
|
||||
}
|
||||
let channel_id: Option<ChannelId> = room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelId>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if channel_id.is_some() {
|
||||
Err(anyhow!("tried to join channel call directly"))?
|
||||
@@ -416,7 +402,6 @@ impl Database {
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
autojoin: bool,
|
||||
connection: ConnectionId,
|
||||
role: ChannelRole,
|
||||
tx: &DatabaseTransaction,
|
||||
@@ -440,8 +425,6 @@ impl Database {
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
role: ActiveValue::set(Some(role)),
|
||||
in_call: ActiveValue::set(autojoin),
|
||||
|
||||
id: ActiveValue::NotSet,
|
||||
location_kind: ActiveValue::NotSet,
|
||||
location_project_id: ActiveValue::NotSet,
|
||||
@@ -1263,7 +1246,6 @@ impl Database {
|
||||
location: Some(proto::ParticipantLocation { variant: location }),
|
||||
participant_index: participant_index as u32,
|
||||
role: db_participant.role.unwrap_or(ChannelRole::Member).into(),
|
||||
in_call: db_participant.in_call,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,8 @@ pub mod channel_message;
|
||||
pub mod channel_message_mention;
|
||||
pub mod contact;
|
||||
pub mod contributor;
|
||||
pub mod extension;
|
||||
pub mod extension_version;
|
||||
pub mod feature_flag;
|
||||
pub mod follower;
|
||||
pub mod language_server;
|
||||
|
||||
27
crates/collab/src/db/tables/extension.rs
Normal file
27
crates/collab/src/db/tables/extension.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use crate::db::ExtensionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "extensions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: ExtensionId,
|
||||
pub external_id: String,
|
||||
pub name: String,
|
||||
pub latest_version: String,
|
||||
pub total_download_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_one = "super::extension_version::Entity")]
|
||||
LatestVersion,
|
||||
}
|
||||
|
||||
impl Related<super::extension_version::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::LatestVersion.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
36
crates/collab/src/db/tables/extension_version.rs
Normal file
36
crates/collab/src/db/tables/extension_version.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::db::ExtensionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "extension_versions")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub extension_id: ExtensionId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub version: String,
|
||||
pub published_at: PrimitiveDateTime,
|
||||
pub authors: String,
|
||||
pub repository: String,
|
||||
pub description: String,
|
||||
pub download_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::extension::Entity",
|
||||
from = "Column::ExtensionId",
|
||||
to = "super::extension::Column::Id"
|
||||
on_condition = r#"super::extension::Column::LatestVersion.into_expr().eq(Column::Version.into_expr())"#
|
||||
)]
|
||||
Extension,
|
||||
}
|
||||
|
||||
impl Related<super::extension::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Extension.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -8,7 +8,6 @@ pub struct Model {
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
pub environment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -20,7 +20,6 @@ pub struct Model {
|
||||
pub calling_connection_server_id: Option<ServerId>,
|
||||
pub participant_index: Option<i32>,
|
||||
pub role: Option<ChannelRole>,
|
||||
pub in_call: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
|
||||
@@ -2,6 +2,7 @@ mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
mod extension_tests;
|
||||
mod feature_flag_tests;
|
||||
mod message_tests;
|
||||
|
||||
@@ -15,8 +16,6 @@ use std::sync::{
|
||||
Arc,
|
||||
};
|
||||
|
||||
const TEST_RELEASE_CHANNEL: &'static str = "test";
|
||||
|
||||
pub struct TestDb {
|
||||
pub db: Option<Arc<Database>>,
|
||||
pub connection: Option<sqlx::AnyConnection>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
db::{
|
||||
tests::{channel_tree, new_test_connection, new_test_user, TEST_RELEASE_CHANNEL},
|
||||
tests::{channel_tree, new_test_connection, new_test_user},
|
||||
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
|
||||
},
|
||||
test_both_dbs,
|
||||
@@ -135,13 +135,7 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let (joined_room, _, _) = db
|
||||
.join_channel(
|
||||
channel_1,
|
||||
user_1,
|
||||
false,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
)
|
||||
.join_channel(channel_1, user_1, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
@@ -150,12 +144,7 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(
|
||||
room_id,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
)
|
||||
.join_room(room_id, user_2, ConnectionId { owner_id, id: 1 },)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
@@ -733,15 +722,9 @@ async fn test_guest_access(db: &Arc<Database>) {
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
db.join_channel(
|
||||
zed_channel,
|
||||
guest,
|
||||
false,
|
||||
guest_connection,
|
||||
TEST_RELEASE_CHANNEL,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_channel(zed_channel, guest, guest_connection)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(db
|
||||
.join_channel_chat(zed_channel, guest_connection, guest)
|
||||
|
||||
@@ -517,7 +517,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let room_id = RoomId::from_proto(
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "test")
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
@@ -531,14 +531,9 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(
|
||||
room_id,
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"test",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||
|
||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
||||
@@ -616,80 +611,3 @@ async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_non_matching_release_channels,
|
||||
test_non_matching_release_channels_postgres,
|
||||
test_non_matching_release_channels_sqlite
|
||||
);
|
||||
|
||||
async fn test_non_matching_release_channels(db: &Arc<Database>) {
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
let user1 = db
|
||||
.create_user(
|
||||
&format!("admin@example.com"),
|
||||
true,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
github_user_id: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user2 = db
|
||||
.create_user(
|
||||
&format!("user@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room = db
|
||||
.create_room(
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
"",
|
||||
"stable",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.call(
|
||||
RoomId::from_proto(room.id),
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
user2.user_id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User attempts to join from preview
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"preview",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// User switches to stable
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"stable",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok())
|
||||
}
|
||||
|
||||
225
crates/collab/src/db/tests/extension_tests.rs
Normal file
225
crates/collab/src/db/tests/extension_tests.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use super::Database;
|
||||
use crate::{
|
||||
db::{ExtensionMetadata, NewExtensionVersion},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use time::{OffsetDateTime, PrimitiveDateTime};
|
||||
|
||||
test_both_dbs!(
|
||||
test_extensions,
|
||||
test_extensions_postgres,
|
||||
test_extensions_sqlite
|
||||
);
|
||||
|
||||
async fn test_extensions(db: &Arc<Database>) {
|
||||
let versions = db.get_known_extension_versions().await.unwrap();
|
||||
assert!(versions.is_empty());
|
||||
|
||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
||||
assert!(extensions.is_empty());
|
||||
|
||||
let t0 = OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
|
||||
let t0 = PrimitiveDateTime::new(t0.date(), t0.time());
|
||||
|
||||
db.insert_extension_versions(
|
||||
&[
|
||||
(
|
||||
"ext1",
|
||||
vec![
|
||||
NewExtensionVersion {
|
||||
name: "Extension 1".into(),
|
||||
version: semver::Version::parse("0.0.1").unwrap(),
|
||||
description: "an extension".into(),
|
||||
authors: vec!["max".into()],
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
},
|
||||
NewExtensionVersion {
|
||||
name: "Extension One".into(),
|
||||
version: semver::Version::parse("0.0.2").unwrap(),
|
||||
description: "a good extension".into(),
|
||||
authors: vec!["max".into(), "marshall".into()],
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
},
|
||||
],
|
||||
),
|
||||
(
|
||||
"ext2",
|
||||
vec![NewExtensionVersion {
|
||||
name: "Extension Two".into(),
|
||||
version: semver::Version::parse("0.2.0").unwrap(),
|
||||
description: "a great extension".into(),
|
||||
authors: vec!["marshall".into()],
|
||||
repository: "ext2/repo".into(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let versions = db.get_known_extension_versions().await.unwrap();
|
||||
assert_eq!(
|
||||
versions,
|
||||
[
|
||||
("ext1".into(), vec!["0.0.1".into(), "0.0.2".into()]),
|
||||
("ext2".into(), vec!["0.2.0".into()])
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
|
||||
// The latest version of each extension is returned.
|
||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
ExtensionMetadata {
|
||||
id: "ext1".into(),
|
||||
name: "Extension One".into(),
|
||||
version: "0.0.2".into(),
|
||||
authors: vec!["max".into(), "marshall".into()],
|
||||
description: "a good extension".into(),
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 0,
|
||||
},
|
||||
ExtensionMetadata {
|
||||
id: "ext2".into(),
|
||||
name: "Extension Two".into(),
|
||||
version: "0.2.0".into(),
|
||||
authors: vec!["marshall".into()],
|
||||
description: "a great extension".into(),
|
||||
repository: "ext2/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 0
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Record extensions being downloaded.
|
||||
for _ in 0..7 {
|
||||
assert!(db.record_extension_download("ext2", "0.0.2").await.unwrap());
|
||||
}
|
||||
|
||||
for _ in 0..3 {
|
||||
assert!(db.record_extension_download("ext1", "0.0.1").await.unwrap());
|
||||
}
|
||||
|
||||
for _ in 0..2 {
|
||||
assert!(db.record_extension_download("ext1", "0.0.2").await.unwrap());
|
||||
}
|
||||
|
||||
// Record download returns false if the extension does not exist.
|
||||
assert!(!db
|
||||
.record_extension_download("no-such-extension", "0.0.2")
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
// Extensions are returned in descending order of total downloads.
|
||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
ExtensionMetadata {
|
||||
id: "ext2".into(),
|
||||
name: "Extension Two".into(),
|
||||
version: "0.2.0".into(),
|
||||
authors: vec!["marshall".into()],
|
||||
description: "a great extension".into(),
|
||||
repository: "ext2/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 7
|
||||
},
|
||||
ExtensionMetadata {
|
||||
id: "ext1".into(),
|
||||
name: "Extension One".into(),
|
||||
version: "0.0.2".into(),
|
||||
authors: vec!["max".into(), "marshall".into()],
|
||||
description: "a good extension".into(),
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 5,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Add more extensions, including a new version of `ext1`, and backfilling
|
||||
// an older version of `ext2`.
|
||||
db.insert_extension_versions(
|
||||
&[
|
||||
(
|
||||
"ext1",
|
||||
vec![NewExtensionVersion {
|
||||
name: "Extension One".into(),
|
||||
version: semver::Version::parse("0.0.3").unwrap(),
|
||||
description: "a real good extension".into(),
|
||||
authors: vec!["max".into(), "marshall".into()],
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
(
|
||||
"ext2",
|
||||
vec![NewExtensionVersion {
|
||||
name: "Extension Two".into(),
|
||||
version: semver::Version::parse("0.1.0").unwrap(),
|
||||
description: "an old extension".into(),
|
||||
authors: vec!["marshall".into()],
|
||||
repository: "ext2/repo".into(),
|
||||
published_at: t0,
|
||||
}],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let versions = db.get_known_extension_versions().await.unwrap();
|
||||
assert_eq!(
|
||||
versions,
|
||||
[
|
||||
(
|
||||
"ext1".into(),
|
||||
vec!["0.0.1".into(), "0.0.2".into(), "0.0.3".into()]
|
||||
),
|
||||
("ext2".into(), vec!["0.1.0".into(), "0.2.0".into()])
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
|
||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
||||
assert_eq!(
|
||||
extensions,
|
||||
&[
|
||||
ExtensionMetadata {
|
||||
id: "ext2".into(),
|
||||
name: "Extension Two".into(),
|
||||
version: "0.2.0".into(),
|
||||
authors: vec!["marshall".into()],
|
||||
description: "a great extension".into(),
|
||||
repository: "ext2/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 7
|
||||
},
|
||||
ExtensionMetadata {
|
||||
id: "ext1".into(),
|
||||
name: "Extension One".into(),
|
||||
version: "0.0.3".into(),
|
||||
authors: vec!["max".into(), "marshall".into()],
|
||||
description: "a real good extension".into(),
|
||||
repository: "ext1/repo".into(),
|
||||
published_at: t0,
|
||||
download_count: 5,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,8 @@ use std::fs;
|
||||
|
||||
pub fn load_dotenv() -> anyhow::Result<()> {
|
||||
let env: toml::map::Map<String, toml::Value> = toml::de::from_str(
|
||||
&fs::read_to_string("./.env.toml").map_err(|_| anyhow!("no .env.toml file found"))?,
|
||||
&fs::read_to_string("./crates/collab/.env.toml")
|
||||
.map_err(|_| anyhow!("no .env.toml file found"))?,
|
||||
)?;
|
||||
|
||||
for (key, value) in env {
|
||||
|
||||
@@ -8,11 +8,14 @@ pub mod rpc;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use aws_config::{BehaviorVersion, Region};
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use db::Database;
|
||||
use executor::Executor;
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
@@ -100,6 +103,11 @@ pub struct Config {
|
||||
pub live_kit_secret: Option<String>,
|
||||
pub rust_log: Option<String>,
|
||||
pub log_json: Option<bool>,
|
||||
pub blob_store_url: Option<String>,
|
||||
pub blob_store_region: Option<String>,
|
||||
pub blob_store_access_key: Option<String>,
|
||||
pub blob_store_secret_key: Option<String>,
|
||||
pub blob_store_bucket: Option<String>,
|
||||
pub zed_environment: Arc<str>,
|
||||
}
|
||||
|
||||
@@ -118,6 +126,7 @@ pub struct MigrateConfig {
|
||||
pub struct AppState {
|
||||
pub db: Arc<Database>,
|
||||
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
pub blob_store_client: Option<aws_sdk_s3::Client>,
|
||||
pub config: Config,
|
||||
}
|
||||
|
||||
@@ -146,8 +155,44 @@ impl AppState {
|
||||
let this = Self {
|
||||
db: Arc::new(db),
|
||||
live_kit_client,
|
||||
blob_store_client: build_blob_store_client(&config).await.log_err(),
|
||||
config,
|
||||
};
|
||||
Ok(Arc::new(this))
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::Client> {
|
||||
let keys = aws_sdk_s3::config::Credentials::new(
|
||||
config
|
||||
.blob_store_access_key
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing blob_store_access_key"))?,
|
||||
config
|
||||
.blob_store_secret_key
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing blob_store_secret_key"))?,
|
||||
None,
|
||||
None,
|
||||
"env",
|
||||
);
|
||||
|
||||
let s3_config = aws_config::defaults(BehaviorVersion::latest())
|
||||
.endpoint_url(
|
||||
config
|
||||
.blob_store_url
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("missing blob_store_url"))?,
|
||||
)
|
||||
.region(Region::new(
|
||||
config
|
||||
.blob_store_region
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("missing blob_store_region"))?,
|
||||
))
|
||||
.credentials_provider(keys)
|
||||
.load()
|
||||
.await;
|
||||
|
||||
Ok(aws_sdk_s3::Client::new(&s3_config))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Extension, Router};
|
||||
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
||||
use collab::{
|
||||
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
|
||||
Config, MigrateConfig, Result,
|
||||
};
|
||||
use db::Database;
|
||||
use std::{
|
||||
env::args,
|
||||
@@ -50,6 +53,8 @@ async fn main() -> Result<()> {
|
||||
let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
|
||||
rpc_server.start().await?;
|
||||
|
||||
fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
|
||||
|
||||
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(collab::rpc::routes(rpc_server.clone()))
|
||||
.merge(
|
||||
|
||||
@@ -28,7 +28,7 @@ use axum::{
|
||||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use connection_pool::ConnectionPool;
|
||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{self, BoxFuture},
|
||||
@@ -102,10 +102,8 @@ impl<R: RequestMessage> Response<R> {
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Session {
|
||||
zed_environment: Arc<str>,
|
||||
user_id: UserId,
|
||||
connection_id: ConnectionId,
|
||||
zed_version: SemanticVersion,
|
||||
db: Arc<tokio::sync::Mutex<DbHandle>>,
|
||||
peer: Arc<Peer>,
|
||||
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
|
||||
@@ -132,19 +130,6 @@ impl Session {
|
||||
_not_send: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_removed_in(&self, endpoint: &str, version: SemanticVersion) -> anyhow::Result<()> {
|
||||
if self.zed_version > version {
|
||||
Err(anyhow!(
|
||||
"{} was removed in {} (you're on {})",
|
||||
endpoint,
|
||||
version,
|
||||
self.zed_version
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Session {
|
||||
@@ -288,11 +273,8 @@ impl Server {
|
||||
.add_request_handler(get_channel_members)
|
||||
.add_request_handler(respond_to_channel_invite)
|
||||
.add_request_handler(join_channel)
|
||||
.add_request_handler(join_channel2)
|
||||
.add_request_handler(join_channel_chat)
|
||||
.add_message_handler(leave_channel_chat)
|
||||
.add_request_handler(join_channel_call)
|
||||
.add_request_handler(leave_channel_call)
|
||||
.add_request_handler(send_channel_message)
|
||||
.add_request_handler(remove_channel_message)
|
||||
.add_request_handler(get_channel_messages)
|
||||
@@ -576,7 +558,7 @@ impl Server {
|
||||
connection: Connection,
|
||||
address: String,
|
||||
user: User,
|
||||
zed_version: SemanticVersion,
|
||||
zed_version: ZedVersion,
|
||||
impersonator: Option<User>,
|
||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
@@ -618,7 +600,7 @@ impl Server {
|
||||
|
||||
{
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
pool.add_connection(connection_id, user_id, user.admin, zed_version);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
@@ -634,9 +616,7 @@ impl Server {
|
||||
let session = Session {
|
||||
user_id,
|
||||
connection_id,
|
||||
zed_version,
|
||||
db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))),
|
||||
zed_environment: this.app_state.config.zed_environment.clone(),
|
||||
peer: this.peer.clone(),
|
||||
connection_pool: this.connection_pool.clone(),
|
||||
live_kit_client: this.app_state.live_kit_client.clone(),
|
||||
@@ -900,11 +880,21 @@ pub async fn handle_websocket_request(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// zed 0.122.x was the first version that sent an app header, so once that hits stable
|
||||
// we can return UPGRADE_REQUIRED instead of unwrap_or_default();
|
||||
let app_version = app_version_header
|
||||
.map(|header| header.0 .0)
|
||||
.unwrap_or_default();
|
||||
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"no version header found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
if !version.is_supported() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
ws.on_upgrade(move |socket| {
|
||||
@@ -920,7 +910,7 @@ pub async fn handle_websocket_request(
|
||||
connection,
|
||||
socket_address,
|
||||
user,
|
||||
app_version,
|
||||
version,
|
||||
impersonator.0,
|
||||
None,
|
||||
Executor::Production,
|
||||
@@ -1035,12 +1025,7 @@ async fn create_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
&live_kit_room,
|
||||
&session.zed_environment,
|
||||
)
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
@@ -1063,19 +1048,14 @@ async fn join_room(
|
||||
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
return join_channel_internal(channel_id, true, Box::new(response), session).await;
|
||||
return join_channel_internal(channel_id, Box::new(response), session).await;
|
||||
}
|
||||
|
||||
let joined_room = {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
session.zed_environment.as_ref(),
|
||||
)
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
room_updated(&room.room, &session.peer);
|
||||
room.into_inner()
|
||||
@@ -1336,6 +1316,22 @@ async fn set_room_participant_role(
|
||||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let user_id = UserId::from_proto(request.user_id);
|
||||
let role = ChannelRole::from(request.role());
|
||||
|
||||
if role == ChannelRole::Talker {
|
||||
let pool = session.connection_pool().await;
|
||||
|
||||
for connection in pool.user_connections(user_id) {
|
||||
if !connection.zed_version.supports_talker_role() {
|
||||
Err(anyhow!(
|
||||
"This user is on zed {} which does not support unmute",
|
||||
connection.zed_version
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (live_kit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
@@ -1343,13 +1339,13 @@ async fn set_room_participant_role(
|
||||
.set_room_participant_role(
|
||||
session.user_id,
|
||||
RoomId::from_proto(request.room_id),
|
||||
UserId::from_proto(request.user_id),
|
||||
ChannelRole::from(request.role()),
|
||||
user_id,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
};
|
||||
@@ -2113,21 +2109,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
|
||||
};
|
||||
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
let peer_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
let follower_connection_id = follower_peer_id.into();
|
||||
if Some(follower_peer_id) != connection_id_to_omit
|
||||
&& connection_ids.contains(&follower_connection_id)
|
||||
{
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
follower_connection_id,
|
||||
request.clone(),
|
||||
)?;
|
||||
for connection_id in connection_ids.iter().cloned() {
|
||||
if Some(connection_id.into()) != peer_id_to_omit && connection_id != session.connection_id {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2726,67 +2717,14 @@ async fn respond_to_channel_invite(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Join the channels' call
|
||||
/// Join the channels' room
|
||||
async fn join_channel(
|
||||
request: proto::JoinChannel,
|
||||
response: Response<proto::JoinChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
session.endpoint_removed_in("join_channel", "0.123.0".parse().unwrap())?;
|
||||
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
join_channel_internal(channel_id, true, Box::new(response), session).await
|
||||
}
|
||||
|
||||
async fn join_channel2(
|
||||
request: proto::JoinChannel2,
|
||||
response: Response<proto::JoinChannel2>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
join_channel_internal(channel_id, false, Box::new(response), session).await
|
||||
}
|
||||
|
||||
async fn join_channel_call(
|
||||
request: proto::JoinChannelCall,
|
||||
response: Response<proto::JoinChannelCall>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let db = session.db().await;
|
||||
let (joined_room, role) = db
|
||||
.set_in_channel_call(channel_id, session.user_id, true)
|
||||
.await?;
|
||||
|
||||
let Some(connection_info) = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
live_kit_info_for_user(live_kit, &session.user_id, role, &joined_room.live_kit_room)
|
||||
}) else {
|
||||
Err(anyhow!("no live kit token info"))?
|
||||
};
|
||||
|
||||
room_updated(&joined_room, &session.peer);
|
||||
response.send(proto::JoinChannelCallResponse {
|
||||
live_kit_connection_info: Some(connection_info),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_channel_call(
|
||||
request: proto::LeaveChannelCall,
|
||||
response: Response<proto::LeaveChannelCall>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let db = session.db().await;
|
||||
let (joined_room, _) = db
|
||||
.set_in_channel_call(channel_id, session.user_id, false)
|
||||
.await?;
|
||||
|
||||
room_updated(&joined_room, &session.peer);
|
||||
response.send(proto::Ack {})?;
|
||||
|
||||
Ok(())
|
||||
join_channel_internal(channel_id, Box::new(response), session).await
|
||||
}
|
||||
|
||||
trait JoinChannelInternalResponse {
|
||||
@@ -2802,15 +2740,9 @@ impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
|
||||
Response::<proto::JoinRoom>::send(self, result)
|
||||
}
|
||||
}
|
||||
impl JoinChannelInternalResponse for Response<proto::JoinChannel2> {
|
||||
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||
Response::<proto::JoinChannel2>::send(self, result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn join_channel_internal(
|
||||
channel_id: ChannelId,
|
||||
autojoin: bool,
|
||||
response: Box<impl JoinChannelInternalResponse>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
@@ -2819,25 +2751,37 @@ async fn join_channel_internal(
|
||||
let db = session.db().await;
|
||||
|
||||
let (joined_room, membership_updated, role) = db
|
||||
.join_channel(
|
||||
channel_id,
|
||||
session.user_id,
|
||||
autojoin,
|
||||
session.connection_id,
|
||||
session.zed_environment.as_ref(),
|
||||
)
|
||||
.join_channel(channel_id, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
if !autojoin {
|
||||
return None;
|
||||
}
|
||||
live_kit_info_for_user(
|
||||
live_kit,
|
||||
&session.user_id,
|
||||
role,
|
||||
&joined_room.room.live_kit_room,
|
||||
)
|
||||
let (can_publish, token) = if role == ChannelRole::Guest {
|
||||
(
|
||||
false,
|
||||
live_kit
|
||||
.guest_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&session.user_id.to_string(),
|
||||
)
|
||||
.trace_err()?,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
true,
|
||||
live_kit
|
||||
.room_token(
|
||||
&joined_room.room.live_kit_room,
|
||||
&session.user_id.to_string(),
|
||||
)
|
||||
.trace_err()?,
|
||||
)
|
||||
};
|
||||
|
||||
Some(LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
can_publish,
|
||||
})
|
||||
});
|
||||
|
||||
response.send(proto::JoinRoomResponse {
|
||||
@@ -2873,35 +2817,6 @@ async fn join_channel_internal(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn live_kit_info_for_user(
|
||||
live_kit: &Arc<dyn live_kit_server::api::Client>,
|
||||
user_id: &UserId,
|
||||
role: ChannelRole,
|
||||
live_kit_room: &String,
|
||||
) -> Option<LiveKitConnectionInfo> {
|
||||
let (can_publish, token) = if role == ChannelRole::Guest {
|
||||
(
|
||||
false,
|
||||
live_kit
|
||||
.guest_token(live_kit_room, &user_id.to_string())
|
||||
.trace_err()?,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
true,
|
||||
live_kit
|
||||
.room_token(live_kit_room, &user_id.to_string())
|
||||
.trace_err()?,
|
||||
)
|
||||
};
|
||||
|
||||
Some(LiveKitConnectionInfo {
|
||||
server_url: live_kit.url().into(),
|
||||
token,
|
||||
can_publish,
|
||||
})
|
||||
}
|
||||
|
||||
/// Start editing the channel notes
|
||||
async fn join_channel_buffer(
|
||||
request: proto::JoinChannelBuffer,
|
||||
|
||||
@@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
|
||||
use rpc::ConnectionId;
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
use util::SemanticVersion;
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct ConnectionPool {
|
||||
@@ -16,10 +17,30 @@ struct ConnectedUser {
|
||||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ZedVersion(pub SemanticVersion);
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for ZedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
self.0 != SemanticVersion::new(0, 123, 0)
|
||||
}
|
||||
pub fn supports_talker_role(&self) -> bool {
|
||||
self.0 >= SemanticVersion::new(0, 125, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Connection {
|
||||
pub user_id: UserId,
|
||||
pub admin: bool,
|
||||
pub zed_version: ZedVersion,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
@@ -29,9 +50,21 @@ impl ConnectionPool {
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
||||
self.connections
|
||||
.insert(connection_id, Connection { user_id, admin });
|
||||
pub fn add_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
zed_version: ZedVersion,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
Connection {
|
||||
user_id,
|
||||
admin,
|
||||
zed_version,
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
connected_user.connection_ids.insert(connection_id);
|
||||
}
|
||||
@@ -57,6 +90,19 @@ impl ConnectionPool {
|
||||
self.connections.values()
|
||||
}
|
||||
|
||||
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
state
|
||||
.connection_ids
|
||||
.iter()
|
||||
.flat_map(|cid| self.connections.get(cid))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
db::ChannelId,
|
||||
tests::{test_server::join_channel_call, TestServer},
|
||||
};
|
||||
use crate::{db::ChannelId, tests::TestServer};
|
||||
use call::ActiveCall;
|
||||
use editor::Editor;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
@@ -35,7 +32,7 @@ async fn test_channel_guests(
|
||||
cx_a.executor().run_until_parked();
|
||||
|
||||
// Client B joins channel A as a guest
|
||||
cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -75,7 +72,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
.await;
|
||||
|
||||
let project_a = client_a.build_test_project(cx_a).await;
|
||||
cx_a.update(|cx| workspace::open_channel(channel_id, client_a.app_state.clone(), None, cx))
|
||||
cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -87,13 +84,11 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// Client B joins channel A as a guest
|
||||
cx_b.update(|cx| workspace::open_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||
cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
join_channel_call(cx_b).await.unwrap();
|
||||
|
||||
// client B opens 1.txt as a guest
|
||||
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||
let room_b = cx_b
|
||||
@@ -109,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
@@ -135,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
||||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||
cx_a.run_until_parked();
|
||||
@@ -228,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
@@ -245,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap_err();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
|
||||
// User B signs the zed CLA.
|
||||
server
|
||||
@@ -269,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{self, UserId},
|
||||
rpc::RECONNECT_TIMEOUT,
|
||||
tests::{room_participants, test_server::join_channel_call, RoomParticipants, TestServer},
|
||||
tests::{room_participants, RoomParticipants, TestServer},
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
@@ -382,7 +382,6 @@ async fn test_channel_room(
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
join_channel_call(cx_a).await.unwrap();
|
||||
|
||||
// Give everyone a chance to observe user A joining
|
||||
executor.run_until_parked();
|
||||
@@ -430,7 +429,7 @@ async fn test_channel_room(
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
join_channel_call(cx_b).await.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
@@ -553,9 +552,6 @@ async fn test_channel_room(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
join_channel_call(cx_a).await.unwrap();
|
||||
join_channel_call(cx_b).await.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
let room_a =
|
||||
|
||||
@@ -24,7 +24,7 @@ use workspace::{
|
||||
|
||||
use super::TestClient;
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_basic_following(
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
@@ -437,7 +437,6 @@ async fn test_basic_following(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
workspace
|
||||
@@ -523,7 +522,6 @@ async fn test_basic_following(
|
||||
workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
None
|
||||
);
|
||||
executor.run_until_parked();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2006,7 +2004,7 @@ async fn join_channel(
|
||||
client: &TestClient,
|
||||
cx: &mut TestAppContext,
|
||||
) -> anyhow::Result<()> {
|
||||
cx.update(|cx| workspace::open_channel(channel_id, client.app_state.clone(), None, cx))
|
||||
cx.update(|cx| workspace::join_channel(channel_id, client.app_state.clone(), None, cx))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -2079,3 +2077,66 @@ async fn test_following_to_channel_notes_other_workspace(
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_while_deactivated(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let (_, client_a, client_b, channel) = TestServer::start2(cx_a, cx_b).await;
|
||||
|
||||
let mut cx_a2 = cx_a.clone();
|
||||
let (workspace_a, cx_a) = client_a.build_test_workspace(cx_a).await;
|
||||
join_channel(channel, &client_a, cx_a).await.unwrap();
|
||||
share_workspace(&workspace_a, cx_a).await.unwrap();
|
||||
|
||||
// a opens 1.txt
|
||||
cx_a.simulate_keystrokes("cmd-p 1 enter");
|
||||
cx_a.run_until_parked();
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
});
|
||||
|
||||
// b joins channel and is following a
|
||||
join_channel(channel, &client_b, cx_b).await.unwrap();
|
||||
cx_b.run_until_parked();
|
||||
let (workspace_b, cx_b) = client_b.active_workspace(cx_b);
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
});
|
||||
|
||||
// stop following
|
||||
cx_b.simulate_keystrokes("down");
|
||||
|
||||
// a opens a different file while not followed
|
||||
cx_a.simulate_keystrokes("cmd-p 2 enter");
|
||||
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "1.txt");
|
||||
});
|
||||
|
||||
// a opens a file in a new window
|
||||
let (_, cx_a2) = client_a.build_test_workspace(&mut cx_a2).await;
|
||||
cx_a2.update(|cx| cx.activate_window());
|
||||
cx_a2.simulate_keystrokes("cmd-p 3 enter");
|
||||
cx_a2.run_until_parked();
|
||||
|
||||
// b starts following a again
|
||||
cx_b.simulate_keystrokes("cmd-ctrl-alt-f");
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// a returns to the shared project
|
||||
cx_a.update(|cx| cx.activate_window());
|
||||
cx_a.run_until_parked();
|
||||
|
||||
workspace_a.update(cx_a, |workspace, cx| {
|
||||
let editor = workspace.active_item(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
|
||||
});
|
||||
|
||||
// b should follow a back
|
||||
workspace_b.update(cx_b, |workspace, cx| {
|
||||
let editor = workspace.active_item_as::<Editor>(cx).unwrap();
|
||||
assert_eq!(editor.tab_description(0, cx).unwrap(), "2.js");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1881,7 +1881,7 @@ fn active_call_events(cx: &mut TestAppContext) -> Rc<RefCell<Vec<room::Event>>>
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mute(
|
||||
async fn test_mute_deafen(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
@@ -1920,7 +1920,7 @@ async fn test_mute(
|
||||
room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
|
||||
|
||||
// Users A and B are both unmuted.
|
||||
// Users A and B are both muted.
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_a, cx_a),
|
||||
&[ParticipantAudioState {
|
||||
@@ -1962,6 +1962,30 @@ async fn test_mute(
|
||||
}]
|
||||
);
|
||||
|
||||
// User A deafens
|
||||
room_a.update(cx_a, |room, cx| room.toggle_deafen(cx));
|
||||
executor.run_until_parked();
|
||||
|
||||
// User A does not hear user B.
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_muted()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_a, cx_a),
|
||||
&[ParticipantAudioState {
|
||||
user_id: client_b.user_id().unwrap(),
|
||||
is_muted: false,
|
||||
audio_tracks_playing: vec![false],
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_b, cx_b),
|
||||
&[ParticipantAudioState {
|
||||
user_id: client_a.user_id().unwrap(),
|
||||
is_muted: true,
|
||||
audio_tracks_playing: vec![true],
|
||||
}]
|
||||
);
|
||||
|
||||
// User B calls user C, C joins.
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| {
|
||||
@@ -1976,6 +2000,22 @@ async fn test_mute(
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
// User A does not hear users B or C.
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_a, cx_a),
|
||||
&[
|
||||
ParticipantAudioState {
|
||||
user_id: client_b.user_id().unwrap(),
|
||||
is_muted: false,
|
||||
audio_tracks_playing: vec![false],
|
||||
},
|
||||
ParticipantAudioState {
|
||||
user_id: client_c.user_id().unwrap(),
|
||||
is_muted: false,
|
||||
audio_tracks_playing: vec![false],
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
participant_audio_state(&room_b, cx_b),
|
||||
&[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{tests::TestDb, NewUserParams, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
AppState, Config,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
|
||||
use client::{
|
||||
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
};
|
||||
use clock::FakeSystemClock;
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
@@ -163,6 +164,7 @@ impl TestServer {
|
||||
client::init_settings(cx);
|
||||
});
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||
{
|
||||
@@ -185,7 +187,7 @@ impl TestServer {
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
|
||||
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||
let server = self.server.clone();
|
||||
let db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
@@ -231,7 +233,7 @@ impl TestServer {
|
||||
server_conn,
|
||||
client_name,
|
||||
user,
|
||||
SemanticVersion::default(),
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
@@ -274,7 +276,7 @@ impl TestServer {
|
||||
collab_ui::init(&app_state, cx);
|
||||
file_finder::init(cx);
|
||||
menu::init();
|
||||
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
|
||||
});
|
||||
|
||||
client
|
||||
@@ -480,6 +482,7 @@ impl TestServer {
|
||||
Arc::new(AppState {
|
||||
db: test_db.db().clone(),
|
||||
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
|
||||
blob_store_client: None,
|
||||
config: Config {
|
||||
http_port: 0,
|
||||
database_url: "".into(),
|
||||
@@ -492,6 +495,11 @@ impl TestServer {
|
||||
rust_log: None,
|
||||
log_json: None,
|
||||
zed_environment: "test".into(),
|
||||
blob_store_url: None,
|
||||
blob_store_region: None,
|
||||
blob_store_access_key: None,
|
||||
blob_store_secret_key: None,
|
||||
blob_store_bucket: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -687,7 +695,7 @@ impl TestClient {
|
||||
channel_id: u64,
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.update(|cx| workspace::open_channel(channel_id, self.app_state.clone(), None, cx))
|
||||
cx.update(|cx| workspace::join_channel(channel_id, self.app_state.clone(), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
@@ -762,11 +770,6 @@ impl TestClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_channel_call(cx: &mut TestAppContext) -> Task<anyhow::Result<()>> {
|
||||
let room = cx.read(|cx| ActiveCall::global(cx).read(cx).room().cloned());
|
||||
room.unwrap().update(cx, |room, cx| room.join_call(cx))
|
||||
}
|
||||
|
||||
pub fn open_channel_notes(
|
||||
channel_id: u64,
|
||||
cx: &mut VisualTestContext,
|
||||
|
||||
@@ -34,6 +34,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extensions_ui.workspace = true
|
||||
feature_flags.workspace = true
|
||||
feedback.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -66,6 +67,7 @@ util.workspace = true
|
||||
vcs_menu.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
sys-locale.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -63,6 +63,7 @@ pub struct ChatPanel {
|
||||
focus_handle: FocusHandle,
|
||||
open_context_menu: Option<(u64, Subscription)>,
|
||||
highlighted_message: Option<(u64, Task<()>)>,
|
||||
last_acknowledged_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -126,6 +127,7 @@ impl ChatPanel {
|
||||
focus_handle: cx.focus_handle(),
|
||||
open_context_menu: None,
|
||||
highlighted_message: None,
|
||||
last_acknowledged_message_id: None,
|
||||
};
|
||||
|
||||
if let Some(channel_id) = ActiveCall::global(cx)
|
||||
@@ -281,6 +283,13 @@ impl ChatPanel {
|
||||
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.active && self.is_scrolled_to_bottom {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
if let Some(channel_id) = self.channel_id(cx) {
|
||||
self.last_acknowledged_message_id = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.last_acknowledge_message_id(channel_id);
|
||||
}
|
||||
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.acknowledge_last_message(cx);
|
||||
});
|
||||
@@ -362,9 +371,9 @@ impl ChatPanel {
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.mb_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div()
|
||||
.overflow_hidden()
|
||||
.max_h_12()
|
||||
.child(reply_to_message_body.element(body_element_id, cx)),
|
||||
),
|
||||
@@ -454,119 +463,145 @@ impl ChatPanel {
|
||||
cx.theme().colors().panel_background
|
||||
};
|
||||
|
||||
v_flex().w_full().relative().child(
|
||||
div()
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.when(!is_continuation_from_previous, |this| {
|
||||
this.mt_2().child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.pl(cx.rem_size() + px(6.0))
|
||||
.pr(px(8.0))
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.child(Label::new(message.sender.github_login.clone())),
|
||||
)
|
||||
.child(
|
||||
Label::new(format_timestamp(
|
||||
OffsetDateTime::now_utc(),
|
||||
message.timestamp,
|
||||
self.local_timezone,
|
||||
v_flex()
|
||||
.w_full()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.bg(background)
|
||||
.rounded_md()
|
||||
.overflow_hidden()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.when(!is_continuation_from_previous, |this| {
|
||||
this.mt_2().child(
|
||||
h_flex()
|
||||
.text_ui_sm()
|
||||
.child(div().absolute().child(
|
||||
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
.child(
|
||||
div()
|
||||
.pl(cx.rem_size() + px(6.0))
|
||||
.pr(px(8.0))
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.child(Label::new(message.sender.github_login.clone())),
|
||||
)
|
||||
.child(
|
||||
Label::new(format_timestamp(
|
||||
OffsetDateTime::now_utc(),
|
||||
message.timestamp,
|
||||
self.local_timezone,
|
||||
None,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|
||||
|this| {
|
||||
const MESSAGE_DELETED: &str = "Message has been deleted";
|
||||
|
||||
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
|
||||
&cx.text_style(),
|
||||
vec![(
|
||||
0..MESSAGE_DELETED.len(),
|
||||
HighlightStyle {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.border_l_2()
|
||||
.text_ui_xs()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.child(body_text),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(
|
||||
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|
||||
|this| {
|
||||
const MESSAGE_DELETED: &str = "Message has been deleted";
|
||||
|
||||
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
|
||||
&cx.text_style(),
|
||||
vec![(
|
||||
0..MESSAGE_DELETED.len(),
|
||||
HighlightStyle {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
);
|
||||
|
||||
this.child(
|
||||
div()
|
||||
.border_l_2()
|
||||
.text_ui_xs()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.child(body_text),
|
||||
.when_some(reply_to_message, |el, reply_to_message| {
|
||||
el.child(self.render_replied_to_message(
|
||||
Some(message.id),
|
||||
&reply_to_message,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
|
||||
.map(|el| {
|
||||
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
|
||||
Self::render_markdown_with_mentions(
|
||||
&self.languages,
|
||||
self.client.id(),
|
||||
&message,
|
||||
)
|
||||
});
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.id(element_id)
|
||||
.group("")
|
||||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.right_0()
|
||||
.w_6()
|
||||
.bg(background)
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when_some(reply_to_message, |el, reply_to_message| {
|
||||
el.child(self.render_replied_to_message(
|
||||
Some(message.id),
|
||||
&reply_to_message,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
|
||||
.map(|el| {
|
||||
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
|
||||
Self::render_markdown_with_mentions(
|
||||
&self.languages,
|
||||
self.client.id(),
|
||||
&message,
|
||||
)
|
||||
});
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.id(element_id)
|
||||
.group("")
|
||||
.child(text.element("body".into(), cx))
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
self.last_acknowledged_message_id
|
||||
.is_some_and(|l| Some(l) == message_id),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.py_2()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(div().w_full().h_0p5().bg(cx.theme().colors().border))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.z_index(1)
|
||||
.right_0()
|
||||
.w_6()
|
||||
.bg(background)
|
||||
.when(!self.has_open_menu(message_id), |el| {
|
||||
el.visible_on_hover("")
|
||||
})
|
||||
.when_some(message_id, |el, message_id| {
|
||||
el.child(
|
||||
popover_menu(("menu", message_id))
|
||||
.trigger(IconButton::new(
|
||||
("trigger", message_id),
|
||||
IconName::Ellipsis,
|
||||
))
|
||||
.menu(move |cx| {
|
||||
Some(Self::render_message_menu(
|
||||
&this,
|
||||
message_id,
|
||||
can_delete_message,
|
||||
cx,
|
||||
))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
.px_1()
|
||||
.rounded_md()
|
||||
.text_ui_xs()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child("New messages"),
|
||||
)
|
||||
.child(div().w_full().h_0p5().bg(cx.theme().colors().border)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
|
||||
@@ -681,8 +716,11 @@ impl ChatPanel {
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let highlight_message_id = scroll_to_message_id;
|
||||
let scroll_to_message_id = this.update(&mut cx, |this, cx| {
|
||||
this.set_active_chat(chat.clone(), cx);
|
||||
|
||||
scroll_to_message_id.or_else(|| this.last_acknowledged_message_id)
|
||||
})?;
|
||||
|
||||
if let Some(message_id) = scroll_to_message_id {
|
||||
@@ -690,21 +728,22 @@ impl ChatPanel {
|
||||
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
|
||||
.await
|
||||
{
|
||||
let task = cx.spawn({
|
||||
let this = this.clone();
|
||||
|
||||
|mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message = Some((message_id, task));
|
||||
if let Some(highlight_message_id) = highlight_message_id {
|
||||
let task = cx.spawn({
|
||||
|this, mut cx| async move {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.highlighted_message.take();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
|
||||
this.highlighted_message = Some((highlight_message_id, task));
|
||||
}
|
||||
|
||||
if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
|
||||
this.message_list.scroll_to(ListOffset {
|
||||
item_ix,
|
||||
@@ -733,7 +772,7 @@ impl Render for ChatPanel {
|
||||
v_flex()
|
||||
.key_context("ChatPanel")
|
||||
.track_focus(&self.focus_handle)
|
||||
.full()
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::send))
|
||||
.child(
|
||||
h_flex().z_index(1).child(
|
||||
@@ -755,11 +794,11 @@ impl Render for ChatPanel {
|
||||
)
|
||||
.child(div().flex_grow().px_2().map(|this| {
|
||||
if self.active_chat.is_some() {
|
||||
this.child(list(self.message_list.clone()).full())
|
||||
this.child(list(self.message_list.clone()).size_full())
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.full()
|
||||
.size_full()
|
||||
.p_4()
|
||||
.child(
|
||||
Label::new("Select a channel to chat in.")
|
||||
@@ -801,18 +840,21 @@ impl Render for ChatPanel {
|
||||
|
||||
el.when_some(reply_message, |el, reply_message| {
|
||||
el.child(
|
||||
div()
|
||||
h_flex()
|
||||
.when(!self.is_scrolled_to_bottom, |el| {
|
||||
el.border_t_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.flex()
|
||||
.w_full()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.overflow_hidden()
|
||||
.items_start()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(self.render_replied_to_message(None, &reply_message, cx))
|
||||
.child(
|
||||
div().flex_shrink().overflow_hidden().child(
|
||||
self.render_replied_to_message(None, &reply_message, cx),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("close-reply-preview", IconName::Close)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
@@ -917,26 +959,58 @@ impl Panel for ChatPanel {
|
||||
|
||||
impl EventEmitter<PanelEvent> for ChatPanel {}
|
||||
|
||||
fn is_12_hour_clock(locale: String) -> bool {
|
||||
[
|
||||
"es-MX", "es-CO", "es-SV", "es-NI",
|
||||
"es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras
|
||||
"en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand
|
||||
"ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan
|
||||
"en-IN", "hi-IN", // India, Hindu
|
||||
"en-PK", "ur-PK", // Pakistan, Urdu
|
||||
"en-PH", "fil-PH", // Philippines, Filipino
|
||||
"bn-BD", "ccp-BD", // Bangladesh, Chakma
|
||||
"en-IE", "ga-IE", // Ireland, Irish
|
||||
"en-MY", "ms-MY", // Malaysia, Malay
|
||||
]
|
||||
.contains(&locale.as_str())
|
||||
}
|
||||
|
||||
fn format_timestamp(
|
||||
reference: OffsetDateTime,
|
||||
timestamp: OffsetDateTime,
|
||||
timezone: UtcOffset,
|
||||
locale: Option<String>,
|
||||
) -> String {
|
||||
let locale = match locale {
|
||||
Some(locale) => locale,
|
||||
None => sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")),
|
||||
};
|
||||
let timestamp_local = timestamp.to_offset(timezone);
|
||||
let timestamp_local_hour = timestamp_local.hour();
|
||||
|
||||
let hour_12 = match timestamp_local_hour {
|
||||
0 => 12, // Midnight
|
||||
13..=23 => timestamp_local_hour - 12, // PM hours
|
||||
_ => timestamp_local_hour, // AM hours
|
||||
};
|
||||
let meridiem = if timestamp_local_hour >= 12 {
|
||||
"pm"
|
||||
} else {
|
||||
"am"
|
||||
};
|
||||
let timestamp_local_minute = timestamp_local.minute();
|
||||
let formatted_time = format!("{:02}:{:02} {}", hour_12, timestamp_local_minute, meridiem);
|
||||
|
||||
let (hour, meridiem) = if is_12_hour_clock(locale) {
|
||||
let meridiem = if timestamp_local_hour >= 12 {
|
||||
"pm"
|
||||
} else {
|
||||
"am"
|
||||
};
|
||||
|
||||
let hour_12 = match timestamp_local_hour {
|
||||
0 => 12, // Midnight
|
||||
13..=23 => timestamp_local_hour - 12, // PM hours
|
||||
_ => timestamp_local_hour, // AM hours
|
||||
};
|
||||
|
||||
(hour_12, Some(meridiem))
|
||||
} else {
|
||||
(timestamp_local_hour, None)
|
||||
};
|
||||
|
||||
let formatted_time = match meridiem {
|
||||
Some(meridiem) => format!("{:02}:{:02} {}", hour, timestamp_local_minute, meridiem),
|
||||
None => format!("{:02}:{:02}", hour, timestamp_local_minute),
|
||||
};
|
||||
|
||||
let reference_local = reference.to_offset(timezone);
|
||||
let reference_local_date = reference_local.date();
|
||||
@@ -950,12 +1024,20 @@ fn format_timestamp(
|
||||
return format!("yesterday at {}", formatted_time);
|
||||
}
|
||||
|
||||
format!(
|
||||
"{:02}/{:02}/{}",
|
||||
timestamp_local_date.month() as u32,
|
||||
timestamp_local_date.day(),
|
||||
timestamp_local_date.year()
|
||||
)
|
||||
match meridiem {
|
||||
Some(_) => format!(
|
||||
"{:02}/{:02}/{}",
|
||||
timestamp_local_date.month() as u32,
|
||||
timestamp_local_date.day(),
|
||||
timestamp_local_date.year()
|
||||
),
|
||||
None => format!(
|
||||
"{:02}/{:02}/{}",
|
||||
timestamp_local_date.day(),
|
||||
timestamp_local_date.month() as u32,
|
||||
timestamp_local_date.year()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1015,13 +1097,135 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_markdown_with_auto_detect_links() {
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let message = channel::ChannelMessage {
|
||||
id: ChannelMessageId::Saved(0),
|
||||
body: "Here is a link https://zed.dev to zeds website".to_string(),
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
sender: Arc::new(client::User {
|
||||
github_login: "fgh".into(),
|
||||
avatar_uri: "avatar_fgh".into(),
|
||||
id: 103,
|
||||
}),
|
||||
nonce: 5,
|
||||
mentions: Vec::new(),
|
||||
reply_to_message_id: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) =
|
||||
marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false);
|
||||
assert_eq!(message.text, body);
|
||||
assert_eq!(1, ranges.len());
|
||||
assert_eq!(
|
||||
message.highlights,
|
||||
vec![(
|
||||
ranges[0].clone(),
|
||||
HighlightStyle {
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_markdown_with_auto_detect_links_and_additional_formatting() {
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let message = channel::ChannelMessage {
|
||||
id: ChannelMessageId::Saved(0),
|
||||
body: "**Here is a link https://zed.dev to zeds website**".to_string(),
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
sender: Arc::new(client::User {
|
||||
github_login: "fgh".into(),
|
||||
avatar_uri: "avatar_fgh".into(),
|
||||
id: 103,
|
||||
}),
|
||||
nonce: 5,
|
||||
mentions: Vec::new(),
|
||||
reply_to_message_id: None,
|
||||
};
|
||||
|
||||
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
|
||||
|
||||
// Note that the "'" was replaced with ’ due to smart punctuation.
|
||||
let (body, ranges) = marked_text_ranges(
|
||||
"«Here is a link »«https://zed.dev»« to zeds website»",
|
||||
false,
|
||||
);
|
||||
assert_eq!(message.text, body);
|
||||
assert_eq!(3, ranges.len());
|
||||
assert_eq!(
|
||||
message.highlights,
|
||||
vec![
|
||||
(
|
||||
ranges[0].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
(
|
||||
ranges[1].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
(
|
||||
ranges[2].clone(),
|
||||
HighlightStyle {
|
||||
font_weight: Some(gpui::FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_locale() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
|
||||
let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-GB"))
|
||||
),
|
||||
"15:30"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_today() {
|
||||
let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);
|
||||
let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"03:30 pm"
|
||||
);
|
||||
}
|
||||
@@ -1032,7 +1236,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"yesterday at 09:00 am"
|
||||
);
|
||||
}
|
||||
@@ -1043,7 +1252,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"yesterday at 08:00 pm"
|
||||
);
|
||||
}
|
||||
@@ -1054,7 +1268,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"yesterday at 06:00 pm"
|
||||
);
|
||||
}
|
||||
@@ -1065,7 +1284,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"yesterday at 11:55 pm"
|
||||
);
|
||||
}
|
||||
@@ -1076,7 +1300,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"yesterday at 08:00 pm"
|
||||
);
|
||||
}
|
||||
@@ -1087,7 +1316,12 @@ mod tests {
|
||||
let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0);
|
||||
|
||||
assert_eq!(
|
||||
format_timestamp(reference, timestamp, test_timezone()),
|
||||
format_timestamp(
|
||||
reference,
|
||||
timestamp,
|
||||
test_timezone(),
|
||||
Some(String::from("en-US"))
|
||||
),
|
||||
"04/10/1990"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -385,6 +385,7 @@ impl Render for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::TestAppContext;
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
@@ -455,8 +456,9 @@ mod tests {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
|
||||
let clock = Arc::new(FakeSystemClock::default());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
|
||||
@@ -34,13 +34,13 @@ use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,
|
||||
Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
|
||||
};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
|
||||
Workspace,
|
||||
OpenChannelNotes, Workspace,
|
||||
};
|
||||
|
||||
actions!(
|
||||
@@ -69,6 +69,19 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<CollabPanel>(cx);
|
||||
});
|
||||
workspace.register_action(|_, _: &OpenChannelNotes, cx| {
|
||||
let channel_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.and_then(|room| room.read(cx).channel_id());
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
let workspace = cx.view().clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -162,9 +175,6 @@ enum ListEntry {
|
||||
depth: usize,
|
||||
has_children: bool,
|
||||
},
|
||||
ChannelCall {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
ChannelNotes {
|
||||
channel_id: ChannelId,
|
||||
},
|
||||
@@ -372,7 +382,6 @@ impl CollabPanel {
|
||||
|
||||
if query.is_empty() {
|
||||
if let Some(channel_id) = room.channel_id() {
|
||||
self.entries.push(ListEntry::ChannelCall { channel_id });
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id });
|
||||
}
|
||||
@@ -470,7 +479,7 @@ impl CollabPanel {
|
||||
&& participant.video_tracks.is_empty(),
|
||||
});
|
||||
}
|
||||
if room.in_call() && !participant.video_tracks.is_empty() {
|
||||
if !participant.video_tracks.is_empty() {
|
||||
self.entries.push(ListEntry::ParticipantScreen {
|
||||
peer_id: Some(participant.peer_id),
|
||||
is_last: true,
|
||||
@@ -504,20 +513,6 @@ impl CollabPanel {
|
||||
role: proto::ChannelRole::Member,
|
||||
}));
|
||||
}
|
||||
} else if let Some(channel_id) = ActiveCall::global(cx).read(cx).pending_channel_id() {
|
||||
self.entries.push(ListEntry::Header(Section::ActiveCall));
|
||||
if !old_entries
|
||||
.iter()
|
||||
.any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
|
||||
{
|
||||
scroll_to_top = true;
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
self.entries.push(ListEntry::ChannelCall { channel_id });
|
||||
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||
self.entries.push(ListEntry::ChannelChat { channel_id });
|
||||
}
|
||||
}
|
||||
|
||||
let mut request_entries = Vec::new();
|
||||
@@ -837,6 +832,8 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ListItem {
|
||||
let user_id = user.id;
|
||||
let is_current_user =
|
||||
self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id);
|
||||
let tooltip = format!("Follow {}", user.github_login);
|
||||
|
||||
let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| {
|
||||
@@ -849,8 +846,18 @@ impl CollabPanel {
|
||||
.selected(is_selected)
|
||||
.end_slot(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, cx| Self::leave_call(cx))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else if role == proto::ChannelRole::Guest {
|
||||
Label::new("Guest").color(Color::Muted).into_any_element()
|
||||
} else if role == proto::ChannelRole::Talker {
|
||||
Label::new("Mic only")
|
||||
.color(Color::Muted)
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
@@ -950,79 +957,6 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_channel_call(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
is_selected: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let (is_in_call, call_participants) = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| (room.read(cx).in_call(), room.read(cx).call_participants(cx)))
|
||||
.unwrap_or_default();
|
||||
|
||||
const FACEPILE_LIMIT: usize = 3;
|
||||
|
||||
let face_pile = if !call_participants.is_empty() {
|
||||
let extra_count = call_participants.len().saturating_sub(FACEPILE_LIMIT);
|
||||
let result = FacePile::new(
|
||||
call_participants
|
||||
.iter()
|
||||
.map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
|
||||
.take(FACEPILE_LIMIT)
|
||||
.chain(if extra_count > 0 {
|
||||
Some(
|
||||
div()
|
||||
.ml_2()
|
||||
.child(Label::new(format!("+{extra_count}")))
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect::<SmallVec<_>>(),
|
||||
);
|
||||
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ListItem::new("channel-call")
|
||||
.selected(is_selected)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, IconName::AudioOn)),
|
||||
)
|
||||
.when(is_in_call, |el| {
|
||||
el.end_slot(
|
||||
IconButton::new(1, IconName::Exit)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
||||
.on_click(cx.listener(|this, _, cx| this.leave_channel_call(cx))),
|
||||
)
|
||||
})
|
||||
.when(!is_in_call, |el| {
|
||||
el.tooltip(move |cx| Tooltip::text("Join audio call", cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_call(channel_id, cx);
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.text_ui()
|
||||
.when(!call_participants.is_empty(), |el| {
|
||||
el.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child("call"),
|
||||
)
|
||||
.children(face_pile)
|
||||
}
|
||||
|
||||
fn render_channel_notes(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
@@ -1030,8 +964,7 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
|
||||
|
||||
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
|
||||
ListItem::new("channel-notes")
|
||||
.selected(is_selected)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@@ -1039,18 +972,21 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, true, cx))
|
||||
.child(IconButton::new(0, IconName::File)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui()
|
||||
.when(has_notes_notification, |el| {
|
||||
el.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child("notes"),
|
||||
.child(IconButton::new(0, IconName::File))
|
||||
.children(has_channel_buffer_changed.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(2.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("notes"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
}
|
||||
|
||||
@@ -1069,18 +1005,21 @@ impl CollabPanel {
|
||||
}))
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, false, cx))
|
||||
.child(IconButton::new(0, IconName::MessageBubbles)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui()
|
||||
.when(has_messages_notification, |el| {
|
||||
el.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child("chat"),
|
||||
.child(IconButton::new(0, IconName::MessageBubbles))
|
||||
.children(has_messages_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(2.))
|
||||
.top(px(4.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
}
|
||||
|
||||
@@ -1102,13 +1041,38 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let this = cx.view().clone();
|
||||
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
|
||||
if !(role == proto::ChannelRole::Guest
|
||||
|| role == proto::ChannelRole::Talker
|
||||
|| role == proto::ChannelRole::Member)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
|
||||
let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
context_menu.entry(
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Mic Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
let Some(room) = call.room() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
user_id,
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
|
||||
}),
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
|
||||
context_menu = context_menu.entry(
|
||||
"Grant Write Access",
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
@@ -1132,10 +1096,16 @@ impl CollabPanel {
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else if role == proto::ChannelRole::Member {
|
||||
context_menu.entry(
|
||||
"Revoke Write Access",
|
||||
);
|
||||
}
|
||||
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
|
||||
let label = if role == proto::ChannelRole::Talker {
|
||||
"Mute"
|
||||
} else {
|
||||
"Revoke Access"
|
||||
};
|
||||
context_menu = context_menu.entry(
|
||||
label,
|
||||
None,
|
||||
cx.handler_for(&this, move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
@@ -1151,12 +1121,12 @@ impl CollabPanel {
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
|
||||
.detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
unreachable!()
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
});
|
||||
|
||||
cx.focus_view(&context_menu);
|
||||
@@ -1338,14 +1308,12 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let this = cx.view().clone();
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
let in_room = room.is_some();
|
||||
let in_call = room.is_some_and(|room| room.read(cx).in_call());
|
||||
let in_room = ActiveCall::global(cx).read(cx).room().is_some();
|
||||
|
||||
let context_menu = ContextMenu::build(cx, |mut context_menu, _| {
|
||||
let user_id = contact.user.id;
|
||||
|
||||
if contact.online && !contact.busy && (!in_room || in_call) {
|
||||
if contact.online && !contact.busy {
|
||||
let label = if in_room {
|
||||
format!("Invite {} to join", contact.user.github_login)
|
||||
} else {
|
||||
@@ -1493,7 +1461,7 @@ impl CollabPanel {
|
||||
if is_active {
|
||||
self.open_channel_notes(channel.id, cx)
|
||||
} else {
|
||||
self.open_channel(channel.id, cx)
|
||||
self.join_channel(channel.id, cx)
|
||||
}
|
||||
}
|
||||
ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
|
||||
@@ -1512,9 +1480,6 @@ impl CollabPanel {
|
||||
ListEntry::ChannelInvite(channel) => {
|
||||
self.respond_to_channel_invite(channel.id, true, cx)
|
||||
}
|
||||
ListEntry::ChannelCall { channel_id } => {
|
||||
self.join_channel_call(*channel_id, cx)
|
||||
}
|
||||
ListEntry::ChannelNotes { channel_id } => {
|
||||
self.open_channel_notes(*channel_id, cx)
|
||||
}
|
||||
@@ -1977,14 +1942,14 @@ impl CollabPanel {
|
||||
.detach_and_prompt_err("Call failed", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn open_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(handle) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
workspace::open_channel(
|
||||
workspace::join_channel(
|
||||
channel_id,
|
||||
workspace.read(cx).app_state().clone(),
|
||||
Some(handle),
|
||||
@@ -1993,23 +1958,6 @@ impl CollabPanel {
|
||||
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
||||
}
|
||||
|
||||
fn join_channel_call(&mut self, _channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
room.update(cx, |room, cx| room.join_call(cx))
|
||||
.detach_and_prompt_err("Failed to join call", cx, |_, _| None)
|
||||
}
|
||||
|
||||
fn leave_channel_call(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
room.update(cx, |room, cx| room.leave_call(cx));
|
||||
}
|
||||
|
||||
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
@@ -2135,9 +2083,6 @@ impl CollabPanel {
|
||||
ListEntry::ParticipantScreen { peer_id, is_last } => self
|
||||
.render_participant_screen(*peer_id, *is_last, is_selected, cx)
|
||||
.into_any_element(),
|
||||
ListEntry::ChannelCall { channel_id } => self
|
||||
.render_channel_call(*channel_id, is_selected, cx)
|
||||
.into_any_element(),
|
||||
ListEntry::ChannelNotes { channel_id } => self
|
||||
.render_channel_notes(*channel_id, is_selected, cx)
|
||||
.into_any_element(),
|
||||
@@ -2150,7 +2095,7 @@ impl CollabPanel {
|
||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).full())
|
||||
.child(list(self.list_state.clone()).size_full())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(div().mx_2().border_primary(cx).border_t())
|
||||
@@ -2203,25 +2148,24 @@ impl CollabPanel {
|
||||
is_collapsed: bool,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let mut channel_link = None;
|
||||
let mut channel_tooltip_text = None;
|
||||
let mut channel_icon = None;
|
||||
|
||||
let text = match section {
|
||||
Section::ActiveCall => {
|
||||
let channel_name = maybe!({
|
||||
let channel_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.channel_id(cx)
|
||||
.or_else(|| ActiveCall::global(cx).read(cx).pending_channel_id())?;
|
||||
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
||||
|
||||
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
|
||||
|
||||
channel_link = Some(channel.link());
|
||||
(channel_icon, channel_tooltip_text) = match channel.visibility {
|
||||
proto::ChannelVisibility::Public => {
|
||||
(Some(IconName::Public), Some("Close Channel"))
|
||||
(Some("icons/public.svg"), Some("Copy public channel link."))
|
||||
}
|
||||
proto::ChannelVisibility::Members => {
|
||||
(Some(IconName::Hash), Some("Close Channel"))
|
||||
(Some("icons/hash.svg"), Some("Copy private channel link."))
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2243,10 +2187,17 @@ impl CollabPanel {
|
||||
};
|
||||
|
||||
let button = match section {
|
||||
Section::ActiveCall => channel_icon.map(|_| {
|
||||
IconButton::new("channel-link", IconName::Close)
|
||||
.on_click(move |_, cx| Self::leave_call(cx))
|
||||
.tooltip(|cx| Tooltip::text("Close channel", cx))
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
IconButton::new("channel-link", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
.visible_on_hover("section-header")
|
||||
.on_click(move |_, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
@@ -2281,9 +2232,6 @@ impl CollabPanel {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
}))
|
||||
})
|
||||
.when_some(channel_icon, |el, channel_icon| {
|
||||
el.start_slot(Icon::new(channel_icon).color(Color::Muted))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
@@ -2589,9 +2537,11 @@ impl CollabPanel {
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel(channel_id, cx);
|
||||
this.open_channel_notes(channel_id, cx);
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
if is_active {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
} else {
|
||||
this.join_channel(channel_id, cx)
|
||||
}
|
||||
}))
|
||||
.on_secondary_mouse_down(cx.listener(
|
||||
move |this, event: &MouseDownEvent, cx| {
|
||||
@@ -2599,39 +2549,86 @@ impl CollabPanel {
|
||||
},
|
||||
))
|
||||
.start_slot(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
div()
|
||||
.relative()
|
||||
.child(
|
||||
Icon::new(if is_public {
|
||||
IconName::Public
|
||||
} else {
|
||||
IconName::Hash
|
||||
})
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(has_notes_notification.then(|| {
|
||||
div()
|
||||
.w_1p5()
|
||||
.z_index(1)
|
||||
.absolute()
|
||||
.right(px(-1.))
|
||||
.top(px(-1.))
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex().id(channel_id as usize).child(
|
||||
div()
|
||||
.text_ui()
|
||||
.when(has_messages_notification || has_notes_notification, |el| {
|
||||
el.font_weight(FontWeight::SEMIBOLD)
|
||||
})
|
||||
.child(channel.name.clone()),
|
||||
),
|
||||
h_flex()
|
||||
.id(channel_id as usize)
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.p_1())),
|
||||
),
|
||||
)
|
||||
.children(face_pile.map(|face_pile| {
|
||||
.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.right(rems(0.))
|
||||
.z_index(1)
|
||||
.h_full()
|
||||
.child(face_pile.p_1())
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.child(
|
||||
IconButton::new("channel_chat", IconName::MessageBubbles)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
|
||||
.visible_on_hover(""),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", IconName::File)
|
||||
.style(ButtonStyle::Filled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
|
||||
.visible_on_hover(""),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip({
|
||||
let channel_store = self.channel_store.clone();
|
||||
move |cx| {
|
||||
cx.new_view(|_| JoinChannelTooltip {
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
has_notes_notification,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -2675,24 +2672,32 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
|
||||
let right = bounds.right();
|
||||
let top = bounds.top();
|
||||
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
cx.paint_quad(
|
||||
fill(
|
||||
Bounds::from_corners(
|
||||
point(start_x, top),
|
||||
point(
|
||||
start_x + thickness,
|
||||
if is_last {
|
||||
start_y
|
||||
} else {
|
||||
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
|
||||
},
|
||||
),
|
||||
),
|
||||
color,
|
||||
),
|
||||
color,
|
||||
));
|
||||
cx.paint_quad(fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
));
|
||||
None,
|
||||
None,
|
||||
);
|
||||
cx.paint_quad(
|
||||
fill(
|
||||
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
|
||||
color,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.w(width)
|
||||
.h(line_height)
|
||||
@@ -2829,14 +2834,6 @@ impl PartialEq for ListEntry {
|
||||
return channel_1.id == channel_2.id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelCall { channel_id } => {
|
||||
if let ListEntry::ChannelCall {
|
||||
channel_id: other_id,
|
||||
} = other
|
||||
{
|
||||
return channel_id == other_id;
|
||||
}
|
||||
}
|
||||
ListEntry::ChannelNotes { channel_id } => {
|
||||
if let ListEntry::ChannelNotes {
|
||||
channel_id: other_id,
|
||||
@@ -2925,17 +2922,25 @@ impl Render for DraggedChannelView {
|
||||
struct JoinChannelTooltip {
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_notes_notification: bool,
|
||||
}
|
||||
|
||||
impl Render for JoinChannelTooltip {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
tooltip_container(cx, |div, cx| {
|
||||
tooltip_container(cx, |container, cx| {
|
||||
let participants = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_participants(self.channel_id);
|
||||
|
||||
div.child(Label::new("Open Channel"))
|
||||
container
|
||||
.child(Label::new("Join channel"))
|
||||
.children(self.has_notes_notification.then(|| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Info))
|
||||
.child(Label::new("Unread notes"))
|
||||
}))
|
||||
.children(participants.iter().map(|participant| {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
|
||||
use ui::{prelude::*, Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{notifications::DetachAndPromptErr, ModalView};
|
||||
|
||||
@@ -43,7 +43,7 @@ impl ChannelModal {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let channel_modal = cx.view().downgrade();
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::new(
|
||||
Picker::uniform_list(
|
||||
ChannelModalDelegate {
|
||||
channel_modal,
|
||||
matching_users: Vec::new(),
|
||||
@@ -177,22 +177,16 @@ impl Render for ChannelModal {
|
||||
.h(rems(22. / 16.))
|
||||
.justify_between()
|
||||
.line_height(rems(1.25))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"is-public",
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
)
|
||||
.on_click(cx.listener(Self::set_channel_visibility)),
|
||||
)
|
||||
.child(Label::new("Public").size(LabelSize::Small)),
|
||||
)
|
||||
.child(CheckboxWithLabel::new(
|
||||
"is-public",
|
||||
Label::new("Public").size(LabelSize::Small),
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
cx.listener(Self::set_channel_visibility),
|
||||
))
|
||||
.children(
|
||||
Some(
|
||||
Button::new("copy-link", "Copy Link")
|
||||
|
||||
@@ -22,7 +22,7 @@ impl ContactFinder {
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
};
|
||||
let picker = cx.new_view(|cx| Picker::new(delegate, cx).modal(false));
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
|
||||
@@ -102,10 +102,6 @@ impl Render for CollabTitlebarItem {
|
||||
room.remote_participants().values().collect::<Vec<_>>();
|
||||
remote_participants.sort_by_key(|p| p.participant_index.0);
|
||||
|
||||
if !room.in_call() {
|
||||
return this;
|
||||
}
|
||||
|
||||
let current_user_face_pile = self.render_collaborator(
|
||||
¤t_user,
|
||||
peer_id,
|
||||
@@ -137,10 +133,6 @@ impl Render for CollabTitlebarItem {
|
||||
== ParticipantLocation::SharedProject { project_id }
|
||||
});
|
||||
|
||||
if !collaborator.in_call {
|
||||
return None;
|
||||
}
|
||||
|
||||
let face_pile = self.render_collaborator(
|
||||
&collaborator.user,
|
||||
collaborator.peer_id,
|
||||
@@ -193,11 +185,12 @@ impl Render for CollabTitlebarItem {
|
||||
let is_local = project.is_local();
|
||||
let is_shared = is_local && project.is_shared();
|
||||
let is_muted = room.is_muted();
|
||||
let is_connected_to_livekit = room.in_call();
|
||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
||||
let is_screen_sharing = room.is_screen_sharing();
|
||||
let read_only = room.read_only();
|
||||
let can_use_microphone = room.can_use_microphone();
|
||||
let can_share_projects = room.can_share_projects();
|
||||
|
||||
this.when(is_local && !read_only, |this| {
|
||||
this.when(is_local && can_share_projects, |this| {
|
||||
this.child(
|
||||
Button::new(
|
||||
"toggle_sharing",
|
||||
@@ -228,28 +221,22 @@ impl Render for CollabTitlebarItem {
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(is_connected_to_livekit, |el| {
|
||||
el.child(
|
||||
div()
|
||||
.child(
|
||||
IconButton::new("leave-call", ui::IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, cx| {
|
||||
ActiveCall::global(cx).update(cx, |call, cx| {
|
||||
if let Some(room) = call.room() {
|
||||
room.update(cx, |room, cx| {
|
||||
room.leave_call(cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.pl_2(),
|
||||
)
|
||||
})
|
||||
.when(!read_only && is_connected_to_livekit, |this| {
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
IconButton::new("leave-call", ui::IconName::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(|cx| Tooltip::text("Leave call", cx))
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}),
|
||||
)
|
||||
.pr_2(),
|
||||
)
|
||||
.when(can_use_microphone, |this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"mute-microphone",
|
||||
@@ -276,7 +263,34 @@ impl Render for CollabTitlebarItem {
|
||||
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
|
||||
)
|
||||
})
|
||||
.when(!read_only && is_connected_to_livekit, |this| {
|
||||
.child(
|
||||
IconButton::new(
|
||||
"mute-sound",
|
||||
if is_deafened {
|
||||
ui::IconName::AudioOff
|
||||
} else {
|
||||
ui::IconName::AudioOn
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
|
||||
.icon_size(IconSize::Small)
|
||||
.selected(is_deafened)
|
||||
.tooltip(move |cx| {
|
||||
if can_use_microphone {
|
||||
Tooltip::with_meta(
|
||||
"Deafen Audio",
|
||||
None,
|
||||
"Mic will be muted",
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::text("Deafen Audio", cx)
|
||||
}
|
||||
})
|
||||
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
|
||||
)
|
||||
.when(can_share_projects, |this| {
|
||||
this.child(
|
||||
IconButton::new("screen-share", ui::IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
@@ -408,14 +422,20 @@ impl CollabTitlebarItem {
|
||||
worktree.root_name()
|
||||
});
|
||||
|
||||
names.next().unwrap_or("")
|
||||
names.next()
|
||||
};
|
||||
let is_project_selected = name.is_some();
|
||||
let name = if let Some(name) = name {
|
||||
util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
|
||||
} else {
|
||||
"Open recent project".to_string()
|
||||
};
|
||||
|
||||
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
|
||||
let workspace = self.workspace.clone();
|
||||
popover_menu("project_name_trigger")
|
||||
.trigger(
|
||||
Button::new("project_name_trigger", name)
|
||||
.when(!is_project_selected, |b| b.color(Color::Muted))
|
||||
.style(ButtonStyle::Subtle)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
|
||||
@@ -553,10 +573,7 @@ impl CollabTitlebarItem {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(Some(&self.project), cx))
|
||||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if cx.active_window().is_none() {
|
||||
} else if cx.active_window().is_none() {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(None, cx))
|
||||
.detach_and_log_err(cx);
|
||||
@@ -679,6 +696,7 @@ impl CollabTitlebarItem {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
@@ -704,6 +722,7 @@ impl CollabTitlebarItem {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.action("Theme", theme_selector::Toggle.boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
|
||||
})
|
||||
|
||||
@@ -22,7 +22,10 @@ pub use panel_settings::{
|
||||
use settings::Settings;
|
||||
use workspace::{notifications::DetachAndPromptErr, AppState};
|
||||
|
||||
actions!(collab, [ToggleScreenSharing, ToggleMute, LeaveCall]);
|
||||
actions!(
|
||||
collab,
|
||||
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
@@ -82,6 +85,12 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, |room, cx| room.toggle_deafen(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
|
||||
@@ -28,6 +28,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user