Compare commits

..

32 Commits

Author SHA1 Message Date
Nathan Sobo
a55e496a23 WIP 2024-06-02 13:55:06 -06:00
Nathan Sobo
f89996ac21 Sketch a new approach to views
In the sketch, the View is a model plus a render function, but the
model is not required to declare up-front how it is rendered. In
this world, we would only use a
2024-05-31 18:28:42 -06:00
Max Brunsfeld
d12b8c3945 Simplify and improve concurrency of git status updates (#12513)
The quest for responsiveness in large git repos continues. This is a
follow-up to https://github.com/zed-industries/zed/pull/12444

Release Notes:

- N/A
2024-05-31 09:10:09 -07:00
Vitaly Slobodin
356fcec337 ruby: Add a new injection for regular expressions (#12533)
# Summary

Hello. This pull request adds a new injection to `injections.scm` for
Ruby language to highlight regular expressions. Thanks.

## Before

![CleanShot 2024-05-31 at 16 25
46@2x](https://github.com/zed-industries/zed/assets/1894248/8b88718e-8f13-4d61-b6f9-6a25b3ebcc57)

## After


![after](https://github.com/zed-industries/zed/assets/1894248/e11f6ec3-45c6-40f8-b6d9-ddbfd16a3331)

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-31 12:02:59 -04:00
Vitaly Slobodin
08123a270a ruby: Add proper indentation for singleton methods (#12535)
Hi. Currently, Zed uses incorrect indentation for singleton methods:

```ruby
def self.build
| # <= cursor position after hitting Enter
end
```

Handling the `singleton_method` token indentation
changes this behavior to the following:

```ruby
def self.build
  | # <= cursor position after hitting Enter
end
```

## Before


https://github.com/zed-industries/zed/assets/1894248/40fc2b37-692f-469f-9cbe-05cbb1ab4c3c

## After



https://github.com/zed-industries/zed/assets/1894248/d9ba8d27-fd17-4c74-b22c-a4de124739a3



Release Notes:

- N/A
2024-05-31 11:36:42 -04:00
张小白
6eb8e83411 docs: Update font features (#12229)
This follows up the changes in #11611 and #11898 

Release Notes:

- N/A
2024-05-31 11:34:16 -04:00
Marshall Bowers
4c51ee7816 assistant: Allow passing module paths to /rustdoc command (#12536)
This PR updates the `/rustdoc` command to accept module paths in
addition to just a crate name.

This will return the docs.rs page just for that particular module.

### Examples

```
/rustdoc bevy
/rustdoc bevy::ecs
/rustdoc bevy::ecs::component
```

<img width="641" alt="Screenshot 2024-05-31 at 11 18 25 AM"
src="https://github.com/zed-industries/zed/assets/1486634/d88af19f-5ba1-4073-8108-63cccd138db6">

<img width="641" alt="Screenshot 2024-05-31 at 11 18 35 AM"
src="https://github.com/zed-industries/zed/assets/1486634/9c414ab1-0be8-4d79-8c64-b45f19266556">


Release Notes:

- N/A
2024-05-31 11:31:22 -04:00
Vitaly Slobodin
660cf214c7 ruby: Capture the heredoc content only and downcase the language (#12532)
# Summary

Hi. Current `heredoc` injection for Ruby language captures the
`heredoc_end` token. That's a bit incorrect because we want to capture
the content only. Suppose we have the following Ruby code:

```ruby
<<~JS
  function myFunc() {
    const myConstant = [];
  }

  let a = '1'
JS
```

And this is its syntax tree:

```
[program] [0, 0] - [7, 0]
  [heredoc_beginning] [0, 0] - [0, 5]
  [heredoc_body] [0, 5] - [6, 2]
    [heredoc_content] [0, 5] - [6, 0]
    [heredoc_end] [6, 0] - [6, 2]
```

Current injection capture all content of the `heredoc_body`:

![CleanShot 2024-05-31 at 17 03
31@2x](https://github.com/zed-industries/zed/assets/1894248/ff8c5195-b532-42d2-91b1-48405a6d3b50)

But we want to capture the `heredoc_content` only and this PR resolves
that, additionally it downcases the language like Zed does in other
languages like Terraform.

![CleanShot 2024-05-31 at 17 05
17@2x](https://github.com/zed-industries/zed/assets/1894248/e81dabd0-3246-4ef2-9524-a7adcb9242ab)


Release Notes:

- N/A
2024-05-31 11:19:10 -04:00
Marshall Bowers
b2565fadfb ruby: Fix injections query location (#12534)
This PR fixes the location of the `injections.scm` query within the Ruby
extension.

Same as #12532, but without the content changes to `injections.scm`.

Release Notes:

- N/A
2024-05-31 10:42:39 -04:00
Felipe Renan
2cff075c53 elixir: Fix mix test $ZED_SYMBOL task (#11879)
$ZED_SYMBOL doesn't really work here once that will try to do something
like this:

  mix test MyModule.MyModuleTest

instead of using the path of the file:

  mix test test/my_module/my_module_test.exs
  
Release Notes:

- Fix mix test $ZED_SYMBOL to use ZED_RELATIVE_FILE instead
- Use ZED_RELATIVE_FILE instead of ZED_FILE to improve mix tasks results
on Elixir umbrella projects
2024-05-31 12:54:14 +02:00
Vladas Zakrevskis
819bb2663d Fix recent project index order (#12507)
Fixed bug introduced in:
https://github.com/zed-industries/zed/pull/12502

Filtering before `enumerate` call breaks project order and instead of
hiding current project it hides some other project.

Release Notes:
- N/A
2024-05-31 05:50:03 +03:00
moshyfawn
dc141d0f61 typescript: Fix shorthand property highlight (#12505)
Release Notes:

- Fixed Typescript shorthand property highlight
([#5239](https://github.com/zed-industries/zed/issues/5239)).

Closes: #5239
2024-05-30 18:27:03 -04:00
Bennet Bo Fenner
22cf73acec indent guides: Use primary buffer language to determine tab size (#12506)
When indent guides were still WIP, I thought it might be a good idea to
detect the tab size for every line individually, so we can handle files
with mixed indentations. However, while optimizing the performance of
indent guides I found that getting the language at a given anchor was
pretty expensive, therefore I only resolved the language for the first
visible row. However, this could lead to some weird flickering, where
the indent guides would use different tab sizes depending on the first
visible row (see #12492). This can be fixed by just using the primary
buffer language size.

So as of right now indent guides cannot handle files with mixed
indentations. Im not sure if anyone actually does/expects this, but one
use case I could imagine is something like this:
User x has a svelte file, where the tab size is set to `4`. However the
svelte code uses typescript inside a script tag, which User x wants to
use a tab size of `2`. The approach used here would not work for this,
but then again I think our formatter does not even support something
like this. Im probably overcomplicating things, so let's stick with the
simple solution for now.

Release Notes:

- Fixed an issue where indent guides would use an incorrect tab size
([#12492](https://github.com/zed-industries/zed/issues/12492)).
2024-05-30 22:55:47 +02:00
Marshall Bowers
1d46a52c62 rustdoc_to_markdown: Don't push blank space after newline (#12504)
This PR fixes a small issue in `rustdoc_to_markdown` where we could push
a blank space after a newline, leading to an unwanted leading space.

Release Notes:

- N/A
2024-05-30 16:38:01 -04:00
Max Brunsfeld
fda975fb76 Re-subscribe to channels after signing back out 2024-05-30 13:32:34 -07:00
Vladas Zakrevskis
0f32145ecb Skip current project in recent projects (#12502)
Discussion: https://github.com/zed-industries/zed/discussions/12497

Release Notes:

- Removed current project from the recent projects modals
2024-05-30 23:30:34 +03:00
Marshall Bowers
6fe665ab94 rustdoc_to_markdown: Support bold and italics (#12501)
This PR extends `rustdoc_to_markdown` with support for bold and italic
text.

Release Notes:

- N/A
2024-05-30 16:06:21 -04:00
Max Brunsfeld
279c5ab81f Reduce DB load upon initial connection due to channel loading (#12500)
#### Lazily loading channels

I've added a new RPC message called `SubscribeToChannels` that the
client now sends when it first renders the channels panel. This causes
the server to load the channels for that client and send updates to that
client as channels are updated. Previously, the server did this upon
connection.

For backwards compatibility, the server will inspect clients' version,
and continue to do this work immediately for old clients.

#### Optimizations

Running collab locally, I realized that upon connecting, we were running
two concurrent transactions that *both* queried the `channel_members`
table: one for loading your channels, and one for loading your channel
invites. I've combined these into one query. In addition, we now use a
join to load channels + members, as opposed to two separate queries.
Even though `where id in` is efficient, it adds an extra round trip to
the database, keeping the transaction open for slightly longer.

Release Notes:

- N/A
2024-05-30 13:02:55 -07:00
Marshall Bowers
99901801f4 rustdoc_to_markdown: Improve paragraph handling (#12498)
This PR improves `rustdoc_to_markdown`'s paragraph handling to produce
better output.

Specifically, there should now be fewer instances where a space is
missing between words as the result of line breaks in the source HTML.

Release Notes:

- N/A
2024-05-30 15:14:02 -04:00
Marshall Bowers
4dc98026c4 rustdoc_to_markdown: Add helper methods for checking HTML attributes (#12496)
This PR adds some helper methods to `HtmlElement` to make it easier to
interact with the element's attributes.

This cleans up a bunch of the code by a fair amount.

Release Notes:

- N/A
2024-05-30 14:15:08 -04:00
Marshall Bowers
c83d1c23d7 rustdoc_to_markdown: Handle "stabs" in item name entries (#12494)
This PR extends `rustdoc_to_markdown` with support for rustdoc's
"stabs".

These are used in item name lists to indicate that the construct is
behind a feature flag:

<img width="641" alt="Screenshot 2024-05-30 at 1 34 53 PM"
src="https://github.com/zed-industries/zed/assets/1486634/0216f325-dc4e-4302-b6db-149ace31deea">

We now treat these specially in the Markdown output:

<img width="813" alt="Screenshot 2024-05-30 at 1 35 27 PM"
src="https://github.com/zed-industries/zed/assets/1486634/96396305-123d-40b2-af49-7eed71b62971">

Release Notes:

- N/A
2024-05-30 13:46:14 -04:00
Marshall Bowers
39a2cdb13f rustdoc_to_markdown: Strip "Copy item path to clipboard" button (#12490)
This PR strips the "Copy item path to clipboard" button from the rustdoc
output.

Release Notes:

- N/A
2024-05-30 12:55:37 -04:00
Max Brunsfeld
8f942bf647 Use repository mutex more sparingly. Don't hold it while running git status. (#12489)
Previously, each git `Repository` object was held inside of a mutex.
This was needed because libgit2's Repository object is (as one would
expect) not thread safe. But now, the two longest-running git operations
that Zed performs, (`status` and `blame`) do not use libgit2 - they
invoke the `git` executable. For these operations, it's not necessary to
hold a lock on the repository.

In this PR, I've moved our mutex usage so that it only wraps the libgit2
calls, not our `git` subprocess spawns. The main user-facing impact of
this is that the UI is much more responsive when initially opening a
project with a very large git repository (e.g. `chromium`, `webkit`,
`linux`).

Release Notes:

- Improved Zed's responsiveness when initially opening a project
containing a very large git repository.
2024-05-30 09:37:11 -07:00
Bennet Bo Fenner
1ecd13ba50 Support copying permalink in multibuffer (#12435)
Closes #11392 

Release Notes:

- Added support for copying permalinks inside multi-buffers
([#11392](https://github.com/zed-industries/zed/issues/11392))
2024-05-30 18:36:24 +02:00
Marshall Bowers
c118012223 rustdoc_to_markdown: Add table support (#12488)
This PR extends `rustdoc_to_markdown` with support for tables:

<img width="1007" alt="Screenshot 2024-05-30 at 12 05 35 PM"
src="https://github.com/zed-industries/zed/assets/1486634/4e9a2a65-8aaa-4df1-98c4-4dd4e7874514">


Release Notes:

- N/A
2024-05-30 12:17:10 -04:00
Marshall Bowers
7a30937e21 Sort file_types.json (#12487)
This PR sorts the `file_types.json` file alphabetically.

This is the command I used to sort it:

```
pnpm --package=json-sort-cli dlx jsonsort assets/icons/file_icons/file_types.json
```

Release Notes:

- N/A
2024-05-30 11:26:52 -04:00
Kirill Bulatov
3c5d141a04 Force 60 minutes timeout for all regular CI jobs (#12486)
After gazing at
https://github.com/zed-industries/zed/actions/runs/9296132630/job/25596939148
for some time, I've decided to add a hard limit on every test-related CI
job.

Release Notes:

- N/A
2024-05-30 18:17:03 +03:00
Marshall Bowers
bf7c6a676a rustdoc_to_markdown: Recognize code blocks in other languages (#12484)
This PR updates `rustdoc_to_markdown` to be able to recognize code
blocks using non-Rust languages.

Release Notes:

- N/A
2024-05-30 10:50:27 -04:00
Antonio Scandurra
a259042f92 Make slash commands more discoverable (#12480)
<img width="648" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/a63df904-fbbe-4e0a-80b2-c98ebee90690">

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-05-30 16:45:05 +02:00
Sean Washington
436a8fa0ce php: Update Pest tree-sitter queries to capture single quotes (#12467)
Improved PHP Pest outline and runnables to support single quoted
arguments
([#12461](https://github.com/zed-industries/zed/issues/12461)).

Release Notes:

- N/A

| Before | After |
|--------|--------|
|
![image](https://github.com/zed-industries/zed/assets/428033/e0966510-da11-4a80-8901-7dba541ab721)
| ![CleanShot 2024-05-29 at 20 13
00@2x](https://github.com/zed-industries/zed/assets/428033/5f7ab492-2791-4a04-9ec3-f0adfa9b2986)
|
| ![CleanShot 2024-05-29 at 20 18
11@2x](https://github.com/zed-industries/zed/assets/428033/ac6bf58b-4e7d-410d-af51-328c41a76ba0)
| ![CleanShot 2024-05-29 at 20 14
35@2x](https://github.com/zed-industries/zed/assets/428033/1d226bb8-f102-4171-906d-e122ab8299cf)
|
2024-05-30 16:37:41 +02:00
Antonio Scandurra
55c47305c8 Align the inline assistant correctly (#12478)
Release Notes:

- Fixed the the alignment for the inline assistant.
2024-05-30 14:29:17 +02:00
Antonio Scandurra
6ff01b17ca Improve model selection in the assistant (#12472)
https://github.com/zed-industries/zed/assets/482957/3b017850-b7b6-457a-9b2f-324d5533442e


Release Notes:

- Improved the UX for selecting a model in the assistant panel. You can
now switch model using just the keyboard by pressing `alt-m`. Also, when
switching models via the UI, settings will now be updated automatically.
2024-05-30 12:36:07 +02:00
130 changed files with 2581 additions and 906 deletions

View File

@@ -23,6 +23,7 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and spelling
runs-on:
- self-hosted
@@ -77,6 +78,7 @@ jobs:
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
runs-on:
- self-hosted
@@ -99,7 +101,9 @@ jobs:
- name: Build other binaries and features
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
# todo(linux): Actually run the tests
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
runs-on:
- self-hosted
@@ -116,14 +120,12 @@ jobs:
- name: cargo clippy
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
runs-on: hosted-windows-1
steps:
@@ -144,6 +146,7 @@ jobs:
run: cargo build -p zed
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
runs-on:
- self-hosted
@@ -254,6 +257,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
timeout-minutes: 60
name: Create a Linux bundle
runs-on:
- self-hosted

View File

@@ -15,6 +15,7 @@ env:
jobs:
style:
timeout-minutes: 60
name: Check formatting and Clippy lints
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -33,6 +34,7 @@ jobs:
- name: Run clippy
run: cargo xtask clippy
tests:
timeout-minutes: 60
name: Run tests
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -49,6 +51,7 @@ jobs:
uses: ./.github/actions/run_tests
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
if: github.repository_owner == 'zed-industries'
runs-on:
@@ -91,6 +94,7 @@ jobs:
run: script/upload-nightly macos
bundle-deb:
timeout-minutes: 60
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on:

5
Cargo.lock generated
View File

@@ -230,6 +230,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
"tokio",
]
@@ -376,6 +377,7 @@ dependencies = [
"settings",
"smol",
"strsim 0.11.1",
"strum",
"telemetry_events",
"theme",
"tiktoken-rs",
@@ -3391,7 +3393,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.0",
"libloading 0.7.4",
]
[[package]]
@@ -6983,6 +6985,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum",
]
[[package]]

View File

@@ -494,5 +494,10 @@ non_canonical_partial_ord_impl = "allow"
reversed_empty_ranges = "allow"
type_complexity = "allow"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(gles)', # used in gpui
] }
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

@@ -1,15 +1,15 @@
{
"stems": {
"Dockerfile": "docker",
"Podfile": "ruby",
"Procfile": "heroku",
"Dockerfile": "docker"
"Procfile": "heroku"
},
"suffixes": {
"astro": "astro",
"Emakefile": "erlang",
"aac": "audio",
"accdb": "storage",
"app.src": "erlang",
"astro": "astro",
"avi": "video",
"avif": "image",
"bak": "backup",
@@ -22,12 +22,12 @@
"c": "c",
"cc": "cpp",
"cjs": "javascript",
"coffee": "coffeescript",
"conf": "settings",
"cpp": "cpp",
"css": "css",
"csv": "storage",
"cts": "typescript",
"coffee": "coffeescript",
"dart": "dart",
"dat": "storage",
"db": "storage",
@@ -61,12 +61,12 @@
"graphql": "graphql",
"graphqls": "graphql",
"h": "c",
"hpp": "cpp",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
"heif": "image",
"heic": "image",
"heif": "image",
"hpp": "cpp",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
@@ -81,9 +81,9 @@
"jpeg": "image",
"jpg": "image",
"js": "javascript",
"jsx": "react",
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -98,9 +98,9 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"mkv": "video",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mov": "video",
@@ -109,8 +109,8 @@
"mts": "typescript",
"myd": "storage",
"myi": "storage",
"nu": "terminal",
"nim": "nim",
"nu": "terminal",
"odp": "document",
"ods": "document",
"odt": "document",
@@ -132,33 +132,33 @@
"psd": "image",
"py": "python",
"qoi": "image",
"r": "r",
"rb": "ruby",
"rebar.config": "erlang",
"rkt": "code",
"rs": "rust",
"r": "r",
"rtf": "document",
"sav": "storage",
"sc": "scala",
"scala": "scala",
"scm": "code",
"sdf": "storage",
"sh": "terminal",
"sql": "storage",
"sqlite": "storage",
"svelte": "template",
"svg": "image",
"sc": "scala",
"scala": "scala",
"sql": "storage",
"swift": "swift",
"tcl": "tcl",
"tf": "terraform",
"tfvars": "terraform",
"tiff": "image",
"toml": "toml",
"ts": "typescript",
"tsv": "storage",
"ttf": "font",
"tsx": "react",
"ttf": "font",
"txt": "document",
"tcl": "tcl",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -190,27 +190,30 @@
"audio": {
"icon": "icons/file_icons/audio.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"code": {
"icon": "icons/file_icons/code.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"collapsed_chevron": {
"icon": "icons/file_icons/chevron_right.svg"
},
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"cpp": {
"icon": "icons/file_icons/cpp.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
},
"coffeescript": {
"icon": "icons/file_icons/coffeescript.svg"
},
"dart": {
"icon": "icons/file_icons/dart.svg"
},
@@ -247,18 +250,18 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},
"graphql": {
"icon": "icons/file_icons/graphql.svg"
},
"haskell": {
"icon": "icons/file_icons/haskell.svg"
},
"heroku": {
"icon": "icons/file_icons/heroku.svg"
},
"image": {
"icon": "icons/file_icons/image.svg"
},
@@ -274,21 +277,18 @@
"lock": {
"icon": "icons/file_icons/lock.svg"
},
"bun": {
"icon": "icons/file_icons/bun.svg"
},
"log": {
"icon": "icons/file_icons/info.svg"
},
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"phoenix": {
"icon": "icons/file_icons/phoenix.svg"
},
@@ -316,36 +316,36 @@
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},
"storage": {
"icon": "icons/file_icons/database.svg"
},
"scala": {
"icon": "icons/file_icons/scala.svg"
},
"swift": {
"icon": "icons/file_icons/swift.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"template": {
"icon": "icons/file_icons/html.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"terraform": {
"icon": "icons/file_icons/terraform.svg"
},
"toml": {
"icon": "icons/file_icons/toml.svg"
},
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"tcl": {
"icon": "icons/file_icons/tcl.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -201,7 +201,8 @@
"context": "AssistantPanel",
"bindings": {
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch"
"ctrl-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
}
},
{

View File

@@ -214,10 +214,11 @@
}
},
{
"context": "AssistantPanel", // Used in the assistant crate, which we're replacing
"context": "AssistantPanel",
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch"
"cmd-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector"
}
},
{

View File

@@ -23,6 +23,7 @@ isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -4,11 +4,12 @@ use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, time::Duration};
use strum::EnumIter;
pub const ANTHROPIC_API_URL: &'static str = "https://api.anthropic.com";
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[default]
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]

View File

@@ -49,6 +49,7 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true

View File

@@ -2,6 +2,7 @@ pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod model_selector;
mod prompts;
mod saved_conversation;
mod search;
@@ -15,6 +16,7 @@ use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use model_selector::*;
pub(crate) use saved_conversation::*;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
@@ -38,7 +40,8 @@ actions!(
InsertActivePrompt,
ToggleHistory,
ApplyEdit,
ConfirmCommand
ConfirmCommand,
ToggleModelSelector
]
);

View File

@@ -1,7 +1,7 @@
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
use crate::slash_command::{rustdoc_command, search_command, tabs_command};
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
assistant_settings::{AssistantDockPosition, AssistantSettings},
codegen::{self, Codegen, CodegenKind},
search::*,
slash_command::{
@@ -9,17 +9,18 @@ use crate::{
SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry,
},
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
MessageStatus, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, Split, ToggleFocus, ToggleHistory,
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
Split, ToggleFocus, ToggleHistory,
};
use crate::{ModelSelector, ToggleModelSelector};
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommandOutput, SlashCommandOutputSection};
use client::telemetry::Telemetry;
use collections::{hash_map, BTreeSet, HashMap, HashSet, VecDeque};
use editor::actions::UnfoldAt;
use editor::actions::ShowCompletions;
use editor::{
actions::{FoldAt, MoveDown, MoveUp},
actions::{FoldAt, MoveDown, MoveToEndOfLine, MoveUp, Newline, UnfoldAt},
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, Flap, ToDisplayPoint,
},
@@ -35,7 +36,7 @@ use futures::future::Shared;
use futures::{FutureExt, StreamExt};
use gpui::{
canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty,
AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, StaticContext, Empty,
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle,
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle,
@@ -64,8 +65,8 @@ use std::{
use telemetry_events::AssistantKind;
use theme::ThemeSettings;
use ui::{
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding, Tab, TabBar,
Tooltip,
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
PopoverMenuHandle, Tab, TabBar, Tooltip,
};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
@@ -119,8 +120,8 @@ pub struct AssistantPanel {
pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
inline_prompt_history: VecDeque<String>,
_watch_saved_conversations: Task<Result<()>>,
model: LanguageModel,
authentication_prompt: Option<AnyView>,
model_menu_handle: PopoverMenuHandle<ContextMenu>,
}
struct ActiveConversationEditor {
@@ -203,7 +204,6 @@ impl AssistantPanel {
}
}),
];
let model = CompletionProvider::global(cx).default_model();
cx.observe_global::<FileIcons>(|_, cx| {
cx.notify();
@@ -212,15 +212,20 @@ impl AssistantPanel {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
true,
);
slash_command_registry.register_command(active_command::ActiveSlashCommand);
slash_command_registry.register_command(tabs_command::TabsSlashCommand);
slash_command_registry.register_command(project_command::ProjectSlashCommand);
slash_command_registry.register_command(search_command::SearchSlashCommand);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand);
slash_command_registry
.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry
.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry
.register_command(search_command::SearchSlashCommand, true);
slash_command_registry
.register_command(rustdoc_command::RustdocSlashCommand, false);
Self {
workspace: workspace_handle,
@@ -244,8 +249,8 @@ impl AssistantPanel {
pending_inline_assist_ids_by_editor: Default::default(),
inline_prompt_history: Default::default(),
_watch_saved_conversations,
model,
authentication_prompt: None,
model_menu_handle: PopoverMenuHandle::default(),
}
})
})
@@ -277,12 +282,20 @@ impl AssistantPanel {
if self.is_authenticated(cx) {
self.authentication_prompt = None;
let model = CompletionProvider::global(cx).default_model();
self.set_model(model, cx);
if let Some(editor) = self.active_conversation_editor() {
editor.update(cx, |active_conversation, cx| {
active_conversation
.conversation
.update(cx, |conversation, cx| {
conversation.completion_provider_changed(cx)
})
})
}
if self.active_conversation_editor().is_none() {
self.new_conversation(cx);
}
cx.notify();
} else if self.authentication_prompt.is_none()
|| prev_settings_version != CompletionProvider::global(cx).settings_version()
{
@@ -290,6 +303,7 @@ impl AssistantPanel {
Some(cx.update_global::<CompletionProvider, _>(|provider, cx| {
provider.authentication_prompt(cx)
}));
cx.notify();
}
}
@@ -438,8 +452,8 @@ impl AssistantPanel {
let inline_assistant = inline_assistant.clone();
move |cx: &mut BlockContext| {
*measurements.lock() = BlockMeasurements {
anchor_x: cx.anchor_x,
gutter_width: cx.gutter_dimensions.width,
gutter_margin: cx.gutter_dimensions.margin,
};
inline_assistant.clone().into_any_element()
}
@@ -734,7 +748,7 @@ impl AssistantPanel {
.map(|message| message.to_request_message(buffer)),
);
}
let model = self.model.clone();
let model = CompletionProvider::global(cx).model();
cx.spawn(|_, mut cx| async move {
// I Don't know if we want to return a ? here.
@@ -809,7 +823,6 @@ impl AssistantPanel {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
self.model.clone(),
self.languages.clone(),
self.slash_commands.clone(),
self.fs.clone(),
@@ -850,53 +863,6 @@ impl AssistantPanel {
cx.notify();
}
fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
let next_model = match &self.model {
LanguageModel::OpenAi(model) => LanguageModel::OpenAi(match &model {
open_ai::Model::ThreePointFiveTurbo => open_ai::Model::Four,
open_ai::Model::Four => open_ai::Model::FourTurbo,
open_ai::Model::FourTurbo => open_ai::Model::FourOmni,
open_ai::Model::FourOmni => open_ai::Model::ThreePointFiveTurbo,
}),
LanguageModel::Anthropic(model) => LanguageModel::Anthropic(match &model {
anthropic::Model::Claude3Opus => anthropic::Model::Claude3Sonnet,
anthropic::Model::Claude3Sonnet => anthropic::Model::Claude3Haiku,
anthropic::Model::Claude3Haiku => anthropic::Model::Claude3Opus,
}),
LanguageModel::ZedDotDev(model) => LanguageModel::ZedDotDev(match &model {
ZedDotDevModel::Gpt3Point5Turbo => ZedDotDevModel::Gpt4,
ZedDotDevModel::Gpt4 => ZedDotDevModel::Gpt4Turbo,
ZedDotDevModel::Gpt4Turbo => ZedDotDevModel::Gpt4Omni,
ZedDotDevModel::Gpt4Omni => ZedDotDevModel::Claude3Opus,
ZedDotDevModel::Claude3Opus => ZedDotDevModel::Claude3Sonnet,
ZedDotDevModel::Claude3Sonnet => ZedDotDevModel::Claude3Haiku,
ZedDotDevModel::Claude3Haiku => {
match CompletionProvider::global(cx).default_model() {
LanguageModel::ZedDotDev(custom @ ZedDotDevModel::Custom(_)) => custom,
_ => ZedDotDevModel::Gpt3Point5Turbo,
}
}
ZedDotDevModel::Custom(_) => ZedDotDevModel::Gpt3Point5Turbo,
}),
};
self.set_model(next_model, cx);
}
fn set_model(&mut self, model: LanguageModel, cx: &mut ViewContext<Self>) {
self.model = model.clone();
if let Some(editor) = self.active_conversation_editor() {
editor.update(cx, |active_conversation, cx| {
active_conversation
.conversation
.update(cx, |conversation, cx| {
conversation.set_model(model, cx);
})
})
}
cx.notify();
}
fn handle_conversation_editor_event(
&mut self,
_: View<ConversationEditor>,
@@ -978,6 +944,18 @@ impl AssistantPanel {
.detach_and_log_err(cx);
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_menu_handle.toggle(cx);
}
fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
if let Some(conversation_editor) = self.active_conversation_editor() {
conversation_editor.update(cx, |conversation_editor, cx| {
conversation_editor.insert_command(name, cx)
});
}
}
fn active_conversation_editor(&self) -> Option<&View<ConversationEditor>> {
Some(&self.active_conversation_editor.as_ref()?.editor)
}
@@ -1015,52 +993,65 @@ impl AssistantPanel {
})
}
fn render_inject_context_menu(&self, _cx: &mut ViewContext<Self>) -> impl Element {
let workspace = self.workspace.clone();
fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
let commands = self.slash_commands.clone();
let assistant_panel = cx.view().downgrade();
let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
Some(
workspace
.read(cx)
.active_item_as::<Editor>(cx)?
.focus_handle(cx),
)
});
popover_menu("inject-context-menu")
.trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
// Tooltip::with_meta("Insert Context", None, "Type # to insert via keyboard", cx)
Tooltip::text("Insert Context", cx)
Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
}))
.menu(move |cx| {
ContextMenu::build(cx, |menu, _cx| {
// menu.entry("Insert Search", None, {
// let assistant = assistant.clone();
// move |_cx| {}
// })
// .entry("Insert Docs", None, {
// let assistant = assistant.clone();
// move |cx| {}
// })
menu.entry("Quote Selection", None, {
let workspace = workspace.clone();
move |cx| {
workspace
.update(cx, |workspace, cx| {
ConversationEditor::quote_selection(
workspace,
&Default::default(),
cx,
)
})
.ok();
ContextMenu::build(cx, |mut menu, _cx| {
for command_name in commands.featured_command_names() {
if let Some(command) = commands.command(&command_name) {
let menu_text = SharedString::from(Arc::from(command.menu_text()));
menu = menu.custom_entry(
{
let command_name = command_name.clone();
move |_cx| {
h_flex()
.w_full()
.justify_between()
.child(Label::new(menu_text.clone()))
.child(
div().ml_4().child(
Label::new(format!("/{command_name}"))
.color(Color::Muted),
),
)
.into_any()
}
},
{
let assistant_panel = assistant_panel.clone();
move |cx| {
assistant_panel
.update(cx, |assistant_panel, cx| {
assistant_panel.insert_command(&command_name, cx)
})
.ok();
}
},
)
}
})
// .entry("Insert Active Prompt", None, {
// let workspace = workspace.clone();
// move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// ConversationEditor::insert_active_prompt(
// workspace,
// &Default::default(),
// cx,
// )
// })
// .ok();
// }
// })
}
if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
menu = menu
.context(active_editor_focus_handle)
.action("Quote Selection", Box::new(QuoteSelection));
}
menu
})
.into()
})
@@ -1133,10 +1124,8 @@ impl AssistantPanel {
cx.spawn(|this, mut cx| async move {
let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
let model = this.update(&mut cx, |this, _| this.model.clone())?;
let conversation = Conversation::deserialize(
saved_conversation,
model,
path.clone(),
languages,
slash_commands,
@@ -1206,7 +1195,10 @@ impl AssistantPanel {
this.child(
h_flex()
.gap_1()
.child(self.render_model(&conversation, cx))
.child(ModelSelector::new(
self.model_menu_handle.clone(),
self.fs.clone(),
))
.children(self.render_remaining_tokens(&conversation, cx)),
)
.child(
@@ -1256,6 +1248,7 @@ impl AssistantPanel {
.on_action(cx.listener(AssistantPanel::select_prev_match))
.on_action(cx.listener(AssistantPanel::handle_editor_cancel))
.on_action(cx.listener(AssistantPanel::reset_credentials))
.on_action(cx.listener(AssistantPanel::toggle_model_selector))
.track_focus(&self.focus_handle)
.child(header)
.children(if self.toolbar.read(cx).hidden() {
@@ -1314,23 +1307,12 @@ impl AssistantPanel {
))
}
fn render_model(
&self,
conversation: &Model<Conversation>,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
Button::new("current_model", conversation.read(cx).model.display_name())
.style(ButtonStyle::Filled)
.tooltip(move |cx| Tooltip::text("Change Model", cx))
.on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
}
fn render_remaining_tokens(
&self,
conversation: &Model<Conversation>,
cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> {
let remaining_tokens = conversation.read(cx).remaining_tokens()?;
let remaining_tokens = conversation.read(cx).remaining_tokens(cx)?;
let remaining_tokens_color = if remaining_tokens <= 0 {
Color::Error
} else if remaining_tokens <= 500 {
@@ -1486,7 +1468,6 @@ pub struct Conversation {
pending_summary: Task<Option<()>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
model: LanguageModel,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
pending_edit_suggestion_parse: Option<Task<()>>,
@@ -1502,7 +1483,6 @@ impl EventEmitter<ConversationEvent> for Conversation {}
impl Conversation {
fn new(
model: LanguageModel,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
telemetry: Option<Arc<Telemetry>>,
@@ -1530,7 +1510,6 @@ impl Conversation {
token_count: None,
pending_token_count: Task::ready(None),
pending_edit_suggestion_parse: None,
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: None,
@@ -1583,7 +1562,6 @@ impl Conversation {
#[allow(clippy::too_many_arguments)]
async fn deserialize(
saved_conversation: SavedConversation,
model: LanguageModel,
path: PathBuf,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
@@ -1640,7 +1618,6 @@ impl Conversation {
token_count: None,
pending_edit_suggestion_parse: None,
pending_token_count: Task::ready(None),
model,
_subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
pending_save: Task::ready(Ok(())),
path: Some(path),
@@ -1769,7 +1746,6 @@ impl Conversation {
let pending_command = PendingSlashCommand {
name: name.to_string(),
argument: argument.map(ToString::to_string),
tooltip_text: command.tooltip_text().into(),
source_range,
status: PendingSlashCommandStatus::Idle,
};
@@ -1938,12 +1914,12 @@ impl Conversation {
}
}
fn remaining_tokens(&self) -> Option<isize> {
Some(self.model.max_token_count() as isize - self.token_count? as isize)
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
let model = CompletionProvider::global(cx).model();
Some(model.max_token_count() as isize - self.token_count? as isize)
}
fn set_model(&mut self, model: LanguageModel, cx: &mut ModelContext<Self>) {
self.model = model;
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
self.count_remaining_tokens(cx);
}
@@ -2079,10 +2055,11 @@ impl Conversation {
}
if let Some(telemetry) = this.telemetry.as_ref() {
let model = CompletionProvider::global(cx).model();
telemetry.report_assistant_event(
this.id.clone(),
AssistantKind::Panel,
this.model.telemetry_id(),
model.telemetry_id(),
response_latency,
error_message,
);
@@ -2111,7 +2088,7 @@ impl Conversation {
.map(|message| message.to_request_message(self.buffer.read(cx)));
LanguageModelRequest {
model: self.model.clone(),
model: CompletionProvider::global(cx).model(),
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
@@ -2300,7 +2277,7 @@ impl Conversation {
.into(),
}));
let request = LanguageModelRequest {
model: self.model.clone(),
model: CompletionProvider::global(cx).model(),
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
@@ -2565,7 +2542,6 @@ struct PendingSlashCommand {
argument: Option<String>,
status: PendingSlashCommandStatus,
source_range: Range<language::Anchor>,
tooltip_text: SharedString,
}
#[derive(Clone)]
@@ -2605,7 +2581,6 @@ pub struct ConversationEditor {
impl ConversationEditor {
fn new(
model: LanguageModel,
language_registry: Arc<LanguageRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
fs: Arc<dyn Fs>,
@@ -2618,7 +2593,6 @@ impl ConversationEditor {
let conversation = cx.new_model(|cx| {
Conversation::new(
model,
language_registry,
slash_command_registry,
Some(telemetry),
@@ -2740,11 +2714,47 @@ impl ConversationEditor {
.collect()
}
fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
if let Some(command) = self.slash_command_registry.command(name) {
self.editor.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0
|| snapshot
.chars_at(newest_cursor)
.next()
.map_or(false, |ch| ch != '\n')
{
editor.move_to_end_of_line(
&MoveToEndOfLine {
stop_at_soft_wraps: false,
},
cx,
);
editor.newline(&Newline, cx);
}
editor.insert(&format!("/{name}"), cx);
if command.requires_argument() {
editor.insert(" ", cx);
editor.show_completions(&ShowCompletions, cx);
}
});
});
if !command.requires_argument() {
self.confirm_command(&ConfirmCommand, cx);
}
}
}
pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
let selections = self.editor.read(cx).selections.disjoint_anchors();
let mut commands_by_range = HashMap::default();
let workspace = self.workspace.clone();
self.conversation.update(cx, |conversation, cx| {
conversation.reparse_slash_commands(cx);
for selection in selections.iter() {
if let Some(command) =
conversation.pending_command_for_position(selection.head().text_anchor, cx)
@@ -2901,9 +2911,8 @@ impl ConversationEditor {
let confirm_command = confirm_command.clone();
let command = command.clone();
move |row, _, _, _cx: &mut WindowContext| {
render_pending_slash_command_toggle(
render_pending_slash_command_gutter_decoration(
row,
command.tooltip_text.clone(),
command.status.clone(),
confirm_command.clone(),
)
@@ -3515,8 +3524,7 @@ impl Render for InlineAssistant {
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.justify_center()
.w(measurements.gutter_width)
.w(measurements.gutter_width + measurements.gutter_margin)
.children(if let Some(error) = self.codegen.read(cx).error() {
let error_message = SharedString::from(error.to_string());
Some(
@@ -3529,12 +3537,7 @@ impl Render for InlineAssistant {
None
}),
)
.child(
h_flex()
.w_full()
.ml(measurements.anchor_x - measurements.gutter_width)
.child(self.render_prompt_editor(cx)),
)
.child(h_flex().flex_1().child(self.render_prompt_editor(cx)))
}
}
@@ -3703,8 +3706,8 @@ impl InlineAssistant {
// This wouldn't need to exist if we could pass parameters when rendering child views.
#[derive(Copy, Clone, Default)]
struct BlockMeasurements {
anchor_x: Pixels,
gutter_width: Pixels,
gutter_margin: Pixels,
}
struct PendingInlineAssist {
@@ -3736,14 +3739,13 @@ fn render_slash_command_output_toggle(
.into_any_element()
}
fn render_pending_slash_command_toggle(
fn render_pending_slash_command_gutter_decoration(
row: MultiBufferRow,
tooltip_text: SharedString,
status: PendingSlashCommandStatus,
confirm_command: Arc<dyn Fn(&mut WindowContext)>,
) -> AnyElement {
let mut icon = IconButton::new(
("slash-command-output-fold-indicator", row.0),
("slash-command-gutter-decoration", row.0),
ui::IconName::TriangleRight,
)
.on_click(move |_e, cx| confirm_command(cx))
@@ -3752,14 +3754,10 @@ fn render_pending_slash_command_toggle(
match status {
PendingSlashCommandStatus::Idle => {
icon = icon
.icon_color(Color::Muted)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
icon = icon.icon_color(Color::Muted);
}
PendingSlashCommandStatus::Running { .. } => {
icon = icon
.selected(true)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx));
icon = icon.selected(true);
}
PendingSlashCommandStatus::Error(error) => {
icon = icon
@@ -3847,15 +3845,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -3986,15 +3977,8 @@ mod tests {
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -4092,15 +4076,8 @@ mod tests {
cx.set_global(settings_store);
init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry,
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry, Default::default(), None, cx));
let buffer = conversation.read(cx).buffer.clone();
let message_1 = conversation.read(cx).message_anchors[0].clone();
@@ -4203,21 +4180,15 @@ mod tests {
let prompt_library = Arc::new(PromptLibrary::default());
let slash_command_registry = SlashCommandRegistry::new();
slash_command_registry.register_command(file_command::FileSlashCommand);
slash_command_registry.register_command(prompt_command::PromptSlashCommand::new(
prompt_library.clone(),
));
slash_command_registry.register_command(file_command::FileSlashCommand, false);
slash_command_registry.register_command(
prompt_command::PromptSlashCommand::new(prompt_library.clone()),
false,
);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry.clone(),
slash_command_registry,
None,
cx,
)
});
let conversation = cx
.new_model(|cx| Conversation::new(registry.clone(), slash_command_registry, None, cx));
let output_ranges = Rc::new(RefCell::new(HashSet::default()));
conversation.update(cx, |_, cx| {
@@ -4390,15 +4361,8 @@ mod tests {
cx.set_global(CompletionProvider::Fake(FakeCompletionProvider::default()));
cx.update(init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let conversation = cx.new_model(|cx| {
Conversation::new(
LanguageModel::default(),
registry.clone(),
Default::default(),
None,
cx,
)
});
let conversation =
cx.new_model(|cx| Conversation::new(registry.clone(), Default::default(), None, cx));
let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
let message_0 =
conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
@@ -4434,7 +4398,6 @@ mod tests {
let deserialized_conversation = Conversation::deserialize(
conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
LanguageModel::default(),
Default::default(),
registry.clone(),
Default::default(),

View File

@@ -12,8 +12,11 @@ use serde::{
Deserialize, Deserializer, Serialize, Serializer,
};
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
#[derive(Clone, Debug, Default, PartialEq)]
use crate::LanguageModel;
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum ZedDotDevModel {
Gpt3Point5Turbo,
Gpt4,
@@ -53,13 +56,10 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
where
E: de::Error,
{
match value {
"gpt-3.5-turbo" => Ok(ZedDotDevModel::Gpt3Point5Turbo),
"gpt-4" => Ok(ZedDotDevModel::Gpt4),
"gpt-4-turbo-preview" => Ok(ZedDotDevModel::Gpt4Turbo),
"gpt-4o" => Ok(ZedDotDevModel::Gpt4Omni),
_ => Ok(ZedDotDevModel::Custom(value.to_owned())),
}
let model = ZedDotDevModel::iter()
.find(|model| model.id() == value)
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
Ok(model)
}
}
@@ -73,24 +73,23 @@ impl JsonSchema for ZedDotDevModel {
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = vec![
"gpt-3.5-turbo".to_owned(),
"gpt-4".to_owned(),
"gpt-4-turbo-preview".to_owned(),
"gpt-4o".to_owned(),
];
let variants = ZedDotDevModel::iter()
.filter_map(|model| {
let id = model.id();
if id.is_empty() {
None
} else {
Some(id.to_string())
}
})
.collect::<Vec<_>>();
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(variants.into_iter().map(|s| s.into()).collect()),
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(serde_json::json!("gpt-4-turbo-preview")),
examples: vec![
serde_json::json!("gpt-3.5-turbo"),
serde_json::json!("gpt-4"),
serde_json::json!("gpt-4-turbo-preview"),
serde_json::json!("custom-model-name"),
],
default: Some(ZedDotDevModel::default().id().into()),
examples: variants.into_iter().map(Into::into).collect(),
..Default::default()
})),
..Default::default()
@@ -145,51 +144,55 @@ pub enum AssistantDockPosition {
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
#[derive(Debug, PartialEq)]
pub enum AssistantProvider {
#[serde(rename = "zed.dev")]
ZedDotDev {
#[serde(default)]
default_model: ZedDotDevModel,
model: ZedDotDevModel,
},
#[serde(rename = "openai")]
OpenAi {
#[serde(default)]
default_model: OpenAiModel,
#[serde(default = "open_ai_url")]
model: OpenAiModel,
api_url: String,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
#[serde(default)]
default_model: AnthropicModel,
#[serde(default = "anthropic_api_url")]
model: AnthropicModel,
api_url: String,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
}
impl Default for AssistantProvider {
fn default() -> Self {
Self::ZedDotDev {
default_model: ZedDotDevModel::default(),
Self::OpenAi {
model: OpenAiModel::default(),
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
}
}
fn open_ai_url() -> String {
open_ai::OPEN_AI_API_URL.to_string()
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContent {
#[serde(rename = "zed.dev")]
ZedDotDev {
default_model: Option<ZedDotDevModel>,
},
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
}
fn anthropic_api_url() -> String {
anthropic::ANTHROPIC_API_URL.to_string()
}
#[derive(Default, Debug, Deserialize, Serialize)]
#[derive(Debug, Default)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
@@ -240,16 +243,16 @@ impl AssistantSettingsContent {
default_width: settings.default_width,
default_height: settings.default_height,
provider: if let Some(open_ai_api_url) = settings.openai_api_url.as_ref() {
Some(AssistantProvider::OpenAi {
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
api_url: open_ai_api_url.clone(),
Some(AssistantProviderContent::OpenAi {
default_model: settings.default_open_ai_model.clone(),
api_url: Some(open_ai_api_url.clone()),
low_speed_timeout_in_seconds: None,
})
} else {
settings.default_open_ai_model.clone().map(|open_ai_model| {
AssistantProvider::OpenAi {
default_model: open_ai_model,
api_url: open_ai_url(),
AssistantProviderContent::OpenAi {
default_model: Some(open_ai_model),
api_url: None,
low_speed_timeout_in_seconds: None,
}
})
@@ -270,6 +273,64 @@ impl AssistantSettingsContent {
}
}
}
pub fn set_model(&mut self, new_model: LanguageModel) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match &mut settings.provider {
Some(AssistantProviderContent::ZedDotDev {
default_model: model,
}) => {
if let LanguageModel::ZedDotDev(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::OpenAi {
default_model: model,
..
}) => {
if let LanguageModel::OpenAi(new_model) = new_model {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::Anthropic {
default_model: model,
..
}) => {
if let LanguageModel::Anthropic(new_model) = new_model {
*model = Some(new_model);
}
}
provider => match new_model {
LanguageModel::ZedDotDev(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
default_model: Some(model),
})
}
LanguageModel::OpenAi(model) => {
*provider = Some(AssistantProviderContent::OpenAi {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Anthropic(model) => {
*provider = Some(AssistantProviderContent::Anthropic {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
},
},
},
AssistantSettingsContent::Legacy(settings) => {
if let LanguageModel::OpenAi(model) = new_model {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -318,7 +379,7 @@ pub struct AssistantSettingsContentV1 {
///
/// This can either be the internal `zed.dev` service or an external `openai` service,
/// each with their respective default models and configurations.
provider: Option<AssistantProvider>,
provider: Option<AssistantProviderContent>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
@@ -376,31 +437,82 @@ impl Settings for AssistantSettings {
if let Some(provider) = value.provider.clone() {
match (&mut settings.provider, provider) {
(
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev {
default_model: default_model_override,
AssistantProvider::ZedDotDev { model },
AssistantProviderContent::ZedDotDev {
default_model: model_override,
},
) => {
*default_model = default_model_override;
merge(model, model_override);
}
(
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProvider::OpenAi {
default_model: default_model_override,
AssistantProviderContent::OpenAi {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
*default_model = default_model_override;
*api_url = api_url_override;
*low_speed_timeout_in_seconds = low_speed_timeout_in_seconds_override;
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(merged, provider_override) => {
*merged = provider_override;
(
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(provider, provider_override) => {
*provider = match provider_override {
AssistantProviderContent::ZedDotDev {
default_model: model,
} => AssistantProvider::ZedDotDev {
model: model.unwrap_or_default(),
},
AssistantProviderContent::OpenAi {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::OpenAi {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Anthropic {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Anthropic {
model: model.unwrap_or_default(),
api_url: api_url
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
low_speed_timeout_in_seconds,
},
};
}
}
}
@@ -410,7 +522,7 @@ impl Settings for AssistantSettings {
}
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
@@ -433,8 +545,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourOmni,
api_url: open_ai_url(),
model: OpenAiModel::FourOmni,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
);
@@ -455,7 +567,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourOmni,
model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
}
@@ -475,8 +587,8 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::Four,
api_url: open_ai_url(),
model: OpenAiModel::Four,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
}
);
@@ -501,7 +613,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
default_model: ZedDotDevModel::Custom("custom".into())
model: ZedDotDevModel::Custom("custom".into())
}
);
}

View File

@@ -391,7 +391,7 @@ mod tests {
use super::*;
use futures::stream::{self};
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,

View File

@@ -25,31 +25,26 @@ use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { default_model } => {
CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
settings_version,
cx,
))
}
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -65,13 +60,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::OpenAi(provider),
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
default_model.clone(),
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -80,13 +75,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
default_model.clone(),
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -94,13 +89,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
}
(
CompletionProvider::ZedDotDev(provider),
AssistantProvider::ZedDotDev { default_model },
AssistantProvider::ZedDotDev { model },
) => {
provider.update(default_model.clone(), settings_version);
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { default_model }) => {
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
model.clone(),
client.clone(),
settings_version,
cx,
@@ -109,13 +104,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
AssistantProvider::OpenAi {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -125,13 +120,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
(
_,
AssistantProvider::Anthropic {
default_model,
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -159,6 +154,25 @@ impl CompletionProvider {
cx.global::<Self>()
}
pub fn available_models(&self) -> Vec<LanguageModel> {
match self {
CompletionProvider::OpenAi(provider) => provider
.available_models()
.map(LanguageModel::OpenAi)
.collect(),
CompletionProvider::Anthropic(provider) => provider
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::ZedDotDev(provider) => provider
.available_models()
.map(LanguageModel::ZedDotDev)
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
}
pub fn settings_version(&self) -> usize {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
@@ -209,17 +223,13 @@ impl CompletionProvider {
}
}
pub fn default_model(&self) -> LanguageModel {
pub fn model(&self) -> LanguageModel {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.default_model()),
CompletionProvider::Anthropic(provider) => {
LanguageModel::Anthropic(provider.default_model())
}
CompletionProvider::ZedDotDev(provider) => {
LanguageModel::ZedDotDev(provider.default_model())
}
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
CompletionProvider::Fake(_) => LanguageModel::default(),
}
}

View File

@@ -12,6 +12,7 @@ use http::HttpClient;
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
@@ -19,7 +20,7 @@ use util::ResultExt;
pub struct AnthropicCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: AnthropicModel,
model: AnthropicModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
@@ -27,7 +28,7 @@ pub struct AnthropicCompletionProvider {
impl AnthropicCompletionProvider {
pub fn new(
default_model: AnthropicModel,
model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -36,7 +37,7 @@ impl AnthropicCompletionProvider {
Self {
api_key: None,
api_url,
default_model,
model,
http_client,
low_speed_timeout,
settings_version,
@@ -45,17 +46,21 @@ impl AnthropicCompletionProvider {
pub fn update(
&mut self,
default_model: AnthropicModel,
model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
AnthropicModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -105,8 +110,8 @@ impl AnthropicCompletionProvider {
.into()
}
pub fn default_model(&self) -> AnthropicModel {
self.default_model.clone()
pub fn model(&self) -> AnthropicModel {
self.model.clone()
}
pub fn count_tokens(
@@ -165,7 +170,7 @@ impl AnthropicCompletionProvider {
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.default_model(),
_ => self.model(),
};
let mut system_message = String::new();

View File

@@ -11,6 +11,7 @@ use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use strum::IntoEnumIterator;
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
@@ -18,7 +19,7 @@ use util::ResultExt;
pub struct OpenAiCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: OpenAiModel,
model: OpenAiModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
@@ -26,7 +27,7 @@ pub struct OpenAiCompletionProvider {
impl OpenAiCompletionProvider {
pub fn new(
default_model: OpenAiModel,
model: OpenAiModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
@@ -35,7 +36,7 @@ impl OpenAiCompletionProvider {
Self {
api_key: None,
api_url,
default_model,
model,
http_client,
low_speed_timeout,
settings_version,
@@ -44,17 +45,21 @@ impl OpenAiCompletionProvider {
pub fn update(
&mut self,
default_model: OpenAiModel,
model: OpenAiModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
OpenAiModel::iter()
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
@@ -104,8 +109,8 @@ impl OpenAiCompletionProvider {
.into()
}
pub fn default_model(&self) -> OpenAiModel {
self.default_model.clone()
pub fn model(&self) -> OpenAiModel {
self.model.clone()
}
pub fn count_tokens(
@@ -152,7 +157,7 @@ impl OpenAiCompletionProvider {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::OpenAi(model) => model,
_ => self.default_model(),
_ => self.model(),
};
Request {

View File

@@ -7,11 +7,12 @@ use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt, TryFutureExt};
use gpui::{AnyView, AppContext, Task};
use std::{future, sync::Arc};
use strum::IntoEnumIterator;
use ui::prelude::*;
pub struct ZedDotDevCompletionProvider {
client: Arc<Client>,
default_model: ZedDotDevModel,
model: ZedDotDevModel,
settings_version: usize,
status: client::Status,
_maintain_client_status: Task<()>,
@@ -19,7 +20,7 @@ pub struct ZedDotDevCompletionProvider {
impl ZedDotDevCompletionProvider {
pub fn new(
default_model: ZedDotDevModel,
model: ZedDotDevModel,
client: Arc<Client>,
settings_version: usize,
cx: &mut AppContext,
@@ -39,24 +40,39 @@ impl ZedDotDevCompletionProvider {
});
Self {
client,
default_model,
model,
settings_version,
status,
_maintain_client_status: maintain_client_status,
}
}
pub fn update(&mut self, default_model: ZedDotDevModel, settings_version: usize) {
self.default_model = default_model;
pub fn update(&mut self, model: ZedDotDevModel, settings_version: usize) {
self.model = model;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = ZedDotDevModel> {
let mut custom_model = if let ZedDotDevModel::Custom(custom_model) = self.model.clone() {
Some(custom_model)
} else {
None
};
ZedDotDevModel::iter().filter_map(move |model| {
if let ZedDotDevModel::Custom(_) = model {
Some(ZedDotDevModel::Custom(custom_model.take()?))
} else {
Some(model)
}
})
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn default_model(&self) -> ZedDotDevModel {
self.default_model.clone()
pub fn model(&self) -> ZedDotDevModel {
self.model.clone()
}
pub fn is_authenticated(&self) -> bool {

View File

@@ -0,0 +1,84 @@
use std::sync::Arc;
use crate::{assistant_settings::AssistantSettings, CompletionProvider, ToggleModelSelector};
use fs::Fs;
use settings::update_settings_file;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, PopoverMenuHandle, Tooltip};
#[derive(IntoElement)]
pub struct ModelSelector {
handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
}
impl ModelSelector {
pub fn new(handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>) -> Self {
ModelSelector { handle, fs }
}
}
impl RenderOnce for ModelSelector {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
popover_menu("model-switcher")
.with_handle(self.handle)
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.display_name()).into_any_element()
},
{
let fs = self.fs.clone();
let model = model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings| settings.set_model(model),
);
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(
CompletionProvider::global(cx).model().display_name(),
)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.anchor(gpui::AnchorCorner::BottomRight)
}
}

View File

@@ -0,0 +1,810 @@
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use collections::HashMap;
use editor::Editor;
use futures::{
future::{self, BoxFuture, Shared},
FutureExt,
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, AppContext, BackgroundExecutor, Bounds, DevicePixels, Empty,
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions, View,
WindowBounds, WindowHandle, WindowOptions,
};
use heed::{types::SerdeBincode, Database, RoTxn};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
use parking_lot::Mutex;
use picker::{Picker, PickerDelegate};
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
future::Future,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{
div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, TitleBar, Tooltip, ViewContext, VisualContext,
};
use util::{paths::PROMPTS_DIR, ResultExt};
use uuid::Uuid;
actions!(
prompt_library,
[NewPrompt, SavePrompt, DeletePrompt, ToggleDefaultPrompt]
);
/// Init starts loading the PromptStore in the background and assigns
/// a shared future to a global.
pub fn init(cx: &mut AppContext) {
let db_path = PROMPTS_DIR.join("prompts-library-db.0.mdb");
let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
.shared();
cx.set_global(GlobalPromptStore(prompt_store_future))
}
/// This function opens a new prompt library window if one doesn't exist already.
/// If one exists, it brings it to the foreground.
///
/// Note that, when opening a new window, this waits for the PromptStore to be
/// initialized. If it was initialized successfully, it returns a window handle
/// to a prompt library.
pub fn open_prompt_library(
language_registry: Arc<LanguageRegistry>,
cx: &mut AppContext,
) -> Task<Result<WindowHandle<PromptLibrary>>> {
let existing_window = cx
.windows()
.into_iter()
.find_map(|window| window.downcast::<PromptLibrary>());
if let Some(existing_window) = existing_window {
existing_window
.update(cx, |_, cx| cx.activate_window())
.ok();
Task::ready(Ok(existing_window))
} else {
let store = PromptStore::global(cx);
cx.spawn(|cx| async move {
let store = store.await?;
cx.update(|cx| {
let bounds = Bounds::centered(
None,
size(DevicePixels::from(1024), DevicePixels::from(768)),
cx,
);
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: None,
appears_transparent: true,
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
)
})
})
}
}
pub struct PromptLibrary {
store: Arc<PromptStore>,
language_registry: Arc<LanguageRegistry>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
picker: View<Picker<PromptPickerDelegate>>,
pending_load: Task<()>,
_subscriptions: Vec<Subscription>,
}
struct PromptPickerDelegate {
store: Arc<PromptStore>,
selected_index: usize,
matches: Vec<PromptMetadata>,
}
enum PromptPickerEvent {
Confirmed { prompt_id: PromptId },
Deleted { prompt_id: PromptId },
ToggledDefault { prompt_id: PromptId },
}
impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
impl PickerDelegate for PromptPickerDelegate {
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...".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search = self.store.search(query);
cx.spawn(|this, mut cx| async move {
let matches = search.await;
this.update(&mut cx, |this, cx| {
this.delegate.selected_index = 0;
this.delegate.matches = matches;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(prompt) = self.matches.get(self.selected_index) {
cx.emit(PromptPickerEvent::Confirmed {
prompt_id: prompt.id,
});
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let prompt = self.matches.get(ix)?;
let default = prompt.default;
let prompt_id = prompt.id;
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
))
.end_slot(if default {
IconButton::new("toggle-default-prompt", IconName::StarFilled)
.shape(IconButtonShape::Square)
.into_any_element()
} else {
Empty.into_any()
})
.end_hover_slot(
h_flex()
.gap_2()
.child(
IconButton::new("delete-prompt", IconName::Trash)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::Deleted { prompt_id })
})),
)
.child(
IconButton::new(
"toggle-default-prompt",
if default {
IconName::StarFilled
} else {
IconName::Star
},
)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::text(
if default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
})),
),
),
)
}
}
impl PromptLibrary {
fn new(
store: Arc<PromptStore>,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = PromptPickerDelegate {
store: store.clone(),
selected_index: 0,
matches: Vec::new(),
};
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx)
.modal(false)
.max_height(None);
picker.focus(cx);
picker
});
let mut this = Self {
store: store.clone(),
language_registry,
prompt_editors: HashMap::default(),
active_prompt_id: None,
pending_load: Task::ready(()),
_subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
picker,
};
if let Some(prompt_id) = store.most_recently_saved() {
this.load_prompt(prompt_id, false, cx);
}
this
}
fn handle_picker_event(
&mut self,
_: View<Picker<PromptPickerDelegate>>,
event: &PromptPickerEvent,
cx: &mut ViewContext<Self>,
) {
match event {
PromptPickerEvent::Confirmed { prompt_id } => {
self.load_prompt(*prompt_id, true, cx);
}
PromptPickerEvent::ToggledDefault { prompt_id } => {
self.toggle_default_for_prompt(*prompt_id, cx);
}
PromptPickerEvent::Deleted { prompt_id } => {
self.delete_prompt(*prompt_id, cx);
}
}
}
pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
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);
}
pub fn save_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
let prompt_metadata = self.store.metadata(active_prompt_id).unwrap();
let body = self
.prompt_editors
.get_mut(&active_prompt_id)
.unwrap()
.update(cx, |editor, cx| editor.snapshot(cx));
let title = title_from_body(body.buffer_chars_at(0).map(|(c, _)| c));
self.store
.save(
active_prompt_id,
title,
prompt_metadata.default,
body.text(),
)
.detach_and_log_err(cx);
self.picker.update(cx, |picker, cx| picker.refresh(cx));
cx.notify();
}
}
pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.delete_prompt(active_prompt_id, cx);
}
}
pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
self.toggle_default_for_prompt(active_prompt_id, cx);
}
}
pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
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();
}
}
pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
if focus {
prompt_editor.update(cx, |editor, cx| editor.focus(cx));
}
self.active_prompt_id = Some(prompt_id);
} else {
let language_registry = self.language_registry.clone();
let prompt = self.store.load(prompt_id);
self.pending_load = cx.spawn(|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 {
Ok(prompt) => {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
buffer.set_language_registry(language_registry);
buffer
});
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, 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);
if focus {
editor.focus(cx);
}
editor
});
this.prompt_editors.insert(prompt_id, editor);
this.active_prompt_id = Some(prompt_id);
cx.notify();
}
Err(error) => {
// TODO: we should show the error in the UI.
log::error!("error while loading prompt: {:?}", error);
}
})
.ok();
});
}
}
pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
if let Some(metadata) = self.store.metadata(prompt_id) {
let confirmation = cx.prompt(
PromptLevel::Warning,
&format!(
"Are you sure you want to delete {}",
metadata.title.unwrap_or("Untitled".into())
),
None,
&["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.active_prompt_id = None;
}
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);
}
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.id("prompt-list")
.bg(cx.theme().colors().surface_background)
.h_full()
.w_1_3()
.overflow_x_hidden()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h(TitleBar::height(cx))
.w_full()
.flex_none()
.justify_end()
.child(
IconButton::new("new-prompt", IconName::Plus)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
.on_click(|_, cx| {
cx.dispatch_action(Box::new(NewPrompt));
}),
),
)
.child(div().flex_grow().child(self.picker.clone()))
}
fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
div()
.w_2_3()
.h_full()
.id("prompt-editor")
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.flex_none()
.min_w_64()
.children(self.active_prompt_id.and_then(|prompt_id| {
let prompt_metadata = self.store.metadata(prompt_id)?;
let editor = self.prompt_editors[&prompt_id].clone();
Some(
v_flex()
.size_full()
.child(
h_flex()
.h(TitleBar::height(cx))
.px(Spacing::Large.rems(cx))
.justify_between()
.child(
Label::new(prompt_metadata.title.unwrap_or("Untitled".into()))
.size(LabelSize::Large),
)
.child(
h_flex()
.gap_4()
.child(
IconButton::new("save-prompt", IconName::Save)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::for_action(
"Save Prompt",
&SavePrompt,
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(SavePrompt));
}),
)
.child(
IconButton::new(
"toggle-default-prompt",
if prompt_metadata.default {
IconName::StarFilled
} else {
IconName::Star
},
)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::for_action(
if prompt_metadata.default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
&ToggleDefaultPrompt,
cx,
)
})
.on_click(
|_, cx| {
cx.dispatch_action(Box::new(
ToggleDefaultPrompt,
));
},
),
)
.child(
IconButton::new("delete-prompt", IconName::Trash)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(DeletePrompt));
}),
),
),
)
.child(div().flex_grow().p(Spacing::Large.rems(cx)).child(editor)),
)
}))
}
}
impl Render for PromptLibrary {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.id("prompt-manager")
.key_context("PromptLibrary")
.on_action(cx.listener(|this, &NewPrompt, cx| this.new_prompt(cx)))
.on_action(cx.listener(|this, &SavePrompt, cx| this.save_active_prompt(cx)))
.on_action(cx.listener(|this, &DeletePrompt, cx| this.delete_active_prompt(cx)))
.on_action(cx.listener(|this, &ToggleDefaultPrompt, cx| {
this.toggle_default_for_active_prompt(cx)
}))
.size_full()
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(self.render_active_prompt(cx))
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct PromptMetadata {
pub id: PromptId,
pub title: Option<SharedString>,
pub default: bool,
pub saved_at: DateTime<Utc>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PromptId(Uuid);
impl PromptId {
pub fn new() -> PromptId {
PromptId(Uuid::new_v4())
}
}
pub struct PromptStore {
executor: BackgroundExecutor,
env: heed::Env,
bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
metadata_cache: Mutex<MetadataCache>,
}
#[derive(Default)]
struct MetadataCache {
metadata: Vec<PromptMetadata>,
metadata_by_id: HashMap<PromptId, PromptMetadata>,
}
impl MetadataCache {
fn from_db(
db: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
txn: &RoTxn,
) -> Result<Self> {
let mut cache = MetadataCache::default();
for result in db.iter(txn)? {
let (prompt_id, metadata) = result?;
cache.metadata.push(metadata.clone());
cache.metadata_by_id.insert(prompt_id, metadata);
}
cache
.metadata
.sort_unstable_by_key(|metadata| Reverse(metadata.saved_at));
Ok(cache)
}
fn insert(&mut self, metadata: PromptMetadata) {
self.metadata_by_id.insert(metadata.id, metadata.clone());
if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) {
*old_metadata = metadata;
} else {
self.metadata.push(metadata);
}
self.metadata.sort_by_key(|m| Reverse(m.saved_at));
}
fn remove(&mut self, id: PromptId) {
self.metadata.retain(|metadata| metadata.id != id);
self.metadata_by_id.remove(&id);
}
}
impl PromptStore {
pub fn global(cx: &AppContext) -> impl Future<Output = Result<Arc<Self>>> {
let store = GlobalPromptStore::global(cx).0.clone();
async move { store.await.map_err(|err| anyhow!(err)) }
}
pub fn new(db_path: PathBuf, executor: BackgroundExecutor) -> Task<Result<Self>> {
executor.spawn({
let executor = executor.clone();
async move {
std::fs::create_dir_all(&db_path)?;
let db_env = unsafe {
heed::EnvOpenOptions::new()
.map_size(1024 * 1024 * 1024) // 1GB
.max_dbs(2) // bodies and metadata
.open(db_path)?
};
let mut txn = db_env.write_txn()?;
let bodies = db_env.create_database(&mut txn, Some("bodies"))?;
let metadata = db_env.create_database(&mut txn, Some("metadata"))?;
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
txn.commit()?;
Ok(PromptStore {
executor,
env: db_env,
bodies,
metadata,
metadata_cache: Mutex::new(metadata_cache),
})
}
})
}
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
let env = self.env.clone();
let bodies = self.bodies;
self.executor.spawn(async move {
let txn = env.read_txn()?;
Ok(bodies
.get(&txn, &id)?
.ok_or_else(|| anyhow!("prompt not found"))?)
})
}
pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
self.metadata_cache.lock().remove(id);
let db_connection = self.env.clone();
let bodies = self.bodies;
let metadata = self.metadata;
self.executor.spawn(async move {
let mut txn = db_connection.write_txn()?;
metadata.delete(&mut txn, &id)?;
bodies.delete(&mut txn, &id)?;
txn.commit()?;
Ok(())
})
}
fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
self.metadata_cache.lock().metadata_by_id.get(&id).cloned()
}
pub fn id_for_title(&self, title: &str) -> Option<PromptId> {
let metadata_cache = self.metadata_cache.lock();
let metadata = metadata_cache
.metadata
.iter()
.find(|metadata| metadata.title.as_ref().map(|title| &***title) == Some(title))?;
Some(metadata.id)
}
pub fn search(&self, query: String) -> Task<Vec<PromptMetadata>> {
let cached_metadata = self.metadata_cache.lock().metadata.clone();
let executor = self.executor.clone();
self.executor.spawn(async move {
if query.is_empty() {
cached_metadata
} else {
let candidates = cached_metadata
.iter()
.enumerate()
.filter_map(|(ix, metadata)| {
Some(StringMatchCandidate::new(
ix,
metadata.title.as_ref()?.to_string(),
))
})
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&AtomicBool::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| cached_metadata[mat.candidate_id].clone())
.collect()
}
})
}
fn save(
&self,
id: PromptId,
title: Option<SharedString>,
default: bool,
body: String,
) -> Task<Result<()>> {
let prompt_metadata = PromptMetadata {
id,
title,
default,
saved_at: Utc::now(),
};
self.metadata_cache.lock().insert(prompt_metadata.clone());
let db_connection = self.env.clone();
let bodies = self.bodies;
let metadata = self.metadata;
self.executor.spawn(async move {
let mut txn = db_connection.write_txn()?;
metadata.put(&mut txn, &id, &prompt_metadata)?;
bodies.put(&mut txn, &id, &body)?;
txn.commit()?;
Ok(())
})
}
fn save_metadata(
&self,
id: PromptId,
title: Option<SharedString>,
default: bool,
) -> Task<Result<()>> {
let prompt_metadata = PromptMetadata {
id,
title,
default,
saved_at: Utc::now(),
};
self.metadata_cache.lock().insert(prompt_metadata.clone());
let db_connection = self.env.clone();
let metadata = self.metadata;
self.executor.spawn(async move {
let mut txn = db_connection.write_txn()?;
metadata.put(&mut txn, &id, &prompt_metadata)?;
txn.commit()?;
Ok(())
})
}
fn most_recently_saved(&self) -> Option<PromptId> {
self.metadata_cache
.lock()
.metadata
.first()
.map(|metadata| metadata.id)
}
}
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
pub struct GlobalPromptStore(
Shared<BoxFuture<'static, Result<Arc<PromptStore>, Arc<anyhow::Error>>>>,
);
impl Global for GlobalPromptStore {}
fn title_from_body<'a>(body: impl IntoIterator<Item = char>) -> Option<SharedString> {
let mut chars = body.into_iter().take_while(|c| *c != '\n').peekable();
let mut level = 0;
while let Some('#') = chars.peek() {
level += 1;
chars.next();
}
if level > 0 {
Some(chars.collect::<String>().trim().to_string().into())
} else {
None
}
}

View File

@@ -73,7 +73,7 @@ fn line_similarity(line1: &str, line2: &str) -> f64 {
#[cfg(test)]
mod test {
use super::*;
use gpui::{AppContext, Context as _};
use gpui::{AppContext, StaticContext as _};
use language::Buffer;
use unindent::Unindent as _;
use util::test::marked_text_ranges;

View File

@@ -19,8 +19,8 @@ impl SlashCommand for ActiveSlashCommand {
"insert active tab".into()
}
fn tooltip_text(&self) -> String {
"insert active tab".into()
fn menu_text(&self) -> String {
"Insert Active Tab".into()
}
fn complete_argument(

View File

@@ -86,11 +86,11 @@ impl SlashCommand for FileSlashCommand {
}
fn description(&self) -> String {
"insert a file".into()
"insert file".into()
}
fn tooltip_text(&self) -> String {
"insert file".into()
fn menu_text(&self) -> String {
"Insert File".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -94,11 +94,11 @@ impl SlashCommand for ProjectSlashCommand {
}
fn description(&self) -> String {
"insert current project context".into()
"insert project metadata".into()
}
fn tooltip_text(&self) -> String {
"insert current project context".into()
fn menu_text(&self) -> String {
"Insert Project Metadata".into()
}
fn complete_argument(

View File

@@ -25,11 +25,11 @@ impl SlashCommand for PromptSlashCommand {
}
fn description(&self) -> String {
"insert a prompt from the library".into()
"insert prompt from library".into()
}
fn tooltip_text(&self) -> String {
"insert prompt".into()
fn menu_text(&self) -> String {
"Insert Prompt from Library".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -17,10 +17,17 @@ impl RustdocSlashCommand {
async fn build_message(
http_client: Arc<HttpClientWithUrl>,
crate_name: String,
module_path: Vec<String>,
) -> Result<String> {
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}/{module_path}",
module_path = module_path.join("/")
);
let mut response = http_client
.get(
&format!("https://docs.rs/{crate_name}"),
&format!("https://docs.rs/{path}"),
AsyncBody::default(),
true,
)
@@ -51,11 +58,11 @@ impl SlashCommand for RustdocSlashCommand {
}
fn description(&self) -> String {
"insert the docs for a Rust crate".into()
"insert Rust docs".into()
}
fn tooltip_text(&self) -> String {
"insert rustdoc".into()
fn menu_text(&self) -> String {
"Insert Rust Documentation".into()
}
fn requires_argument(&self) -> bool {
@@ -87,14 +94,28 @@ impl SlashCommand for RustdocSlashCommand {
};
let http_client = workspace.read(cx).client().http_client();
let crate_name = argument.to_string();
let mut path_components = argument.split("::");
let crate_name = match path_components
.next()
.ok_or_else(|| anyhow!("missing crate name"))
{
Ok(crate_name) => crate_name.to_string(),
Err(err) => return Task::ready(Err(err)),
};
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let text = cx.background_executor().spawn({
let crate_name = crate_name.clone();
async move { Self::build_message(http_client, crate_name).await }
let module_path = module_path.clone();
async move { Self::build_message(http_client, crate_name, module_path).await }
});
let crate_name = SharedString::from(crate_name);
let module_path = if module_path.is_empty() {
None
} else {
Some(SharedString::from(module_path.join("::")))
};
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
@@ -107,6 +128,7 @@ impl SlashCommand for RustdocSlashCommand {
id,
unfold,
crate_name: crate_name.clone(),
module_path: module_path.clone(),
}
.into_any_element()
}),
@@ -121,17 +143,23 @@ struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub crate_name: SharedString,
pub module_path: Option<SharedString>,
}
impl RenderOnce for RustdocPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let crate_path = self
.module_path
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
.unwrap_or(self.crate_name.to_string());
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust))
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
.child(Label::new(format!("rustdoc: {crate_path}")))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -32,11 +32,11 @@ impl SlashCommand for SearchSlashCommand {
}
fn description(&self) -> String {
"semantically search files".into()
"semantic search".into()
}
fn tooltip_text(&self) -> String {
"search".into()
fn menu_text(&self) -> String {
"Semantic Search".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -17,11 +17,11 @@ impl SlashCommand for TabsSlashCommand {
}
fn description(&self) -> String {
"insert content from open tabs".into()
"insert open tabs".into()
}
fn tooltip_text(&self) -> String {
"insert open tabs".into()
fn menu_text(&self) -> String {
"Insert Open Tabs".into()
}
fn requires_argument(&self) -> bool {

View File

@@ -20,7 +20,7 @@ pub trait SlashCommand: 'static + Send + Sync {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String;
fn tooltip_text(&self) -> String;
fn menu_text(&self) -> String;
fn complete_argument(
&self,
query: String,

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use collections::HashMap;
use collections::{BTreeSet, HashMap};
use derive_more::{Deref, DerefMut};
use gpui::Global;
use gpui::{AppContext, ReadGlobal};
@@ -16,6 +16,7 @@ impl Global for GlobalSlashCommandRegistry {}
#[derive(Default)]
struct SlashCommandRegistryState {
commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
featured_commands: BTreeSet<Arc<str>>,
}
#[derive(Default)]
@@ -40,16 +41,19 @@ impl SlashCommandRegistry {
Arc::new(Self {
state: RwLock::new(SlashCommandRegistryState {
commands: HashMap::default(),
featured_commands: BTreeSet::default(),
}),
})
}
/// Registers the provided [`SlashCommand`].
pub fn register_command(&self, command: impl SlashCommand) {
self.state
.write()
.commands
.insert(command.name().into(), Arc::new(command));
pub fn register_command(&self, command: impl SlashCommand, is_featured: bool) {
let mut state = self.state.write();
let command_name: Arc<str> = command.name().into();
if is_featured {
state.featured_commands.insert(command_name.clone());
}
state.commands.insert(command_name, Arc::new(command));
}
/// Returns the names of registered [`SlashCommand`]s.
@@ -57,6 +61,16 @@ impl SlashCommandRegistry {
self.state.read().commands.keys().cloned().collect()
}
/// Returns the names of registered, featured [`SlashCommand`]s.
pub fn featured_command_names(&self) -> Vec<Arc<str>> {
self.state
.read()
.featured_commands
.iter()
.cloned()
.collect()
}
/// Returns the [`SlashCommand`] with the given name.
pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
self.state.read().commands.get(name).cloned()

View File

@@ -6,8 +6,8 @@ use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use editor::{Editor, MultiBuffer};
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
actions, AppContext, AsyncAppContext, Global, Model, ModelContext, SemanticVersion,
SharedString, StaticContext as _, Task, View, ViewContext, VisualContext, WindowContext,
};
use isahc::AsyncBody;

View File

@@ -9,8 +9,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, EventEmitter, Global, Model, ModelContext, StaticContext,
Subscription, Task, WeakModel,
};
use postage::watch;
use project::Project;

View File

@@ -12,7 +12,7 @@ use collections::{BTreeMap, HashMap, HashSet};
use fs::Fs;
use futures::{FutureExt, StreamExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
AppContext, AsyncAppContext, StaticContext, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::LanguageRegistry;
use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};

View File

@@ -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, EventEmitter, Model, ModelContext, StaticContext, Task};
use language::proto::serialize_version;
use rpc::{
proto::{self, PeerId},

View File

@@ -8,7 +8,7 @@ use client::{
use collections::HashSet;
use futures::lock::Mutex;
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, StaticContext, Task, WeakModel,
};
use rand::prelude::*;
use std::{

View File

@@ -7,8 +7,8 @@ use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, U
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, EventEmitter, Global, Model, ModelContext, SharedString,
StaticContext, Task, WeakModel,
};
use language::Capability;
use rpc::{
@@ -62,6 +62,7 @@ pub struct ChannelStore {
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
client: Arc<Client>,
did_subscribe: bool,
user_store: Model<UserStore>,
_rpc_subscriptions: [Subscription; 2],
_watch_connection_status: Task<Option<()>>,
@@ -243,6 +244,20 @@ impl ChannelStore {
.log_err();
}),
channel_states: Default::default(),
did_subscribe: false,
}
}
pub fn initialize(&mut self) {
if !self.did_subscribe {
if self
.client
.send(proto::SubscribeToChannels {})
.log_err()
.is_some()
{
self.did_subscribe = true;
}
}
}
@@ -1035,7 +1050,7 @@ impl ChannelStore {
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
cx.notify();
self.did_subscribe = false;
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn(move |this, mut cx| async move {
if wait_for_reconnect {

View File

@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, TestAppContext};
use gpui::{AppContext, Model, StaticContext, TestAppContext};
use http::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;

View File

@@ -1701,7 +1701,7 @@ mod tests {
use crate::test::FakeServer;
use clock::FakeSystemClock;
use gpui::{BackgroundExecutor, Context, TestAppContext};
use gpui::{BackgroundExecutor, StaticContext, TestAppContext};
use http::FakeHttpClient;
use parking_lot::Mutex;
use settings::SettingsStore;

View File

@@ -1,7 +1,7 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{anyhow, Result};
use futures::{stream::BoxStream, StreamExt};
use gpui::{BackgroundExecutor, Context, Model, TestAppContext};
use gpui::{BackgroundExecutor, StaticContext, Model, TestAppContext};
use parking_lot::Mutex;
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},

View File

@@ -654,6 +654,7 @@ pub struct ChannelsForUser {
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub invited_channels: Vec<Channel>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub observed_channel_messages: Vec<proto::ChannelMessageId>,

View File

@@ -416,7 +416,9 @@ impl Database {
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<MembershipUpdated> {
let new_channels = self.get_user_channels(user_id, Some(channel), tx).await?;
let new_channels = self
.get_user_channels(user_id, Some(channel), false, tx)
.await?;
let removed_channels = self
.get_channel_descendants_excluding_self([channel], tx)
.await?
@@ -481,44 +483,10 @@ impl Database {
.await
}
/// Returns all channel invites for the user with the given ID.
pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result<Vec<Channel>> {
self.transaction(|tx| async move {
let mut role_for_channel: HashMap<ChannelId, ChannelRole> = HashMap::default();
let channel_invites = channel_member::Entity::find()
.filter(
channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(false)),
)
.all(&*tx)
.await?;
for invite in channel_invites {
role_for_channel.insert(invite.channel_id, invite.role);
}
let channels = channel::Entity::find()
.filter(channel::Column::Id.is_in(role_for_channel.keys().copied()))
.all(&*tx)
.await?;
let channels = channels.into_iter().map(Channel::from_model).collect();
Ok(channels)
})
.await
}
/// Returns all channels for the user with the given ID.
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
self.transaction(|tx| async move {
let tx = tx;
self.get_user_channels(user_id, None, &tx).await
})
.await
self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })
.await
}
/// Returns all channels for the user with the given ID that are descendants
@@ -527,25 +495,37 @@ impl Database {
&self,
user_id: UserId,
ancestor_channel: Option<&channel::Model>,
include_invites: bool,
tx: &DatabaseTransaction,
) -> Result<ChannelsForUser> {
let mut filter = channel_member::Column::UserId
.eq(user_id)
.and(channel_member::Column::Accepted.eq(true));
let mut filter = channel_member::Column::UserId.eq(user_id);
if !include_invites {
filter = filter.and(channel_member::Column::Accepted.eq(true))
}
if let Some(ancestor) = ancestor_channel {
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
}
let channel_memberships = channel_member::Entity::find()
let mut channels = Vec::<channel::Model>::new();
let mut invited_channels = Vec::<Channel>::new();
let mut channel_memberships = Vec::<channel_member::Model>::new();
let mut rows = channel_member::Entity::find()
.filter(filter)
.all(tx)
.await?;
let channels = channel::Entity::find()
.filter(channel::Column::Id.is_in(channel_memberships.iter().map(|m| m.channel_id)))
.all(tx)
.inner_join(channel::Entity)
.select_also(channel::Entity)
.stream(tx)
.await?;
while let Some(row) = rows.next().await {
if let (membership, Some(channel)) = row? {
if membership.accepted {
channel_memberships.push(membership);
channels.push(channel);
} else {
invited_channels.push(Channel::from_model(channel));
}
}
}
drop(rows);
let mut descendants = self
.get_channel_descendants_excluding_self(channels.iter(), tx)
@@ -643,6 +623,7 @@ impl Database {
Ok(ChannelsForUser {
channel_memberships,
channels,
invited_channels,
hosted_projects,
channel_participants,
latest_buffer_versions,

View File

@@ -176,23 +176,23 @@ async fn test_channel_invites(db: &Arc<Database>) {
.unwrap();
let user_2_invites = db
.get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2]
.get_channels_for_user(user_2)
.await
.unwrap()
.invited_channels
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]);
let user_3_invites = db
.get_channel_invites_for_user(user_3) // -> [channel_1_1]
.get_channels_for_user(user_3)
.await
.unwrap()
.invited_channels
.into_iter()
.map(|channel| channel.id)
.collect::<Vec<_>>();
assert_eq!(user_3_invites, &[channel_1_1]);
let (mut members, _) = db

View File

@@ -557,6 +557,7 @@ impl Server {
.add_request_handler(user_handler(request_contact))
.add_request_handler(user_handler(remove_contact))
.add_request_handler(user_handler(respond_to_contact_request))
.add_message_handler(subscribe_to_channels)
.add_request_handler(user_handler(create_channel))
.add_request_handler(user_handler(delete_channel))
.add_request_handler(user_handler(invite_channel_member))
@@ -1105,34 +1106,25 @@ impl Server {
.await?;
}
let (contacts, channels_for_user, channel_invites, dev_server_projects) =
future::try_join4(
self.app_state.db.get_contacts(user.id),
self.app_state.db.get_channels_for_user(user.id),
self.app_state.db.get_channel_invites_for_user(user.id),
self.app_state.db.dev_server_projects_update(user.id),
)
.await?;
let (contacts, dev_server_projects) = future::try_join(
self.app_state.db.get_contacts(user.id),
self.app_state.db.dev_server_projects_update(user.id),
)
.await?;
{
let mut pool = self.connection_pool.lock();
pool.add_connection(connection_id, user.id, user.admin, zed_version);
for membership in &channels_for_user.channel_memberships {
pool.subscribe_to_channel(user.id, membership.channel_id, membership.role)
}
self.peer.send(
connection_id,
build_initial_contacts_update(contacts, &pool),
)?;
self.peer.send(
connection_id,
build_update_user_channels(&channels_for_user),
)?;
self.peer.send(
connection_id,
build_channels_update(channels_for_user, channel_invites),
)?;
}
if should_auto_subscribe_to_channels(zed_version) {
subscribe_user_to_channels(user.id, session).await?;
}
send_dev_server_projects_update(user.id, dev_server_projects, session).await;
if let Some(incoming_call) =
@@ -3399,6 +3391,36 @@ async fn remove_contact(
Ok(())
}
fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
version.0.minor() < 139
}
async fn subscribe_to_channels(_: proto::SubscribeToChannels, session: Session) -> Result<()> {
subscribe_user_to_channels(
session.user_id().ok_or_else(|| anyhow!("must be a user"))?,
&session,
)
.await?;
Ok(())
}
async fn subscribe_user_to_channels(user_id: UserId, session: &Session) -> Result<(), Error> {
let channels_for_user = session.db().await.get_channels_for_user(user_id).await?;
let mut pool = session.connection_pool().await;
for membership in &channels_for_user.channel_memberships {
pool.subscribe_to_channel(user_id, membership.channel_id, membership.role)
}
session.peer.send(
session.connection_id,
build_update_user_channels(&channels_for_user),
)?;
session.peer.send(
session.connection_id,
build_channels_update(channels_for_user),
)?;
Ok(())
}
/// Creates a new channel.
async fn create_channel(
request: proto::CreateChannel,
@@ -5034,7 +5056,7 @@ fn notify_membership_updated(
..Default::default()
};
let mut update = build_channels_update(result.new_channels, vec![]);
let mut update = build_channels_update(result.new_channels);
update.delete_channels = result
.removed_channels
.into_iter()
@@ -5064,10 +5086,7 @@ fn build_update_user_channels(channels: &ChannelsForUser) -> proto::UpdateUserCh
}
}
fn build_channels_update(
channels: ChannelsForUser,
channel_invites: Vec<db::Channel>,
) -> proto::UpdateChannels {
fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
let mut update = proto::UpdateChannels::default();
for channel in channels.channels {
@@ -5086,7 +5105,7 @@ fn build_channels_update(
});
}
for channel in channel_invites {
for channel in channels.invited_channels {
update.channel_invitations.push(channel.to_proto());
}

View File

@@ -572,8 +572,7 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
let title = remote_workspace
.update(&mut cx, |ws, cx| {
let active_item = ws.active_item(cx).unwrap();
active_item.tab_description(0, &cx).unwrap()
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
})
.unwrap();

View File

@@ -7,7 +7,7 @@ use collab_ui::{
};
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
point, BackgroundExecutor, BorrowAppContext, StaticContext, Entity, SharedString, TestAppContext,
View, VisualContext, VisualTestContext,
};
use language::Capability;

View File

@@ -18,7 +18,7 @@ use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use git::GitHostingProviderRegistry;
use gpui::{BackgroundExecutor, Context, Model, Task, TestAppContext, View, VisualTestContext};
use gpui::{BackgroundExecutor, StaticContext, Model, Task, TestAppContext, View, VisualTestContext};
use http::FakeHttpClient;
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
@@ -277,7 +277,11 @@ impl TestServer {
node_runtime: FakeNodeRuntime::new(),
});
let os_keymap = "keymaps/default-macos.json";
let os_keymap = if cfg!(target_os = "linux") {
"keymaps/default-linux.json"
} else {
"keymaps/default-macos.json"
};
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);

View File

@@ -2161,6 +2161,9 @@ impl CollabPanel {
}
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
self.channel_store.update(cx, |channel_store, _| {
channel_store.initialize();
});
v_flex()
.size_full()
.child(list(self.list_state.clone()).size_full())

View File

@@ -9,7 +9,7 @@ use collections::{HashMap, HashSet};
use command_palette_hooks::CommandPaletteFilter;
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{
actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
actions, AppContext, AsyncAppContext, StaticContext, Entity, EntityId, EventEmitter, Global, Model,
ModelContext, Task, WeakModel,
};
use http::github::latest_github_release;

View File

@@ -292,7 +292,7 @@ mod tests {
};
use fs::FakeFs;
use futures::StreamExt;
use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};
use gpui::{BackgroundExecutor, StaticContext, TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, SharedString, Task};
use gpui::{AppContext, AsyncAppContext, StaticContext, Global, Model, ModelContext, SharedString, Task};
use rpc::{
proto::{self, DevServerStatus},
TypedEnvelope,

View File

@@ -19,9 +19,9 @@ use futures::{
StreamExt as _,
};
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render,
SharedString, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
actions, div, svg, AnyElement, AnyView, AppContext, EventEmitter, FocusHandle, FocusableView,
HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString,
StaticContext, Styled, StyledText, Subscription, Task, View, ViewContext, VisualContext,
WeakView, WindowContext,
};
use language::{

View File

@@ -41,7 +41,7 @@ pub struct MovePageDown {
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MoveToEndOfLine {
#[serde(default = "default_true")]
pub(super) stop_at_soft_wraps: bool,
pub stop_at_soft_wraps: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]

View File

@@ -1054,7 +1054,7 @@ pub mod tests {
movement,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
};
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, StaticContext, Element, Hsla};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,

View File

@@ -3,7 +3,7 @@ use super::{
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
use gpui::{AppContext, Font, LineWrapper, Model, ModelContext, Pixels, StaticContext, Task};
use language::{Chunk, Point};
use lazy_static::lazy_static;
use multi_buffer::MultiBufferSnapshot;

View File

@@ -39,6 +39,7 @@ pub mod tasks;
#[cfg(test)]
mod editor_tests;
pub mod sketch;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use ::git::diff::{DiffHunk, DiffHunkStatus};
@@ -67,11 +68,11 @@ use git::diff_hunk_to_display;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
ParentElement, Pixels, Render, SharedString, Size, StaticContext, StrikethroughStyle, Styled,
StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View,
ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -3761,7 +3762,7 @@ impl Editor {
}))
}
fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
pub fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
if self.pending_rename.is_some() {
return;
}
@@ -9957,10 +9958,33 @@ impl Editor {
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
let (path, repo) = maybe!({
let (path, selection, repo) = maybe!({
let project_handle = self.project.as_ref()?.clone();
let project = project_handle.read(cx);
let buffer = self.buffer().read(cx).as_singleton()?;
let selection = self.selections.newest::<Point>(cx);
let selection_range = selection.range();
let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
(buffer, selection_range.start.row..selection_range.end.row)
} else {
let buffer_ranges = self
.buffer()
.read(cx)
.range_to_buffer_ranges(selection_range, cx);
let (buffer, range, _) = if selection.reversed {
buffer_ranges.first()
} else {
buffer_ranges.last()
}?;
let snapshot = buffer.read(cx).snapshot();
let selection = text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row;
(buffer.clone(), selection)
};
let path = buffer
.read(cx)
.file()?
@@ -9969,21 +9993,17 @@ impl Editor {
.to_str()?
.to_string();
let repo = project.get_repo(&buffer.read(cx).project_path(cx)?, cx)?;
Some((path, repo))
Some((path, selection, repo))
})
.ok_or_else(|| anyhow!("unable to open git repository"))?;
const REMOTE_NAME: &str = "origin";
let origin_url = repo
.lock()
.remote_url(REMOTE_NAME)
.ok_or_else(|| anyhow!("remote \"{REMOTE_NAME}\" not found"))?;
let sha = repo
.lock()
.head_sha()
.ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
let selections = self.selections.all::<Point>(cx);
let selection = selections.iter().peekable().next();
let (provider, remote) =
parse_git_remote_url(GitHostingProviderRegistry::default_global(cx), &origin_url)
@@ -9994,12 +10014,7 @@ impl Editor {
BuildPermalinkParams {
sha: &sha,
path: &path,
selection: selection.map(|selection| {
let range = selection.range();
let start = range.start.row;
let end = range.end.row;
start..end
}),
selection: Some(selection),
},
))
}

View File

@@ -5569,7 +5569,7 @@ mod tests {
use language::language_settings;
use log::info;
use std::num::NonZeroU32;
use ui::Context;
use ui::StaticContext;
use util::test::sample_text;
#[gpui::test]

View File

@@ -107,7 +107,7 @@ pub fn diff_hunk_to_display(
mod tests {
use crate::Point;
use crate::{editor_tests::init_test, hunk_status};
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use language::Capability::ReadWrite;
use multi_buffer::{ExcerptRange, MultiBuffer, MultiBufferRow};
use project::{FakeFs, Project};

View File

@@ -519,7 +519,7 @@ async fn parse_markdown(text: &str, language_registry: &Arc<LanguageRegistry>) -
#[cfg(test)]
mod tests {
use super::*;
use gpui::Context;
use gpui::StaticContext;
use language::{Point, Rope};
use project::FakeFs;
use rand::prelude::*;

View File

@@ -13,7 +13,8 @@ use multi_buffer::{
use settings::{Settings, SettingsStore};
use text::{BufferId, Point};
use ui::{
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
div, ActiveTheme, IntoElement, ParentElement, StaticContext as _, Styled, ViewContext,
VisualContext,
};
use util::{debug_panic, RangeExt};

View File

@@ -1268,7 +1268,7 @@ pub mod tests {
ExcerptRange,
};
use futures::StreamExt;
use gpui::{Context, TestAppContext, WindowHandle};
use gpui::{StaticContext, TestAppContext, WindowHandle};
use itertools::Itertools;
use language::{
language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,

View File

@@ -8,7 +8,7 @@ use collections::HashSet;
use futures::future::try_join_all;
use git::repository::GitFileStatus;
use gpui::{
point, AnyElement, AppContext, AsyncWindowContext, Context, Entity, EntityId, EventEmitter,
point, AnyElement, AppContext, AsyncWindowContext, StaticContext, Entity, EntityId, EventEmitter,
IntoElement, Model, ParentElement, Pixels, SharedString, Styled, Task, View, ViewContext,
VisualContext, WeakView, WindowContext,
};

View File

@@ -579,7 +579,7 @@ mod tests {
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
};
use gpui::{font, Context as _};
use gpui::{font, StaticContext as _};
use language::Capability;
use project::Project;
use settings::SettingsStore;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
use gpui::{StaticContext, View, ViewContext, VisualContext, WindowContext};
use language::Language;
use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;

View File

@@ -0,0 +1,27 @@
use crate::{Editor, EditorElement, EditorStyle};
use gpui::{sketch::View, Model};
use ui::ViewContext;
pub struct PaneItemProps {}
struct TabProps {}
pub trait RenderEditor {
fn tab_view(&self, props: EditorStyle) -> View<Editor, TabProps>;
fn render(&self, props: EditorStyle) -> EditorElement;
}
impl RenderEditor for Model<Editor> {
fn tab_view(&self, style: EditorStyle) -> View<Editor, TabProps> {
View::new(
self.clone(),
move |_, _: TabProps, cx: &mut ViewContext<Editor>| {
cx.view().model.render(style.clone())
},
)
}
fn render(&self, style: EditorStyle) -> EditorElement {
todo!()
// EditorElement::new(self, style)
}
}

View File

@@ -5,7 +5,7 @@ use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
};
use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext};
use gpui::{Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, StaticContext, ViewContext};
use project::Project;
use util::test::{marked_text_offsets, marked_text_ranges};

View File

@@ -23,7 +23,7 @@ use std::{
},
};
use ui::Context;
use ui::StaticContext;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},

View File

@@ -27,7 +27,7 @@ impl SlashCommand for ExtensionSlashCommand {
self.command.description.clone()
}
fn tooltip_text(&self) -> String {
fn menu_text(&self) -> String {
self.command.tooltip_text.clone()
}

View File

@@ -28,7 +28,7 @@ use futures::{
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
};
use gpui::{
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
actions, AppContext, AsyncAppContext, StaticContext, EventEmitter, Global, Model, ModelContext, Task,
WeakModel,
};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
@@ -1178,8 +1178,8 @@ impl ExtensionStore {
}
for (slash_command_name, slash_command) in &manifest.slash_commands {
this.slash_command_registry
.register_command(ExtensionSlashCommand {
this.slash_command_registry.register_command(
ExtensionSlashCommand {
command: crate::wit::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
@@ -1188,7 +1188,9 @@ impl ExtensionStore {
},
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
});
},
false,
);
}
}
this.wasm_extensions.extend(wasm_extensions);

View File

@@ -10,7 +10,7 @@ use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use http::{FakeHttpClient, Response};
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
use node_runtime::FakeNodeRuntime;

View File

@@ -12,19 +12,13 @@ use std::os::unix::fs::MetadataExt;
use async_tar::Archive;
use futures::{future::BoxFuture, AsyncRead, Stream, StreamExt};
use git::repository::{GitRepository, RealGitRepository};
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
use rope::Rope;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
use smol::io::AsyncWriteExt;
use std::io::Write;
use std::sync::Arc;
use std::{
io,
io::{self, Write},
path::{Component, Path, PathBuf},
pin::Pin,
sync::Arc,
time::{Duration, SystemTime},
};
use tempfile::{NamedTempFile, TempDir};
@@ -36,6 +30,10 @@ use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
use git::repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))]
use parking_lot::Mutex;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
#[cfg(any(test, feature = "test-support"))]
use std::ffi::OsStr;
#[async_trait::async_trait]
@@ -83,7 +81,7 @@ pub trait Fs: Send + Sync {
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
#[cfg(any(test, feature = "test-support"))]
@@ -506,16 +504,13 @@ impl Fs for RealFs {
})))
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
LibGitRepository::open(dotgit_path)
.log_err()
.map::<Arc<Mutex<dyn GitRepository>>, _>(|libgit_repository| {
Arc::new(Mutex::new(RealGitRepository::new(
libgit_repository,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
})
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
let repo = git2::Repository::open(dotgit_path).log_err()?;
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),
self.git_hosting_provider_registry.clone(),
)))
}
fn is_fake(&self) -> bool {
@@ -1489,7 +1484,7 @@ impl Fs for FakeFs {
}))
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<Mutex<dyn GitRepository>>> {
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
let state = self.state.lock();
let entry = state.read_path(abs_dot_git).unwrap();
let mut entry = entry.lock();

View File

@@ -14,8 +14,6 @@ use std::{
use sum_tree::MapSeekTarget;
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
#[derive(Clone, Debug, Hash, PartialEq)]
pub struct Branch {
pub is_head: bool,
@@ -24,7 +22,7 @@ pub struct Branch {
pub unix_timestamp: Option<i64>,
}
pub trait GitRepository: Send {
pub trait GitRepository: Send + Sync {
fn reload_index(&self);
/// Loads a git repository entry's contents.
@@ -58,19 +56,19 @@ impl std::fmt::Debug for dyn GitRepository {
}
pub struct RealGitRepository {
pub repository: LibGitRepository,
pub repository: Mutex<git2::Repository>,
pub git_binary_path: PathBuf,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
}
impl RealGitRepository {
pub fn new(
repository: LibGitRepository,
repository: git2::Repository,
git_binary_path: Option<PathBuf>,
hosting_provider_registry: Arc<GitHostingProviderRegistry>,
) -> Self {
Self {
repository,
repository: Mutex::new(repository),
git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
hosting_provider_registry,
}
@@ -79,13 +77,13 @@ impl RealGitRepository {
impl GitRepository for RealGitRepository {
fn reload_index(&self) {
if let Ok(mut index) = self.repository.index() {
if let Ok(mut index) = self.repository.lock().index() {
_ = index.read(false);
}
}
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
const STAGE_NORMAL: i32 = 0;
let index = repo.index()?;
@@ -101,7 +99,7 @@ impl GitRepository for RealGitRepository {
Ok(Some(String::from_utf8(content)?))
}
match logic(&self.repository, relative_file_path) {
match logic(&self.repository.lock(), relative_file_path) {
Ok(value) => return value,
Err(err) => log::error!("Error loading head text: {:?}", err),
}
@@ -109,31 +107,35 @@ impl GitRepository for RealGitRepository {
}
fn remote_url(&self, name: &str) -> Option<String> {
let remote = self.repository.find_remote(name).ok()?;
let repo = self.repository.lock();
let remote = repo.find_remote(name).ok()?;
remote.url().map(|url| url.to_string())
}
fn branch_name(&self) -> Option<String> {
let head = self.repository.head().log_err()?;
let repo = self.repository.lock();
let head = repo.head().log_err()?;
let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string())
}
fn head_sha(&self) -> Option<String> {
let head = self.repository.head().ok()?;
head.target().map(|oid| oid.to_string())
Some(self.repository.lock().head().ok()?.target()?.to_string())
}
fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?;
GitStatus::new(&self.git_binary_path, working_directory, path_prefix)
.context("failed to read git work directory")?
.to_path_buf();
GitStatus::new(&self.git_binary_path, &working_directory, path_prefix)
}
fn branches(&self) -> Result<Vec<Branch>> {
let local_branches = self.repository.branches(Some(BranchType::Local))?;
let repo = self.repository.lock();
let local_branches = repo.branches(Some(BranchType::Local))?;
let valid_branches = local_branches
.filter_map(|branch| {
branch.ok().and_then(|(branch, _)| {
@@ -158,36 +160,40 @@ impl GitRepository for RealGitRepository {
}
fn change_branch(&self, name: &str) -> Result<()> {
let revision = self.repository.find_branch(name, BranchType::Local)?;
let repo = self.repository.lock();
let revision = repo.find_branch(name, BranchType::Local)?;
let revision = revision.get();
let as_tree = revision.peel_to_tree()?;
self.repository.checkout_tree(as_tree.as_object(), None)?;
self.repository.set_head(
repo.checkout_tree(as_tree.as_object(), None)?;
repo.set_head(
revision
.name()
.ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
)?;
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let current_commit = self.repository.head()?.peel_to_commit()?;
self.repository.branch(name, &current_commit, false)?;
fn create_branch(&self, name: &str) -> Result<()> {
let repo = self.repository.lock();
let current_commit = repo.head()?.peel_to_commit()?;
repo.branch(name, &current_commit, false)?;
Ok(())
}
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
let working_directory = self
.repository
.lock()
.workdir()
.with_context(|| format!("failed to get git working directory for file {:?}", path))?;
.with_context(|| format!("failed to get git working directory for file {:?}", path))?
.to_path_buf();
const REMOTE_NAME: &str = "origin";
let remote_url = self.remote_url(REMOTE_NAME);
crate::blame::Blame::for_path(
&self.git_binary_path,
working_directory,
&working_directory,
path,
&content,
remote_url,
@@ -210,8 +216,8 @@ pub struct FakeGitRepositoryState {
}
impl FakeGitRepository {
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
Arc::new(Mutex::new(FakeGitRepository { state }))
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
Arc::new(FakeGitRepository { state })
}
}

View File

@@ -27,7 +27,7 @@ use util::ResultExt;
use crate::{
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, StaticContext,
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
@@ -1263,7 +1263,7 @@ impl AppContext {
}
}
impl Context for AppContext {
impl StaticContext for AppContext {
type Result<T> = T;
/// Build an entity that is owned by the application. The given function will be invoked with

View File

@@ -1,8 +1,8 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel,
Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext,
WindowHandle,
Render, Reservation, Result, StaticContext, Task, View, ViewContext, VisualContext,
WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
@@ -19,7 +19,7 @@ pub struct AsyncAppContext {
pub(crate) foreground_executor: ForegroundExecutor,
}
impl Context for AsyncAppContext {
impl StaticContext for AsyncAppContext {
type Result<T> = Result<T>;
fn new_model<T: 'static>(
@@ -301,7 +301,7 @@ impl AsyncWindowContext {
}
}
impl Context for AsyncWindowContext {
impl StaticContext for AsyncWindowContext {
type Result<T> = Result<T>;
fn new_model<T>(

View File

@@ -1,4 +1,4 @@
use crate::{seal::Sealed, AppContext, Context, Entity, ModelContext};
use crate::{seal::Sealed, AppContext, StaticContext, Entity, ModelContext};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
@@ -398,7 +398,7 @@ impl<T: 'static> Model<T> {
}
/// Read the entity referenced by this model with the given function.
pub fn read_with<R, C: Context>(
pub fn read_with<R, C: StaticContext>(
&self,
cx: &C,
f: impl FnOnce(&T, &AppContext) -> R,
@@ -417,7 +417,7 @@ impl<T: 'static> Model<T> {
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> C::Result<R>
where
C: Context,
C: StaticContext,
{
cx.update_model(self, update)
}
@@ -595,7 +595,7 @@ impl<T: 'static> WeakModel<T> {
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Result<R>
where
C: Context,
C: StaticContext,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(
@@ -610,7 +610,7 @@ impl<T: 'static> WeakModel<T> {
/// been released.
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
where
C: Context,
C: StaticContext,
Result<C::Result<R>>: crate::Flatten<R>,
{
crate::Flatten::flatten(

View File

@@ -1,6 +1,6 @@
use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
EventEmitter, Model, Reservation, Subscription, Task, View, WeakModel, WindowContext,
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Effect, Entity, EntityId, EventEmitter,
Model, Reservation, StaticContext, Subscription, Task, View, WeakModel, WindowContext,
WindowHandle,
};
use anyhow::Result;
@@ -220,7 +220,7 @@ impl<'a, T> ModelContext<'a, T> {
}
}
impl<'a, T> Context for ModelContext<'a, T> {
impl<'a, T> StaticContext for ModelContext<'a, T> {
type Result<U> = U;
fn new_model<U: 'static>(

View File

@@ -1,6 +1,6 @@
use crate::{
Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace,
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable,
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, StaticContext, DrawPhase, Drawable,
Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
@@ -29,7 +29,7 @@ pub struct TestAppContext {
on_quit: Rc<RefCell<Vec<Box<dyn FnOnce() + 'static>>>>,
}
impl Context for TestAppContext {
impl StaticContext for TestAppContext {
type Result<T> = T;
fn new_model<T: 'static>(
@@ -844,8 +844,8 @@ impl VisualTestContext {
}
}
impl Context for VisualTestContext {
type Result<T> = <TestAppContext as Context>::Result<T>;
impl StaticContext for VisualTestContext {
type Result<T> = <TestAppContext as StaticContext>::Result<T>;
fn new_model<T: 'static>(
&mut self,

View File

@@ -87,6 +87,8 @@ pub mod prelude;
mod scene;
mod shared_string;
mod shared_uri;
/// todo!
pub mod sketch;
mod style;
mod styled;
mod subscription;
@@ -156,7 +158,7 @@ use taffy::TaffyLayoutEngine;
/// The context trait, allows the different contexts in GPUI to be used
/// interchangeably for certain operations.
pub trait Context {
pub trait StaticContext {
/// The result type for this context, used for async contexts that
/// can't hold a direct reference to the application context.
type Result<T>;
@@ -226,7 +228,7 @@ impl<T: 'static> Reservation<T> {
/// This trait is used for the different visual contexts in GPUI that
/// require a window to be present.
pub trait VisualContext: Context {
pub trait VisualContext: StaticContext {
/// Construct a new view in the window referenced by this context.
fn new_view<V>(
&mut self,

View File

@@ -3,7 +3,7 @@
//! application to avoid having to import each trait individually.
pub use crate::{
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
util::FluentBuilder, BorrowAppContext, BorrowWindow, Element, FocusableElement,
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
StatefulInteractiveElement, Styled, VisualContext,
StatefulInteractiveElement, StaticContext, Styled, VisualContext,
};

107
crates/gpui/src/sketch.rs Normal file
View File

@@ -0,0 +1,107 @@
use crate::{AnyElement, AnyModel, IntoElement, ViewContext, WindowContext};
use std::{any::Any, marker::PhantomData, sync::Arc};
pub struct Model<M> {
state_type: PhantomData<M>,
}
pub trait Context {
type ModelContext<'a, M>;
fn update<M, F, R>(&mut self, model: &Model<M>, update: F) -> R
where
M: 'static,
F: for<'a> FnOnce(&mut M, &mut Self::ModelContext<'a, M>) -> R;
}
impl Context for WindowContext<'_> {
type ModelContext<'a, M> = ViewContext<'a, M>;
fn update<M, F, R>(&mut self, model: &Model<M>, update: F) -> R
where
M: 'static,
F: for<'a> FnOnce(&mut M, &mut Self::ModelContext<'a, M>) -> R,
{
todo!()
}
}
impl<M> Model<M> {
pub fn update<C, F, R>(&self, cx: &mut C, update: F) -> R
where
C: Context,
F: for<'a> FnOnce(&mut M, &mut C::ModelContext<'a, M>) -> R,
{
todo!()
}
}
/// A view is the combination of a model with a compatible render function for that model.
pub struct View<M, P> {
/// A handle to the state we will render
pub model: Model<M>,
/// A recipe for displaying the state based on properties
pub component: Arc<dyn StatefulComponent<M, P>>,
}
impl<M: 'static, P: 'static> View<M, P> {
/// Creates a new `View` with the specified model and render function.
pub fn new<F, E>(model: Model<M>, render: F) -> Self
where
F: 'static + Fn(&mut M, P, &mut ViewContext<M>) -> E,
E: IntoElement,
{
View {
model,
component: Arc::new(
move |model: &mut M, props: P, cx: &mut ViewContext<'_, M>| {
render(model, props, cx).into_any_element()
},
),
}
}
pub fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement {
self.model
.update(cx, |model, cx| self.component.render(model, props, cx))
}
}
/// A mapping from properties P to an element tree.
pub trait Component<P>: 'static {
/// Render the properties
fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement;
}
/// A mapping from a stateful model M and properties P to an element tree.
pub trait StatefulComponent<M, P>: 'static {
/// Render the model with the given properties
fn render(&self, model: &mut M, props: P, cx: &mut ViewContext<M>) -> AnyElement;
}
impl<P, F> Component<P> for F
where
F: Fn(P, &mut WindowContext) -> AnyElement + 'static,
{
fn render(&self, props: P, cx: &mut WindowContext) -> AnyElement {
(self)(props, cx)
}
}
impl<M, P, F> StatefulComponent<M, P> for F
where
F: for<'a, 'b, 'c> Fn(&'a mut M, P, &'b mut ViewContext<'c, M>) -> AnyElement + 'static,
{
fn render(&self, model: &mut M, props: P, cx: &mut ViewContext<M>) -> AnyElement {
(self)(model, props, cx)
}
}
/// A dynamically typed view. It can be rendered with props P or downcast back to a typed view.
pub struct AnyView<P> {
model: AnyModel,
/// An upcasted render function that takes the dynamic reference.
render: Arc<dyn Fn(&AnyModel, P, &mut WindowContext) -> AnyElement>,
/// The original render function to enable downcasting to a View.
typed_render: Arc<dyn Any>,
}

View File

@@ -1,7 +1,7 @@
use crate::{
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
StaticContext, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
@@ -3536,7 +3536,7 @@ impl WindowContext<'_> {
}
}
impl Context for WindowContext<'_> {
impl StaticContext for WindowContext<'_> {
type Result<T> = T;
fn new_model<T>(&mut self, build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T) -> Model<T>
@@ -4198,7 +4198,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
}
impl<V> Context for ViewContext<'_, V> {
impl<V> StaticContext for ViewContext<'_, V> {
type Result<U> = U;
fn new_model<T: 'static>(
@@ -4348,7 +4348,7 @@ impl<V: 'static + Render> WindowHandle<V> {
/// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn root<C>(&self, cx: &mut C) -> Result<View<V>>
where
C: Context,
C: StaticContext,
{
Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| {
root_view
@@ -4366,7 +4366,7 @@ impl<V: 'static + Render> WindowHandle<V> {
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Result<R>
where
C: Context,
C: StaticContext,
{
cx.update_window(self.any_handle, |root_view, cx| {
let view = root_view
@@ -4400,7 +4400,7 @@ impl<V: 'static + Render> WindowHandle<V> {
/// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn read_with<C, R>(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result<R>
where
C: Context,
C: StaticContext,
{
cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx))
}
@@ -4410,7 +4410,7 @@ impl<V: 'static + Render> WindowHandle<V> {
/// This will fail if the window is closed or if the root view's type does not match `V`.
pub fn root_view<C>(&self, cx: &C) -> Result<View<V>>
where
C: Context,
C: StaticContext,
{
cx.read_window(self, |root_view, _cx| root_view.clone())
}
@@ -4491,7 +4491,7 @@ impl AnyWindowHandle {
update: impl FnOnce(AnyView, &mut WindowContext<'_>) -> R,
) -> Result<R>
where
C: Context,
C: StaticContext,
{
cx.update_window(self, update)
}
@@ -4501,7 +4501,7 @@ impl AnyWindowHandle {
/// This will fail if the window has been closed.
pub fn read<T, C, R>(self, cx: &C, read: impl FnOnce(View<T>, &AppContext) -> R) -> Result<R>
where
C: Context,
C: StaticContext,
T: 'static,
{
let view = self

View File

@@ -3,7 +3,9 @@ use client::DevServerProjectId;
use client::{user::UserStore, Client, ClientSettings};
use fs::Fs;
use futures::Future;
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
use gpui::{
AppContext, AsyncAppContext, Global, Model, ModelContext, StaticContext, Task, WeakModel,
};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use postage::stream::Stream;

View File

@@ -1,5 +1,5 @@
use gpui::{
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, Context,
canvas, div, fill, img, opaque_grey, point, size, AnyElement, AppContext, Bounds, StaticContext,
EventEmitter, FocusHandle, FocusableView, Img, InteractiveElement, IntoElement, Model,
ObjectFit, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
WindowContext,

View File

@@ -3153,10 +3153,7 @@ impl BufferSnapshot {
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<IndentGuide> {
fn tab_size_for_row(this: &BufferSnapshot, row: BufferRow, cx: &AppContext) -> u32 {
let language = this.language_at(Point::new(row, 0));
language_settings(language, None, cx).tab_size.get() as u32
}
let tab_size = language_settings(self.language(), None, cx).tab_size.get() as u32;
let start_row = range.start.to_point(self).row;
let end_row = range.end.to_point(self).row;
@@ -3167,9 +3164,6 @@ impl BufferSnapshot {
let mut result_vec = Vec::new();
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
// TODO: This should be calculated for every row but it is pretty expensive
let tab_size = tab_size_for_row(self, start_row, cx);
while let Some((first_row, mut line_indent)) = row_indents.next() {
let current_depth = indent_stack.len() as u32;

View File

@@ -7,7 +7,7 @@ use clock::ReplicaId;
use collections::BTreeMap;
use futures::FutureExt as _;
use gpui::{AppContext, BorrowAppContext, Model};
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use indoc::indoc;
use proto::deserialize_operation;
use rand::prelude::*;

View File

@@ -3,7 +3,7 @@ use copilot::Copilot;
use editor::{actions::MoveToEnd, Editor, EditorEvent};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle,
actions, div, AnchorCorner, AnyElement, AppContext, StaticContext, EventEmitter, FocusHandle,
FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription,
View, ViewContext, VisualContext, WeakModel, WindowContext,
};

View File

@@ -4,7 +4,7 @@ use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{Context, TestAppContext, VisualTestContext};
use gpui::{StaticContext, TestAppContext, VisualTestContext};
use language::{
tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageServerName,
};

View File

@@ -319,7 +319,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, TestAppContext};
use gpui::{BorrowAppContext, StaticContext, TestAppContext};
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
use settings::SettingsStore;
use std::num::NonZeroU32;

View File

@@ -135,7 +135,7 @@ async fn get_cached_server_binary(
#[cfg(test)]
mod tests {
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use unindent::Unindent;
#[gpui::test]

View File

@@ -201,7 +201,7 @@ pub(super) fn python_task_context() -> ContextProviderWithTasks {
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};
use gpui::{BorrowAppContext, ModelContext, StaticContext, TestAppContext};
use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
use settings::SettingsStore;
use std::num::NonZeroU32;

View File

@@ -504,7 +504,7 @@ mod tests {
use super::*;
use crate::language;
use gpui::{BorrowAppContext, Context, Hsla, TestAppContext};
use gpui::{BorrowAppContext, Hsla, StaticContext, TestAppContext};
use language::language_settings::AllLanguageSettings;
use settings::SettingsStore;
use theme::SyntaxTheme;

View File

@@ -435,7 +435,7 @@ async fn get_cached_eslint_server_binary(
#[cfg(test)]
mod tests {
use gpui::{Context, TestAppContext};
use gpui::{StaticContext, TestAppContext};
use unindent::Unindent;
#[gpui::test]

View File

@@ -5,6 +5,8 @@
; Properties
(property_identifier) @property
(shorthand_property_identifier) @property
(shorthand_property_identifier_pattern) @property
; Function and method calls

View File

@@ -42,7 +42,7 @@ use theme::SyntaxTheme;
use util::post_inc;
#[cfg(any(test, feature = "test-support"))]
use gpui::Context;
use gpui::StaticContext;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@@ -4649,7 +4649,7 @@ where
mod tests {
use super::*;
use futures::StreamExt;
use gpui::{AppContext, Context, TestAppContext};
use gpui::{AppContext, StaticContext, TestAppContext};
use language::{Buffer, Rope};
use parking_lot::RwLock;
use rand::prelude::*;

View File

@@ -4,7 +4,7 @@ use client::{ChannelId, Client, UserStore};
use collections::HashMap;
use db::smol::stream::StreamExt;
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Global, Model, ModelContext, Task,
AppContext, AsyncAppContext, StaticContext as _, EventEmitter, Global, Model, ModelContext, Task,
};
use rpc::{proto, Notification, TypedEnvelope};
use std::{ops::Range, sync::Arc};

View File

@@ -20,3 +20,4 @@ isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true

View File

@@ -4,8 +4,8 @@ use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::time::Duration;
use std::{convert::TryFrom, future::Future};
use std::{convert::TryFrom, future::Future, time::Duration};
use strum::EnumIter;
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
@@ -44,7 +44,7 @@ impl From<Role> for String {
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
#[serde(rename = "gpt-3.5-turbo", alias = "gpt-3.5-turbo-0613")]
ThreePointFiveTurbo,

View File

@@ -3,7 +3,7 @@ use anyhow::Result;
use client::Client;
use collections::{HashMap, HashSet};
use futures::{FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ModelContext, Task, WeakModel};
use gpui::{AppContext, AsyncAppContext, StaticContext, Global, Model, ModelContext, Task, WeakModel};
use postage::stream::Stream;
use rpc::proto;
use std::{sync::Arc, time::Duration};

View File

@@ -35,8 +35,9 @@ use fuzzy::CharBag;
use git::{blame::Blame, repository::GitRepository};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Context, Entity,
EventEmitter, Model, ModelContext, PromptLevel, SharedString, Task, WeakModel, WindowContext,
AnyModel, AppContext, AsyncAppContext, BackgroundExecutor, BorrowAppContext, Entity,
EventEmitter, Model, ModelContext, PromptLevel, SharedString, StaticContext, Task, WeakModel,
WindowContext,
};
use itertools::Itertools;
use language::{
@@ -7856,10 +7857,7 @@ impl Project {
None
} else {
let relative_path = repo.relativize(&snapshot, &path).ok()?;
local_repo_entry
.repo()
.lock()
.load_index_text(&relative_path)
local_repo_entry.repo().load_index_text(&relative_path)
};
Some((buffer, base_text))
}
@@ -8194,7 +8192,7 @@ impl Project {
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<Arc<Mutex<dyn GitRepository>>> {
) -> Option<Arc<dyn GitRepository>> {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.as_local()?
@@ -8202,10 +8200,7 @@ impl Project {
.local_git_repo(&project_path.path)
}
pub fn get_first_worktree_root_repo(
&self,
cx: &AppContext,
) -> Option<Arc<Mutex<dyn GitRepository>>> {
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
let root_entry = worktree.root_git_entry()?;
@@ -8255,8 +8250,7 @@ impl Project {
cx.background_executor().spawn(async move {
let (repo, relative_path, content) = blame_params?;
let lock = repo.lock();
lock.blame(&relative_path, content)
repo.blame(&relative_path, content)
.with_context(|| format!("Failed to blame {:?}", relative_path.0))
})
} else {

View File

@@ -13,7 +13,7 @@ use futures::{
channel::mpsc::{unbounded, UnboundedSender},
StreamExt,
};
use gpui::{AppContext, Context, Model, ModelContext, Task};
use gpui::{AppContext, Model, ModelContext, StaticContext, Task};
use itertools::Itertools;
use language::{ContextProvider, Language, Location};
use task::{

View File

@@ -1,7 +1,8 @@
use crate::Project;
use collections::HashMap;
use gpui::{
AnyWindowHandle, AppContext, Context, Entity, Model, ModelContext, SharedString, WeakModel,
AnyWindowHandle, AppContext, Entity, Model, ModelContext, SharedString, StaticContext,
WeakModel,
};
use itertools::Itertools;
use settings::{Settings, SettingsLocation};

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