Compare commits
68 Commits
git-panel-
...
gpui3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c43ec918e | ||
|
|
1e61989790 | ||
|
|
c3e5bc767a | ||
|
|
cf8c356d27 | ||
|
|
3da7e7195e | ||
|
|
10f88c841c | ||
|
|
eb4f81abd8 | ||
|
|
cd8fdeb15b | ||
|
|
72d264063f | ||
|
|
77e2816265 | ||
|
|
49cc0b8d68 | ||
|
|
7344dbbe2d | ||
|
|
c345532126 | ||
|
|
67870c8d53 | ||
|
|
edbb526401 | ||
|
|
5987d8b78d | ||
|
|
933157bae8 | ||
|
|
d4dc607ac2 | ||
|
|
fdb8e92178 | ||
|
|
8d61c42a8f | ||
|
|
d0984600f7 | ||
|
|
be776138b3 | ||
|
|
0240f32099 | ||
|
|
7ac00f30fa | ||
|
|
282ad91336 | ||
|
|
857000f535 | ||
|
|
6d36e6f27a | ||
|
|
33fbfb83e5 | ||
|
|
299852cc7d | ||
|
|
7db32fbdf4 | ||
|
|
54d59981da | ||
|
|
714af18afc | ||
|
|
906a2d9b9b | ||
|
|
c244416361 | ||
|
|
5607cf85c5 | ||
|
|
b9250d08f9 | ||
|
|
6a5a9c507c | ||
|
|
1a90642088 | ||
|
|
fe59d983eb | ||
|
|
5ef8df22ac | ||
|
|
35407dcf8b | ||
|
|
1ded8e4b8c | ||
|
|
dd32313cd0 | ||
|
|
c55123afa6 | ||
|
|
d0cb78d09d | ||
|
|
1b82ba050f | ||
|
|
864fe2a306 | ||
|
|
3d53f6da1d | ||
|
|
d7e3b10776 | ||
|
|
e6eeac8422 | ||
|
|
6a705fda07 | ||
|
|
76e6c7f6e3 | ||
|
|
1491ca7dcb | ||
|
|
c842988c15 | ||
|
|
f33adc1a7a | ||
|
|
ad50231829 | ||
|
|
fc152eb459 | ||
|
|
c404896a32 | ||
|
|
34d5f264b4 | ||
|
|
cf818948dd | ||
|
|
1122e106a9 | ||
|
|
cb729c4c7f | ||
|
|
e28104b2e8 | ||
|
|
a7159de184 | ||
|
|
3144490fac | ||
|
|
561660ac07 | ||
|
|
d4d6be3f8d | ||
|
|
368ff811b9 |
4
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -26,8 +26,8 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
|
||||
description: Drag images / videos into the text input below
|
||||
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
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,3 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Language Request
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update_top_ranking_issues:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'zed-industries/zed'
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Set up uv
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
@@ -140,7 +140,7 @@ jobs:
|
||||
name: Create a Linux *.tar.gz bundle for ARM
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
- hosted-linux-arm-1
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
|
||||
1357
Cargo.lock
generated
@@ -141,8 +141,6 @@ members = [
|
||||
"crates/worktree",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zeta",
|
||||
"crates/git_ui",
|
||||
|
||||
#
|
||||
# Extensions
|
||||
@@ -228,7 +226,6 @@ fs = { path = "crates/fs" }
|
||||
fsevent = { path = "crates/fsevent" }
|
||||
fuzzy = { path = "crates/fuzzy" }
|
||||
git = { path = "crates/git" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
@@ -328,7 +325,6 @@ workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
|
||||
#
|
||||
# External crates
|
||||
@@ -362,6 +358,7 @@ cargo_metadata = "0.19"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
convert_case = "0.6.0"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
|
||||
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
|
||||
<path d="M22 21H7"/><path d="m5 11 9 9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 365 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>
|
||||
|
Before Width: | Height: | Size: 348 B |
@@ -59,11 +59,6 @@
|
||||
"gitignore": "vcs",
|
||||
"gitkeep": "vcs",
|
||||
"gitmodules": "vcs",
|
||||
"TAG_EDITMSG": "vcs",
|
||||
"MERGE_MSG": "vcs",
|
||||
"COMMIT_EDITMSG": "vcs",
|
||||
"NOTES_EDITMSG": "vcs",
|
||||
"EDIT_DESCRIPTION": "vcs",
|
||||
"gleam": "gleam",
|
||||
"go": "go",
|
||||
"gql": "graphql",
|
||||
@@ -113,7 +108,6 @@
|
||||
"mdf": "storage",
|
||||
"mdx": "document",
|
||||
"metadata": "code",
|
||||
"metal": "metal",
|
||||
"mjs": "javascript",
|
||||
"mka": "audio",
|
||||
"mkv": "video",
|
||||
@@ -323,9 +317,6 @@
|
||||
"lua": {
|
||||
"icon": "icons/file_icons/lua.svg"
|
||||
},
|
||||
"metal": {
|
||||
"icon": "icons/file_icons/metal.svg"
|
||||
},
|
||||
"nim": {
|
||||
"icon": "icons/file_icons/nim.svg"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>
|
||||
|
Before Width: | Height: | Size: 269 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
|
||||
|
Before Width: | Height: | Size: 348 B |
@@ -1,12 +0,0 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2131_1193)">
|
||||
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<circle cx="7" cy="4.5" r="1" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2131_1193">
|
||||
<rect width="14" height="14" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 479 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>
|
||||
|
Before Width: | Height: | Size: 289 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>
|
||||
|
Before Width: | Height: | Size: 291 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>
|
||||
|
Before Width: | Height: | Size: 301 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>
|
||||
|
Before Width: | Height: | Size: 291 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 405 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 404 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
|
||||
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 268 B |
@@ -468,21 +468,13 @@
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"cmd-v": "editor::Paste",
|
||||
"cmd-z": "editor::Undo",
|
||||
"cmd-shift-z": "editor::Redo",
|
||||
"ctrl-shift-z": "zeta::RateCompletions",
|
||||
"up": "editor::MoveUp",
|
||||
"ctrl-up": "editor::MoveToStartOfParagraph",
|
||||
"pageup": "editor::MovePageUp",
|
||||
@@ -230,7 +229,7 @@
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat"
|
||||
"cmd-enter": "assistant2::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -541,18 +540,12 @@
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion",
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
@@ -795,24 +788,5 @@
|
||||
"ctrl-k left": "pane::SplitLeft",
|
||||
"ctrl-k right": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "zeta::ThumbsUp",
|
||||
"shift-down": "zeta::NextEdit",
|
||||
"shift-up": "zeta::PreviousEdit",
|
||||
"right": "zeta::PreviewCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "RateCompletionModal > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "zeta::FocusCompletions",
|
||||
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
|
||||
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -55,14 +53,6 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateSelection",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-m": "editor::ScrollCursorCenter",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateSelection",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"alt-f3": "editor::SelectAllMatches", // find_all_under
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
|
||||
@@ -15,10 +15,8 @@
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
|
||||
"ctrl-a": "editor::MoveToBeginningOfLine",
|
||||
"ctrl-e": "editor::MoveToEndOfLine",
|
||||
"alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-d": "editor::Delete",
|
||||
@@ -55,14 +53,6 @@
|
||||
"shift shift": "file_finder::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-r": "search::SelectPrevMatch",
|
||||
"ctrl-g": "buffer_search::Dismiss"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"cmd-d": "editor::DuplicateSelection",
|
||||
"cmd-d": "editor::DuplicateLineDown",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-d": "editor::DuplicateSelection",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
|
||||
@@ -144,15 +144,15 @@
|
||||
// 4. Highlight the full line (default):
|
||||
// "all"
|
||||
"current_line_highlight": "all",
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether to display inline and alongside documentation for items in the
|
||||
// completions menu
|
||||
"show_completion_documentation": true,
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Show method signatures in the editor, when inside parentheses.
|
||||
"auto_signature_help": false,
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
@@ -474,14 +474,6 @@
|
||||
// Default width of the chat panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to the git panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the git panel.
|
||||
"default_width": 360
|
||||
},
|
||||
"message_editor": {
|
||||
// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
// For example: typing `:wave:` gets replaced with `👋`.
|
||||
@@ -572,11 +564,9 @@
|
||||
// What to do after closing the current tab.
|
||||
//
|
||||
// 1. Activate the tab that was open previously (default)
|
||||
// "history"
|
||||
// 2. Activate the right neighbour tab if present
|
||||
// "neighbour"
|
||||
// 3. Activate the left neighbour tab if present
|
||||
// "left_neighbour"
|
||||
// "History"
|
||||
// 2. Activate the neighbour tab (prefers the right one, if present)
|
||||
// "Neighbour"
|
||||
"activate_on_close": "history",
|
||||
/// Which files containing diagnostic errors/warnings to mark in the tabs.
|
||||
/// Diagnostics are only shown when file icons are also active.
|
||||
@@ -695,7 +685,6 @@
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
|
||||
@@ -3,9 +3,9 @@ use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, CursorStyle, EventEmitter,
|
||||
InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, ViewContext, VisualContext as _,
|
||||
actions, percentage, Animation, AnimationExt as _, AppContext, AppContext, CursorStyle,
|
||||
EventEmitter, InteractiveElement as _, Model, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, View, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
@@ -46,34 +46,38 @@ struct PendingWork<'a> {
|
||||
struct Content {
|
||||
icon: Option<gpui::AnyElement>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
on_click:
|
||||
Option<Arc<dyn Fn(&mut ActivityIndicator, &Model<ActivityIndicator>, &mut AppContext)>>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Model<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
model.notify(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| model.notify(cx)).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
cx.observe(auto_updater, |_, _, cx| model.notify(cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
@@ -86,7 +90,8 @@ impl ActivityIndicator {
|
||||
|
||||
cx.subscribe(&this, move |_, _, event, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let create_buffer =
|
||||
project.update(cx, |project, model, cx| project.create_buffer(model, cx));
|
||||
let project = project.clone();
|
||||
let error = error.clone();
|
||||
let lsp_name = lsp_name.clone();
|
||||
@@ -105,8 +110,8 @@ impl ActivityIndicator {
|
||||
})?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
Box::new(cx.new_model(|model, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), model, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
@@ -123,29 +128,44 @@ impl ActivityIndicator {
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn show_error_message(
|
||||
&mut self,
|
||||
_: &ShowErrorMessage,
|
||||
model: &Model<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
},
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
fn dismiss_error_message(
|
||||
&mut self,
|
||||
_: &DismissErrorMessage,
|
||||
model: &Model<Self>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
updater.update(cx, |updater, model, cx| {
|
||||
updater.dismiss_error(model, cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
@@ -183,7 +203,7 @@ impl ActivityIndicator {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Option<Content> {
|
||||
fn content_to_render(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
return Some(Content {
|
||||
@@ -194,7 +214,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
this.project.update(cx, |project, model, cx| {
|
||||
project.remove_environment_error(cx, worktree_id);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -352,7 +372,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!("Formatting failed: {}. Click to see logs.", failure),
|
||||
on_click: Some(Arc::new(|indicator, cx| {
|
||||
indicator.project.update(cx, |project, cx| {
|
||||
indicator.project.update(cx, |project, model, cx| {
|
||||
project.reset_last_formatting_failure(cx);
|
||||
});
|
||||
cx.dispatch_action(Box::new(workspace::OpenLog));
|
||||
@@ -442,8 +462,12 @@ impl ActivityIndicator {
|
||||
None
|
||||
}
|
||||
|
||||
fn toggle_language_server_work_context_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.context_menu_handle.toggle(cx);
|
||||
fn toggle_language_server_work_context_menu(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.context_menu_handle.toggle(model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,15 +476,20 @@ impl EventEmitter<Event> for ActivityIndicator {}
|
||||
const MAX_MESSAGE_LEN: usize = 50;
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
.on_action(model.listener(Self::show_error_message))
|
||||
.on_action(model.listener(Self::dismiss_error_message));
|
||||
let Some(content) = self.content_to_render(model, cx) else {
|
||||
return result;
|
||||
};
|
||||
let this = cx.view().downgrade();
|
||||
let this = model.downgrade();
|
||||
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
|
||||
result.gap_2().child(
|
||||
PopoverMenu::new("activity-indicator-popover")
|
||||
@@ -480,13 +509,15 @@ impl Render for ActivityIndicator {
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::text(&content.message, cx))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(&content.message, cx)
|
||||
})
|
||||
} else {
|
||||
button.child(Label::new(content.message).size(LabelSize::Small))
|
||||
}
|
||||
})
|
||||
.when_some(content.on_click, |this, handler| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
this.on_click(model.listener(move |this, _, model, window, cx| {
|
||||
handler(this, cx);
|
||||
}))
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
@@ -494,10 +525,10 @@ impl Render for ActivityIndicator {
|
||||
),
|
||||
)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.menu(move |cx| {
|
||||
.menu(move |window, cx| {
|
||||
let strong_this = this.upgrade()?;
|
||||
let mut has_work = false;
|
||||
let menu = ContextMenu::build(cx, |mut menu, cx| {
|
||||
let menu = ContextMenu::build(window, cx, |mut menu, model, window, cx| {
|
||||
for work in strong_this.read(cx).pending_language_server_work(cx) {
|
||||
has_work = true;
|
||||
let this = this.clone();
|
||||
@@ -522,16 +553,17 @@ impl Render for ActivityIndicator {
|
||||
.into_any_element()
|
||||
},
|
||||
move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
this.update(cx, |this, model, cx| {
|
||||
this.project.update(cx, |project, model, cx| {
|
||||
project.cancel_language_server_work(
|
||||
language_server_id,
|
||||
Some(token.clone()),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
this.context_menu_handle.hide(cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
@@ -554,5 +586,11 @@ impl Render for ActivityIndicator {
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
_: Option<&dyn ItemHandle>,
|
||||
_: &Model<Self>,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,9 +321,9 @@ fn update_active_language_model_from_settings(cx: &mut AppContext) {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_active_model(&provider_name, &model_id, cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, model, cx| {
|
||||
registry.select_active_model(&provider_name, &model_id, model, cx);
|
||||
registry.select_inline_alternative_models(inline_alternatives, model, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -35,7 +35,7 @@ use std::{
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::{Context as _, IconName};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
@@ -51,7 +51,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -59,6 +59,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -70,7 +71,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
let message_2 = context.update(cx, |context, cx| {
|
||||
let message_2 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -83,8 +84,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "1"), (1..1, "2")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
@@ -94,7 +95,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let message_3 = context.update(cx, |context, cx| {
|
||||
let message_3 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -108,7 +109,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let message_4 = context.update(cx, |context, cx| {
|
||||
let message_4 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -123,8 +124,8 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "C"), (5..5, "D")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
@@ -137,7 +138,9 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Deleting across message boundaries merges the messages.
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(1..4, "")], None, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -147,7 +150,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Undoing the deletion should also undo the merge.
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -159,7 +162,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Redoing the deletion should also redo the merge.
|
||||
buffer.update(cx, |buffer, cx| buffer.redo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.redo(cx));
|
||||
assert_eq!(
|
||||
messages(&context, cx),
|
||||
vec![
|
||||
@@ -169,7 +172,7 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Ensure we can still insert after a merged message.
|
||||
let message_5 = context.update(cx, |context, cx| {
|
||||
let message_5 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
@@ -193,7 +196,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -201,6 +204,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -212,11 +216,11 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, model, cx)
|
||||
});
|
||||
|
||||
let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx));
|
||||
let (_, message_2) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
|
||||
let message_2 = message_2.unwrap();
|
||||
|
||||
// We recycle newlines in the middle of a split message
|
||||
@@ -229,7 +233,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx));
|
||||
let (_, message_3) = context.update(cx, |context, model, cx| context.split_message(3..3, cx));
|
||||
let message_3 = message_3.unwrap();
|
||||
|
||||
// We don't recycle newlines at the end of a split message
|
||||
@@ -243,7 +247,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx));
|
||||
let (_, message_4) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
|
||||
let message_4 = message_4.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
|
||||
assert_eq!(
|
||||
@@ -256,7 +260,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
]
|
||||
);
|
||||
|
||||
let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx));
|
||||
let (_, message_5) = context.update(cx, |context, model, cx| context.split_message(9..9, cx));
|
||||
let message_5 = message_5.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
|
||||
assert_eq!(
|
||||
@@ -271,7 +275,7 @@ fn test_message_splitting(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
let (message_6, message_7) =
|
||||
context.update(cx, |context, cx| context.split_message(14..16, cx));
|
||||
context.update(cx, |context, model, cx| context.split_message(14..16, cx));
|
||||
let message_6 = message_6.unwrap();
|
||||
let message_7 = message_7.unwrap();
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
|
||||
@@ -297,7 +301,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -305,6 +309,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -316,20 +321,26 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
vec![(message_1.id, Role::User, 0..0)]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa")], None, model, cx)
|
||||
});
|
||||
let message_2 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "bbb")], None, model, cx)
|
||||
});
|
||||
|
||||
let message_3 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(8..8, "ccc")], None, model, cx)
|
||||
});
|
||||
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
|
||||
assert_eq!(
|
||||
@@ -351,7 +362,7 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
let message_4 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -412,7 +423,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -420,6 +431,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -432,7 +444,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
|
||||
context.update(cx, |_, cx| {
|
||||
context.update(cx, |_, model, cx| {
|
||||
cx.subscribe(&context, {
|
||||
let context_ranges = context_ranges.clone();
|
||||
move |context, _, event, _| {
|
||||
@@ -446,7 +458,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
}
|
||||
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||
for range in removed {
|
||||
context_ranges.parsed_commands.remove(range);
|
||||
context_ranges.parsed_commands.remove(&range);
|
||||
}
|
||||
for command in updated {
|
||||
context_ranges
|
||||
@@ -467,7 +479,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
|
||||
// Insert a slash command
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
|
||||
});
|
||||
assert_text_and_context_ranges(
|
||||
@@ -480,7 +492,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Edit the argument of the slash command.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
let edit_offset = buffer.text().find("lib.rs").unwrap();
|
||||
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
|
||||
});
|
||||
@@ -494,7 +506,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Edit the name of the slash command, using one that doesn't exist.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
let edit_offset = buffer.text().find("/file").unwrap();
|
||||
buffer.edit(
|
||||
[(edit_offset..edit_offset + "/file".len(), "/unknown")],
|
||||
@@ -512,7 +524,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Undoing the insertion of an non-existent slash command resorts the previous one.
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_text_and_context_ranges(
|
||||
&buffer,
|
||||
&context_ranges,
|
||||
@@ -523,7 +535,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let (command_output_tx, command_output_rx) = mpsc::unbounded();
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
|
||||
context.insert_command_output(
|
||||
command_source_range,
|
||||
@@ -619,7 +631,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let mut actual_marked_text = String::new();
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.update(cx, |buffer, model, _| {
|
||||
struct Endpoint {
|
||||
offset: usize,
|
||||
marker: char,
|
||||
@@ -703,7 +715,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
|
||||
// Create a new context
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
Some(project),
|
||||
@@ -711,12 +723,13 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Insert an assistant message to simulate a response.
|
||||
let assistant_message_id = context.update(cx, |context, cx| {
|
||||
let assistant_message_id = context.update(cx, |context, model, cx| {
|
||||
let user_message_id = context.messages(cx).next().unwrap().id;
|
||||
context
|
||||
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
|
||||
@@ -903,7 +916,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// When setting the message role to User, the steps are cleared.
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
@@ -932,7 +945,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// When setting the message role back to Assistant, the steps are reparsed.
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
|
||||
});
|
||||
expect_patches(
|
||||
@@ -968,7 +981,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
|
||||
// Ensure steps are re-parsed when deserializing.
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new_model(|cx| {
|
||||
let deserialized_context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
@@ -978,6 +991,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1013,9 +1027,14 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
fn edit(context: &Model<Context>, new_text_marked_with_edits: &str, cx: &mut TestAppContext) {
|
||||
context.update(cx, |context, cx| {
|
||||
context.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit_via_marked_text(
|
||||
&new_text_marked_with_edits.unindent(),
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
@@ -1031,7 +1050,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
let expected_marked_text = expected_marked_text.unindent();
|
||||
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
|
||||
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
|
||||
let (buffer_text, ranges, patches) = context.update(cx, |context, model, cx| {
|
||||
context.buffer.read_with(cx, |buffer, _| {
|
||||
let ranges = context
|
||||
.patches
|
||||
@@ -1084,7 +1103,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
cx.update(assistant_panel::init);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry.clone(),
|
||||
None,
|
||||
@@ -1092,31 +1111,32 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
|
||||
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
|
||||
let message_1 = context.update(cx, |context, cx| {
|
||||
let message_1 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
let message_2 = context.update(cx, |context, cx| {
|
||||
let message_2 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
});
|
||||
let _message_3 = context.update(cx, |context, cx| {
|
||||
let _message_3 = context.update(cx, |context, model, cx| {
|
||||
context
|
||||
.insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
|
||||
.unwrap()
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
buffer.update(cx, |buffer, model, cx| buffer.undo(cx));
|
||||
assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
|
||||
assert_eq!(
|
||||
cx.read(|cx| messages(&context, cx)),
|
||||
@@ -1128,7 +1148,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
|
||||
let deserialized_context = cx.new_model(|cx| {
|
||||
let deserialized_context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
serialized_context,
|
||||
Default::default(),
|
||||
@@ -1138,6 +1158,7 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1187,7 +1208,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
i as ReplicaId,
|
||||
@@ -1198,6 +1219,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1232,15 +1254,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
match rng.gen_range(0..100) {
|
||||
0..=29 if mutation_count > 0 => {
|
||||
log::info!("Context {}: edit buffer", context_index);
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.randomly_edit(&mut rng, 1, cx)
|
||||
});
|
||||
});
|
||||
mutation_count -= 1;
|
||||
}
|
||||
30..=44 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let range = context.buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
log::info!("Context {}: split message at {:?}", context_index, range);
|
||||
context.split_message(range, cx);
|
||||
@@ -1248,7 +1270,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
45..=59 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let role = *[Role::User, Role::Assistant, Role::System]
|
||||
.choose(&mut rng)
|
||||
@@ -1265,7 +1287,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
60..=74 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
let command_text = "/".to_string()
|
||||
+ slash_commands
|
||||
.command_names()
|
||||
@@ -1274,7 +1296,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
.clone()
|
||||
.as_ref();
|
||||
|
||||
let command_range = context.buffer.update(cx, |buffer, cx| {
|
||||
let command_range = context.buffer.update(cx, |buffer, model, cx| {
|
||||
let offset = buffer.random_byte_range(0, &mut rng).start;
|
||||
buffer.edit(
|
||||
[(offset..offset, format!("\n{}\n", command_text))],
|
||||
@@ -1342,7 +1364,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
mutation_count -= 1;
|
||||
}
|
||||
75..=84 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let new_status = match rng.gen_range(0..3) {
|
||||
0 => MessageStatus::Done,
|
||||
@@ -1390,7 +1412,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
);
|
||||
|
||||
network.lock().broadcast(replica_id, ops_to_send);
|
||||
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.apply_ops(ops_to_receive, cx)
|
||||
});
|
||||
} else if rng.gen_bool(0.1) && replica_id != 0 {
|
||||
log::info!("Context {}: disconnecting", context_index);
|
||||
network.lock().disconnect_peer(replica_id);
|
||||
@@ -1402,7 +1426,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
.map(ContextOperation::from_proto)
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.unwrap();
|
||||
context.update(cx, |context, cx| context.apply_ops(ops, cx));
|
||||
context.update(cx, |context, model, cx| context.apply_ops(ops, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1449,7 +1473,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
registry,
|
||||
None,
|
||||
@@ -1457,6 +1481,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
Arc::new(ToolWorkingSet::default()),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1471,7 +1496,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
|
||||
let message_1 = context.read(cx).message_anchors[0].clone();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
|
||||
@@ -1484,22 +1509,28 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
"Empty messages should not have any cache anchors."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, "aaa")], None, model, cx)
|
||||
});
|
||||
let message_2 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(4..4, "bbbbbbb")], None, model, cx)
|
||||
});
|
||||
let message_3 = context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(12..12, "cccccc")], None, model, cx)
|
||||
});
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc");
|
||||
@@ -1511,12 +1542,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
0,
|
||||
"Messages should not be marked for cache before going over the token minimum."
|
||||
);
|
||||
context.update(cx, |context, _| {
|
||||
context.update(cx, |context, model, _| {
|
||||
context.token_count = Some(20);
|
||||
});
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, true, cx)
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, true, model, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
messages_cache(&context, cx)
|
||||
@@ -1528,12 +1559,12 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
context
|
||||
.update(cx, |context, cx| {
|
||||
.update(cx, |context, model, cx| {
|
||||
context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1544,7 +1575,7 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
vec![false, true, true, false],
|
||||
"Most recent message should also be cached if not a speculative request."
|
||||
);
|
||||
context.update(cx, |context, cx| {
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.update_cache_status_for_completion(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1563,8 +1594,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
"All user messages prior to anchor should be marked as cached."
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(14..14, "d")], None, model, cx)
|
||||
});
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1582,8 +1615,10 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
|
||||
],
|
||||
"Modifying a message should invalidate it's cache but leave previous messages."
|
||||
);
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(2..2, "e")], None, model, cx)
|
||||
});
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.mark_cache_anchors(cache_configuration, false, cx)
|
||||
});
|
||||
assert_eq!(
|
||||
@@ -1642,8 +1677,9 @@ impl SlashCommand for FakeSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(vec![]))
|
||||
}
|
||||
@@ -1657,9 +1693,10 @@ impl SlashCommand for FakeSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
Task::ready(Ok(SlashCommandOutput {
|
||||
text: format!("Executed fake command: {}", self.0),
|
||||
|
||||
@@ -14,9 +14,7 @@ use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, EventEmitter, Model, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
@@ -109,11 +107,16 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new_model(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
let context_server_manager = cx.new_model(|model, cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
@@ -127,10 +130,10 @@ impl ContextStore {
|
||||
slash_commands,
|
||||
tools,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
_watch_updates: model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
@@ -148,12 +151,12 @@ impl ContextStore {
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_changed(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.handle_project_changed(project.clone(), model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
this.register_context_server_handlers(model, cx);
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
this.update(&mut cx, |this, model, cx| this.reload(model, cx))?
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -166,7 +169,7 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.host_contexts = envelope
|
||||
.payload
|
||||
.contexts
|
||||
@@ -176,7 +179,7 @@ impl ContextStore {
|
||||
summary: context.summary,
|
||||
})
|
||||
.collect();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -186,7 +189,7 @@ impl ContextStore {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
let operations = this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
@@ -215,14 +218,14 @@ impl ContextStore {
|
||||
_: TypedEnvelope<proto::CreateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::CreateContextResponse> {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, cx| {
|
||||
let (context_id, operations) = this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("can only create contexts as the host"));
|
||||
}
|
||||
|
||||
let context = this.create(cx);
|
||||
let context = this.create(model, cx);
|
||||
let context_id = context.read(cx).id().clone();
|
||||
cx.emit(ContextStoreEvent::ContextCreated(context_id.clone()));
|
||||
model.emit(ContextStoreEvent::ContextCreated(context_id.clone()), cx);
|
||||
|
||||
anyhow::Ok((
|
||||
context_id,
|
||||
@@ -243,12 +246,14 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::UpdateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let operation_proto = envelope.payload.operation.context("invalid operation")?;
|
||||
let operation = ContextOperation::from_proto(operation_proto)?;
|
||||
context.update(cx, |context, cx| context.apply_ops([operation], cx));
|
||||
context.update(cx, |context, model, cx| {
|
||||
context.apply_ops([operation], model, cx)
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
@@ -259,7 +264,7 @@ impl ContextStore {
|
||||
envelope: TypedEnvelope<proto::SynchronizeContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.project.read(cx).is_via_collab() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
@@ -298,7 +303,12 @@ impl ContextStore {
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
|
||||
fn handle_project_changed(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
@@ -319,7 +329,7 @@ impl ContextStore {
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
|
||||
.map(|subscription| subscription.set_model(model, &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
@@ -330,22 +340,23 @@ impl ContextStore {
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
project::Event::HostReshared | project::Event::Rejoined => {
|
||||
self.synchronize_contexts(cx);
|
||||
self.synchronize_contexts(model, cx);
|
||||
}
|
||||
project::Event::DisconnectedFromHost => {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Weak(context.downgrade());
|
||||
strong_context.update(cx, |context, cx| {
|
||||
strong_context.update(cx, |context, model, cx| {
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
context.set_capability(language::Capability::ReadOnly, cx);
|
||||
context.set_capability(language::Capability::ReadOnly, model, cx);
|
||||
}
|
||||
});
|
||||
true
|
||||
@@ -354,14 +365,14 @@ impl ContextStore {
|
||||
}
|
||||
});
|
||||
self.host_contexts.clear();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||
let context = cx.new_model(|cx| {
|
||||
pub fn create(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Context> {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::local(
|
||||
self.languages.clone(),
|
||||
Some(self.project.clone()),
|
||||
@@ -369,16 +380,18 @@ impl ContextStore {
|
||||
self.prompt_builder.clone(),
|
||||
self.slash_commands.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.register_context(&context, cx);
|
||||
self.register_context(&context, model, cx);
|
||||
context
|
||||
}
|
||||
|
||||
pub fn create_remote_context(
|
||||
&mut self,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
@@ -394,11 +407,11 @@ impl ContextStore {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
let request = self.client.request(proto::CreateContext { project_id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_id = ContextId::from_proto(response.context_id);
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
@@ -409,6 +422,7 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -423,12 +437,12 @@ impl ContextStore {
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context(&context, model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
@@ -438,7 +452,8 @@ impl ContextStore {
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
return Task::ready(Ok(existing_context));
|
||||
@@ -459,9 +474,9 @@ impl ContextStore {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::deserialize(
|
||||
saved_context,
|
||||
path.clone(),
|
||||
@@ -471,14 +486,15 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.register_context(&context, model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
@@ -514,7 +530,8 @@ impl ContextStore {
|
||||
pub fn open_remote_context(
|
||||
&mut self,
|
||||
context_id: ContextId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
@@ -537,10 +554,10 @@ impl ContextStore {
|
||||
let prompt_builder = self.prompt_builder.clone();
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let tools = self.tools.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
let context = cx.new_model(|model, cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
@@ -551,6 +568,7 @@ impl ContextStore {
|
||||
tools,
|
||||
Some(project),
|
||||
Some(telemetry),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -565,19 +583,24 @@ impl ContextStore {
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context(&context, model, cx);
|
||||
this.synchronize_contexts(model, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
|
||||
fn register_context(
|
||||
&mut self,
|
||||
context: &Model<Context>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let handle = if self.project_is_shared {
|
||||
ContextHandle::Strong(context.clone())
|
||||
} else {
|
||||
@@ -592,7 +615,8 @@ impl ContextStore {
|
||||
&mut self,
|
||||
context: Model<Context>,
|
||||
event: &ContextEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
@@ -651,7 +675,7 @@ impl ContextStore {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn synchronize_contexts(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
@@ -674,36 +698,37 @@ impl ContextStore {
|
||||
project_id,
|
||||
contexts,
|
||||
});
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = request.await?;
|
||||
model
|
||||
.spawn(cx, |this, cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
@@ -740,9 +765,9 @@ impl ContextStore {
|
||||
&self.host_contexts
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn reload(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
fs.create_dir(contexts_dir()).await?;
|
||||
|
||||
let mut paths = fs.read_dir(contexts_dir()).await?;
|
||||
@@ -778,14 +803,14 @@ impl ContextStore {
|
||||
}
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn restart_context_servers(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.update_model(
|
||||
&self.context_server_manager,
|
||||
|context_server_manager, cx| {
|
||||
@@ -798,7 +823,7 @@ impl ContextStore {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
|
||||
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
Self::handle_context_server_event,
|
||||
@@ -810,7 +835,8 @@ impl ContextStore {
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let slash_command_working_set = self.slash_commands.clone();
|
||||
let tool_working_set = self.tools.clone();
|
||||
|
||||
@@ -140,7 +140,7 @@ impl ResolvedPatch {
|
||||
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
|
||||
}
|
||||
}
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit(
|
||||
edits,
|
||||
Some(AutoindentMode::Block {
|
||||
@@ -461,7 +461,7 @@ impl AssistantPatch {
|
||||
// Expand the context ranges of each edit and group edits with overlapping context ranges.
|
||||
let mut edit_groups_by_buffer = HashMap::default();
|
||||
for (buffer, edits) in edits_by_buffer {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
|
||||
if let Ok(snapshot) = buffer.update(cx, |buffer, model, _| buffer.text_snapshot()) {
|
||||
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
|
||||
}
|
||||
}
|
||||
@@ -918,7 +918,7 @@ mod tests {
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let (text, _) = marked_text_ranges(text_with_expected_range, false);
|
||||
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
|
||||
let buffer = cx.new_model(|model, cx| Buffer::local(text.clone(), model, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
|
||||
let text_with_actual_range = generate_marked_text(&text, &[range], false);
|
||||
@@ -932,8 +932,9 @@ mod tests {
|
||||
new_text: String,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let buffer =
|
||||
cx.new_model(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.new_model(|model, cx| {
|
||||
Buffer::local(old_text, model, cx).with_language(Arc::new(rust_lang()), model, cx)
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let resolved_edits = edits
|
||||
.into_iter()
|
||||
|
||||
@@ -11,8 +11,8 @@ use futures::{
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
|
||||
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
|
||||
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
|
||||
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
|
||||
};
|
||||
use heed::{
|
||||
types::{SerdeBincode, SerdeJson, Str},
|
||||
@@ -38,8 +38,8 @@ use std::{
|
||||
use text::LineEnding;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
div, prelude::*, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
|
||||
SharedString, Styled, Tooltip, ViewContext, VisualContext,
|
||||
div, prelude::*, AppContext, IconButtonShape, KeyBinding, ListItem, ListItemSpacing,
|
||||
ParentElement, Render, SharedString, Styled, Tooltip, VisualContext,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
@@ -88,7 +88,7 @@ pub fn open_prompt_library(
|
||||
.find_map(|window| window.downcast::<PromptLibrary>());
|
||||
if let Some(existing_window) = existing_window {
|
||||
existing_window
|
||||
.update(cx, |_, cx| cx.activate_window())
|
||||
.update(cx, |_, model, cx| cx.activate_window())
|
||||
.ok();
|
||||
Task::ready(Ok(existing_window))
|
||||
} else {
|
||||
@@ -109,7 +109,11 @@ pub fn open_prompt_library(
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
|
||||
|cx| {
|
||||
cx.new_model(|model, cx| {
|
||||
PromptLibrary::new(store, language_registry, model, cx)
|
||||
})
|
||||
},
|
||||
)
|
||||
})?
|
||||
})
|
||||
@@ -121,14 +125,14 @@ pub struct PromptLibrary {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_editors: HashMap<PromptId, PromptEditor>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
picker: View<Picker<PromptPickerDelegate>>,
|
||||
picker: Model<Picker<PromptPickerDelegate>>,
|
||||
pending_load: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct PromptEditor {
|
||||
title_editor: View<Editor>,
|
||||
body_editor: View<Editor>,
|
||||
title_editor: Model<Editor>,
|
||||
body_editor: Model<Editor>,
|
||||
token_count: Option<usize>,
|
||||
pending_token_count: Task<Option<()>>,
|
||||
next_title_and_body_to_save: Option<(String, Rope)>,
|
||||
@@ -158,7 +162,11 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
fn no_matches_text(
|
||||
&self,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> SharedString {
|
||||
if self.store.prompt_count() == 0 {
|
||||
"No prompts.".into()
|
||||
} else {
|
||||
@@ -170,23 +178,31 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_index = ix;
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
PromptPickerEvent::Selected {
|
||||
prompt_id: prompt.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Search...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let (matches, selected_index) = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -201,30 +217,34 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.set_selected_index(selected_index, cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
if let Some(prompt) = self.matches.get(self.selected_index) {
|
||||
cx.emit(PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
});
|
||||
model.emit(
|
||||
cx,
|
||||
PromptPickerEvent::Confirmed {
|
||||
prompt_id: prompt.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let prompt = self.matches.get(ix)?;
|
||||
let default = prompt.default;
|
||||
@@ -232,18 +252,18 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
let element = ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
|
||||
prompt.title.clone().unwrap_or("Untitled".into()),
|
||||
)))
|
||||
.end_slot::<IconButton>(default.then(|| {
|
||||
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
|
||||
.toggle_state(true)
|
||||
.selected(true)
|
||||
.icon_color(Color::Accent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
.tooltip(move |window, cx| Tooltip::text("Remove from Default Prompt", cx))
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
|
||||
}))
|
||||
}))
|
||||
.end_hover_slot(
|
||||
@@ -253,11 +273,12 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
div()
|
||||
.id("built-in-prompt")
|
||||
.child(Icon::new(IconName::FileLock).color(Color::Muted))
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -266,19 +287,19 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
IconButton::new("delete-prompt", IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::Deleted { prompt_id })
|
||||
.tooltip(move |window, cx| Tooltip::text("Delete Prompt", cx))
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::Deleted { prompt_id }, cx)
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-default-prompt", IconName::Sparkle)
|
||||
.toggle_state(default)
|
||||
.selected(default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if default { Color::Accent } else { Color::Muted })
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(
|
||||
if default {
|
||||
"Remove from Default Prompt"
|
||||
@@ -288,15 +309,20 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
model.emit(PromptPickerEvent::ToggledDefault { prompt_id }, cx)
|
||||
})),
|
||||
),
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
|
||||
fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
|
||||
fn render_editor(
|
||||
&self,
|
||||
editor: &Model<Editor>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Div {
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_md()
|
||||
@@ -313,7 +339,8 @@ impl PromptLibrary {
|
||||
fn new(
|
||||
store: Arc<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let delegate = PromptPickerDelegate {
|
||||
store: store.clone(),
|
||||
@@ -321,11 +348,11 @@ impl PromptLibrary {
|
||||
matches: Vec::new(),
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx)
|
||||
let picker = cx.new_model(|model, cx| {
|
||||
let picker = Picker::uniform_list(delegate, model, cx)
|
||||
.modal(false)
|
||||
.max_height(None);
|
||||
picker.focus(cx);
|
||||
picker.focus(window, cx);
|
||||
picker
|
||||
});
|
||||
Self {
|
||||
@@ -341,47 +368,52 @@ impl PromptLibrary {
|
||||
|
||||
fn handle_picker_event(
|
||||
&mut self,
|
||||
_: View<Picker<PromptPickerDelegate>>,
|
||||
_: Model<Picker<PromptPickerDelegate>>,
|
||||
event: &PromptPickerEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
PromptPickerEvent::Selected { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, false, cx);
|
||||
self.load_prompt(*prompt_id, false, model, cx);
|
||||
}
|
||||
PromptPickerEvent::Confirmed { prompt_id } => {
|
||||
self.load_prompt(*prompt_id, true, cx);
|
||||
self.load_prompt(*prompt_id, true, model, cx);
|
||||
}
|
||||
PromptPickerEvent::ToggledDefault { prompt_id } => {
|
||||
self.toggle_default_for_prompt(*prompt_id, cx);
|
||||
self.toggle_default_for_prompt(*prompt_id, model, cx);
|
||||
}
|
||||
PromptPickerEvent::Deleted { prompt_id } => {
|
||||
self.delete_prompt(*prompt_id, cx);
|
||||
self.delete_prompt(*prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn new_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
// If we already have an untitled prompt, use that instead
|
||||
// of creating a new one.
|
||||
if let Some(metadata) = self.store.first() {
|
||||
if metadata.title.is_none() {
|
||||
self.load_prompt(metadata.id, true, cx);
|
||||
self.load_prompt(metadata.id, true, model, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let prompt_id = PromptId::new();
|
||||
let save = self.store.save(prompt_id, None, false, "".into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.load_prompt(prompt_id, true, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn save_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
const SAVE_THROTTLE: Duration = Duration::from_millis(500);
|
||||
|
||||
if prompt_id.is_built_in() {
|
||||
@@ -391,7 +423,7 @@ impl PromptLibrary {
|
||||
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
|
||||
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
let title = prompt_editor.title_editor.read(cx).text(cx);
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, model, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
@@ -407,10 +439,10 @@ impl PromptLibrary {
|
||||
|
||||
prompt_editor.next_title_and_body_to_save = Some((title, body));
|
||||
if prompt_editor.pending_save.is_none() {
|
||||
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
|
||||
prompt_editor.pending_save = Some(model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
loop {
|
||||
let title_and_body = this.update(&mut cx, |this, _| {
|
||||
let title_and_body = this.update(&mut cx, |this, _, _| {
|
||||
this.prompt_editors
|
||||
.get_mut(&prompt_id)?
|
||||
.next_title_and_body_to_save
|
||||
@@ -427,9 +459,10 @@ impl PromptLibrary {
|
||||
.save(prompt_id, title, prompt_metadata.default, body)
|
||||
.await
|
||||
.log_err();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
executor.timer(SAVE_THROTTLE).await;
|
||||
@@ -449,77 +482,90 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.delete_prompt(active_prompt_id, cx);
|
||||
self.delete_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.duplicate_prompt(active_prompt_id, cx);
|
||||
self.duplicate_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_active_prompt(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt_id) = self.active_prompt_id {
|
||||
self.toggle_default_for_prompt(active_prompt_id, cx);
|
||||
self.toggle_default_for_prompt(active_prompt_id, model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn toggle_default_for_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
self.store
|
||||
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
|
||||
pub fn load_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
focus: bool,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
if focus {
|
||||
prompt_editor
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, model, cx| editor.focus(window, cx));
|
||||
}
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
self.set_active_prompt(Some(prompt_id), model, cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
self.pending_load = model.spawn(cx, |this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
let markdown = language_registry.language_for_name("Markdown").await;
|
||||
this.update(&mut cx, |this, cx| match prompt {
|
||||
this.update(&mut cx, |this, model, cx| match prompt {
|
||||
Ok(prompt) => {
|
||||
let title_editor = cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_width(cx);
|
||||
editor.set_placeholder_text("Untitled", cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
|
||||
let title_editor = cx.new_model(|model, cx| {
|
||||
let mut editor = Editor::auto_width(model, cx);
|
||||
editor.set_placeholder_text("Untitled", model, cx);
|
||||
editor.set_text(prompt_metadata.title.unwrap_or_default(), model, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), model, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
let body_editor = cx.new_view(|cx| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let mut buffer = Buffer::local(prompt, cx);
|
||||
buffer.set_language(markdown.log_err(), cx);
|
||||
let body_editor = cx.new_model(|model, cx| {
|
||||
// todo!: Remove this type annotation on cx
|
||||
let buffer = cx.new_model(|model, cx: &mut AppContext| {
|
||||
let mut buffer = Buffer::local(prompt, model, cx);
|
||||
buffer.set_language(markdown.log_err(), model, cx);
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer
|
||||
});
|
||||
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, model, cx);
|
||||
if prompt_id.is_built_in() {
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_inline_completions(Some(false), cx);
|
||||
editor.set_show_inline_completions(Some(false), model, cx);
|
||||
}
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, model, cx);
|
||||
editor.set_show_gutter(false, model, cx);
|
||||
editor.set_show_wrap_guides(false, model, cx);
|
||||
editor.set_show_indent_guides(false, model, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Some(Box::new(
|
||||
@@ -530,7 +576,7 @@ impl PromptLibrary {
|
||||
),
|
||||
)));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
editor.focus(window, cx);
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -567,9 +613,14 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
|
||||
fn set_active_prompt(
|
||||
&mut self,
|
||||
prompt_id: Option<PromptId>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.active_prompt_id = prompt_id;
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
self.picker.update(cx, |picker, model, cx| {
|
||||
if let Some(prompt_id) = prompt_id {
|
||||
if picker
|
||||
.delegate
|
||||
@@ -589,13 +640,13 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
picker.focus(cx);
|
||||
picker.focus(window);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn delete_prompt(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
let confirmation = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
@@ -607,25 +658,32 @@ impl PromptLibrary {
|
||||
&["Delete", "Cancel"],
|
||||
);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.set_active_prompt(None, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
if confirmation.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.active_prompt_id == Some(prompt_id) {
|
||||
this.set_active_prompt(None, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model.notify(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
pub fn duplicate_prompt(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
|
||||
const DUPLICATE_SUFFIX: &str = " copy";
|
||||
let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
|
||||
@@ -655,31 +713,39 @@ impl PromptLibrary {
|
||||
let save = self
|
||||
.store
|
||||
.save(new_id, Some(title.into()), false, body.into());
|
||||
self.picker.update(cx, |picker, cx| picker.refresh(cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |prompt_library, cx| {
|
||||
prompt_library.load_prompt(new_id, true, cx)
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.refresh(cx));
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
save.await?;
|
||||
this.update(&mut cx, |prompt_library, cx| {
|
||||
prompt_library.load_prompt(new_id, true, cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
|
||||
fn focus_active_prompt(&mut self, _: &Tab, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let Some(active_prompt) = self.active_prompt_id {
|
||||
self.prompt_editors[&active_prompt]
|
||||
.body_editor
|
||||
.update(cx, |editor, cx| editor.focus(cx));
|
||||
.update(cx, |editor, model, cx| editor.focus(window, cx));
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| picker.focus(cx));
|
||||
fn focus_picker(&mut self, _: &menu::Cancel, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.picker
|
||||
.update(cx, |picker, model, cx| picker.focus(window));
|
||||
}
|
||||
|
||||
pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
|
||||
pub fn inline_assist(
|
||||
&mut self,
|
||||
action: &InlineAssist,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(active_prompt_id) = self.active_prompt_id else {
|
||||
cx.propagate();
|
||||
return;
|
||||
@@ -693,13 +759,13 @@ impl PromptLibrary {
|
||||
let initial_prompt = action.prompt.clone();
|
||||
if provider.is_authenticated(cx) {
|
||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
|
||||
assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
|
||||
})
|
||||
} else {
|
||||
for window in cx.windows() {
|
||||
if let Some(workspace) = window.downcast::<Workspace>() {
|
||||
let panel = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
.update(cx, |workspace, model, cx| {
|
||||
cx.activate_window();
|
||||
workspace.focus_panel::<AssistantPanel>(cx)
|
||||
})
|
||||
@@ -713,7 +779,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
|
||||
fn move_down_from_title(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveDown,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.body_editor);
|
||||
@@ -721,7 +792,12 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
|
||||
fn move_up_from_body(
|
||||
&mut self,
|
||||
_: &editor::actions::MoveUp,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(prompt_id) = self.active_prompt_id {
|
||||
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
|
||||
cx.focus_view(&prompt_editor.title_editor);
|
||||
@@ -732,18 +808,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_title_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
title_editor: View<Editor>,
|
||||
title_editor: Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, model, cx);
|
||||
self.count_tokens(prompt_id, model, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
title_editor.update(cx, |title_editor, cx| {
|
||||
title_editor.change_selections(None, cx, |selections| {
|
||||
title_editor.update(cx, |title_editor, model, cx| {
|
||||
title_editor.change_selections(None, model, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -756,18 +833,19 @@ impl PromptLibrary {
|
||||
fn handle_prompt_body_editor_event(
|
||||
&mut self,
|
||||
prompt_id: PromptId,
|
||||
body_editor: View<Editor>,
|
||||
body_editor: Model<Editor>,
|
||||
event: &EditorEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
self.save_prompt(prompt_id, cx);
|
||||
self.count_tokens(prompt_id, cx);
|
||||
self.save_prompt(prompt_id, model, cx);
|
||||
self.count_tokens(prompt_id, model, cx);
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
body_editor.update(cx, |body_editor, cx| {
|
||||
body_editor.change_selections(None, cx, |selections| {
|
||||
body_editor.update(cx, |body_editor, model, cx| {
|
||||
body_editor.change_selections(None, model, cx, |selections| {
|
||||
let cursor = selections.oldest_anchor().head();
|
||||
selections.select_anchor_ranges([cursor..cursor]);
|
||||
});
|
||||
@@ -777,7 +855,7 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
|
||||
fn count_tokens(&mut self, prompt_id: PromptId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
return;
|
||||
};
|
||||
@@ -785,7 +863,7 @@ impl PromptLibrary {
|
||||
let editor = &prompt.body_editor.read(cx);
|
||||
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
|
||||
let body = buffer.as_rope().clone();
|
||||
prompt.pending_token_count = cx.spawn(|this, mut cx| {
|
||||
prompt.pending_token_count = model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
@@ -808,10 +886,10 @@ impl PromptLibrary {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
prompt_editor.token_count = Some(token_count);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
.log_err()
|
||||
@@ -819,7 +897,7 @@ impl PromptLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_prompt_list(&mut self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("prompt-list")
|
||||
.capture_action(cx.listener(Self::focus_active_prompt))
|
||||
@@ -839,7 +917,9 @@ impl PromptLibrary {
|
||||
IconButton::new("new-prompt", IconName::Plus)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("New Prompt", &NewPrompt, window, cx)
|
||||
})
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(Box::new(NewPrompt));
|
||||
}),
|
||||
@@ -848,7 +928,11 @@ impl PromptLibrary {
|
||||
.child(div().flex_grow().child(self.picker.clone()))
|
||||
}
|
||||
|
||||
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
|
||||
fn render_active_prompt(
|
||||
&mut self,
|
||||
model: &Model<PromptLibrary>,
|
||||
cx: &mut AppContext,
|
||||
) -> gpui::Stateful<Div> {
|
||||
div()
|
||||
.w_2_3()
|
||||
.h_full()
|
||||
@@ -873,7 +957,7 @@ impl PromptLibrary {
|
||||
.overflow_hidden()
|
||||
.pl(DynamicSpacing::Base16.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.on_click(cx.listener(move |_, _, cx| {
|
||||
.on_click(model.listener(move |_, _, model, window, cx| {
|
||||
cx.focus(&focus_handle);
|
||||
}))
|
||||
.child(
|
||||
@@ -927,9 +1011,11 @@ impl PromptLibrary {
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
inlay_hints_style:
|
||||
editor::make_inlay_hints_style(cx),
|
||||
inline_completion_styles:
|
||||
editor::make_suggestion_styles(cx),
|
||||
editor::make_inlay_hints_style(model, cx),
|
||||
suggestions_style: HighlightStyle {
|
||||
color: Some(cx.theme().status().predictive),
|
||||
..HighlightStyle::default()
|
||||
},
|
||||
..EditorStyle::default()
|
||||
},
|
||||
)),
|
||||
@@ -957,7 +1043,7 @@ impl PromptLibrary {
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
let token_count =
|
||||
token_count.clone();
|
||||
|
||||
@@ -976,6 +1062,7 @@ impl PromptLibrary {
|
||||
.0)
|
||||
.unwrap_or_default()
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -995,11 +1082,12 @@ impl PromptLibrary {
|
||||
Icon::new(IconName::FileLock)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Built-in prompt",
|
||||
None,
|
||||
BUILT_IN_TOOLTIP_TEXT,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1013,10 +1101,11 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Prompt",
|
||||
&DeletePrompt,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1034,10 +1123,11 @@ impl PromptLibrary {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Duplicate Prompt",
|
||||
&DuplicatePrompt,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -1053,7 +1143,7 @@ impl PromptLibrary {
|
||||
IconName::Sparkle,
|
||||
)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.toggle_state(prompt_metadata.default)
|
||||
.selected(prompt_metadata.default)
|
||||
.selected_icon(IconName::SparkleFilled)
|
||||
.icon_color(if prompt_metadata.default {
|
||||
Color::Accent
|
||||
@@ -1062,7 +1152,7 @@ impl PromptLibrary {
|
||||
})
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::Large)
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text(
|
||||
if prompt_metadata.default {
|
||||
"Remove from Default Prompt"
|
||||
@@ -1096,8 +1186,13 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
impl Render for PromptLibrary {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(cx);
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let theme = cx.theme().clone();
|
||||
|
||||
h_flex()
|
||||
@@ -1113,7 +1208,7 @@ impl Render for PromptLibrary {
|
||||
.overflow_hidden()
|
||||
.font(ui_font)
|
||||
.text_color(theme.colors().text)
|
||||
.child(self.render_prompt_list(cx))
|
||||
.child(self.render_prompt_list(model, cx))
|
||||
.map(|el| {
|
||||
if self.store.prompt_count() == 0 {
|
||||
el.child(
|
||||
@@ -1149,7 +1244,7 @@ impl Render for PromptLibrary {
|
||||
Button::new("create-prompt", "New Prompt")
|
||||
.full_width()
|
||||
.key_binding(KeyBinding::for_action(
|
||||
&NewPrompt, cx,
|
||||
&NewPrompt, window, cx,
|
||||
))
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(NewPrompt.boxed_clone())
|
||||
@@ -1160,7 +1255,7 @@ impl Render for PromptLibrary {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
el.child(self.render_active_prompt(cx))
|
||||
el.child(self.render_active_prompt(model, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::AfterCompletion;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use gpui::{AppContext, AppContext, Model, Task, WeakView};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::CompletionIntent;
|
||||
@@ -41,8 +41,8 @@ pub mod terminal_command;
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
}
|
||||
|
||||
pub(crate) struct SlashCommandLine {
|
||||
@@ -55,8 +55,8 @@ pub(crate) struct SlashCommandLine {
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
editor: Option<WeakModel<ContextEditor>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
@@ -71,7 +71,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_name: &str,
|
||||
command_range: Range<Anchor>,
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
@@ -120,12 +121,12 @@ impl SlashCommandCompletionProvider {
|
||||
let editor = editor.clone();
|
||||
let workspace = workspace.clone();
|
||||
Arc::new(
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
if !requires_argument
|
||||
&& (!accepts_arguments || intent.is_complete())
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
.update(cx, |editor, model, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
@@ -165,7 +166,8 @@ impl SlashCommandCompletionProvider {
|
||||
command_range: Range<Anchor>,
|
||||
argument_range: Range<Anchor>,
|
||||
last_argument_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let new_cancel_flag = Arc::new(AtomicBool::new(false));
|
||||
let mut flag = self.cancel_flag.lock();
|
||||
@@ -203,12 +205,12 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
let command_range = command_range.clone();
|
||||
let command_name = command_name.clone();
|
||||
move |intent: CompletionIntent, cx: &mut WindowContext| {
|
||||
move |intent: CompletionIntent, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
if new_argument.after_completion.run()
|
||||
|| intent.is_complete()
|
||||
{
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
.update(cx, |editor, model, cx| {
|
||||
editor.run_command(
|
||||
command_range.clone(),
|
||||
&command_name,
|
||||
@@ -260,10 +262,11 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_position: Anchor,
|
||||
_: editor::CompletionContext,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
model: &Model<Editor>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let Some((name, arguments, command_range, last_argument_range)) =
|
||||
buffer.update(cx, |buffer, _cx| {
|
||||
buffer.update(cx, |buffer, model, _cx| {
|
||||
let position = buffer_position.to_point(buffer);
|
||||
let line_start = Point::new(position.row, 0);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
@@ -318,7 +321,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
self.complete_command_name(&name, command_range, last_argument_range, cx)
|
||||
self.complete_command_name(&name, command_range, last_argument_range, model, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +330,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
_: &Model<Editor>,
|
||||
_: &mut AppContext,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
}
|
||||
@@ -337,7 +341,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
_: Model<Buffer>,
|
||||
_: project::Completion,
|
||||
_: bool,
|
||||
_: &mut ViewContext<Editor>,
|
||||
_: &Model<Editor>,
|
||||
_: &mut AppContext,
|
||||
) -> Task<Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
@@ -348,7 +353,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
model: &Model<Editor>,
|
||||
cx: &mut AppContext,
|
||||
) -> bool {
|
||||
let buffer = buffer.read(cx);
|
||||
let position = position.to_point(buffer);
|
||||
|
||||
@@ -14,7 +14,7 @@ use language_model::{
|
||||
use semantic_index::{FileSummary, SemanticDb};
|
||||
use smol::channel;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use ui::{prelude::*, BorrowAppContext, WindowContext};
|
||||
use ui::{prelude::*, BorrowAppContext};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for AutoCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
// There's no autocomplete for a prompt, since it's arbitrary text.
|
||||
// However, we can use this opportunity to kick off a drain of the backlog.
|
||||
@@ -76,7 +77,7 @@ impl SlashCommand for AutoCommand {
|
||||
|
||||
let cx: &mut AppContext = cx;
|
||||
|
||||
cx.spawn(|cx: gpui::AsyncAppContext| async move {
|
||||
cx.spawn(|cx: AsyncAppContext| async move {
|
||||
let task = project_index.read_with(&cx, |project_index, cx| {
|
||||
project_index.flush_summary_backlogs(cx)
|
||||
})?;
|
||||
@@ -96,9 +97,10 @@ impl SlashCommand for AutoCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
@@ -115,7 +117,7 @@ impl SlashCommand for AutoCommand {
|
||||
return Task::ready(Err(anyhow!("no project indexer")));
|
||||
};
|
||||
|
||||
let task = cx.spawn(|cx: gpui::AsyncWindowContext| async move {
|
||||
let task = cx.spawn(|cx: AsyncAppContext| async move {
|
||||
let summaries = project_index
|
||||
.read_with(&cx, |project_index, cx| project_index.all_summaries(cx))?
|
||||
.await?;
|
||||
|
||||
@@ -107,8 +107,9 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -122,11 +123,12 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let output = workspace.update(cx, |workspace, model, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let fs = workspace.project().read(cx).fs().clone();
|
||||
let path = Self::path_to_cargo_toml(project, cx);
|
||||
|
||||
@@ -8,7 +8,7 @@ use context_server::{
|
||||
manager::{ContextServer, ContextServerManager},
|
||||
types::Prompt,
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
@@ -77,8 +77,9 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
|
||||
return Task::ready(Err(anyhow!("Failed to complete argument")));
|
||||
@@ -128,9 +129,10 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let server_id = self.server_id.clone();
|
||||
let prompt_name = self.prompt.name.clone();
|
||||
|
||||
@@ -36,8 +36,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -47,9 +48,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
|
||||
@@ -6,7 +6,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::future;
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use text::OffsetRangeExt;
|
||||
@@ -40,8 +40,9 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -51,9 +52,10 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
_arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut paths = HashSet::default();
|
||||
let mut file_command_old_outputs = Vec::new();
|
||||
@@ -77,6 +79,7 @@ impl SlashCommand for DeltaSlashCommand {
|
||||
context_buffer.clone(),
|
||||
workspace.clone(),
|
||||
delegate.clone(),
|
||||
model,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl DiagnosticsSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -118,8 +118,8 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -172,9 +172,9 @@ impl SlashCommand for DiagnosticsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -43,7 +43,7 @@ impl DocsSlashCommand {
|
||||
/// access the workspace so we can read the project.
|
||||
fn ensure_rust_doc_providers_are_registered(
|
||||
&self,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
@@ -164,8 +164,9 @@ impl SlashCommand for DocsSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
@@ -272,9 +273,10 @@ impl SlashCommand for DocsSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
if arguments.is_empty() {
|
||||
return Task::ready(Err(anyhow!("missing an argument")));
|
||||
|
||||
@@ -124,8 +124,9 @@ impl SlashCommand for FetchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -135,9 +136,9 @@ impl SlashCommand for FetchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(argument) = arguments.first() else {
|
||||
return Task::ready(Err(anyhow!("missing URL")));
|
||||
|
||||
@@ -28,7 +28,7 @@ impl FileSlashCommand {
|
||||
&self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
workspace: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
@@ -134,8 +134,8 @@ impl SlashCommand for FileSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
@@ -187,9 +187,9 @@ impl SlashCommand for FileSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -35,8 +35,9 @@ impl SlashCommand for NowSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -46,9 +47,10 @@ impl SlashCommand for NowSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
_cx: &mut WindowContext,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let now = Local::now();
|
||||
let text = format!("Today is {now}.", now = now.to_rfc2822());
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::PromptBuilder;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection, SlashCommandResult};
|
||||
use feature_flags::FeatureFlag;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{Anchor, CodeLabel, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelTool};
|
||||
use schemars::JsonSchema;
|
||||
@@ -67,8 +67,9 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -78,9 +79,10 @@ impl SlashCommand for ProjectSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<Anchor>],
|
||||
context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let current_model = model_registry.active_model();
|
||||
|
||||
@@ -37,8 +37,8 @@ impl SlashCommand for PromptSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancellation_flag: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
@@ -64,9 +64,9 @@ impl SlashCommand for PromptSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let title = arguments.to_owned().join(" ");
|
||||
if title.trim().is_empty() {
|
||||
|
||||
@@ -58,8 +58,9 @@ impl SlashCommand for SearchSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -69,9 +70,9 @@ impl SlashCommand for SearchSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: language::BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window, cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{AppContext, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use ui::{IconName, SharedString, WindowContext};
|
||||
use ui::{IconName, SharedString};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct SelectionCommand;
|
||||
@@ -47,8 +47,9 @@ impl SlashCommand for SelectionCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -58,9 +59,10 @@ impl SlashCommand for SelectionCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let mut events = vec![];
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -56,9 +57,10 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
cx.background_executor()
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, sync::atomic::AtomicBool};
|
||||
use ui::{IconName, WindowContext};
|
||||
use ui::IconName;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub(crate) struct OutlineSlashCommand;
|
||||
@@ -34,8 +34,9 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Err(anyhow!("this command does not require argument")))
|
||||
}
|
||||
@@ -49,11 +50,12 @@ impl SlashCommand for OutlineSlashCommand {
|
||||
_arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let output = workspace.update(cx, |workspace, cx| {
|
||||
let output = workspace.update(cx, |workspace, model, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return Task::ready(Err(anyhow!("no active tab")));
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
use ui::{prelude::*, ActiveTheme, WindowContext};
|
||||
use ui::{prelude::*, ActiveTheme};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -51,8 +51,9 @@ impl SlashCommand for TabSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let mut has_all_tabs_completion_item = false;
|
||||
let argument_set = arguments
|
||||
@@ -73,8 +74,8 @@ impl SlashCommand for TabSlashCommand {
|
||||
|
||||
let active_item_path = workspace.as_ref().and_then(|workspace| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let snapshot = active_item_buffer(workspace, cx).ok()?;
|
||||
.update(cx, |workspace, model, cx| {
|
||||
let snapshot = active_item_buffer(workspace, model, cx).ok()?;
|
||||
snapshot.resolve_file_path(cx, true)
|
||||
})
|
||||
.ok()
|
||||
@@ -82,7 +83,7 @@ impl SlashCommand for TabSlashCommand {
|
||||
});
|
||||
let current_query = arguments.last().cloned().unwrap_or_default();
|
||||
let tab_items_search =
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, cx);
|
||||
tab_items_for_queries(workspace, &[current_query], cancel, false, model, cx);
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
cx.spawn(|_| async move {
|
||||
@@ -137,9 +138,10 @@ impl SlashCommand for TabSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let tab_items_search = tab_items_for_queries(
|
||||
Some(workspace),
|
||||
@@ -160,11 +162,12 @@ impl SlashCommand for TabSlashCommand {
|
||||
}
|
||||
|
||||
fn tab_items_for_queries(
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
queries: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
strict_match: bool,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<anyhow::Result<Vec<(Option<PathBuf>, BufferSnapshot, usize)>>> {
|
||||
let empty_query = queries.is_empty() || queries.iter().all(|query| query.trim().is_empty());
|
||||
let queries = queries.to_owned();
|
||||
@@ -174,7 +177,7 @@ fn tab_items_for_queries(
|
||||
.context("no workspace")?
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
if strict_match && empty_query {
|
||||
let snapshot = active_item_buffer(workspace, cx)?;
|
||||
let snapshot = active_item_buffer(workspace, model, cx)?;
|
||||
let full_path = snapshot.resolve_file_path(cx, true);
|
||||
return anyhow::Ok(vec![(full_path, snapshot, 0)]);
|
||||
}
|
||||
@@ -285,7 +288,8 @@ fn tab_items_for_queries(
|
||||
|
||||
fn active_item_buffer(
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ui::ViewContext<Workspace>,
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) -> anyhow::Result<BufferSnapshot> {
|
||||
let active_editor = workspace
|
||||
.active_item(cx)
|
||||
|
||||
@@ -53,8 +53,9 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
self: Arc<Self>,
|
||||
_arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
Task::ready(Ok(Vec::new()))
|
||||
}
|
||||
@@ -64,15 +65,16 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
_delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||
};
|
||||
|
||||
let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else {
|
||||
let Some(active_terminal) = resolve_active_terminal(&workspace, model, cx) else {
|
||||
return Task::ready(Err(anyhow::anyhow!("no active terminal")));
|
||||
};
|
||||
|
||||
@@ -107,9 +109,10 @@ impl SlashCommand for TerminalSlashCommand {
|
||||
}
|
||||
|
||||
fn resolve_active_terminal(
|
||||
workspace: &View<Workspace>,
|
||||
cx: &WindowContext,
|
||||
) -> Option<View<TerminalView>> {
|
||||
workspace: &Model<Workspace>,
|
||||
window: &Window,
|
||||
cx: &AppContext,
|
||||
) -> Option<Model<TerminalView>> {
|
||||
if let Some(terminal_view) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::SlashCommandWorkingSet;
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ enum SlashCommandEntry {
|
||||
Info(SlashCommandInfo),
|
||||
Advert {
|
||||
name: SharedString,
|
||||
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
|
||||
on_confirm: fn(&mut WindowContext<'_>),
|
||||
renderer: fn(&mut gpui::Window, &mut gpui::AppContext) -> AnyElement,
|
||||
on_confirm: fn(&mut gpui::Window, &mut gpui::AppContext),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ impl AsRef<str> for SlashCommandEntry {
|
||||
pub(crate) struct SlashCommandDelegate {
|
||||
all_commands: Vec<SlashCommandEntry>,
|
||||
filtered_commands: Vec<SlashCommandEntry>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> SlashCommandSelector<T> {
|
||||
pub(crate) fn new(
|
||||
working_set: Arc<SlashCommandWorkingSet>,
|
||||
active_context_editor: WeakView<ContextEditor>,
|
||||
active_context_editor: WeakModel<ContextEditor>,
|
||||
trigger: T,
|
||||
) -> Self {
|
||||
SlashCommandSelector {
|
||||
@@ -73,18 +73,23 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_index = ix.min(self.filtered_commands.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Select a command...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_commands.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -104,10 +109,10 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.filtered_commands = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -139,25 +144,31 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ret
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
model: &Model<Picker>,
|
||||
window: &mut Window,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(command) = self.filtered_commands.get(self.selected_index) {
|
||||
match command {
|
||||
SlashCommandEntry::Info(info) => {
|
||||
self.active_context_editor
|
||||
.update(cx, |context_editor, cx| {
|
||||
.update(cx, |context_editor, model, cx| {
|
||||
context_editor.insert_command(&info.name, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
SlashCommandEntry::Advert { on_confirm, .. } => {
|
||||
on_confirm(cx);
|
||||
on_confirm(window, cx);
|
||||
}
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
@@ -167,7 +178,8 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let command_info = self.filtered_commands.get(ix)?;
|
||||
|
||||
@@ -176,10 +188,13 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = info.description.clone();
|
||||
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
|
||||
move |cx| {
|
||||
cx.new_model(|_, _| Tooltip::new(description.clone()))
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -229,15 +244,15 @@ impl PickerDelegate for SlashCommandDelegate {
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.child(renderer(cx)),
|
||||
.selected(selected)
|
||||
.child(renderer(window, cx)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let all_models = self
|
||||
.working_set
|
||||
.featured_command_names(cx)
|
||||
@@ -305,14 +320,15 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
|
||||
selected_index: 0,
|
||||
};
|
||||
|
||||
let picker_view = cx.new_view(|cx| {
|
||||
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
|
||||
let picker_view = cx.new_model(|model, cx| {
|
||||
let picker =
|
||||
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()));
|
||||
picker
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.active_context_editor
|
||||
.update(cx, |this, _| this.slash_menu_handle.clone())
|
||||
.update(cx, |this, model, _| this.slash_menu_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |_cx| Some(picker_view.clone()))
|
||||
|
||||
@@ -13,55 +13,30 @@ path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
assets.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
handlebars.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
ordered-float.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
similar.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
terminal.workspace = true
|
||||
theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
@@ -70,8 +45,3 @@ unindent.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand.workspace = true
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -15,16 +15,15 @@ use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
|
||||
use crate::ui::ContextPill;
|
||||
|
||||
pub struct ActiveThread {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread: Model<Thread>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
|
||||
rendered_messages_by_id: HashMap<MessageId, Model<Markdown>>,
|
||||
last_error: Option<ThreadError>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
@@ -32,13 +31,14 @@ pub struct ActiveThread {
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
thread: Model<Thread>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.observe(&thread, |_, _, cx| model.notify(cx)),
|
||||
cx.subscribe(&thread, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
@@ -50,9 +50,9 @@ impl ActiveThread {
|
||||
messages: Vec::new(),
|
||||
rendered_messages_by_id: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.view().downgrade();
|
||||
move |ix, cx: &mut WindowContext| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, cx))
|
||||
let this = model.downgrade();
|
||||
move |ix, window: &mut gpui::Window, cx: &mut gpui::AppContext| {
|
||||
this.update(cx, |this, model, cx| this.render_message(ix, model, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
@@ -61,7 +61,7 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
this.push_message(&message.id, message.text.clone(), cx);
|
||||
this.push_message(&message.id, message.text.clone(), model, cx);
|
||||
}
|
||||
|
||||
this
|
||||
@@ -83,7 +83,13 @@ impl ActiveThread {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
text: String,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let old_len = self.messages.len();
|
||||
self.messages.push(*id);
|
||||
self.list_state.splice(old_len..old_len, 1);
|
||||
@@ -92,7 +98,7 @@ impl ActiveThread {
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = theme_settings.buffer_font_size;
|
||||
|
||||
let mut text_style = cx.text_style();
|
||||
let mut text_style = window.text_style();
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_size: Some(ui_font_size.into()),
|
||||
@@ -121,12 +127,14 @@ impl ActiveThread {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let markdown = cx.new_view(|cx| {
|
||||
let markdown = cx.new_model(|model, cx| {
|
||||
Markdown::new(
|
||||
text,
|
||||
markdown_style,
|
||||
Some(self.language_registry.clone()),
|
||||
None,
|
||||
model,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -137,7 +145,8 @@ impl ActiveThread {
|
||||
&mut self,
|
||||
_: Model<Thread>,
|
||||
event: &ThreadEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
@@ -147,8 +156,8 @@ impl ActiveThread {
|
||||
ThreadEvent::SummaryChanged => {}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
markdown.update(cx, |markdown, cx| {
|
||||
markdown.append(text, cx);
|
||||
markdown.update(cx, |markdown, model, cx| {
|
||||
markdown.append(text, model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -159,10 +168,10 @@ impl ActiveThread {
|
||||
.message(*message_id)
|
||||
.map(|message| message.text.clone())
|
||||
{
|
||||
self.push_message(message_id, message_text, cx);
|
||||
self.push_message(message_id, message_text, model, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
ThreadEvent::UsePendingTools => {
|
||||
let pending_tool_uses = self
|
||||
@@ -176,13 +185,14 @@ impl ActiveThread {
|
||||
|
||||
for tool_use in pending_tool_uses {
|
||||
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
|
||||
let task = tool.run(tool_use.input, self.workspace.clone(), model, cx);
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, model, cx| {
|
||||
thread.insert_tool_output(
|
||||
tool_use.assistant_message_id,
|
||||
tool_use.id.clone(),
|
||||
task,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -193,7 +203,7 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_message(&self, ix: usize, model: &Model<Self>, cx: &mut AppContext) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
@@ -203,8 +213,6 @@ impl ActiveThread {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let context = self.thread.read(cx).context_for_message(message_id);
|
||||
|
||||
let (role_icon, role_name) = match message.role {
|
||||
Role::User => (IconName::Person, "You"),
|
||||
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
|
||||
@@ -232,23 +240,19 @@ impl ActiveThread {
|
||||
.child(Label::new(role_name).size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
|
||||
.when_some(context, |parent, context| {
|
||||
parent.child(
|
||||
h_flex().flex_wrap().gap_2().p_1p5().children(
|
||||
context
|
||||
.iter()
|
||||
.map(|context| ContextPill::new(context.clone())),
|
||||
),
|
||||
)
|
||||
}),
|
||||
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
list(self.list_state.clone()).flex_1()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
mod active_thread;
|
||||
mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod inline_assistant;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext};
|
||||
use prompts::PromptLoadingParams;
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt;
|
||||
|
||||
pub use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
@@ -34,43 +19,15 @@ actions!(
|
||||
NewThread,
|
||||
ToggleModelSelector,
|
||||
OpenHistory,
|
||||
Chat,
|
||||
ToggleInlineAssist,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist
|
||||
Chat
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
|
||||
AssistantSettings::register(cx);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
fs: fs.clone(),
|
||||
repo_path: stdout_is_a_pty
|
||||
.then(|| std::env::current_dir().log_err())
|
||||
.flatten(),
|
||||
cx,
|
||||
}))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
terminal_inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ use anyhow::Result;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
|
||||
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
prelude::*, px, svg, Action, AnyElement, AppContext, AppContext, EventEmitter, FocusHandle,
|
||||
FocusableView, FontWeight, Model, Pixels, Task, View, WeakView,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::LanguageModelSelector;
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -19,12 +20,12 @@ use crate::message_editor::MessageEditor;
|
||||
use crate::thread::{ThreadError, ThreadId};
|
||||
use crate::thread_history::{PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus};
|
||||
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
||||
|workspace: &mut Workspace, model: &Model<Workspace>, _cx: &mut AppContext| {
|
||||
workspace.register_action(model, |workspace, _: &ToggleFocus, cx| {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(cx);
|
||||
});
|
||||
},
|
||||
@@ -38,22 +39,23 @@ enum ActiveView {
|
||||
}
|
||||
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
thread: View<ActiveThread>,
|
||||
message_editor: View<MessageEditor>,
|
||||
thread: Model<ActiveThread>,
|
||||
message_editor: Model<MessageEditor>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
history: View<ThreadHistory>,
|
||||
history: Model<ThreadHistory>,
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
pub fn load(
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
workspace: WeakModel<Workspace>,
|
||||
window: AnyWindowHandle,
|
||||
cx: AsyncAppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let tools = Arc::new(ToolWorkingSet::default());
|
||||
let thread_store = workspace
|
||||
@@ -64,7 +66,7 @@ impl AssistantPanel {
|
||||
.await?;
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
|
||||
cx.new_model(|model, cx| Self::new(workspace, thread_store, tools, model, cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -73,34 +75,37 @@ impl AssistantPanel {
|
||||
workspace: &Workspace,
|
||||
thread_store: Model<ThreadStore>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
||||
let thread = thread_store.update(cx, |this, model, cx| this.create_thread(model, cx));
|
||||
let language_registry = workspace.project().read(cx).languages().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let weak_self = cx.view().downgrade();
|
||||
let weak_self = model.downgrade();
|
||||
|
||||
Self {
|
||||
active_view: ActiveView::Thread,
|
||||
workspace: workspace.clone(),
|
||||
language_registry: language_registry.clone(),
|
||||
thread_store: thread_store.clone(),
|
||||
thread: cx.new_view(|cx| {
|
||||
thread: cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
workspace.clone(),
|
||||
workspace,
|
||||
language_registry,
|
||||
tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
|
||||
message_editor: cx.new_model(|model, cx| MessageEditor::new(thread.clone(), model, cx)),
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
|
||||
history: cx
|
||||
.new_model(|model, cx| ThreadHistory::new(weak_self, thread_store, model, cx)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,52 +113,63 @@ impl AssistantPanel {
|
||||
self.local_timezone
|
||||
}
|
||||
|
||||
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn new_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let thread = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.create_thread(cx));
|
||||
.update(cx, |this, model, cx| this.create_thread(model, cx));
|
||||
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_view(|cx| {
|
||||
self.thread = cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let Some(thread) = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.open_thread(thread_id, cx))
|
||||
.update(cx, |this, model, cx| this.open_thread(thread_id, model, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.active_view = ActiveView::Thread;
|
||||
self.thread = cx.new_view(|cx| {
|
||||
self.thread = cx.new_model(|model, cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
self.workspace.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.tools.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
self.message_editor = cx.new_model(|model, cx| MessageEditor::new(thread, model, cx));
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
|
||||
pub(crate) fn delete_thread(
|
||||
&mut self,
|
||||
thread_id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.thread_store.update(cx, |this, model, cx| {
|
||||
this.delete_thread(thread_id, model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +189,7 @@ impl Panel for AssistantPanel {
|
||||
"AssistantPanel2"
|
||||
}
|
||||
|
||||
fn position(&self, _cx: &WindowContext) -> DockPosition {
|
||||
fn position(&self, _window: &Window, cx: &AppContext) -> DockPosition {
|
||||
DockPosition::Right
|
||||
}
|
||||
|
||||
@@ -181,25 +197,26 @@ impl Panel for AssistantPanel {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_position(&mut self, _position: DockPosition, model: &Model<Self>, _cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
fn size(&self, _cx: &WindowContext) -> Pixels {
|
||||
fn size(&self, _window: &Window, cx: &AppContext) -> Pixels {
|
||||
px(640.)
|
||||
}
|
||||
|
||||
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_size(&mut self, _size: Option<Pixels>, model: &Model<Self>, _cx: &mut AppContext) {}
|
||||
|
||||
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
|
||||
fn set_active(&mut self, _active: bool, model: &Model<Self>, _cx: &mut AppContext) {}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::AssistantPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
|
||||
fn icon(&self, _window: &Window, cx: &AppContext) -> Option<IconName> {
|
||||
Some(IconName::ZedAssistant)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &AppContext) -> Option<&'static str> {
|
||||
Some("Assistant Panel")
|
||||
}
|
||||
|
||||
@@ -209,7 +226,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_toolbar(&self, model: &Model<Self>, cx: &mut AppContext) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
h_flex()
|
||||
@@ -225,6 +242,7 @@ impl AssistantPanel {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(DynamicSpacing::Base08.rems(cx))
|
||||
.child(self.render_language_model_selector(model, cx))
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("new-thread", IconName::Plus)
|
||||
@@ -238,6 +256,7 @@ impl AssistantPanel {
|
||||
"New Thread",
|
||||
&NewThread,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -258,6 +277,7 @@ impl AssistantPanel {
|
||||
"Open History",
|
||||
&OpenHistory,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -271,7 +291,7 @@ impl AssistantPanel {
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
|
||||
.tooltip(move |window, cx| Tooltip::text("Configure Assistant", cx))
|
||||
.on_click(move |_event, _cx| {
|
||||
println!("Configure Assistant");
|
||||
}),
|
||||
@@ -279,18 +299,83 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_language_model_selector(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Change Model", &ToggleModelSelector, model, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
if self.thread.read(cx).is_empty() {
|
||||
return self.render_thread_empty_state(cx).into_any_element();
|
||||
return self.render_thread_empty_state(model, cx).into_any_element();
|
||||
}
|
||||
|
||||
self.thread.clone().into_any()
|
||||
}
|
||||
|
||||
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render_thread_empty_state(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let recent_threads = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.recent_threads(3, cx));
|
||||
.update(cx, |this, model, cx| this.recent_threads(3, model, cx));
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -306,6 +391,46 @@ impl AssistantPanel {
|
||||
.mb_4(),
|
||||
),
|
||||
)
|
||||
.child(v_flex())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Context Examples:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Terminal)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("Terminal").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.p_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Icon::new(IconName::Folder)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(Label::new("/src/components").size(LabelSize::Small)),
|
||||
),
|
||||
)
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
@@ -318,7 +443,7 @@ impl AssistantPanel {
|
||||
v_flex().gap_2().children(
|
||||
recent_threads
|
||||
.into_iter()
|
||||
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
|
||||
.map(|thread| PastThread::new(thread, model.downgrade())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -329,6 +454,7 @@ impl AssistantPanel {
|
||||
.key_binding(KeyBinding::for_action_in(
|
||||
&OpenHistory,
|
||||
&self.focus_handle(cx),
|
||||
model,
|
||||
cx,
|
||||
))
|
||||
.on_click(move |_event, cx| {
|
||||
@@ -339,7 +465,7 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
fn render_last_error(&self, model: &Model<Self>, cx: &mut AppContext) -> Option<AnyElement> {
|
||||
let last_error = self.thread.read(cx).last_error()?;
|
||||
|
||||
Some(
|
||||
@@ -353,19 +479,23 @@ impl AssistantPanel {
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
|
||||
ThreadError::PaymentRequired => self.render_payment_required_error(model, cx),
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
self.render_max_monthly_spend_reached_error(model, cx)
|
||||
}
|
||||
ThreadError::Message(error_message) => {
|
||||
self.render_error_message(&error_message, cx)
|
||||
self.render_error_message(&error_message, model, cx)
|
||||
}
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_payment_required_error(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
|
||||
v_flex()
|
||||
@@ -390,28 +520,32 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
|
||||
fn render_max_monthly_spend_reached_error(
|
||||
&self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
|
||||
|
||||
v_flex()
|
||||
@@ -436,23 +570,23 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
|
||||
cx.listener(|this, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
model.listener(|this, model, _, cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
@@ -462,7 +596,8 @@ impl AssistantPanel {
|
||||
fn render_error_message(
|
||||
&self,
|
||||
error_message: &SharedString,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
@@ -489,11 +624,11 @@ impl AssistantPanel {
|
||||
.mt_1()
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.thread.update(cx, |this, model, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
@@ -502,7 +637,12 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("AssistantPanel2")
|
||||
.justify_between()
|
||||
@@ -512,20 +652,20 @@ impl Render for AssistantPanel {
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
|
||||
this.active_view = ActiveView::History;
|
||||
this.history.focus_handle(cx).focus(cx);
|
||||
cx.notify();
|
||||
this.history.focus_handle(cx).focus(window);
|
||||
model.notify(cx);
|
||||
}))
|
||||
.child(self.render_toolbar(cx))
|
||||
.child(self.render_toolbar(model, cx))
|
||||
.map(|parent| match self.active_view {
|
||||
ActiveView::Thread => parent
|
||||
.child(self.render_active_thread_or_empty_state(cx))
|
||||
.child(self.render_active_thread_or_empty_state(model, cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.message_editor.clone()),
|
||||
)
|
||||
.children(self.render_last_error(cx)),
|
||||
.children(self.render_last_error(model, cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,485 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use gpui::Pixels;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
#[default]
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
#[serde(rename = "zed.dev")]
|
||||
ZedDotDev { default_model: Option<CloudModel> },
|
||||
#[serde(rename = "openai")]
|
||||
OpenAi {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
default_model: Option<AnthropicModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
#[serde(rename = "ollama")]
|
||||
Ollama {
|
||||
default_model: Option<OllamaModel>,
|
||||
api_url: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AssistantSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"google".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContentV1 {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
enabled: Option<bool>,
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The provider of the assistant service.
|
||||
///
|
||||
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||
/// each with their respective default models and configurations.
|
||||
provider: Option<AssistantProviderContentV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct LegacyAssistantSettingsContent {
|
||||
/// Whether to show the assistant panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the assistant.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
/// Default width in pixels when the assistant is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
pub default_width: Option<f32>,
|
||||
/// Default height in pixels when the assistant is docked to the bottom.
|
||||
///
|
||||
/// Default: 320
|
||||
pub default_height: Option<f32>,
|
||||
/// The default OpenAI model to use when creating new chats.
|
||||
///
|
||||
/// Default: gpt-4-1106-preview
|
||||
pub default_open_ai_model: Option<OpenAiModel>,
|
||||
/// OpenAI API base URL to use when creating new chats.
|
||||
///
|
||||
/// Default: https://api.openai.com/v1
|
||||
pub openai_api_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
merge(
|
||||
&mut settings.default_width,
|
||||
value.default_width.map(Into::into),
|
||||
);
|
||||
merge(
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
value.enable_experimental_live_diffs,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-5-sonnet".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
}),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::post_inc;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub kind: ContextKind,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
FetchedUrl,
|
||||
}
|
||||
@@ -1,101 +1,15 @@
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
use gpui::{DismissEvent, SharedString, Task, WeakView};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
mode: ContextPickerMode,
|
||||
picker: View<Picker<ContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
workspace: workspace.clone(),
|
||||
message_editor: message_editor.clone(),
|
||||
entries: vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "fetch".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
],
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerMode::Default,
|
||||
picker,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_mode(&mut self) {
|
||||
self.mode = ContextPickerMode::Default;
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContextPicker {}
|
||||
|
||||
impl FocusableView for ContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
match &self.mode {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
|
||||
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w(px(400.))
|
||||
.min_w(px(400.))
|
||||
.map(|parent| match &self.mode {
|
||||
ContextPickerMode::Default => parent.child(self.picker.clone()),
|
||||
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
|
||||
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
})
|
||||
}
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct ContextPicker<T: PopoverTrigger> {
|
||||
message_editor: WeakModel<MessageEditor>,
|
||||
trigger: T,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -106,96 +20,114 @@ struct ContextPickerEntry {
|
||||
}
|
||||
|
||||
pub(crate) struct ContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
all_entries: Vec<ContextPickerEntry>,
|
||||
filtered_entries: Vec<ContextPickerEntry>,
|
||||
message_editor: WeakModel<MessageEditor>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> ContextPicker<T> {
|
||||
pub(crate) fn new(message_editor: WeakModel<MessageEditor>, trigger: T) -> Self {
|
||||
ContextPicker {
|
||||
message_editor,
|
||||
trigger,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.entries.len()
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_ix
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
fn set_selected_index(&mut self, ix: usize, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::AppContext) -> Arc<str> {
|
||||
"Select a context source…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
Task::ready(())
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
model: &Model<Picker>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
let all_commands = self.all_entries.clone();
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let filtered_commands = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
all_commands
|
||||
} else {
|
||||
all_commands
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.delegate.filtered_entries = filtered_commands;
|
||||
this.delegate.set_selected_index(0, cx);
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(entry) = self.entries.get(self.selected_ix) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
match entry.name.to_string().as_str() {
|
||||
"file" => {
|
||||
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.message_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
"fetch" => {
|
||||
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.message_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.focus_self();
|
||||
fn confirm(&mut self, _secondary: bool, model: &Model<Picker>, cx: &mut AppContext) {
|
||||
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
|
||||
self.message_editor
|
||||
.update(cx, |_message_editor, model, _cx| {
|
||||
println!("Insert context from {}", entry.name);
|
||||
})
|
||||
.log_err();
|
||||
.ok();
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
|
||||
})
|
||||
.log_err();
|
||||
fn dismissed(&mut self, model: &Model<Picker>, _cx: &mut AppContext) {}
|
||||
|
||||
fn editor_position(&self) -> PickerEditorPosition {
|
||||
PickerEditorPosition::End
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
model: &Model<Picker>,
|
||||
_cx: &mut AppContext,
|
||||
) -> Option<Self::ListItem> {
|
||||
let entry = &self.entries[ix];
|
||||
let entry = self.filtered_entries.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Dense)
|
||||
.toggle_state(selected)
|
||||
.selected(selected)
|
||||
.tooltip({
|
||||
let description = entry.description.clone();
|
||||
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
|
||||
move |cx| {
|
||||
cx.new_model(|_model, _cx| Tooltip::new(description.clone()))
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -225,3 +157,51 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let entries = vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
description: "Insert any directory".into(),
|
||||
icon: IconName::Folder,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "file".into(),
|
||||
description: "Insert any file".into(),
|
||||
icon: IconName::File,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "web".into(),
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
];
|
||||
|
||||
let delegate = ContextPickerDelegate {
|
||||
all_entries: entries.clone(),
|
||||
message_editor: self.message_editor.clone(),
|
||||
filtered_entries: entries,
|
||||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let picker = cx.new_model(|model, cx| {
|
||||
Picker::uniform_list(delegate, model, cx).max_height(Some(rems(20.).into()))
|
||||
});
|
||||
|
||||
let handle = self
|
||||
.message_editor
|
||||
.update(cx, |this, model, _| this.context_picker_handle.clone())
|
||||
.ok();
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(picker.clone()))
|
||||
.trigger(self.trigger)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.when_some(handle, |this, handle| this.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
picker: View<Picker<FetchContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FetchContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FetchContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FetchContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
Html,
|
||||
Plaintext,
|
||||
Json,
|
||||
}
|
||||
|
||||
pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl FetchContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
message_editor,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let Some(content_type) = response.headers().get("content-type") else {
|
||||
bail!("missing Content-Type header");
|
||||
};
|
||||
let content_type = content_type
|
||||
.to_str()
|
||||
.context("invalid Content-Type header")?;
|
||||
let content_type = match content_type {
|
||||
"text/html" => ContentType::Html,
|
||||
"text/plain" => ContentType::Plaintext,
|
||||
"application/json" => ContentType::Json,
|
||||
_ => ContentType::Html,
|
||||
};
|
||||
|
||||
match content_type {
|
||||
ContentType::Html => {
|
||||
let mut handlers: Vec<TagHandler> = vec![
|
||||
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
|
||||
Rc::new(RefCell::new(markdown::ParagraphHandler)),
|
||||
Rc::new(RefCell::new(markdown::HeadingHandler)),
|
||||
Rc::new(RefCell::new(markdown::ListHandler)),
|
||||
Rc::new(RefCell::new(markdown::TableHandler::new())),
|
||||
Rc::new(RefCell::new(markdown::StyledTextHandler)),
|
||||
];
|
||||
if url.contains("wikipedia.org") {
|
||||
use html_to_markdown::structure::wikipedia;
|
||||
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
|
||||
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
|
||||
handlers.push(Rc::new(
|
||||
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
|
||||
));
|
||||
} else {
|
||||
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
|
||||
}
|
||||
|
||||
convert_html_to_markdown(&body[..], &mut handlers)
|
||||
}
|
||||
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
|
||||
ContentType::Json => {
|
||||
let json: serde_json::Value = serde_json::from_slice(&body)?;
|
||||
|
||||
Ok(format!(
|
||||
"```json\n{}\n```",
|
||||
serde_json::to_string_pretty(&json)?
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
|
||||
"Enter a URL…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
self.url = query;
|
||||
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let http_client = workspace.read(cx).client().http_client().clone();
|
||||
let url = self.url.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let text = Self::build_message(http_client, &url).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, _cx| {
|
||||
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(self.url.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
use std::fmt::Write as _;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
|
||||
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for FileContextPicker {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FileContextPicker {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
message_editor,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
&mut self,
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Task<Vec<PathMatch>> {
|
||||
if query.is_empty() {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().read(cx);
|
||||
let entries = workspace.recent_navigation_history(Some(10), cx);
|
||||
|
||||
let entries = entries
|
||||
.into_iter()
|
||||
.map(|entries| entries.0)
|
||||
.chain(project.worktrees(cx).flat_map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
let id = worktree.id();
|
||||
worktree
|
||||
.child_entries(Path::new(""))
|
||||
.filter(|entry| entry.kind.is_file())
|
||||
.map(move |entry| project::ProjectPath {
|
||||
worktree_id: id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_prefix: Arc<str> = Arc::default();
|
||||
Task::ready(
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
|
||||
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
|
||||
full_path.push(&entry.path);
|
||||
Some(PathMatch {
|
||||
score: 0.,
|
||||
positions: Vec::new(),
|
||||
worktree_id: entry.worktree_id.to_usize(),
|
||||
path: full_path.into(),
|
||||
path_prefix: path_prefix.clone(),
|
||||
distance_to_relative_ancestor: 0,
|
||||
is_dir: false,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
let candidate_sets = worktrees
|
||||
.into_iter()
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
|
||||
PathMatchCandidateSet {
|
||||
snapshot: worktree.snapshot(),
|
||||
include_ignored: worktree
|
||||
.root_entry()
|
||||
.map_or(false, |entry| entry.is_ignored),
|
||||
include_root_name: true,
|
||||
candidates: project::Candidates::Files,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
fuzzy::match_path_sets(
|
||||
candidate_sets.as_slice(),
|
||||
query.as_str(),
|
||||
None,
|
||||
false,
|
||||
100,
|
||||
&cancellation_flag,
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for FileContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||
"Search files…".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
// TODO: This should be probably be run in the background.
|
||||
let paths = search_task.await;
|
||||
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.delegate.matches = paths;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let mat = &self.matches[self.selected_index];
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(project) = workspace
|
||||
.upgrade()
|
||||
.map(|workspace| workspace.read(cx).project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let path = mat.path.clone();
|
||||
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(open_buffer_task) = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, path.clone()), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
text.push_str("```\n");
|
||||
|
||||
message_editor.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| {
|
||||
this.reset_mode();
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(mat.path.to_string_lossy().to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(text, "```").unwrap();
|
||||
|
||||
if let Some(path) = path {
|
||||
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
|
||||
write!(text, "{} ", extension).unwrap();
|
||||
}
|
||||
|
||||
write!(text, "{}", path.display()).unwrap();
|
||||
} else {
|
||||
write!(text, "untitled").unwrap();
|
||||
}
|
||||
|
||||
if let Some(row_range) = row_range {
|
||||
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
|
||||
}
|
||||
|
||||
text.push('\n');
|
||||
text
|
||||
}
|
||||
@@ -1,109 +1,70 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
|
||||
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use picker::Picker;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
PopoverMenuHandle,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
use crate::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
editor: View<Editor>,
|
||||
context: Vec<Context>,
|
||||
next_context_id: ContextId,
|
||||
context_picker: View<ContextPicker>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
|
||||
language_model_selector: View<LanguageModelSelector>,
|
||||
editor: Model<Editor>,
|
||||
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
|
||||
use_tools: bool,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let weak_self = cx.view().downgrade();
|
||||
pub fn new(thread: Model<Thread>, model: &Model<Self>, cx: &mut AppContext) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::auto_height(80, cx);
|
||||
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
|
||||
editor: cx.new_model(|model, cx| {
|
||||
let mut editor = Editor::auto_height(80, model, cx);
|
||||
editor.set_placeholder_text("Ask anything…", model, cx);
|
||||
|
||||
editor
|
||||
}),
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
|
||||
context_picker_handle: PopoverMenuHandle::default(),
|
||||
language_model_selector: cx.new_view(|cx| {
|
||||
LanguageModelSelector::new(
|
||||
|model, _cx| {
|
||||
println!("Selected {:?}", model.name());
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
use_tools: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_context(
|
||||
&mut self,
|
||||
kind: ContextKind,
|
||||
name: impl Into<SharedString>,
|
||||
text: impl Into<SharedString>,
|
||||
) {
|
||||
self.context.push(Context {
|
||||
id: self.next_context_id.post_inc(),
|
||||
name: name.into(),
|
||||
kind,
|
||||
text: text.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
fn chat(&mut self, _: &Chat, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.send_to_model(RequestKind::Chat, model, cx);
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = model_registry.active_model()?;
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let user_message = self.editor.update(cx, |editor, model, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(cx);
|
||||
text
|
||||
});
|
||||
let context = self.context.drain(..).collect::<Vec<_>>();
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
self.thread.update(cx, |thread, model, cx| {
|
||||
thread.insert_user_message(user_message, cx);
|
||||
let mut request = thread.to_completion_request(request_kind, cx);
|
||||
|
||||
if self.use_tools {
|
||||
@@ -124,55 +85,6 @@ impl MessageEditor {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
|
||||
LanguageModelSelectorPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
div()
|
||||
.overflow_x_hidden()
|
||||
.flex_grow()
|
||||
.whitespace_nowrap()
|
||||
.child(match (active_provider, active_model) {
|
||||
(Some(provider), Some(model)) => h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(
|
||||
model.icon().unwrap_or_else(|| provider.icon()),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new(model.name().0)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
_ => Label::new("No model selected")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for MessageEditor {
|
||||
@@ -182,11 +94,15 @@ impl FocusableView for MessageEditor {
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let context_picker = self.context_picker.clone();
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
@@ -196,46 +112,12 @@ impl Render for MessageEditor {
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |_cx| Some(context_picker.clone()))
|
||||
.trigger(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)
|
||||
.attach(gpui::AnchorCorner::TopLeft)
|
||||
.anchor(gpui::AnchorCorner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-16.0),
|
||||
})
|
||||
.with_handle(self.context_picker_handle.clone()),
|
||||
)
|
||||
.children(self.context.iter().map(|context| {
|
||||
ContextPill::new(context.clone()).on_remove({
|
||||
let context = context.clone();
|
||||
Rc::new(cx.listener(move |this, _event, cx| {
|
||||
this.context.retain(|other| other.id != context.id);
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}))
|
||||
.when(!self.context.is_empty(), |parent| {
|
||||
parent.child(
|
||||
IconButton::new("remove-all-context", IconName::Eraser)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
|
||||
.on_click(cx.listener(|this, _event, cx| {
|
||||
this.context.clear();
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}),
|
||||
h_flex().gap_2().child(ContextPicker::new(
|
||||
model.downgrade(),
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small),
|
||||
)),
|
||||
)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
@@ -268,26 +150,27 @@ impl Render for MessageEditor {
|
||||
self.use_tools.into(),
|
||||
cx.listener(|this, selection, _cx| {
|
||||
this.use_tools = match selection {
|
||||
ToggleState::Selected => true,
|
||||
ToggleState::Unselected | ToggleState::Indeterminate => false,
|
||||
Selection::Selected => true,
|
||||
Selection::Unselected | Selection::Indeterminate => false,
|
||||
};
|
||||
}),
|
||||
)))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(self.render_language_model_selector(cx))
|
||||
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
|
||||
.child(Label::new("or"))
|
||||
.child(
|
||||
ButtonLike::new("chat")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Submit"))
|
||||
.child(Label::new("Chat"))
|
||||
.children(
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
|
||||
KeyBinding::for_action_in(&Chat, &focus_handle, model, cx)
|
||||
.map(|binding| binding.into_any_element()),
|
||||
)
|
||||
.on_click(move |_event, cx| {
|
||||
focus_handle.dispatch_action(&Chat, cx);
|
||||
focus_handle.dispatch_action(&Chat, model, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::{BufferSnapshot, LanguageName, Point};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::LineEnding;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptDiagnosticContext {
|
||||
pub line_number: usize,
|
||||
pub error_message: String,
|
||||
pub code_content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContentPromptContext {
|
||||
pub content_type: String,
|
||||
pub language_name: Option<String>,
|
||||
pub is_insert: bool,
|
||||
pub is_truncated: bool,
|
||||
pub document_content: String,
|
||||
pub user_prompt: String,
|
||||
pub rewrite_section: Option<String>,
|
||||
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TerminalAssistantPromptContext {
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: Option<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub latest_output: Vec<String>,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ProjectSlashCommandPromptContext {
|
||||
pub context_buffer: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
params: PromptLoadingParams,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
.spawn(async move {
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
found_dir_once = true;
|
||||
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::debug!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for event in changed_paths {
|
||||
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::debug!("Registering built-in prompt template: {}", id);
|
||||
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_inline_transformation_prompt(
|
||||
&self,
|
||||
user_prompt: String,
|
||||
language_name: Option<&LanguageName>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
) -> Result<String, RenderError> {
|
||||
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||
None | Some("Markdown" | "Plain Text") => "text",
|
||||
Some(_) => "code",
|
||||
};
|
||||
|
||||
const MAX_CTX: usize = 50000;
|
||||
let is_insert = range.is_empty();
|
||||
let mut is_truncated = false;
|
||||
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
|
||||
start..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
|
||||
range.end..end
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
|
||||
let mut document_content = String::new();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
if is_insert {
|
||||
document_content.push_str("<insert_here></insert_here>");
|
||||
} else {
|
||||
document_content.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
document_content.push_str("\n</rewrite_this>");
|
||||
}
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
document_content.push_str(chunk);
|
||||
}
|
||||
|
||||
let rewrite_section = if !is_insert {
|
||||
let mut section = String::new();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
section.push_str(chunk);
|
||||
}
|
||||
Some(section)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||
.map(|entry| {
|
||||
let start = entry.range.start;
|
||||
ContentPromptDiagnosticContext {
|
||||
line_number: (start.row + 1) as usize,
|
||||
error_message: entry.diagnostic.message.clone(),
|
||||
code_content: buffer.text_for_range(entry.range.clone()).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let context = ContentPromptContext {
|
||||
content_type: content_type.to_string(),
|
||||
language_name: language_name.map(|s| s.to_string()),
|
||||
is_insert,
|
||||
is_truncated,
|
||||
document_content,
|
||||
user_prompt,
|
||||
rewrite_section,
|
||||
diagnostic_errors,
|
||||
};
|
||||
self.handlebars.lock().render("content_prompt", &context)
|
||||
}
|
||||
|
||||
pub fn generate_terminal_assistant_prompt(
|
||||
&self,
|
||||
user_prompt: &str,
|
||||
shell: Option<&str>,
|
||||
working_directory: Option<&str>,
|
||||
latest_output: &[String],
|
||||
) -> Result<String, RenderError> {
|
||||
let context = TerminalAssistantPromptContext {
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: shell.map(|s| s.to_string()),
|
||||
working_directory: working_directory.map(|s| s.to_string()),
|
||||
latest_output: latest_output.to_vec(),
|
||||
user_prompt: user_prompt.to_string(),
|
||||
};
|
||||
|
||||
self.handlebars
|
||||
.lock()
|
||||
.render("terminal_assistant_prompt", &context)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
|
||||
use gpui::{AppContext, EventEmitter, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
@@ -17,8 +17,6 @@ use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{Context, ContextKind};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
Chat,
|
||||
@@ -64,7 +62,6 @@ pub struct Thread {
|
||||
pending_summary: Task<Option<()>>,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
context_by_message: HashMap<MessageId, Vec<Context>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
@@ -74,7 +71,7 @@ pub struct Thread {
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, model: &Model<Self>, _cx: &mut AppContext) -> Self {
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
@@ -82,7 +79,6 @@ impl Thread {
|
||||
pending_summary: Task::ready(None),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
context_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
tools,
|
||||
@@ -112,9 +108,14 @@ impl Thread {
|
||||
self.summary.clone()
|
||||
}
|
||||
|
||||
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
|
||||
pub fn set_summary(
|
||||
&mut self,
|
||||
summary: impl Into<SharedString>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.summary = Some(summary.into());
|
||||
cx.emit(ThreadEvent::SummaryChanged);
|
||||
model.emit(ThreadEvent::SummaryChanged, cx);
|
||||
}
|
||||
|
||||
pub fn message(&self, id: MessageId) -> Option<&Message> {
|
||||
@@ -129,10 +130,6 @@ impl Thread {
|
||||
&self.tools
|
||||
}
|
||||
|
||||
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
|
||||
self.context_by_message.get(&id)
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
@@ -140,19 +137,19 @@ impl Thread {
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
context: Vec<Context>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let message_id = self.insert_message(Role::User, text, cx);
|
||||
self.context_by_message.insert(message_id, context);
|
||||
self.insert_message(Role::User, text, model, cx)
|
||||
}
|
||||
|
||||
pub fn insert_message(
|
||||
&mut self,
|
||||
role: Role,
|
||||
text: impl Into<String>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> MessageId {
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let id = self.next_message_id.post_inc();
|
||||
self.messages.push(Message {
|
||||
id,
|
||||
@@ -160,8 +157,7 @@ impl Thread {
|
||||
text: text.into(),
|
||||
});
|
||||
self.touch_updated_at();
|
||||
cx.emit(ThreadEvent::MessageAdded(id));
|
||||
id
|
||||
model.emit(ThreadEvent::MessageAdded(id), cx);
|
||||
}
|
||||
|
||||
pub fn to_completion_request(
|
||||
@@ -191,41 +187,6 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(context) = self.context_for_message(message.id) {
|
||||
let mut file_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
|
||||
for context in context.iter() {
|
||||
match context.kind {
|
||||
ContextKind::File => {
|
||||
file_context.push_str(&context.text);
|
||||
file_context.push('\n');
|
||||
}
|
||||
ContextKind::FetchedUrl => {
|
||||
fetch_context.push_str(&context.name);
|
||||
fetch_context.push('\n');
|
||||
fetch_context.push_str(&context.text);
|
||||
fetch_context.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut context_text = String::new();
|
||||
if !file_context.is_empty() {
|
||||
context_text.push_str("The following files are available:\n");
|
||||
context_text.push_str(&file_context);
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_text.push_str("The following fetched results are available\n");
|
||||
context_text.push_str(&fetch_context);
|
||||
}
|
||||
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(context_text))
|
||||
}
|
||||
|
||||
if !message.text.is_empty() {
|
||||
request_message
|
||||
.content
|
||||
@@ -250,7 +211,8 @@ impl Thread {
|
||||
&mut self,
|
||||
request: LanguageModelRequest,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
|
||||
@@ -275,10 +237,13 @@ impl Thread {
|
||||
if let Some(last_message) = thread.messages.last_mut() {
|
||||
if last_message.role == Role::Assistant {
|
||||
last_message.text.push_str(&chunk);
|
||||
cx.emit(ThreadEvent::StreamedAssistantText(
|
||||
last_message.id,
|
||||
chunk,
|
||||
));
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::StreamedAssistantText(
|
||||
last_message.id,
|
||||
chunk,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,8 +274,8 @@ impl Thread {
|
||||
}
|
||||
|
||||
thread.touch_updated_at();
|
||||
cx.emit(ThreadEvent::StreamedCompletion);
|
||||
cx.notify();
|
||||
model.emit(ThreadEvent::StreamedCompletion, cx);
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
smol::future::yield_now().await;
|
||||
@@ -335,25 +300,31 @@ impl Thread {
|
||||
.update(&mut cx, |_thread, cx| match result.as_ref() {
|
||||
Ok(stop_reason) => match stop_reason {
|
||||
StopReason::ToolUse => {
|
||||
cx.emit(ThreadEvent::UsePendingTools);
|
||||
model.emit(ThreadEvent::UsePendingTools, cx);
|
||||
}
|
||||
StopReason::EndTurn => {}
|
||||
StopReason::MaxTokens => {}
|
||||
},
|
||||
Err(error) => {
|
||||
if error.is::<PaymentRequiredError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
|
||||
model.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired), cx);
|
||||
} else if error.is::<MaxMonthlySpendReachedError>() {
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached),
|
||||
);
|
||||
} else {
|
||||
let error_message = error
|
||||
.chain()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
|
||||
SharedString::from(error_message.clone()),
|
||||
)));
|
||||
model.emit(
|
||||
cx,
|
||||
ThreadEvent::ShowError(ThreadError::Message(SharedString::from(
|
||||
error_message.clone(),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -366,7 +337,7 @@ impl Thread {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn summarize(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
|
||||
return;
|
||||
};
|
||||
@@ -388,7 +359,7 @@ impl Thread {
|
||||
cache: false,
|
||||
});
|
||||
|
||||
self.pending_summary = cx.spawn(|this, mut cx| {
|
||||
self.pending_summary = model.spawn(cx, |this, mut cx| {
|
||||
async move {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
@@ -405,12 +376,12 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if !new_summary.is_empty() {
|
||||
this.summary = Some(new_summary.into());
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::SummaryChanged);
|
||||
model.emit(ThreadEvent::SummaryChanged, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -424,7 +395,8 @@ impl Thread {
|
||||
assistant_message_id: MessageId,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
output: Task<Result<String>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let insert_output_task = cx.spawn(|thread, mut cx| {
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
@@ -450,7 +422,7 @@ impl Thread {
|
||||
is_error: false,
|
||||
});
|
||||
|
||||
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
|
||||
model.emit(ThreadEvent::ToolFinished { tool_use_id }, cx);
|
||||
}
|
||||
Err(err) => {
|
||||
tool_results.push(LanguageModelToolResult {
|
||||
|
||||
@@ -10,19 +10,20 @@ use crate::AssistantPanel;
|
||||
|
||||
pub struct ThreadHistory {
|
||||
focus_handle: FocusHandle,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
thread_store: Model<ThreadStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
focus_handle: window.focus_handle(),
|
||||
assistant_panel,
|
||||
thread_store,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
@@ -37,8 +38,15 @@ impl FocusableView for ThreadHistory {
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let threads = self
|
||||
.thread_store
|
||||
.update(cx, |this, model, cx| this.threads(cx));
|
||||
|
||||
v_flex()
|
||||
.id("thread-history-container")
|
||||
@@ -85,11 +93,11 @@ impl Render for ThreadHistory {
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: Model<Thread>,
|
||||
assistant_panel: WeakView<AssistantPanel>,
|
||||
assistant_panel: WeakModel<AssistantPanel>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
|
||||
pub fn new(thread: Model<Thread>, assistant_panel: WeakModel<AssistantPanel>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
assistant_panel,
|
||||
@@ -98,7 +106,7 @@ impl PastThread {
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::AppContext) -> impl IntoElement {
|
||||
let (id, summary) = {
|
||||
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
let thread = self.thread.read(cx);
|
||||
@@ -113,7 +121,7 @@ impl RenderOnce for PastThread {
|
||||
.unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, _cx| this.local_timezone())
|
||||
.update(cx, |this, model, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
@@ -133,7 +141,7 @@ impl RenderOnce for PastThread {
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
.update(cx, |this, model, cx| {
|
||||
this.delete_thread(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
@@ -146,7 +154,7 @@ impl RenderOnce for PastThread {
|
||||
let id = id.clone();
|
||||
move |_event, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
.update(cx, |this, model, cx| {
|
||||
this.open_thread(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
|
||||
use gpui::{prelude::*, AppContext, Model, Task};
|
||||
use project::Project;
|
||||
use unindent::Unindent;
|
||||
use util::ResultExt as _;
|
||||
@@ -28,11 +28,16 @@ impl ThreadStore {
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let this = cx.new_model(|model: &Model<Self>, cx: &mut AppContext| {
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new_model(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
let context_server_manager = cx.new_model(|model, cx| {
|
||||
ContextServerManager::new(
|
||||
context_server_factory_registry,
|
||||
project.clone(),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
@@ -52,7 +57,7 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
|
||||
pub fn threads(&self, model: &Model<Self>, cx: &AppContext) -> Vec<Model<Thread>> {
|
||||
let mut threads = self
|
||||
.threads
|
||||
.iter()
|
||||
@@ -63,28 +68,38 @@ impl ThreadStore {
|
||||
threads
|
||||
}
|
||||
|
||||
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
|
||||
pub fn recent_threads(
|
||||
&self,
|
||||
limit: usize,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<Model<Thread>> {
|
||||
self.threads(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
|
||||
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
|
||||
pub fn create_thread(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Model<Thread> {
|
||||
let thread = cx.new_model(|model, cx| Thread::new(self.tools.clone(), model, cx));
|
||||
self.threads.push(thread.clone());
|
||||
thread
|
||||
}
|
||||
|
||||
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
|
||||
pub fn open_thread(
|
||||
&self,
|
||||
id: &ThreadId,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Model<Thread>> {
|
||||
self.threads
|
||||
.iter()
|
||||
.find(|thread| thread.read(cx).id() == id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
|
||||
pub fn delete_thread(&mut self, id: &ThreadId, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.threads.retain(|thread| thread.read(cx).id() != id);
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
|
||||
fn register_context_server_handlers(&self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
Self::handle_context_server_event,
|
||||
@@ -96,7 +111,8 @@ impl ThreadStore {
|
||||
&mut self,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
event: &context_server::manager::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let tool_working_set = self.tools.clone();
|
||||
match event {
|
||||
@@ -153,23 +169,23 @@ impl ThreadStore {
|
||||
|
||||
impl ThreadStore {
|
||||
/// Creates some mocked recent threads for testing purposes.
|
||||
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn mock_recent_threads(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
use language_model::Role;
|
||||
|
||||
self.threads.push(cx.new_model(|cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), cx);
|
||||
thread.set_summary("Introduction to quantum computing", cx);
|
||||
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
|
||||
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
|
||||
self.threads.push(cx.new_model(|model, cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), model, cx);
|
||||
thread.set_summary("Introduction to quantum computing", model, cx);
|
||||
thread.insert_user_message("Hello! Can you help me understand quantum computing?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", model, cx);
|
||||
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", model, cx);
|
||||
thread
|
||||
}));
|
||||
|
||||
self.threads.push(cx.new_model(|cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), cx);
|
||||
thread.set_summary("Rust web development and async programming", cx);
|
||||
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
|
||||
self.threads.push(cx.new_model(|model, cx| {
|
||||
let mut thread = Thread::new(self.tools.clone(), model, cx);
|
||||
thread.set_summary("Rust web development and async programming", model, cx);
|
||||
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
|
||||
|
||||
```rust
|
||||
@@ -205,8 +221,8 @@ impl ThreadStore {
|
||||
actix-web = \"4.0\"
|
||||
```
|
||||
|
||||
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
|
||||
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
|
||||
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), model, cx);
|
||||
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", model, cx);
|
||||
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
|
||||
|
||||
1. **Syntax**: Async functions are declared using the `async` keyword:
|
||||
@@ -235,7 +251,7 @@ impl ThreadStore {
|
||||
|
||||
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
|
||||
|
||||
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
|
||||
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), model, cx);
|
||||
thread
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
mod context_pill;
|
||||
|
||||
pub use context_pill::*;
|
||||
@@ -1,49 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape};
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContextPill {
|
||||
context: Context,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
}
|
||||
|
||||
impl ContextPill {
|
||||
pub fn new(context: Context) -> Self {
|
||||
Self {
|
||||
context,
|
||||
on_remove: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
||||
self.on_remove = Some(on_remove);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContextPill {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
||||
.when_some(self.on_remove, |parent, on_remove| {
|
||||
parent.child(
|
||||
IconButton::new("remove", IconName::Close)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click({
|
||||
let on_remove = on_remove.clone();
|
||||
move |event, cx| on_remove(event, cx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pub use crate::slash_command_registry::*;
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -78,8 +78,9 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
cancel: Arc<AtomicBool>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>>;
|
||||
fn requires_argument(&self) -> bool;
|
||||
fn accepts_arguments(&self) -> bool {
|
||||
@@ -90,21 +91,27 @@ pub trait SlashCommand: 'static + Send + Sync {
|
||||
arguments: &[String],
|
||||
context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
context_buffer: BufferSnapshot,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
// TODO: We're just using the `LspAdapterDelegate` here because that is
|
||||
// what the extension API is already expecting.
|
||||
//
|
||||
// It may be that `LspAdapterDelegate` needs a more general name, or
|
||||
// perhaps another kind of delegate is needed here.
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult>;
|
||||
}
|
||||
|
||||
pub type RenderFoldPlaceholder = Arc<
|
||||
dyn Send
|
||||
+ Sync
|
||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||
+ Fn(
|
||||
ElementId,
|
||||
Arc<dyn Fn(&mut gpui::Window, &mut gpui::AppContext)>,
|
||||
&mut gpui::Window,
|
||||
&mut gpui::AppContext,
|
||||
) -> AnyElement,
|
||||
>;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::{atomic::AtomicBool, Arc};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -97,8 +97,9 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
self: Arc<Self>,
|
||||
arguments: &[String],
|
||||
_cancel: Arc<AtomicBool>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
_workspace: Option<WeakModel<Workspace>>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
@@ -127,9 +128,10 @@ impl SlashCommand for ExtensionSlashCommand {
|
||||
arguments: &[String],
|
||||
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
|
||||
_context_buffer: BufferSnapshot,
|
||||
_workspace: WeakView<Workspace>,
|
||||
_workspace: WeakModel<Workspace>,
|
||||
delegate: Option<Arc<dyn LspAdapterDelegate>>,
|
||||
cx: &mut WindowContext,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let command = self.command.clone();
|
||||
let arguments = arguments.to_owned();
|
||||
|
||||
@@ -4,7 +4,7 @@ mod tool_working_set;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||
use gpui::{AppContext, Task, WeakView};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::tool_registry::*;
|
||||
@@ -31,7 +31,8 @@ pub trait Tool: 'static + Send + Sync {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
workspace: WeakModel<Workspace>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<String>>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::Tool;
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{Task, WeakView, WindowContext};
|
||||
use gpui::{Task, WeakView};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -41,8 +41,9 @@ impl Tool for NowTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_workspace: WeakView<workspace::Workspace>,
|
||||
_cx: &mut WindowContext,
|
||||
_workspace: WeakModel<workspace::Workspace>,
|
||||
_window: &mut gpui::Window,
|
||||
_cx: &mut gpui::AppContext,
|
||||
) -> Task<Result<String>> {
|
||||
let input: FileToolInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
|
||||
@@ -3,8 +3,7 @@ use client::{Client, TelemetrySettings};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
|
||||
SemanticVersion, Task, WindowContext,
|
||||
actions, AppContext, AsyncAppContext, Context as _, Global, Model, SemanticVersion, Task,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use paths::remote_servers_dir;
|
||||
@@ -131,16 +130,16 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
workspace.register_action(model, |_, action: &Check, cx| check(action, model, cx));
|
||||
|
||||
workspace.register_action(|_, action, cx| {
|
||||
workspace.register_action(model, |_, action, cx| {
|
||||
view_release_notes(action, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
let version = release_channel::AppVersion::global(cx);
|
||||
let auto_updater = cx.new_model(|cx| {
|
||||
let auto_updater = cx.new_model(|model, cx| {
|
||||
let updater = AutoUpdater::new(version, http_client);
|
||||
|
||||
let poll_for_updates = ReleaseChannel::try_global(cx)
|
||||
@@ -153,7 +152,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
{
|
||||
let mut update_subscription = AutoUpdateSetting::get_global(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
.then(|| updater.start_polling(model, cx));
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |updater, cx| {
|
||||
if AutoUpdateSetting::get_global(cx).0 {
|
||||
@@ -172,7 +171,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
|
||||
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
pub fn check(_: &Check, window: &mut gpui::Window, cx: &mut gpui::AppContext) {
|
||||
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
@@ -201,7 +200,7 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
}
|
||||
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
updater.update(cx, |updater, model, cx| updater.poll(model, cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
@@ -249,30 +248,30 @@ impl AutoUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
pub fn start_polling(&self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
loop {
|
||||
this.update(&mut cx, |this, cx| this.poll(cx))?;
|
||||
this.update(&mut cx, |this, model, cx| this.poll(model, cx))?;
|
||||
cx.background_executor().timer(POLL_INTERVAL).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn poll(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if self.pending_poll.is_some() || self.status.is_updated() {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
|
||||
self.pending_poll = Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let result = Self::update(this.upgrade()?, cx.clone()).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_poll = None;
|
||||
if let Err(error) = result {
|
||||
log::error!("auto-update failed: error:{:?}", error);
|
||||
this.status = AutoUpdateStatus::Errored;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
@@ -287,9 +286,9 @@ impl AutoUpdater {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn dismiss_error(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
self.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
|
||||
@@ -432,15 +431,16 @@ impl AutoUpdater {
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
let (client, current_version, release_channel) =
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
model.notify(cx);
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let release =
|
||||
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
|
||||
@@ -455,16 +455,16 @@ impl AutoUpdater {
|
||||
};
|
||||
|
||||
if !should_download {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Downloading;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
@@ -485,9 +485,9 @@ impl AutoUpdater {
|
||||
let downloaded_asset = temp_dir.path().join(filename);
|
||||
download_release(&downloaded_asset, release, client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
let binary_path = match OS {
|
||||
@@ -496,11 +496,11 @@ impl AutoUpdater {
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated { binary_path };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,7 @@ mod update_notification;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
|
||||
use gpui::{actions, prelude::*, AppContext, Model, SharedString, View};
|
||||
use http_client::HttpClient;
|
||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
@@ -18,8 +18,8 @@ actions!(auto_update, [ViewReleaseNotesLocally]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, cx);
|
||||
workspace.register_action(model, |workspace, _: &ViewReleaseNotesLocally, cx| {
|
||||
view_release_notes_locally(workspace, model, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
@@ -31,7 +31,11 @@ struct ReleaseNotesBody {
|
||||
release_notes: String,
|
||||
}
|
||||
|
||||
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
fn view_release_notes_locally(
|
||||
workspace: &mut Workspace,
|
||||
model: &Model<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let release_channel = ReleaseChannel::global(cx);
|
||||
|
||||
let url = match release_channel {
|
||||
@@ -60,7 +64,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.language_for_name("Markdown");
|
||||
|
||||
workspace
|
||||
.with_local_workspace(cx, move |_, cx| {
|
||||
.with_local_workspace(model, cx, move |_, model, cx| {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let response = client.get(&url, Default::default(), true).await;
|
||||
@@ -78,27 +82,29 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let buffer = project.update(cx, |project, model, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
});
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer =
|
||||
cx.new_model(|model, cx| MultiBuffer::singleton(buffer, model, cx));
|
||||
|
||||
let tab_description = SharedString::from(body.title.to_string());
|
||||
let editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, cx)
|
||||
let editor = cx.new_model(|model, cx| {
|
||||
Editor::for_multibuffer(buffer, Some(project), true, model, cx)
|
||||
});
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
let view: Model<MarkdownPreviewView> = MarkdownPreviewView::new(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace_handle,
|
||||
language_registry,
|
||||
Some(tab_description),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(
|
||||
@@ -107,7 +113,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -117,7 +123,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
pub fn notify_of_any_new_update(model: &Model<Workspace>, cx: &mut AppContext) -> Option<()> {
|
||||
let updater = AutoUpdater::get(cx)?;
|
||||
let version = updater.read(cx).current_version();
|
||||
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
|
||||
@@ -130,9 +136,9 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
|
||||
workspace.show_notification(
|
||||
NotificationId::unique::<UpdateNotification>(),
|
||||
cx,
|
||||
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
|
||||
|cx| cx.new_model(|_, _| UpdateNotification::new(version, workspace_handle)),
|
||||
);
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.update(cx, |updater, model, cx| {
|
||||
updater
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, WeakView,
|
||||
div, AppContext, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SemanticVersion, StatefulInteractiveElement, Styled, WeakView,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use release_channel::ReleaseChannel;
|
||||
@@ -12,13 +12,18 @@ use workspace::{
|
||||
|
||||
pub struct UpdateNotification {
|
||||
version: SemanticVersion,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: WeakModel<Workspace>,
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
let app_name = ReleaseChannel::global(cx).display_name();
|
||||
|
||||
v_flex()
|
||||
@@ -37,7 +42,10 @@ impl Render for UpdateNotification {
|
||||
.id("cancel")
|
||||
.child(Icon::new(IconName::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
.on_click(
|
||||
model
|
||||
.listener(|this, model, _, cx| this.dismiss(&menu::Cancel, cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -45,10 +53,10 @@ impl Render for UpdateNotification {
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
.on_click(model.listener(|this, model, _, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
crate::view_release_notes_locally(workspace, cx);
|
||||
.update(cx, |workspace, model, cx| {
|
||||
crate::view_release_notes_locally(workspace, model, cx);
|
||||
})
|
||||
.log_err();
|
||||
this.dismiss(&menu::Cancel, cx)
|
||||
@@ -58,11 +66,11 @@ impl Render for UpdateNotification {
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakView<Workspace>) -> Self {
|
||||
pub fn new(version: SemanticVersion, workspace: WeakModel<Workspace>) -> Self {
|
||||
Self { version, workspace }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
pub fn dismiss(&mut self, _: &Cancel, model: &Model<Self>, cx: &mut AppContext) {
|
||||
model.emit(DismissEvent, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Element, EventEmitter, FocusableView, IntoElement, ParentElement, Render, StyledText,
|
||||
Subscription, ViewContext,
|
||||
AppContext, Element, EventEmitter, FocusableView, IntoElement, Model, ParentElement, Render,
|
||||
StyledText, Subscription,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use std::cmp;
|
||||
@@ -37,7 +37,12 @@ impl Breadcrumbs {
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut AppContext,
|
||||
) -> impl IntoElement {
|
||||
const MAX_SEGMENTS: usize = 12;
|
||||
let element = h_flex().text_ui(cx);
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
@@ -64,7 +69,7 @@ impl Render for Breadcrumbs {
|
||||
}
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
let mut text_style = cx.text_style();
|
||||
let mut text_style = window.text_style();
|
||||
if let Some(font) = segment.font {
|
||||
text_style.font_family = font.family;
|
||||
text_style.font_features = font.features;
|
||||
@@ -94,23 +99,25 @@ impl Render for Breadcrumbs {
|
||||
let editor = editor.clone();
|
||||
move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, model, cx)
|
||||
}
|
||||
}
|
||||
})
|
||||
.tooltip(move |cx| {
|
||||
.tooltip(move |window, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let focus_handle = editor.read(cx).focus_handle(cx);
|
||||
Tooltip::for_action_in(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
Tooltip::for_action(
|
||||
"Show symbol outline",
|
||||
&editor::actions::ToggleOutline,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -128,26 +135,30 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
self.active_item = None;
|
||||
|
||||
let Some(item) = active_pane_item else {
|
||||
return ToolbarItemLocation::Hidden;
|
||||
};
|
||||
|
||||
let this = cx.view().downgrade();
|
||||
let this = model.downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
if let Some(active_item) = this.active_item.as_ref() {
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
))
|
||||
model.emit(
|
||||
cx,
|
||||
ToolbarItemEvent::ChangeLocation(
|
||||
active_item.breadcrumb_location(cx),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
@@ -158,7 +169,7 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
item.breadcrumb_location(cx)
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &Model<Self>, _: &mut AppContext) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ test-support = [
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"livekit_client/test-support",
|
||||
"livekit_client_macos/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
|
||||
|
||||
/// Whether your current project should be shared when joining an empty channel.
|
||||
///
|
||||
/// Default: false
|
||||
/// Default: true
|
||||
pub share_on_join: Option<bool>,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
@@ -34,7 +34,7 @@ pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppConte
|
||||
);
|
||||
CallSettings::register(cx);
|
||||
|
||||
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
|
||||
cx.set_global(GlobalActiveCall(active_call));
|
||||
}
|
||||
|
||||
@@ -96,7 +96,12 @@ pub struct ActiveCall {
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
model: &Model<Self>,
|
||||
_: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
@@ -105,8 +110,8 @@ impl ActiveCall {
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
|
||||
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
@@ -122,22 +127,22 @@ impl ActiveCall {
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, cx)
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
})?;
|
||||
|
||||
@@ -149,7 +154,7 @@ impl ActiveCall {
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
@@ -174,12 +179,13 @@ impl ActiveCall {
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
@@ -192,20 +198,22 @@ impl ActiveCall {
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
|
||||
.await?,
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, move |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
room.update(&mut cx, move |room, model, cx| {
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -214,8 +222,8 @@ impl ActiveCall {
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(move |this, mut cx| async move {
|
||||
let room = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
@@ -229,14 +237,16 @@ impl ActiveCall {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(Some(room.clone()), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
|
||||
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
@@ -247,18 +257,20 @@ impl ActiveCall {
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, _model, cx| {
|
||||
this.report_call_event("invite", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result
|
||||
})
|
||||
@@ -267,7 +279,8 @@ impl ActiveCall {
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
_: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
@@ -291,7 +304,11 @@ impl ActiveCall {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn accept_incoming(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
@@ -313,18 +330,20 @@ impl ActiveCall {
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
@@ -341,13 +360,14 @@ impl ActiveCall {
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
room.update(cx, |room, model, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,27 +381,29 @@ impl ActiveCall {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
cx.emit(Event::RoomLeft { channel_id });
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
model.emit(Event::RoomLeft { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| room.leave(model, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -390,11 +412,12 @@ impl ActiveCall {
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
@@ -403,11 +426,14 @@ impl ActiveCall {
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.unshare_project(project, model, cx)
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
@@ -420,12 +446,13 @@ impl ActiveCall {
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
@@ -434,26 +461,29 @@ impl ActiveCall {
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<Model<Room>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&room, |this, room, cx| {
|
||||
model.observe(&room, cx, |this, room, model, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, cx).detach_and_log_err(cx);
|
||||
this.set_room(None, model, cx).detach_and_log_err(cx)
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}),
|
||||
model.subscribe(&room, cx, |_, _, event, model, cx| {
|
||||
model.emit(event.clone(), cx)
|
||||
}),
|
||||
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
@@ -461,8 +491,10 @@ impl ActiveCall {
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
cx.emit(Event::RoomJoined { channel_id });
|
||||
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
|
||||
model.emit(Event::RoomJoined { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.set_location(location.as_ref(), model, cx)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
|
||||
@@ -13,9 +13,7 @@ use client::{
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use livekit::{
|
||||
@@ -121,11 +119,12 @@ impl Room {
|
||||
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
spawn_room_connection(livekit_connection_info, cx);
|
||||
spawn_room_connection(livekit_connection_info, model, cx);
|
||||
|
||||
let maintain_connection = cx.spawn({
|
||||
let maintain_connection = model.spawn(cx, {
|
||||
let client = client.clone();
|
||||
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
|
||||
});
|
||||
@@ -147,11 +146,11 @@ impl Room {
|
||||
pending_participants: Default::default(),
|
||||
pending_call_count: 0,
|
||||
client_subscriptions: vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
|
||||
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
|
||||
],
|
||||
_subscriptions: vec![
|
||||
cx.on_release(Self::released),
|
||||
cx.on_app_quit(Self::app_will_quit),
|
||||
model.on_release(cx, Self::released),
|
||||
model.on_app_quit(cx, Self::app_will_quit),
|
||||
],
|
||||
leave_when_empty: false,
|
||||
pending_room_update: None,
|
||||
@@ -174,13 +173,14 @@ impl Room {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let response = client.request(proto::CreateRoom {}).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
let mut room = Self::new(
|
||||
room_proto.id,
|
||||
None,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
if let Some(participant) = room_proto.participants.first() {
|
||||
@@ -191,8 +191,8 @@ impl Room {
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
let initial_project_id = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project.clone(), cx)
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
Some(initial_project_id)
|
||||
@@ -201,9 +201,9 @@ impl Room {
|
||||
};
|
||||
|
||||
let did_join = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await;
|
||||
match did_join {
|
||||
@@ -251,7 +251,11 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Future<Output = ()> {
|
||||
let task = if self.status.is_online() {
|
||||
let leave = self.leave_internal(cx);
|
||||
Some(cx.background_executor().spawn(async move {
|
||||
@@ -279,19 +283,20 @@ impl Room {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Model<Self>> {
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
response.channel_id.map(ChannelId),
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = room.channel_id.is_none();
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
room.apply_room_update(room_proto, model, cx)?;
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(room)
|
||||
@@ -305,8 +310,8 @@ impl Room {
|
||||
&& self.pending_call_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
@@ -330,16 +335,16 @@ impl Room {
|
||||
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
|
||||
for project in self.shared_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.unshare(cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.unshare(model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
for project in self.joined_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.disconnected_from_host(cx);
|
||||
project.close(cx);
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.disconnected_from_host(model, cx);
|
||||
project.close(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -369,9 +374,9 @@ impl Room {
|
||||
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Rejoining;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
// Wait for client to re-establish a connection to the server.
|
||||
@@ -385,7 +390,8 @@ impl Room {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade() else { break };
|
||||
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
|
||||
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
|
||||
{
|
||||
Ok(task) => {
|
||||
if task.await.log_err().is_some() {
|
||||
return true;
|
||||
@@ -434,14 +440,15 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
|
||||
.await?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut reshared_projects = Vec::new();
|
||||
let mut rejoined_projects = Vec::new();
|
||||
@@ -489,27 +496,29 @@ impl Room {
|
||||
rejoined_projects,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = response.await?;
|
||||
let message_id = response.message_id;
|
||||
let response = response.payload;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)?;
|
||||
this.apply_room_update(room_proto, model, cx)?;
|
||||
|
||||
for reshared_project in response.reshared_projects {
|
||||
if let Some(project) = projects.get(&reshared_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.reshared(reshared_project, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.reshared(reshared_project, model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for rejoined_project in response.rejoined_projects {
|
||||
if let Some(project) = projects.get(&rejoined_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.rejoined(rejoined_project, message_id, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project
|
||||
.rejoined(rejoined_project, message_id, model, cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -567,12 +576,13 @@ impl Room {
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
role: proto::ChannelRole,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
let role = role.into();
|
||||
cx.spawn(|_, _| async move {
|
||||
cx.spawn(|_| async move {
|
||||
client
|
||||
.request(proto::SetRoomParticipantRole {
|
||||
room_id,
|
||||
@@ -644,19 +654,26 @@ impl Room {
|
||||
.payload
|
||||
.room
|
||||
.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.apply_room_update(room, model, cx)
|
||||
})?
|
||||
}
|
||||
|
||||
fn apply_room_update(&mut self, room: proto::Room, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
fn apply_room_update(
|
||||
&mut self,
|
||||
room: proto::Room,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
log::trace!(
|
||||
"client {:?}. room update: {:?}",
|
||||
self.client.user_id(),
|
||||
&room
|
||||
);
|
||||
|
||||
self.pending_room_update = Some(self.start_room_connection(room, cx));
|
||||
self.pending_room_update = Some(self.start_room_connection(room, model, cx));
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -675,7 +692,8 @@ impl Room {
|
||||
fn start_room_connection(
|
||||
&self,
|
||||
mut room: proto::Room,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
@@ -684,7 +702,8 @@ impl Room {
|
||||
fn start_room_connection(
|
||||
&self,
|
||||
mut room: proto::Room,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
// Filter ourselves out from the room's participants.
|
||||
let local_participant_ix = room
|
||||
@@ -706,17 +725,17 @@ impl Room {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
self.user_store.update(cx, move |user_store, model, cx| {
|
||||
(
|
||||
user_store.get_users(remote_participant_user_ids, cx),
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
user_store.get_users(remote_participant_user_ids, model, cx),
|
||||
user_store.get_users(pending_participant_user_ids, model, cx),
|
||||
)
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.participant_user_ids.clear();
|
||||
|
||||
if let Some(participant) = local_participant {
|
||||
@@ -728,18 +747,20 @@ impl Room {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
for project in mem::take(&mut this.shared_projects) {
|
||||
if let Some(project) = project.upgrade() {
|
||||
this.unshare_project(project, cx).log_err();
|
||||
this.unshare_project(project, model, cx).log_err();
|
||||
}
|
||||
}
|
||||
this.local_participant.projects.clear();
|
||||
if let Some(livekit_room) = &mut this.live_kit {
|
||||
livekit_room.stop_publishing(cx);
|
||||
livekit_room.stop_publishing(model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.set_role(role, model, cx)
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -778,20 +799,23 @@ impl Room {
|
||||
|
||||
for project in &participant.projects {
|
||||
if !old_projects.contains(&project.id) {
|
||||
cx.emit(Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update(cx, |project, model, cx| {
|
||||
if project.remote_id() == Some(*unshared_project_id) {
|
||||
project.disconnected_from_host(cx);
|
||||
project.disconnected_from_host(model, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -801,9 +825,12 @@ impl Room {
|
||||
false
|
||||
}
|
||||
});
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let role = participant.role();
|
||||
@@ -820,9 +847,12 @@ impl Room {
|
||||
{
|
||||
remote_participant.location = location;
|
||||
remote_participant.role = role;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.remote_participants.insert(
|
||||
@@ -857,6 +887,7 @@ impl Room {
|
||||
publication,
|
||||
participant: livekit_participant.clone(),
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
.warn_on_err();
|
||||
@@ -872,9 +903,12 @@ impl Room {
|
||||
true
|
||||
} else {
|
||||
for project in &participant.projects {
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -912,21 +946,21 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
this.leave(cx).detach();
|
||||
this.leave(model, cx).detach();
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
this.user_store.update(cx, |user_store, model, cx| {
|
||||
let participant_indices_by_user_id = this
|
||||
.remote_participants
|
||||
.iter()
|
||||
.map(|(user_id, participant)| (*user_id, participant.participant_index))
|
||||
.collect();
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, cx);
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -935,7 +969,8 @@ impl Room {
|
||||
fn livekit_room_updated(
|
||||
&mut self,
|
||||
event: RoomEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
log::trace!(
|
||||
"client {:?}. livekit event: {:?}",
|
||||
@@ -963,17 +998,23 @@ impl Room {
|
||||
}
|
||||
match track {
|
||||
livekit::track::RemoteTrack::Audio(track) => {
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let stream = play_remote_audio_track(&track, cx.background_executor())?;
|
||||
participant.audio_tracks.insert(track_id, (track, stream));
|
||||
participant.muted = publication.is_muted();
|
||||
}
|
||||
livekit::track::RemoteTrack::Video(track) => {
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
participant.video_tracks.insert(track_id, track);
|
||||
}
|
||||
}
|
||||
@@ -994,15 +1035,21 @@ impl Room {
|
||||
livekit::track::RemoteTrack::Audio(track) => {
|
||||
participant.audio_tracks.remove(&track.sid());
|
||||
participant.muted = true;
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
livekit::track::RemoteTrack::Video(track) => {
|
||||
participant.video_tracks.remove(&track.sid());
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1080,12 +1127,12 @@ impl Room {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
RoomEvent::Disconnected { reason } => {
|
||||
log::info!("disconnected from room: {reason:?}");
|
||||
self.leave(cx).detach_and_log_err(cx);
|
||||
self.leave(model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1113,17 +1160,18 @@ impl Room {
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project_id: Option<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
self.pending_call_count += 1;
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::Call {
|
||||
room_id,
|
||||
@@ -1131,10 +1179,10 @@ impl Room {
|
||||
initial_project_id,
|
||||
})
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_call_count -= 1;
|
||||
if this.should_leave() {
|
||||
this.leave(cx).detach_and_log_err(cx);
|
||||
this.leave(model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})?;
|
||||
result?;
|
||||
@@ -1147,16 +1195,17 @@ impl Room {
|
||||
id: u64,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let project =
|
||||
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, _, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
!project.read(cx).is_disconnected(cx)
|
||||
@@ -1173,7 +1222,8 @@ impl Room {
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some(project_id) = project.read(cx).remote_id() {
|
||||
return Task::ready(Ok(project_id));
|
||||
@@ -1185,19 +1235,19 @@ impl Room {
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
project.shared(response.project_id, cx)
|
||||
project.update(&mut cx, |project, model, cx| {
|
||||
project.shared(response.project_id, model, cx)
|
||||
})??;
|
||||
|
||||
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.shared_projects.insert(project.downgrade());
|
||||
let active_project = this.local_participant.active_project.as_ref();
|
||||
if active_project.map_or(false, |location| *location == project) {
|
||||
this.set_location(Some(&project), cx)
|
||||
this.set_location(Some(&project), model, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1211,7 +1261,8 @@ impl Room {
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
@@ -1219,10 +1270,11 @@ impl Room {
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))?;
|
||||
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
|
||||
|
||||
if self.local_participant.active_project == Some(project.downgrade()) {
|
||||
self.set_location(Some(&project), cx).detach_and_log_err(cx);
|
||||
self.set_location(Some(&project), model, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1230,7 +1282,8 @@ impl Room {
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
@@ -1254,7 +1307,7 @@ impl Room {
|
||||
proto::participant_location::Variant::External(proto::participant_location::External {})
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::UpdateParticipantLocation {
|
||||
@@ -1288,12 +1341,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
@@ -1330,13 +1377,21 @@ impl Room {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_microphone(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Windows is not supported yet")))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[track_caller]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_microphone(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
@@ -1344,13 +1399,13 @@ impl Room {
|
||||
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.microphone_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
(live_kit.room.local_participant(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let (track, stream) = capture_local_audio_track(cx.background_executor())?.await;
|
||||
|
||||
let publication = participant
|
||||
@@ -1363,7 +1418,7 @@ impl Room {
|
||||
)
|
||||
.await
|
||||
.map_err(|error| anyhow!("failed to publish track: {error}"));
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1394,7 +1449,7 @@ impl Room {
|
||||
track_publication: publication,
|
||||
_stream: Box::new(stream),
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1403,7 +1458,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.microphone_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1413,12 +1468,12 @@ impl Room {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
Task::ready(Err(anyhow!("Windows is not supported yet")))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
@@ -1429,7 +1484,7 @@ impl Room {
|
||||
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
(live_kit.room.local_participant(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
@@ -1437,7 +1492,7 @@ impl Room {
|
||||
|
||||
let sources = cx.screen_capture_sources();
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let sources = sources.await??;
|
||||
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
|
||||
|
||||
@@ -1455,7 +1510,7 @@ impl Room {
|
||||
.await
|
||||
.map_err(|error| anyhow!("error publishing screen track {error:?}"));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1483,7 +1538,7 @@ impl Room {
|
||||
track_publication: publication,
|
||||
_stream: Box::new(stream),
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::StartScreenshare, cx);
|
||||
@@ -1494,7 +1549,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1503,7 +1558,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
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;
|
||||
@@ -1519,17 +1574,17 @@ impl Room {
|
||||
let muted = live_kit.muted_by_user;
|
||||
let should_undeafen = was_deafened && !live_kit.deafened;
|
||||
|
||||
if let Some(task) = self.set_mute(muted, cx) {
|
||||
if let Some(task) = self.set_mute(muted, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_undeafen {
|
||||
self.set_deafened(false, cx);
|
||||
self.set_deafened(false, model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
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.
|
||||
@@ -1537,17 +1592,17 @@ impl Room {
|
||||
live_kit.deafened = deafened;
|
||||
let should_change_mute = !live_kit.muted_by_user;
|
||||
|
||||
self.set_deafened(deafened, cx);
|
||||
self.set_deafened(deafened, model, cx);
|
||||
|
||||
if should_change_mute {
|
||||
if let Some(task) = self.set_mute(deafened, cx) {
|
||||
if let Some(task) = self.set_mute(deafened, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
@@ -1559,7 +1614,7 @@ impl Room {
|
||||
match mem::take(&mut live_kit.screen_track) {
|
||||
LocalTrack::None => Err(anyhow!("screen was not shared")),
|
||||
LocalTrack::Pending { .. } => {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
LocalTrack::Published {
|
||||
@@ -1572,7 +1627,7 @@ impl Room {
|
||||
cx.background_executor()
|
||||
.spawn(async move { local_participant.unpublish_track(&sid).await })
|
||||
.detach_and_log_err(cx);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||
Ok(())
|
||||
@@ -1580,11 +1635,16 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_deafened(&mut self, deafened: bool, cx: &mut ModelContext<Self>) -> Option<()> {
|
||||
fn set_deafened(
|
||||
&mut self,
|
||||
deafened: bool,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
for (_, participant) in live_kit.room.remote_participants() {
|
||||
for (_, publication) in participant.track_publications() {
|
||||
if publication.kind() == TrackKind::Audio {
|
||||
@@ -1600,10 +1660,11 @@ impl Room {
|
||||
fn set_mute(
|
||||
&mut self,
|
||||
should_mute: bool,
|
||||
cx: &mut ModelContext<Room>,
|
||||
model: &Model<Room>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
if should_mute {
|
||||
Audio::play_sound(Sound::Mute, cx);
|
||||
@@ -1616,7 +1677,7 @@ impl Room {
|
||||
if should_mute {
|
||||
None
|
||||
} else {
|
||||
Some(self.share_microphone(cx))
|
||||
Some(self.share_microphone(model, cx))
|
||||
}
|
||||
}
|
||||
LocalTrack::Pending { .. } => None,
|
||||
@@ -1640,59 +1701,62 @@ impl Room {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn spawn_room_connection(
|
||||
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
cx: &mut ModelContext<'_, Room>,
|
||||
model: &Model<Room>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn spawn_room_connection(
|
||||
livekit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
cx: &mut ModelContext<'_, Room>,
|
||||
model: &Model<Room>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some(connection_info) = livekit_connection_info {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (room, mut events) = livekit::Room::connect(
|
||||
&connection_info.server_url,
|
||||
&connection_info.token,
|
||||
RoomOptions::default(),
|
||||
)
|
||||
.await?;
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
let (room, mut events) = livekit::Room::connect(
|
||||
&connection_info.server_url,
|
||||
&connection_info.token,
|
||||
RoomOptions::default(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let _handle_updates = cx.spawn(|this, mut cx| async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
if this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.livekit_room_updated(event, cx).warn_on_err();
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let _handle_updates = model.spawn(cx, |this, mut cx| async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
if this
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
this.livekit_room_updated(event, model, cx).warn_on_err();
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let muted_by_user = Room::mute_on_join(cx);
|
||||
this.live_kit = Some(LiveKitRoom {
|
||||
room: Arc::new(room),
|
||||
screen_track: LocalTrack::None,
|
||||
microphone_track: LocalTrack::None,
|
||||
next_publish_id: 0,
|
||||
muted_by_user,
|
||||
deafened: false,
|
||||
speaking: false,
|
||||
_handle_updates,
|
||||
});
|
||||
|
||||
if !muted_by_user && this.can_use_microphone(cx) {
|
||||
this.share_microphone(model, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
});
|
||||
|
||||
let muted_by_user = Room::mute_on_join(cx);
|
||||
this.live_kit = Some(LiveKitRoom {
|
||||
room: Arc::new(room),
|
||||
screen_track: LocalTrack::None,
|
||||
microphone_track: LocalTrack::None,
|
||||
next_publish_id: 0,
|
||||
muted_by_user,
|
||||
deafened: false,
|
||||
speaking: false,
|
||||
_handle_updates,
|
||||
});
|
||||
|
||||
if !muted_by_user && this.can_use_microphone(cx) {
|
||||
this.share_microphone(cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1710,17 +1774,17 @@ struct LiveKitRoom {
|
||||
|
||||
impl LiveKitRoom {
|
||||
#[cfg(target_os = "windows")]
|
||||
fn stop_publishing(&mut self, _cx: &mut ModelContext<Room>) {}
|
||||
fn stop_publishing(&mut self, model: &Model<Room>, _cx: &mut AppContext) {}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
|
||||
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
|
||||
let mut tracks_to_unpublish = Vec::new();
|
||||
if let LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
|
||||
{
|
||||
tracks_to_unpublish.push(track_publication.sid());
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
if let LocalTrack::Published {
|
||||
@@ -1728,7 +1792,7 @@ impl LiveKitRoom {
|
||||
} = mem::replace(&mut self.screen_track, LocalTrack::None)
|
||||
{
|
||||
tracks_to_unpublish.push(track_publication.sid());
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
let participant = self.room.local_participant();
|
||||
|
||||
@@ -8,8 +8,8 @@ use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAY
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
@@ -27,7 +27,7 @@ impl Global for GlobalActiveCall {}
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
CallSettings::register(cx);
|
||||
|
||||
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||
let active_call = cx.new_model(|model, cx| ActiveCall::new(client, user_store, model, cx));
|
||||
cx.set_global(GlobalActiveCall(active_call));
|
||||
}
|
||||
|
||||
@@ -89,7 +89,12 @@ pub struct ActiveCall {
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
|
||||
fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
@@ -98,8 +103,8 @@ impl ActiveCall {
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
|
||||
client.add_request_handler(model.downgrade(), Self::handle_incoming_call),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
@@ -115,22 +120,22 @@ impl ActiveCall {
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, cx)
|
||||
.update(&mut cx, |user_store, model, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
})?;
|
||||
|
||||
@@ -142,7 +147,7 @@ impl ActiveCall {
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
@@ -167,12 +172,13 @@ impl ActiveCall {
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
@@ -185,20 +191,22 @@ impl ActiveCall {
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
|
||||
.await?,
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project, model, cx)
|
||||
})?
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, move |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
room.update(&mut cx, move |room, model, cx| {
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -207,8 +215,8 @@ impl ActiveCall {
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(move |this, mut cx| async move {
|
||||
let room = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
@@ -222,14 +230,16 @@ impl ActiveCall {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(Some(room.clone()), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
|
||||
this.update(&mut cx, |this, _, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
@@ -240,18 +250,20 @@ impl ActiveCall {
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("invite", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result
|
||||
})
|
||||
@@ -260,7 +272,8 @@ impl ActiveCall {
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
@@ -284,7 +297,11 @@ impl ActiveCall {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn accept_incoming(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
@@ -306,18 +323,20 @@ impl ActiveCall {
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn decline_incoming(&mut self, _: &Model<Self>, _: &mut AppContext) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
@@ -334,13 +353,14 @@ impl ActiveCall {
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
room.update(cx, |room, model, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,27 +374,29 @@ impl ActiveCall {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.set_room(room.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
})?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
pub fn hang_up(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.report_call_event("hang up", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
cx.emit(Event::RoomLeft { channel_id });
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
model.emit(Event::RoomLeft { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| room.leave(model, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -383,11 +405,12 @@ impl ActiveCall {
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
room.update(cx, |room, model, cx| room.share_project(project, model, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
@@ -396,11 +419,14 @@ impl ActiveCall {
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.unshare_project(project, model, cx)
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
@@ -413,12 +439,13 @@ impl ActiveCall {
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||
return room.update(cx, |room, model, cx| room.set_location(project, model, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
@@ -427,26 +454,29 @@ impl ActiveCall {
|
||||
fn set_room(
|
||||
&mut self,
|
||||
room: Option<Model<Room>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&room, |this, room, cx| {
|
||||
model.observe(&room, cx, |this, room, model, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, cx).detach_and_log_err(cx);
|
||||
this.set_room(None, model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}),
|
||||
model.subscribe(&room, cx, |_, _, event, model, cx| {
|
||||
model.emit(event.clone(), cx)
|
||||
}),
|
||||
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
@@ -454,8 +484,10 @@ impl ActiveCall {
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
cx.emit(Event::RoomJoined { channel_id });
|
||||
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
|
||||
model.emit(Event::RoomJoined { channel_id }, cx);
|
||||
room.update(cx, |room, model, cx| {
|
||||
room.set_location(location.as_ref(), model, cx)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
|
||||
@@ -11,9 +11,7 @@ use client::{
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
@@ -110,14 +108,15 @@ impl Room {
|
||||
live_kit_connection_info: Option<proto::LiveKitConnectionInfo>,
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
|
||||
let room = livekit_client_macos::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 {
|
||||
let _maintain_room = model.spawn(cx, |this, mut cx| async move {
|
||||
while let Some(status) = status.next().await {
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
@@ -126,14 +125,14 @@ impl Room {
|
||||
};
|
||||
|
||||
if status == livekit_client_macos::ConnectionState::Disconnected {
|
||||
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
|
||||
this.update(&mut cx, |this, model, cx| this.leave(model, cx).log_err())
|
||||
.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _handle_updates = cx.spawn({
|
||||
let _handle_updates = model.spawn(cx, {
|
||||
let room = room.clone();
|
||||
move |this, mut cx| async move {
|
||||
let mut updates = room.updates();
|
||||
@@ -144,8 +143,8 @@ impl Room {
|
||||
break;
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.live_kit_room_updated(update, cx).log_err()
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.live_kit_room_updated(update, model, cx).log_err()
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -153,21 +152,22 @@ impl Room {
|
||||
});
|
||||
|
||||
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(cx) {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(cx);
|
||||
model
|
||||
.spawn(cx, |this, mut cx| async move {
|
||||
connect.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
if this.can_use_microphone(cx) {
|
||||
if let Some(live_kit) = &this.live_kit {
|
||||
if !live_kit.muted_by_user && !live_kit.deafened {
|
||||
return this.share_microphone(model, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Task::ready(Ok(()))
|
||||
})?
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(LiveKitRoom {
|
||||
room,
|
||||
@@ -184,7 +184,7 @@ impl Room {
|
||||
None
|
||||
};
|
||||
|
||||
let maintain_connection = cx.spawn({
|
||||
let maintain_connection = model.spawn(cx, {
|
||||
let client = client.clone();
|
||||
move |this, cx| Self::maintain_connection(this, client.clone(), cx).log_err()
|
||||
});
|
||||
@@ -206,11 +206,11 @@ impl Room {
|
||||
pending_participants: Default::default(),
|
||||
pending_call_count: 0,
|
||||
client_subscriptions: vec![
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_room_updated)
|
||||
client.add_message_handler(model.downgrade(), Self::handle_room_updated)
|
||||
],
|
||||
_subscriptions: vec![
|
||||
cx.on_release(Self::released),
|
||||
cx.on_app_quit(Self::app_will_quit),
|
||||
model.on_release(cx, Self::released),
|
||||
model.on_app_quit(cx, Self::app_will_quit),
|
||||
],
|
||||
leave_when_empty: false,
|
||||
pending_room_update: None,
|
||||
@@ -233,13 +233,14 @@ impl Room {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let response = client.request(proto::CreateRoom {}).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
let mut room = Self::new(
|
||||
room_proto.id,
|
||||
None,
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
if let Some(participant) = room_proto.participants.first() {
|
||||
@@ -250,8 +251,8 @@ impl Room {
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
let initial_project_id = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project.clone(), cx)
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.share_project(initial_project.clone(), model, cx)
|
||||
})?
|
||||
.await?;
|
||||
Some(initial_project_id)
|
||||
@@ -260,9 +261,9 @@ impl Room {
|
||||
};
|
||||
|
||||
let did_join = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = true;
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
room.call(called_user_id, initial_project_id, model, cx)
|
||||
})?
|
||||
.await;
|
||||
match did_join {
|
||||
@@ -310,7 +311,11 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(&mut self, cx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> impl Future<Output = ()> {
|
||||
let task = if self.status.is_online() {
|
||||
let leave = self.leave_internal(cx);
|
||||
Some(cx.background_executor().spawn(async move {
|
||||
@@ -338,19 +343,20 @@ impl Room {
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Model<Self>> {
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.new_model(|cx| {
|
||||
let room = cx.new_model(|model, cx| {
|
||||
Self::new(
|
||||
room_proto.id,
|
||||
response.channel_id.map(ChannelId),
|
||||
response.live_kit_connection_info,
|
||||
client,
|
||||
user_store,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.update(&mut cx, |room, model, cx| {
|
||||
room.leave_when_empty = room.channel_id.is_none();
|
||||
room.apply_room_update(room_proto, cx)?;
|
||||
room.apply_room_update(room_proto, model, cx)?;
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
Ok(room)
|
||||
@@ -364,8 +370,8 @@ impl Room {
|
||||
&& self.pending_call_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
pub(crate) fn leave(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
model.notify(cx);
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
@@ -389,16 +395,16 @@ impl Room {
|
||||
pub(crate) fn clear_state(&mut self, cx: &mut AppContext) {
|
||||
for project in self.shared_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.unshare(cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.unshare(model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
for project in self.joined_projects.drain() {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.disconnected_from_host(cx);
|
||||
project.close(cx);
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.disconnected_from_host(model, cx);
|
||||
project.close(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -428,9 +434,9 @@ impl Room {
|
||||
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Rejoining;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
// Wait for client to re-establish a connection to the server.
|
||||
@@ -444,7 +450,8 @@ impl Room {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade() else { break };
|
||||
match this.update(&mut cx, |this, cx| this.rejoin(cx)) {
|
||||
match this.update(&mut cx, |this, model, cx| this.rejoin(model, cx))
|
||||
{
|
||||
Ok(task) => {
|
||||
if task.await.log_err().is_some() {
|
||||
return true;
|
||||
@@ -493,14 +500,15 @@ impl Room {
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade() {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
this.update(&mut cx, |this, cx| this.leave(cx))?.await?;
|
||||
this.update(&mut cx, |this, model, cx| this.leave(model, cx))?
|
||||
.await?;
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut reshared_projects = Vec::new();
|
||||
let mut rejoined_projects = Vec::new();
|
||||
@@ -548,27 +556,29 @@ impl Room {
|
||||
rejoined_projects,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = response.await?;
|
||||
let message_id = response.message_id;
|
||||
let response = response.payload;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)?;
|
||||
this.apply_room_update(room_proto, model, cx)?;
|
||||
|
||||
for reshared_project in response.reshared_projects {
|
||||
if let Some(project) = projects.get(&reshared_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.reshared(reshared_project, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.reshared(reshared_project, model, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for rejoined_project in response.rejoined_projects {
|
||||
if let Some(project) = projects.get(&rejoined_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.rejoined(rejoined_project, message_id, cx).log_err();
|
||||
project.update(cx, |project, model, cx| {
|
||||
project
|
||||
.rejoined(rejoined_project, message_id, model, cx)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -626,12 +636,13 @@ impl Room {
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
role: proto::ChannelRole,
|
||||
cx: &ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
let role = role.into();
|
||||
cx.spawn(|_, _| async move {
|
||||
cx.spawn(|_| async move {
|
||||
client
|
||||
.request(proto::SetRoomParticipantRole {
|
||||
room_id,
|
||||
@@ -703,13 +714,16 @@ impl Room {
|
||||
.payload
|
||||
.room
|
||||
.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| this.apply_room_update(room, cx))?
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.apply_room_update(room, model, cx)
|
||||
})?
|
||||
}
|
||||
|
||||
fn apply_room_update(
|
||||
&mut self,
|
||||
mut room: proto::Room,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
// Filter ourselves out from the room's participants.
|
||||
let local_participant_ix = room
|
||||
@@ -731,18 +745,18 @@ impl Room {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
self.user_store.update(cx, move |user_store, model, cx| {
|
||||
(
|
||||
user_store.get_users(remote_participant_user_ids, cx),
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
user_store.get_users(remote_participant_user_ids, model, cx),
|
||||
user_store.get_users(pending_participant_user_ids, model, cx),
|
||||
)
|
||||
});
|
||||
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
self.pending_room_update = Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.participant_user_ids.clear();
|
||||
|
||||
if let Some(participant) = local_participant {
|
||||
@@ -754,18 +768,20 @@ impl Room {
|
||||
if role == proto::ChannelRole::Guest {
|
||||
for project in mem::take(&mut this.shared_projects) {
|
||||
if let Some(project) = project.upgrade() {
|
||||
this.unshare_project(project, cx).log_err();
|
||||
this.unshare_project(project, model, cx).log_err();
|
||||
}
|
||||
}
|
||||
this.local_participant.projects.clear();
|
||||
if let Some(live_kit_room) = &mut this.live_kit {
|
||||
live_kit_room.stop_publishing(cx);
|
||||
live_kit_room.stop_publishing(model, cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| project.set_role(role, cx));
|
||||
project.update(cx, |project, model, cx| {
|
||||
project.set_role(role, model, cx)
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -799,20 +815,23 @@ impl Room {
|
||||
|
||||
for project in &participant.projects {
|
||||
if !old_projects.contains(&project.id) {
|
||||
cx.emit(Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectShared {
|
||||
owner: user.clone(),
|
||||
project_id: project.id,
|
||||
worktree_root_names: project.worktree_root_names.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
project.update(cx, |project, cx| {
|
||||
project.update(cx, |project, model, cx| {
|
||||
if project.remote_id() == Some(*unshared_project_id) {
|
||||
project.disconnected_from_host(cx);
|
||||
project.disconnected_from_host(model, cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
@@ -822,9 +841,12 @@ impl Room {
|
||||
false
|
||||
}
|
||||
});
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let role = participant.role();
|
||||
@@ -841,9 +863,12 @@ impl Room {
|
||||
{
|
||||
remote_participant.location = location;
|
||||
remote_participant.role = role;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::ParticipantLocationChanged {
|
||||
participant_id: peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.remote_participants.insert(
|
||||
@@ -876,6 +901,7 @@ impl Room {
|
||||
for track in video_tracks {
|
||||
this.live_kit_room_updated(
|
||||
RoomUpdate::SubscribedToRemoteVideoTrack(track),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
@@ -889,6 +915,7 @@ impl Room {
|
||||
track.clone(),
|
||||
publication.clone(),
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
@@ -902,9 +929,12 @@ impl Room {
|
||||
true
|
||||
} else {
|
||||
for project in &participant.projects {
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteProjectUnshared {
|
||||
project_id: project.id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -942,26 +972,26 @@ impl Room {
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
this.leave(cx).detach();
|
||||
this.leave(model, cx).detach();
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
this.user_store.update(cx, |user_store, model, cx| {
|
||||
let participant_indices_by_user_id = this
|
||||
.remote_participants
|
||||
.iter()
|
||||
.map(|(user_id, participant)| (*user_id, participant.participant_index))
|
||||
.collect();
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, cx);
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, model, cx);
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -979,7 +1009,8 @@ impl Room {
|
||||
fn live_kit_room_updated(
|
||||
&mut self,
|
||||
update: RoomUpdate,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
match update {
|
||||
RoomUpdate::SubscribedToRemoteVideoTrack(track) => {
|
||||
@@ -990,9 +1021,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
|
||||
participant.video_tracks.insert(track_id.clone(), track);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::UnsubscribedFromRemoteVideoTrack {
|
||||
@@ -1005,9 +1039,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.video_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteVideoTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::ActiveSpeakersChanged { speakers } => {
|
||||
@@ -1061,9 +1098,12 @@ impl Room {
|
||||
participant.audio_tracks.insert(track_id.clone(), track);
|
||||
participant.muted = publication.is_muted();
|
||||
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::UnsubscribedFromRemoteAudioTrack {
|
||||
@@ -1076,9 +1116,12 @@ impl Room {
|
||||
.get_mut(&user_id)
|
||||
.ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?;
|
||||
participant.audio_tracks.remove(&track_id);
|
||||
cx.emit(Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
});
|
||||
model.emit(
|
||||
Event::RemoteAudioTracksChanged {
|
||||
participant_id: participant.peer_id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
RoomUpdate::LocalAudioTrackUnpublished { publication } => {
|
||||
@@ -1104,7 +1147,7 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1132,17 +1175,18 @@ impl Room {
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project_id: Option<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
let room_id = self.id;
|
||||
self.pending_call_count += 1;
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::Call {
|
||||
room_id,
|
||||
@@ -1150,10 +1194,10 @@ impl Room {
|
||||
initial_project_id,
|
||||
})
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.pending_call_count -= 1;
|
||||
if this.should_leave() {
|
||||
this.leave(cx).detach_and_log_err(cx);
|
||||
this.leave(model, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})?;
|
||||
result?;
|
||||
@@ -1166,16 +1210,17 @@ impl Room {
|
||||
id: u64,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.emit(Event::RemoteProjectJoined { project_id: id }, cx);
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let project =
|
||||
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade() {
|
||||
!project.read(cx).is_disconnected(cx)
|
||||
@@ -1192,7 +1237,8 @@ impl Room {
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some(project_id) = project.read(cx).remote_id() {
|
||||
return Task::ready(Ok(project_id));
|
||||
@@ -1204,19 +1250,19 @@ impl Room {
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
project.shared(response.project_id, cx)
|
||||
project.update(&mut cx, |project, model, cx| {
|
||||
project.shared(response.project_id, model, cx)
|
||||
})??;
|
||||
|
||||
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.shared_projects.insert(project.downgrade());
|
||||
let active_project = this.local_participant.active_project.as_ref();
|
||||
if active_project.map_or(false, |location| *location == project) {
|
||||
this.set_location(Some(&project), cx)
|
||||
this.set_location(Some(&project), model, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1230,7 +1276,8 @@ impl Room {
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: Model<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
@@ -1238,10 +1285,11 @@ impl Room {
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))?;
|
||||
project.update(cx, |this, model, cx| this.unshare(model, cx))?;
|
||||
|
||||
if self.local_participant.active_project == Some(project.downgrade()) {
|
||||
self.set_location(Some(&project), cx).detach_and_log_err(cx);
|
||||
self.set_location(Some(&project), model, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1249,7 +1297,8 @@ impl Room {
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Model<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
@@ -1273,7 +1322,7 @@ impl Room {
|
||||
proto::participant_location::Variant::External(proto::participant_location::External {})
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
cx.background_executor().spawn(async move {
|
||||
client
|
||||
.request(proto::UpdateParticipantLocation {
|
||||
@@ -1307,12 +1356,6 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn muted_by_user(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
.map_or(false, |live_kit| live_kit.muted_by_user)
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
self.live_kit
|
||||
.as_ref()
|
||||
@@ -1340,7 +1383,11 @@ impl Room {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_microphone(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
@@ -1348,18 +1395,18 @@ impl Room {
|
||||
let publish_id = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.microphone_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
publish_id
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let track = LocalAudioTrack::create();
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, _| {
|
||||
.update(&mut cx, |this, model, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_audio_track(track))
|
||||
@@ -1370,7 +1417,7 @@ impl Room {
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1398,7 +1445,7 @@ impl Room {
|
||||
live_kit.microphone_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1407,7 +1454,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.microphone_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1416,7 +1463,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn share_screen(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn share_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
} else if self.is_screen_sharing() {
|
||||
@@ -1426,13 +1473,13 @@ impl Room {
|
||||
let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
(live_kit.room.display_sources(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let publish_track = async {
|
||||
let displays = displays.await?;
|
||||
let display = displays
|
||||
@@ -1441,7 +1488,7 @@ impl Room {
|
||||
let track = LocalVideoTrack::screen_share_for_display(display);
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, _| {
|
||||
.update(&mut cx, |this, model, _| {
|
||||
this.live_kit
|
||||
.as_ref()
|
||||
.map(|live_kit| live_kit.room.publish_video_track(track))
|
||||
@@ -1453,7 +1500,7 @@ impl Room {
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
.update(&mut cx, |this, model, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
@@ -1476,7 +1523,7 @@ impl Room {
|
||||
live_kit.screen_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
};
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::StartScreenshare, cx);
|
||||
@@ -1488,7 +1535,7 @@ impl Room {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -1497,7 +1544,7 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_mute(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
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;
|
||||
@@ -1513,19 +1560,19 @@ impl Room {
|
||||
let muted = live_kit.muted_by_user;
|
||||
let should_undeafen = was_deafened && !live_kit.deafened;
|
||||
|
||||
if let Some(task) = self.set_mute(muted, cx) {
|
||||
if let Some(task) = self.set_mute(muted, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_undeafen {
|
||||
if let Some(task) = self.set_deafened(false, cx) {
|
||||
if let Some(task) = self.set_deafened(false, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_deafen(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn toggle_deafen(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
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.
|
||||
@@ -1533,19 +1580,19 @@ impl Room {
|
||||
live_kit.deafened = deafened;
|
||||
let should_change_mute = !live_kit.muted_by_user;
|
||||
|
||||
if let Some(task) = self.set_deafened(deafened, cx) {
|
||||
if let Some(task) = self.set_deafened(deafened, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
if should_change_mute {
|
||||
if let Some(task) = self.set_mute(deafened, cx) {
|
||||
if let Some(task) = self.set_mute(deafened, model, cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_screen(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn unshare_screen(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
@@ -1557,14 +1604,14 @@ impl Room {
|
||||
match mem::take(&mut live_kit.screen_track) {
|
||||
LocalTrack::None => Err(anyhow!("screen was not shared")),
|
||||
LocalTrack::Pending { .. } => {
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
Ok(())
|
||||
}
|
||||
LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} => {
|
||||
live_kit.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||
Ok(())
|
||||
@@ -1575,10 +1622,11 @@ impl Room {
|
||||
fn set_deafened(
|
||||
&mut self,
|
||||
deafened: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
let mut track_updates = Vec::new();
|
||||
for participant in self.remote_participants.values() {
|
||||
@@ -1609,10 +1657,11 @@ impl Room {
|
||||
fn set_mute(
|
||||
&mut self,
|
||||
should_mute: bool,
|
||||
cx: &mut ModelContext<Room>,
|
||||
model: &Model<Room>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
let live_kit = self.live_kit.as_mut()?;
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
|
||||
if should_mute {
|
||||
Audio::play_sound(Sound::Mute, cx);
|
||||
@@ -1625,7 +1674,7 @@ impl Room {
|
||||
if should_mute {
|
||||
None
|
||||
} else {
|
||||
Some(self.share_microphone(cx))
|
||||
Some(self.share_microphone(model, cx))
|
||||
}
|
||||
}
|
||||
LocalTrack::Pending { .. } => None,
|
||||
@@ -1660,13 +1709,13 @@ struct LiveKitRoom {
|
||||
}
|
||||
|
||||
impl LiveKitRoom {
|
||||
fn stop_publishing(&mut self, cx: &mut ModelContext<Room>) {
|
||||
fn stop_publishing(&mut self, model: &Model<Room>, cx: &mut AppContext) {
|
||||
if let LocalTrack::Published {
|
||||
track_publication, ..
|
||||
} = mem::replace(&mut self.microphone_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
if let LocalTrack::Published {
|
||||
@@ -1674,7 +1723,7 @@ impl LiveKitRoom {
|
||||
} = mem::replace(&mut self.screen_track, LocalTrack::None)
|
||||
{
|
||||
self.room.unpublish_track(track_publication);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Channel, ChannelStore};
|
||||
use anyhow::Result;
|
||||
use client::{ChannelId, Client, Collaborator, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task};
|
||||
use language::proto::serialize_version;
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
@@ -62,17 +62,21 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let buffer = cx.new_model(|model, cx| {
|
||||
let capability = channel_store.read(cx).channel_capability(channel.id);
|
||||
language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text)
|
||||
})?;
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?;
|
||||
buffer.update(&mut cx, |buffer, model, cx| {
|
||||
buffer.apply_ops(operations, model, cx)
|
||||
})?;
|
||||
|
||||
let subscription = client.subscribe_to_entity(channel.id.0)?;
|
||||
|
||||
anyhow::Ok(cx.new_model(|cx| {
|
||||
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||
cx.on_release(Self::release).detach();
|
||||
anyhow::Ok(cx.new_model(|model, cx| {
|
||||
model
|
||||
.subscribe(&buffer, cx, Self::on_buffer_update)
|
||||
.detach();
|
||||
model.on_release(cx, Self::release).detach();
|
||||
let mut this = Self {
|
||||
buffer,
|
||||
buffer_epoch: response.epoch,
|
||||
@@ -81,11 +85,11 @@ impl ChannelBuffer {
|
||||
collaborators: Default::default(),
|
||||
acknowledge_task: None,
|
||||
channel_id: channel.id,
|
||||
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||
subscription: Some(subscription.set_model(model, &mut cx.to_async())),
|
||||
user_store,
|
||||
channel_store,
|
||||
};
|
||||
this.replace_collaborators(response.collaborators, cx);
|
||||
this.replace_collaborators(response.collaborators, model, cx);
|
||||
this
|
||||
})?)
|
||||
}
|
||||
@@ -114,7 +118,8 @@ impl ChannelBuffer {
|
||||
pub(crate) fn replace_collaborators(
|
||||
&mut self,
|
||||
collaborators: Vec<proto::Collaborator>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut new_collaborators = HashMap::default();
|
||||
for collaborator in collaborators {
|
||||
@@ -125,14 +130,14 @@ impl ChannelBuffer {
|
||||
|
||||
for (_, old_collaborator) in &self.collaborators {
|
||||
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id, cx)
|
||||
self.buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id, model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
self.collaborators = new_collaborators;
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
async fn handle_update_channel_buffer(
|
||||
@@ -147,10 +152,10 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
model.notify(cx);
|
||||
this.buffer
|
||||
.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))
|
||||
.update(cx, |buffer, model, cx| buffer.apply_ops(ops, model, cx))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -161,10 +166,10 @@ impl ChannelBuffer {
|
||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.replace_collaborators(message.payload.collaborators, cx);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.replace_collaborators(message.payload.collaborators, model, cx);
|
||||
model.emit(ChannelBufferEvent::CollaboratorsChanged, cx);
|
||||
model.notify(cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -172,7 +177,8 @@ impl ChannelBuffer {
|
||||
&mut self,
|
||||
_: Model<language::Buffer>,
|
||||
event: &language::BufferEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
match event {
|
||||
language::BufferEvent::Operation {
|
||||
@@ -195,20 +201,20 @@ impl ChannelBuffer {
|
||||
.log_err();
|
||||
}
|
||||
language::BufferEvent::Edited => {
|
||||
cx.emit(ChannelBufferEvent::BufferEdited);
|
||||
model.emit(ChannelBufferEvent::BufferEdited, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
|
||||
pub fn acknowledge_buffer_version(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let version = buffer.version();
|
||||
let buffer_id = buffer.remote_id().into();
|
||||
let client = self.client.clone();
|
||||
let epoch = self.epoch();
|
||||
|
||||
self.acknowledge_task = Some(cx.spawn(move |_, cx| async move {
|
||||
self.acknowledge_task = Some(cx.spawn(move |cx| async move {
|
||||
cx.background_executor()
|
||||
.timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
@@ -242,19 +248,19 @@ impl ChannelBuffer {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub(crate) fn disconnect(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
log::info!("channel buffer {} disconnected", self.channel_id);
|
||||
if self.connected {
|
||||
self.connected = false;
|
||||
self.subscription.take();
|
||||
cx.emit(ChannelBufferEvent::Disconnected);
|
||||
cx.notify()
|
||||
model.emit(ChannelBufferEvent::Disconnected, cx);
|
||||
model.notify(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn channel_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(ChannelBufferEvent::ChannelChanged);
|
||||
cx.notify()
|
||||
pub(crate) fn channel_changed(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
model.emit(ChannelBufferEvent::ChannelChanged, cx);
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
|
||||
@@ -7,9 +7,7 @@ use client::{
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Model, Task, WeakModel};
|
||||
use rand::prelude::*;
|
||||
use rpc::AnyProtoClient;
|
||||
use std::{
|
||||
@@ -119,8 +117,8 @@ impl ChannelChat {
|
||||
})
|
||||
.await?;
|
||||
|
||||
let handle = cx.new_model(|cx| {
|
||||
cx.on_release(Self::release).detach();
|
||||
let handle = cx.new_model(|model, cx| {
|
||||
model.on_release(cx, Self::release).detach();
|
||||
Self {
|
||||
channel_id: channel.id,
|
||||
user_store: user_store.clone(),
|
||||
@@ -134,7 +132,7 @@ impl ChannelChat {
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
|
||||
_subscription: subscription.set_model(model, &mut cx.to_async()),
|
||||
}
|
||||
})?;
|
||||
Self::handle_loaded_messages(
|
||||
@@ -171,7 +169,8 @@ impl ChannelChat {
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
message: MessageParams,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<Task<Result<u64>>> {
|
||||
if message.text.trim().is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
@@ -200,6 +199,7 @@ impl ChannelChat {
|
||||
},
|
||||
&(),
|
||||
),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
@@ -207,7 +207,7 @@ impl ChannelChat {
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
|
||||
// todo - handle messages that fail to send (e.g. >1024 chars)
|
||||
Ok(cx.spawn(move |this, mut cx| async move {
|
||||
Ok(model.spawn(cx, move |this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
@@ -221,8 +221,8 @@ impl ChannelChat {
|
||||
let response = response.message.ok_or_else(|| anyhow!("invalid message"))?;
|
||||
let id = response.id;
|
||||
let message = ChannelMessage::from_proto(response, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
if this.first_loaded_message_id.is_none() {
|
||||
this.first_loaded_message_id = Some(id);
|
||||
}
|
||||
@@ -231,15 +231,20 @@ impl ChannelChat {
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn remove_message(&mut self, id: u64, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
pub fn remove_message(
|
||||
&mut self,
|
||||
id: u64,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let response = self.rpc.request(proto::RemoveChannelMessage {
|
||||
channel_id: self.channel_id.0,
|
||||
message_id: id,
|
||||
});
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
response.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(id, cx);
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_removed(id, model, cx);
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -249,13 +254,15 @@ impl ChannelChat {
|
||||
&mut self,
|
||||
id: u64,
|
||||
message: MessageParams,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
self.message_update(
|
||||
ChannelMessageId::Saved(id),
|
||||
message.text.clone(),
|
||||
message.mentions.clone(),
|
||||
Some(OffsetDateTime::now_utc()),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -268,13 +275,17 @@ impl ChannelChat {
|
||||
nonce: Some(nonce.into()),
|
||||
mentions: mentions_to_proto(&message.mentions),
|
||||
});
|
||||
Ok(cx.spawn(move |_, _| async move {
|
||||
Ok(cx.spawn(move |_| async move {
|
||||
request.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<Option<()>>> {
|
||||
pub fn load_more_messages(
|
||||
&mut self,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Option<()>>> {
|
||||
if self.loaded_all_messages {
|
||||
return None;
|
||||
}
|
||||
@@ -283,7 +294,7 @@ impl ChannelChat {
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
let before_message_id = self.first_loaded_message_id()?;
|
||||
Some(cx.spawn(move |this, mut cx| {
|
||||
Some(model.spawn(cx, move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
@@ -329,7 +340,7 @@ impl ChannelChat {
|
||||
) -> Option<usize> {
|
||||
loop {
|
||||
let step = chat
|
||||
.update(&mut cx, |chat, cx| {
|
||||
.update(&mut cx, |chat, model, cx| {
|
||||
if let Some(first_id) = chat.first_loaded_message_id() {
|
||||
if first_id <= message_id {
|
||||
let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&());
|
||||
@@ -347,7 +358,7 @@ impl ChannelChat {
|
||||
);
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue(chat.load_more_messages(cx))
|
||||
ControlFlow::Continue(chat.load_more_messages(model, cx))
|
||||
})
|
||||
.log_err()?;
|
||||
match step {
|
||||
@@ -357,7 +368,7 @@ impl ChannelChat {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn acknowledge_last_message(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
|
||||
if self
|
||||
.last_acknowledged_id
|
||||
@@ -370,8 +381,8 @@ impl ChannelChat {
|
||||
})
|
||||
.ok();
|
||||
self.last_acknowledged_id = Some(latest_message_id);
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
|
||||
self.channel_store.update(cx, |store, model, cx| {
|
||||
store.acknowledge_message_id(self.channel_id, latest_message_id, model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -388,7 +399,7 @@ impl ChannelChat {
|
||||
let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
|
||||
|
||||
let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
|
||||
let loaded_message_ids = this.update(cx, |this, _| {
|
||||
let loaded_message_ids = this.update(cx, |this, model, _| {
|
||||
let mut loaded_message_ids: HashSet<u64> = HashSet::default();
|
||||
for message in loaded_messages.iter() {
|
||||
if let Some(saved_message_id) = message.id.into() {
|
||||
@@ -425,68 +436,69 @@ impl ChannelChat {
|
||||
.await?;
|
||||
Some(messages_from_proto(response.messages, &user_store, cx).await?)
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, model, cx| {
|
||||
this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(loaded_messages, cx);
|
||||
this.insert_messages(loaded_messages, model, cx);
|
||||
if let Some(loaded_ancestors) = loaded_ancestors {
|
||||
this.insert_messages(loaded_ancestors, cx);
|
||||
this.insert_messages(loaded_ancestors, model, cx);
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
pub fn rejoin(&mut self, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::JoinChannelChat {
|
||||
channel_id: channel_id.0,
|
||||
})
|
||||
.await?;
|
||||
Self::handle_loaded_messages(
|
||||
this.clone(),
|
||||
user_store.clone(),
|
||||
rpc.clone(),
|
||||
response.messages,
|
||||
response.done,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, _| {
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
body: pending_message.body,
|
||||
mentions: mentions_to_proto(&pending_message.mentions),
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
reply_to_message_id: pending_message.reply_to_message_id,
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
model
|
||||
.spawn(cx, move |this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::JoinChannelChat {
|
||||
channel_id: channel_id.0,
|
||||
})
|
||||
.await?;
|
||||
Self::handle_loaded_messages(
|
||||
this.clone(),
|
||||
user_store.clone(),
|
||||
rpc.clone(),
|
||||
response.messages,
|
||||
response.done,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
let pending_messages = this.update(&mut cx, |this, _, _| {
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id: channel_id.0,
|
||||
body: pending_message.body,
|
||||
mentions: mentions_to_proto(&pending_message.mentions),
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
reply_to_message_id: pending_message.reply_to_message_id,
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
@@ -531,7 +543,7 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
@@ -539,12 +551,15 @@ impl ChannelChat {
|
||||
let message_id = message.id;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
cx.emit(ChannelChatEvent::NewMessage {
|
||||
channel_id: this.channel_id,
|
||||
message_id,
|
||||
})
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), model, cx);
|
||||
model.emit(
|
||||
ChannelChatEvent::NewMessage {
|
||||
channel_id: this.channel_id,
|
||||
message_id,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -555,8 +570,8 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::RemoveChannelMessage>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.message_removed(message.payload.message_id, cx)
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_removed(message.payload.message_id, model, cx)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -566,7 +581,7 @@ impl ChannelChat {
|
||||
message: TypedEnvelope<proto::ChannelMessageUpdate>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
@@ -574,19 +589,25 @@ impl ChannelChat {
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.message_update(
|
||||
message.id,
|
||||
message.body,
|
||||
message.mentions,
|
||||
message.edited_at,
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
fn insert_messages(
|
||||
&mut self,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>(&())
|
||||
@@ -631,21 +652,27 @@ impl ChannelChat {
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn message_removed(&mut self, id: u64, cx: &mut ModelContext<Self>) {
|
||||
fn message_removed(&mut self, id: u64, model: &Model<Self>, cx: &mut AppContext) {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
|
||||
let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &());
|
||||
if let Some(item) = cursor.item() {
|
||||
@@ -658,7 +685,7 @@ impl ChannelChat {
|
||||
|
||||
// If the message that was deleted was the last acknowledged message,
|
||||
// replace the acknowledged message with an earlier one.
|
||||
self.channel_store.update(cx, |store, _| {
|
||||
self.channel_store.update(cx, |store, model, _| {
|
||||
let summary = self.messages.summary();
|
||||
if summary.count == 0 {
|
||||
store.set_acknowledged_message_id(self.channel_id, None);
|
||||
@@ -669,10 +696,13 @@ impl ChannelChat {
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(ChannelChatEvent::MessagesUpdated {
|
||||
old_range: deleted_message_ix..deleted_message_ix + 1,
|
||||
new_count: 0,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::MessagesUpdated {
|
||||
old_range: deleted_message_ix..deleted_message_ix + 1,
|
||||
new_count: 0,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -683,7 +713,8 @@ impl ChannelChat {
|
||||
body: String,
|
||||
mentions: Vec<(Range<usize>, u64)>,
|
||||
edited_at: Option<OffsetDateTime>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
|
||||
let mut messages = cursor.slice(&id, Bias::Left, &());
|
||||
@@ -701,12 +732,15 @@ impl ChannelChat {
|
||||
drop(cursor);
|
||||
self.messages = messages;
|
||||
|
||||
cx.emit(ChannelChatEvent::UpdateMessage {
|
||||
message_ix: ix,
|
||||
message_id: id,
|
||||
});
|
||||
model.emit(
|
||||
ChannelChatEvent::UpdateMessage {
|
||||
message_ix: ix,
|
||||
message_id: id,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,8 +762,8 @@ impl ChannelMessage {
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
.update(cx, |user_store, model, cx| {
|
||||
user_store.get_user(message.sender_id, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
@@ -779,8 +813,8 @@ impl ChannelMessage {
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
.update(cx, |user_store, model, cx| {
|
||||
user_store.get_users(unique_user_ids, model, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, User
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, SharedString,
|
||||
Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
@@ -23,7 +23,7 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
cx.new_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
cx.new_model(|model, cx| ChannelStore::new(client.clone(), user_store.clone(), model, cx));
|
||||
cx.set_global(GlobalChannelStore(channel_store));
|
||||
}
|
||||
|
||||
@@ -160,32 +160,37 @@ impl ChannelStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Self {
|
||||
let rpc_subscriptions = [
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_channels),
|
||||
client.add_message_handler(model.downgrade(), Self::handle_update_user_channels),
|
||||
];
|
||||
|
||||
let mut connection_status = client.status();
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
let watch_connection_status = cx.spawn(|this, mut cx| async move {
|
||||
let watch_connection_status = model.spawn(cx, |this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade()?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
this.update(&mut cx, |this, model, cx| this.handle_connect(model, cx))
|
||||
.ok()?
|
||||
.await
|
||||
.log_err()?;
|
||||
}
|
||||
client::Status::SignedOut | client::Status::UpgradeRequired => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx))
|
||||
.ok();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.handle_disconnect(false, model, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx))
|
||||
.ok();
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.handle_disconnect(true, model, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,12 +210,12 @@ impl ChannelStore {
|
||||
_rpc_subscriptions: rpc_subscriptions,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
disconnect_channel_buffers_task: None,
|
||||
_update_channels: cx.spawn(|this, mut cx| async move {
|
||||
_update_channels: model.spawn(cx, |this, mut cx| async move {
|
||||
maybe!(async move {
|
||||
while let Some(update_channels) = update_channels_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
let update_task = this.update(&mut cx, |this, cx| {
|
||||
this.update_channels(update_channels, cx)
|
||||
let update_task = this.update(&mut cx, |this, model, cx| {
|
||||
this.update_channels(update_channels, model, cx)
|
||||
})?;
|
||||
if let Some(update_task) = update_task {
|
||||
update_task.await.log_err();
|
||||
@@ -307,15 +312,17 @@ impl ChannelStore {
|
||||
pub fn open_channel_buffer(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<ChannelBuffer>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_store = cx.handle();
|
||||
let channel_store = model.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_buffers,
|
||||
|channel, cx| ChannelBuffer::new(channel, client, user_store, channel_store, cx),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -323,7 +330,8 @@ impl ChannelStore {
|
||||
pub fn fetch_channel_messages(
|
||||
&self,
|
||||
message_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ChannelMessage>>> {
|
||||
let request = if message_ids.is_empty() {
|
||||
None
|
||||
@@ -333,13 +341,13 @@ impl ChannelStore {
|
||||
.request(proto::GetChannelMessagesById { message_ids }),
|
||||
)
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
if let Some(request) = request {
|
||||
let response = request.await?;
|
||||
let this = this
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("channel store dropped"))?;
|
||||
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
|
||||
let user_store = this.update(&mut cx, |this, _, _| this.user_store.clone())?;
|
||||
ChannelMessage::from_proto_vec(response.messages, &user_store, &mut cx).await
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
@@ -384,26 +392,28 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.acknowledge_message_id(message_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn update_latest_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.update_latest_message_id(message_id);
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
}
|
||||
|
||||
pub fn acknowledge_notes_version(
|
||||
@@ -411,13 +421,14 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.acknowledge_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn update_latest_notes_version(
|
||||
@@ -425,27 +436,30 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.update_latest_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
model.notify(cx)
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<ChannelChat>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let this = cx.handle();
|
||||
let this = model.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_chats,
|
||||
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
|
||||
model,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -460,7 +474,8 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
get_map: fn(&mut Self) -> &mut HashMap<ChannelId, OpenedModelHandle<T>>,
|
||||
load: F,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<T>>>
|
||||
where
|
||||
F: 'static + FnOnce(Arc<Channel>, AsyncAppContext) -> Fut,
|
||||
@@ -483,9 +498,9 @@ impl ChannelStore {
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
let task = cx
|
||||
.spawn(move |this, mut cx| async move {
|
||||
let channel = this.update(&mut cx, |this, _| {
|
||||
let task = model
|
||||
.spawn(cx, move |this, mut cx| async move {
|
||||
let channel = this.update(&mut cx, |this, _, _| {
|
||||
this.channel_for_id(channel_id).cloned().ok_or_else(|| {
|
||||
Arc::new(anyhow!("no channel for id: {}", channel_id))
|
||||
})
|
||||
@@ -496,25 +511,26 @@ impl ChannelStore {
|
||||
.shared();
|
||||
|
||||
e.insert(OpenedModelHandle::Loading(task.clone()));
|
||||
cx.spawn({
|
||||
let task = task.clone();
|
||||
move |this, mut cx| async move {
|
||||
let result = task.await;
|
||||
this.update(&mut cx, |this, _| match result {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
get_map(this).remove(&channel_id);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
model
|
||||
.spawn(cx, {
|
||||
let task = task.clone();
|
||||
move |this, mut cx| async move {
|
||||
let result = task.await;
|
||||
this.update(&mut cx, |this, _, _| match result {
|
||||
Ok(model) => {
|
||||
get_map(this).insert(
|
||||
channel_id,
|
||||
OpenedModelHandle::Open(model.downgrade()),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
get_map(this).remove(&channel_id);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
break task;
|
||||
}
|
||||
}
|
||||
@@ -572,11 +588,12 @@ impl ChannelStore {
|
||||
&self,
|
||||
name: &str,
|
||||
parent_id: Option<ChannelId>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<ChannelId>> {
|
||||
let client = self.client.clone();
|
||||
let name = name.trim_start_matches('#').to_owned();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::CreateChannel {
|
||||
name,
|
||||
@@ -589,12 +606,13 @@ impl ChannelStore {
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
let channel_id = ChannelId(channel.id);
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
@@ -603,7 +621,7 @@ impl ChannelStore {
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame completes
|
||||
cx.emit(ChannelEvent::ChannelCreated(channel_id));
|
||||
model.emit(ChannelEvent::ChannelCreated(channel_id), cx);
|
||||
})?;
|
||||
|
||||
Ok(channel_id)
|
||||
@@ -614,10 +632,11 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
to: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |_, _| async move {
|
||||
cx.spawn(move |_| async move {
|
||||
let _ = client
|
||||
.request(proto::MoveChannel {
|
||||
channel_id: channel_id.0,
|
||||
@@ -633,10 +652,11 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |_, _| async move {
|
||||
cx.spawn(move |_| async move {
|
||||
let _ = client
|
||||
.request(proto::SetChannelVisibility {
|
||||
channel_id: channel_id.0,
|
||||
@@ -653,15 +673,16 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::InviteChannelMember {
|
||||
channel_id: channel_id.0,
|
||||
@@ -670,9 +691,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
result?;
|
||||
@@ -685,15 +706,16 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("invite request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::RemoveChannelMember {
|
||||
channel_id: channel_id.0,
|
||||
@@ -701,9 +723,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
result?;
|
||||
Ok(())
|
||||
@@ -715,15 +737,16 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
role: proto::ChannelRole,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||
return Task::ready(Err(anyhow!("member request already in progress")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
let client = self.client.clone();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let result = client
|
||||
.request(proto::SetChannelMemberRole {
|
||||
channel_id: channel_id.0,
|
||||
@@ -732,9 +755,9 @@ impl ChannelStore {
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.outgoing_invites.remove(&(channel_id, user_id));
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})?;
|
||||
|
||||
result?;
|
||||
@@ -746,11 +769,12 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
new_name: &str,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let name = new_name.to_string();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
let channel = client
|
||||
.request(proto::RenameChannel {
|
||||
channel_id: channel_id.0,
|
||||
@@ -759,12 +783,13 @@ impl ChannelStore {
|
||||
.await?
|
||||
.channel
|
||||
.ok_or_else(|| anyhow!("missing channel in response"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
let task = this.update_channels(
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel],
|
||||
..Default::default()
|
||||
},
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
assert!(task.is_none());
|
||||
@@ -773,7 +798,7 @@ impl ChannelStore {
|
||||
// before this frame is rendered. But we can't guarantee that the collab panel's future
|
||||
// will resolve before this flush_effects finishes. Synchronously emitting this event
|
||||
// ensures that the collab panel will observe this creation before the frame complete
|
||||
cx.emit(ChannelEvent::ChannelRenamed(channel_id))
|
||||
model.emit(ChannelEvent::ChannelRenamed(channel_id), cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -783,7 +808,8 @@ impl ChannelStore {
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
accept: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
@@ -801,11 +827,12 @@ impl ChannelStore {
|
||||
channel_id: ChannelId,
|
||||
query: String,
|
||||
limit: u16,
|
||||
cx: &mut ModelContext<Self>,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ChannelMembership>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.downgrade();
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
cx.spawn(move |mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::GetChannelMembers {
|
||||
channel_id: channel_id.0,
|
||||
@@ -813,7 +840,7 @@ impl ChannelStore {
|
||||
limit: limit as u64,
|
||||
})
|
||||
.await?;
|
||||
user_store.update(&mut cx, |user_store, _| {
|
||||
user_store.update(&mut cx, |user_store, model, _| {
|
||||
user_store.insert(response.users);
|
||||
response
|
||||
.members
|
||||
@@ -855,7 +882,7 @@ impl ChannelStore {
|
||||
message: TypedEnvelope<proto::UpdateChannels>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update(&mut cx, |this, _, _| {
|
||||
this.update_channels_tx
|
||||
.unbounded_send(message.payload)
|
||||
.unwrap();
|
||||
@@ -868,13 +895,14 @@ impl ChannelStore {
|
||||
message: TypedEnvelope<proto::UpdateUserChannels>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for buffer_version in message.payload.observed_channel_buffer_version {
|
||||
let version = language::proto::deserialize_version(&buffer_version.version);
|
||||
this.acknowledge_notes_version(
|
||||
ChannelId(buffer_version.channel_id),
|
||||
buffer_version.epoch,
|
||||
&version,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -882,6 +910,7 @@ impl ChannelStore {
|
||||
this.acknowledge_message_id(
|
||||
ChannelId(message_id.channel_id),
|
||||
message_id.message_id,
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -896,7 +925,7 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
fn handle_connect(&mut self, model: &Model<Self>, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
self.channel_participants.clear();
|
||||
@@ -907,8 +936,8 @@ impl ChannelStore {
|
||||
for chat in self.opened_chats.values() {
|
||||
if let OpenedModelHandle::Open(chat) = chat {
|
||||
if let Some(chat) = chat.upgrade() {
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.rejoin(cx);
|
||||
chat.update(cx, |chat, model, cx| {
|
||||
chat.rejoin(model, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -937,17 +966,17 @@ impl ChannelStore {
|
||||
buffers: buffer_versions,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
model.spawn(cx, |this, mut cx| async move {
|
||||
let mut response = response.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
this.opened_buffers.retain(|_, buffer| match buffer {
|
||||
OpenedModelHandle::Open(channel_buffer) => {
|
||||
let Some(channel_buffer) = channel_buffer.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
channel_buffer.update(cx, |channel_buffer, cx| {
|
||||
channel_buffer.update(cx, |channel_buffer, model, cx| {
|
||||
let channel_id = channel_buffer.channel_id;
|
||||
if let Some(remote_buffer) = response
|
||||
.buffers
|
||||
@@ -960,12 +989,13 @@ impl ChannelStore {
|
||||
|
||||
channel_buffer.replace_collaborators(
|
||||
mem::take(&mut remote_buffer.collaborators),
|
||||
model,
|
||||
cx,
|
||||
);
|
||||
|
||||
let operations = channel_buffer
|
||||
.buffer()
|
||||
.update(cx, |buffer, cx| {
|
||||
.update(cx, |buffer, model, cx| {
|
||||
let outgoing_operations =
|
||||
buffer.serialize_ops(Some(remote_version), cx);
|
||||
let incoming_operations =
|
||||
@@ -973,7 +1003,7 @@ impl ChannelStore {
|
||||
.into_iter()
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
buffer.apply_ops(incoming_operations, cx);
|
||||
buffer.apply_ops(incoming_operations, model, cx);
|
||||
anyhow::Ok(outgoing_operations)
|
||||
})
|
||||
.log_err();
|
||||
@@ -999,7 +1029,7 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
channel_buffer.disconnect(cx);
|
||||
channel_buffer.disconnect(model, cx);
|
||||
false
|
||||
})
|
||||
}
|
||||
@@ -1011,21 +1041,28 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
cx.notify();
|
||||
fn handle_disconnect(
|
||||
&mut self,
|
||||
wait_for_reconnect: bool,
|
||||
model: &Model<Self>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
model.notify(cx);
|
||||
self.did_subscribe = false;
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
model.spawn(cx, move |this, mut cx| async move {
|
||||
if wait_for_reconnect {
|
||||
cx.background_executor().timer(RECONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
if let OpenedModelHandle::Open(buffer) = buffer {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
|
||||
buffer.update(cx, |buffer, model, cx| {
|
||||
buffer.disconnect(model, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1039,7 +1076,8 @@ impl ChannelStore {
|
||||
pub(crate) fn update_channels(
|
||||
&mut self,
|
||||
payload: proto::UpdateChannels,
|
||||
cx: &mut ModelContext<ChannelStore>,
|
||||
model: &Model<ChannelStore>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if !payload.remove_channel_invitations.is_empty() {
|
||||
self.channel_invitations
|
||||
@@ -1127,7 +1165,7 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
if payload.channel_participants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -1142,13 +1180,13 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
let users = self
|
||||
.user_store
|
||||
.update(cx, |user_store, cx| user_store.get_users(all_user_ids, cx));
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
let users = self.user_store.update(cx, |user_store, model, cx| {
|
||||
user_store.get_users(all_user_ids, model, cx)
|
||||
});
|
||||
Some(model.spawn(cx, |this, mut cx| async move {
|
||||
let users = users.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update(&mut cx, |this, model, cx| {
|
||||
for entry in &channel_participants {
|
||||
let mut participants: Vec<_> = entry
|
||||
.participant_user_ids
|
||||
@@ -1167,7 +1205,7 @@ impl ChannelStore {
|
||||
.insert(ChannelId(entry.channel_id), participants);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
model.notify(cx);
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
let user_id = 5;
|
||||
let channel_id = 5;
|
||||
let channel_store = cx.update(init_test);
|
||||
let client = channel_store.update(cx, |s, _| s.client());
|
||||
let client = channel_store.update(cx, |s, model, _| s.client());
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
// Get the available channels.
|
||||
@@ -169,9 +169,9 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_store.update(cx, |store, cx| {
|
||||
let channel = channel_store.update(cx, |store, model, cx| {
|
||||
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
||||
store.open_channel_chat(channel_id, cx)
|
||||
store.open_channel_chat(channel_id, model, cx)
|
||||
});
|
||||
let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
|
||||
server.respond(
|
||||
@@ -221,7 +221,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
let channel = channel.await.unwrap();
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
@@ -270,7 +270,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
@@ -281,8 +281,8 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
channel.load_more_messages(cx).unwrap().detach();
|
||||
channel.update(cx, |channel, model, cx| {
|
||||
channel.load_more_messages(model, cx).unwrap().detach();
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
@@ -323,7 +323,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.update(cx, |channel, _| {
|
||||
channel.update(cx, |channel, model, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
@@ -346,7 +346,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http.clone(), cx);
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let user_store = cx.new_model(|model, cx| UserStore::new(client.clone(), model, cx));
|
||||
|
||||
client::init(&client, cx);
|
||||
crate::init(&client, user_store, cx);
|
||||
@@ -359,7 +359,9 @@ fn update_channels(
|
||||
message: proto::UpdateChannels,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let task = channel_store.update(cx, |store, cx| store.update_channels(message, cx));
|
||||
let task = channel_store.update(cx, |store, model, cx| {
|
||||
store.update_channels(message, model, cx)
|
||||
});
|
||||
assert!(task.is_none());
|
||||
}
|
||||
|
||||
@@ -369,7 +371,7 @@ fn assert_channels(
|
||||
expected_channels: &[(usize, String)],
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let actual = channel_store.update(cx, |store, _| {
|
||||
let actual = channel_store.update(cx, |store, model, _| {
|
||||
store
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string()))
|
||||
|
||||
@@ -1962,7 +1962,7 @@ mod tests {
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
AnyProtoClient::from(client.clone()).add_model_message_handler(
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
|
||||
match model.update(&mut cx, |model, _| model.id).unwrap() {
|
||||
match model.update(&mut cx, |model, _, _| model.id).unwrap() {
|
||||
1 => done_tx1.try_send(()).unwrap(),
|
||||
2 => done_tx2.try_send(()).unwrap(),
|
||||
_ => unreachable!(),
|
||||
@@ -1970,15 +1970,15 @@ mod tests {
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
let model1 = cx.new_model(|_| TestModel {
|
||||
let model1 = cx.new_model(|_, _| TestModel {
|
||||
id: 1,
|
||||
subscription: None,
|
||||
});
|
||||
let model2 = cx.new_model(|_| TestModel {
|
||||
let model2 = cx.new_model(|_, _| TestModel {
|
||||
id: 2,
|
||||
subscription: None,
|
||||
});
|
||||
let model3 = cx.new_model(|_| TestModel {
|
||||
let model3 = cx.new_model(|_, _| TestModel {
|
||||
id: 3,
|
||||
subscription: None,
|
||||
});
|
||||
@@ -2018,7 +2018,7 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
let model = cx.new_model(|_, _| TestModel::default());
|
||||
let (done_tx1, _done_rx1) = smol::channel::unbounded();
|
||||
let (done_tx2, mut done_rx2) = smol::channel::unbounded();
|
||||
let subscription1 = client.add_message_handler(
|
||||
@@ -2053,20 +2053,20 @@ mod tests {
|
||||
});
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
let model = cx.new_model(|_| TestModel::default());
|
||||
let model = cx.new_model(|_, _| TestModel::default());
|
||||
let (done_tx, mut done_rx) = smol::channel::unbounded();
|
||||
let subscription = client.add_message_handler(
|
||||
model.clone().downgrade(),
|
||||
move |model: Model<TestModel>, _: TypedEnvelope<proto::Ping>, mut cx| {
|
||||
model
|
||||
.update(&mut cx, |model, _| model.subscription.take())
|
||||
.update(&mut cx, |this, model, _| this.subscription.take())
|
||||
.unwrap();
|
||||
done_tx.try_send(()).unwrap();
|
||||
async { Ok(()) }
|
||||
},
|
||||
);
|
||||
model.update(cx, |model, _| {
|
||||
model.subscription = Some(subscription);
|
||||
model.update(cx, |this, model, _| {
|
||||
this.subscription = Some(subscription);
|
||||
});
|
||||
server.send(proto::Ping {});
|
||||
done_rx.next().await.unwrap();
|
||||
|
||||
@@ -18,8 +18,7 @@ use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
|
||||
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use worktree::{UpdatedEntriesSet, WorktreeId};
|
||||
@@ -356,24 +355,6 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_inline_completion_rating_event(
|
||||
self: &Arc<Self>,
|
||||
rating: InlineCompletionRating,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
output_excerpt: Arc<str>,
|
||||
feedback: String,
|
||||
) {
|
||||
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
|
||||
rating,
|
||||
input_events,
|
||||
input_excerpt,
|
||||
output_excerpt,
|
||||
feedback,
|
||||
});
|
||||
self.report_event(event);
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ impl FakeServer {
|
||||
client: Arc<Client>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Model<UserStore> {
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client, cx));
|
||||
let user_store = cx.new_model(|model, cx| UserStore::new(client, model, cx));
|
||||
assert_eq!(
|
||||
self.receive::<proto::GetUsers>()
|
||||
.await
|
||||
|
||||