Compare commits

...

278 Commits

Author SHA1 Message Date
Piotr Osiewicz
d895f53337 tasks: Provide environment variable autocomplete for tasks modal
Fixes #12099
2024-05-23 16:00:33 +02:00
Mikayla Maki
3eb0418bda Make a macro for less boilerplate when moving variables (#12182)
Also: 
- Simplify open listener implementation
- Add set_global API to global traits

Release Notes:

- N/A
2024-05-22 22:07:29 -07:00
Conrad Irwin
8b57d6d4c6 remote config fixes (#12178)
Release Notes:

- N/A
2024-05-22 22:28:00 -06:00
Conrad Irwin
af8641ce5b reconnect ssh (#12147)
Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-05-22 21:25:38 -06:00
Nathan Sobo
ea166f0b27 Add a send button to the assistant (#12171)
![CleanShot 2024-05-22 at 18 20
45@2x](https://github.com/zed-industries/zed/assets/1789/dac9fcde-9fcb-4c40-b5da-ebdc847b3962)

Include the keybinding to help people discover how to submit from the
keyboard.

I'm also shifting from the word "Conversation" to "Context".

Release Notes:

- Added a send button to the assistant panel.
2024-05-22 19:17:28 -06:00
Marshall Bowers
457fbd742f zig: Pin ZLS to v0.11.0 (#12173)
This PR updates the Zig extension to pin ZLS to v0.11.0, as the more
recent releases of ZLS don't have `.tar.gz` assets available.

Note that this depends on the next version of the `zed_extension_api`,
which has yet to be released.

Release Notes:

- N/A
2024-05-22 20:47:49 -04:00
Marshall Bowers
054c36cc29 zed_extension_api: Add github_release_by_tag_name (#12172)
This PR adds a new `github_release_by_tag_name` method to the
`zed_extension_api` to allow for retrieving a GitHub release by its tag
name.

Release Notes:

- N/A
2024-05-22 20:40:31 -04:00
Marshall Bowers
85ff80f3c0 Restrict v0.0.7 of the zed_extension_api to dev builds, for now (#12170)
This PR restricts usage of v0.0.7 of the `zed_extension_api` to dev
builds, for now.

As we're still making changes to it, we don't want to ship a version of
Zed to Preview/Stable that claims to support a yet-unreleased version of
the extension API.

Release Notes:

- N/A
2024-05-22 19:45:34 -04:00
Marshall Bowers
80bd40cfa3 zed_extension_api: Fork new version (#12160)
This PR forks a new version of the `zed_extension_api` in preparation
for some upcoming changes that require breaking changes to the WIT.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-22 19:07:52 -04:00
Marshall Bowers
2564d5d648 docs: Remove references to language_overrides (#12169)
This PR replaces references to `language_overrides` in the docs with
just `languages`.

`language_overrides` is an alias for `languages`, and we want to move
towards just using `languages`.

Release Notes:

- N/A
2024-05-22 18:37:24 -04:00
Max Brunsfeld
770a702981 Write a randomized test and fix bugs in the logic for omitting slash commands from completion requests (#12164)
Release Notes:

- N/A
2024-05-22 15:20:55 -07:00
Nate Butler
0a848f29e8 Prompt library updates (#11988)
Restructure prompts & the prompt library.

- Prompts are now written in markdown
- The prompt manager has a picker and editable prompts
- Saving isn't wired up yet
- This also removes the "Insert active prompt" button as this concept doesn't exist anymore, and will be replaced with slash commands.

I didn't staff flag this, but if you do play around with it expect it to still be pretty rough.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-05-22 18:04:47 -04:00
Max Brunsfeld
a73a3ef243 Add slash commands for adding context into the assistant (#12102)
Tasks

* [x] remove old flaps and output when editing a slash command
* [x] the completing a command name that takes args, insert a space to
prepare for typing an arg
* [x] always trigger completions when  typing in a slash command
* [x] don't show line numbers
* [x] implement `prompt` command
* [x] `current-file` command
* [x] state gets corrupted on `duplicate line up` on a slash command
* [x] exclude slash command source from completion request

Next steps:
* show output token count in flap trailer
* add `/project` command that matches project ambient context
* delete ambient context

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-05-22 14:06:28 -07:00
Joseph T. Lyons
d6e59bfae1 Fix proxy setting documentation (#12151)
Release Notes:

- N/A
2024-05-22 16:35:44 -04:00
Conrad Irwin
c2f650fe49 Fix some edge-cases in vim visual delete (#12131)
Release Notes:

- vim: Fixed `shift-d` in visual and visual block mode.
2024-05-22 12:54:41 -06:00
Conrad Irwin
c084b6aade remoting fixes (#12137)
Release Notes:

- N/A
2024-05-22 12:49:42 -06:00
Remco Smits
8af9fa6320 php: Bump to v0.0.4 (#12140)
This PR bumps the PHP extension to v0.0.4.

Changes:

- https://github.com/zed-industries/zed/pull/11514

Release Notes:

- N/A
2024-05-22 14:11:48 -04:00
Piotr Osiewicz
58796a8480 tasks: Expose captured variables to ContextProvider (#12134)
This PR changes the interface of ContextProvider, allowing it to inspect
*all* variables set so far during the process of building
`TaskVariables`. This makes it possible to capture e.g. an identifier in
tree-sitter query, process it and then export it as a task variable.

Notably, the list of variables includes captures prefixed with leading
underscore; they are removed after all calls to `build_context`, but it
makes it possible to capture something and then conditionally preserve
it (and perhaps modify it).

Release Notes:

- N/A
2024-05-22 19:45:43 +02:00
Joseph T. Lyons
ba9449692e v0.138.x dev 2024-05-22 11:26:58 -04:00
Thorsten Ball
aa539fc347 lsp: Fix wrong WorkspaceFolder when opening only file (#12129)
This fixes #11361 and #8764 by making sure we pass a directory as
`WorkspaceFolder` to the language server.

We already compute the `working_dir` correctly when
`self.root_path.is_file()`, but we didn't use it.

Release Notes:

- Fixed language servers (such as `gopls`) not starting up correctly
when opening a single file in Zed.
([#11361](https://github.com/zed-industries/zed/issues/11361) and
[#8764](https://github.com/zed-industries/zed/issues/8764)).
2024-05-22 16:32:06 +02:00
Thorsten Ball
49dffabab9 macOS: Allow creating directories in file-open panel (#12121)
I don't know whether there are any hard UI guidelines that dictate
whether this should be allowed or not, but I think it's very handy and
missed it.

I also think it makes sense to have this in a directory-centric editor
in which opening a directory creates a new window.

Release Notes:

- Added ability to create directory in open-file dialog on macOS.

![screenshot-2024-05-22-15 05
03@2x](https://github.com/zed-industries/zed/assets/1185253/939a2a88-16b2-4a91-a344-f73c5615d831)
2024-05-22 15:24:02 +02:00
Thorsten Ball
1771eded54 tasks: Fix $ZED_SELECTED_TEXT ignoring line_mode (#12120)
When you press `V` to go into visual-line mode in Vim,
`selections.line_mode` is true and the selection contains _lines_.

But `$ZED_SELECTED_TEXT` always contained just the cursor location or
any non-line-mode selection that was previously made.

Release Notes:

- Fixed `$ZED_SELECTED_TEXT` variable in Tasks ignoring whether
visual-line-mode in Vim was used.
2024-05-22 14:29:05 +02:00
Remco Smits
bfdd9d89a7 php: Add runnable tests (#11514)
### This pull request adds the following:
- Missing mapping for the `yield` keyword.
- Outline scheme for `describe`, `it` and `test`
function_call_expressions (to support Pest runnable)
- Pest runnable support
- PHPUnit runnable support
- Task for running selected PHP code.


## Queries explanations

#### Query 1 (PHPUnit: Run specific method test):
1. Class is not abstract (because you cannot run tests from an abstract
class)
2. Class has `Test` suffix
3. Method has public modifier(or no modifiers, default is public)
4. Method has `test` prefix

#### Query 2 (PHPUnit: Run specific method test with `@test`
annotation):
1. Class is not abstract (because you cannot run tests from an abstract
class)
2. Class has `Test` suffix
3. Method has public modifier(or no modifiers, default is public)
4. Method has `@test` annotation

#### Query 3 (PHPUnit: Run specific method test with `#[Test]`
attribute):
1. Class is not abstract (because you cannot run tests from an abstract
class)
2. Class has `Test` suffix
3. Method has public modifier(or no modifiers, default is public)
4. Method has `#[Test]` attribute

#### Query 4 (PHPUnit: Run all tests inside the class):
1. Class is not abstract (because you cannot run tests from an abstract
class)
2. Class has `Test` suffix

#### Query 5 (Pest: Run function test)
1. Function expression has one of the following names: `describe`, `it`
or `test`
2. Function expression first argument is a string

### **PHPUnit: Example for valid test class**
<img width="549" alt="Screenshot 2024-05-08 at 10 41 34"
src="https://github.com/zed-industries/zed/assets/62463826/e84269de-4f53-410b-b93b-713f9448dc79">


### **PHPUnit: Example for invalid test class**
All the methods should be ignored because you cannot run tests on an
abstract class.
<img width="608" alt="Screenshot 2024-05-07 at 22 28 57"
src="https://github.com/zed-industries/zed/assets/62463826/8c6b3921-5266-4d88-ada5-5cd827bcf242">

### **Pest: Example**

https://github.com/zed-industries/zed/assets/62463826/bce133eb-0a6f-4ca2-9739-12d9169bb9d6

You should now see all your **Pest** tests inside the buffer symbols
modal.
![Screenshot 2024-05-08 at 22 51
25](https://github.com/zed-industries/zed/assets/62463826/9c818b74-383c-45e5-9b41-8dec92759a14)

Release Notes:

- Added test runnable detection for PHP (PHPUnit & Pest).
- Added task for running selected PHP code.
- Added `describe`, `test` and `it` functions to buffer symbols, to
support Pest runnable.
- Added `yield` keyword to PHP keyword mapping.
2024-05-22 13:49:20 +02:00
Kirill Bulatov
c4e87444e7 Tidy up the code (#12116)
Small follow-ups for https://github.com/zed-industries/zed/pull/12063
and https://github.com/zed-industries/zed/pull/12103

Release Notes:

- N/A
2024-05-22 14:36:15 +03:00
Piotr Osiewicz
c440f3a71b tasks: Fix runnables retrieval to not bail when a single tag can't be matched (#12113)
This can happen with queries without `@run` indicator.

Release Notes:

- N/A
2024-05-22 13:26:12 +02:00
Raphael Lüthy
e68ef944d9 Separate actions for accepting the inline suggestions and completions (#12094)
Release Notes:
- Added `editor::AcceptInlineCompletion` action (bound to Tab by
default) for accepting inline completions. ([6788](https://github.com/zed-industries/zed/issues/6788))

---------

Signed-off-by: Raphael Lüthy <raphael.luethy@fhnw.ch>
Co-authored-by: Conrad Irvin <conrad@zed.dev>
2024-05-22 13:51:21 +03:00
Thorsten Ball
7c9c80d663 go: Highlight constant identifiers (#12111)
Release Notes:

- N/A
2024-05-22 08:37:20 +02:00
d1y
a33aedff81 gomod and gowork add gopls server (#12109)
<img width="684" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/c22e00d2-e197-44b3-864f-db20eaf47ff7">

Release Notes:

- Added `gopls` support when opening `go.mod` or `go.work` files.

Co-authored-by: Thorsten Ball <thorsten@zed.dev>
2024-05-22 08:16:55 +02:00
Thorsten Ball
8168ec2a28 go: Add runnables (#12110)
This adds support for runnables to Go.

It adds the following tasks:

- `go test $ZED_GO_PACKAGE -run $ZED_SYMBOL`
- `go test $ZED_GO_PACKAGE`
- `go test ./...`
- `go run $ZED_GO_PACKAGE` if it has a `main` function

Release Notes:

- Added built-in Go runnables and tasks that allow users to run Go test
functions, test packages, or run `main` functions.

Demo:



https://github.com/zed-industries/zed/assets/1185253/a6271d80-faf4-466a-bf63-efbec8fe6c35




https://github.com/zed-industries/zed/assets/1185253/92f2b616-7501-463d-b613-1ec1084ae0cd
2024-05-22 07:18:49 +02:00
Conrad Irwin
e5b9e2044e Allow ssh connection for setting up zed (#12063)
Co-Authored-By: Mikayla <mikayla@zed.dev>



Release Notes:

- Magic `ssh` login feature for remote development

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2024-05-21 22:39:16 -06:00
Kirill Bulatov
3382e79ef9 Improve file finder match results (#12103) 2024-05-22 07:35:00 +03:00
Thorsten Ball
c290d924f1 Allow formatting of unsaved buffers with prettier (#12095)
This fixes #4529 by allowing unsaved buffers to be formatted with
prettier.

Steps to do that:

1. Create a new buffer
2. Set language for the buffer (e.g.: `language selector: toggle` and
JSON)
3. In settings, set prettier parser for language (can't be inferred,
since we don't have filename) and allow formatting with prettier:

   ```json
   {
     "languages": {
       "JSON": {
         "prettier": {
           "allowed": true,
           "parser": "json"
         }
       }
     }
   }
   ```

4. Use `editor: format`

Release Notes:

- Added ability to format unsaved buffers with Prettier. Requirement is
to set a Prettier parser in the user settings. Example for JSON: `{
"languages": { "JSON": { "prettier": { "allowed": true, "parser": "json"
} } } }` ([#4529](https://github.com/zed-industries/zed/issues/4529)).

Demo:


https://github.com/zed-industries/zed/assets/1185253/d24e490b-2e2c-4a5d-95a8-fc8675523780
2024-05-22 06:19:32 +02:00
张小白
b451af4906 Fix npm install error with some languages (#12087)
If you have already installed `node` using `brew install node`, you are
fine. If you did not install `node` on you local machine, it fails.

The `node_binary` path is actually not included in environment variable.
When run `npm install`, some extensions like `eslint`, may run some
commands like `sh -c node .....`. Since `node_binary` path is not
included in `PATH` variable, `sh -c node ...` will fail complaining that
"command not found". If you have installed `node` before, `node` is
already included in `PATH`, so you are fine. If not, it fails.

Closes #11890

Release Notes:

- Fixed Zed's internal Node runtime not being put in `$PATH` correctly
when running language servers and other commands with `node`.
([#11890](https://github.com/zed-industries/zed/issues/11890))

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-05-22 06:14:44 +02:00
Vitaly Slobodin
71a94c775b ruby: Bump to v0.0.4 (#12101)
This PR bumps the Ruby extension to v0.0.4.
    
Changes:

- #11869
- #12012
- #12052
    
Release Notes:

- N/A
2024-05-21 15:28:44 -04:00
Vitaly Slobodin
99570f9361 ruby: Add support for running tests (#12052)
Hello, this pull request adds two things related to each other. I hope
it's fine to submit both in the same pull request but I am totally fine
with submitting them in separate pull requests, just let me know. This
is an initial version for both features. Thanks!

## Symbols outline support for testing frameworks: minitest and RSPec

Symbols outline support in
[Minitest](https://github.com/minitest/minitest) (the testing framework
that comes with Ruby on Rails out of the box) and RSpec (another testing
framework that is popular in Ruby and Ruby on Rails world). Here are
some screenshots:

### Minitest

Given this Ruby code:

```ruby
require "test_helper"

class CategoryTest < ActiveSupport::TestCase
  context "validations" do
    subject { build(:category) }

    should validate_presence_of(:title)
    should validate_length_of(:title).is_at_most(255)
    should validate_uniqueness_of(:title)
  end
end

class TestNamesWithMiniTest < ActiveSupport::TestCase
  def test_foo_1; assert true; end
  def test_foo_2; assert true; end
  def test_bar_1; assert true; end
  def test_bar_2; assert true; end
end
```

We have this symbols outline:

![CleanShot 2024-05-20 at 12 35
46@2x](https://github.com/zed-industries/zed/assets/1894248/c63a61d8-38cc-4969-a49b-dd9ce6920a0e)

### RSpec

I used `mastodon` application for testing because it's written in Ruby.
Given the following file
https://github.com/mastodon/mastodon/blob/main/spec/models/account_spec.rb
We have the following symbols outline:

![CleanShot 2024-05-20 at 12 44
42@2x](https://github.com/zed-industries/zed/assets/1894248/a754cf4c-f9cc-43f3-b365-1ce0ff942941)



## Running Ruby tests

### Minitest

Given the same file as above, we have the following workflow:



https://github.com/zed-industries/zed/assets/1894248/dc335495-3460-4a6d-95c4-e4cbc87a1ea0



### RSpec

Given the following file
`https://github.com/mastodon/mastodon/blob/main/spec/models/account_spec.rb`
We have the following workflow:



https://github.com/zed-industries/zed/assets/1894248/a17067ea-73b6-4229-8f1b-1b88dde63401

<hr />

Release Notes: Added Ruby test runnables support
2024-05-21 22:07:44 +03:00
Antonio Scandurra
f3710877f1 Introduce Editor::insert_flaps and Editor::remove_flaps (#12096)
This pull request introduces the ability to add flaps, custom foldable
regions whose first foldable line can be associated with:

- A toggle in the gutter
- A trailer showed at the end of the line, before the inline blame
information


https://github.com/zed-industries/zed/assets/482957/c53a9148-f31a-4743-af64-18afa73c404c

To achieve this, we changed `FoldMap::fold` to accept a piece of text to
display when the range is folded. We use this capability in flaps to
avoid displaying the ellipsis character.

We want to use this new API in the assistant to fold context while still
giving visual cues as to what that context is.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-05-21 20:23:37 +02:00
Thorsten Ball
b89f360199 lsp: Handle client/unregisterCapability to fix gopls (#12086)
This fixes #10224 by handling `client/unregisterCapability` requests
that have a `workspace/didChangeWatchedFiles` method.

While debugging the issue, I found out that `gopls` seems to block
indefinitely when there's no reply to the `client/unregisterCapability`
request. Even an empty response would fix the issue.

Seems like gopls 15.x and later seem to handle nested subfolders well,
but do not handle unanswered requests.

Instead of replying with an empty response, I decided to change how we
handle file watching and keep a list of all registered paths so that we
can then unregister paths and recreate the glob patterns.

Release Notes:

- Fixed `gopls` not working correctly when the `go.mod` file was in a
subfolder and not the root folder of the project opened in Zed.
([#10224](https://github.com/zed-industries/zed/issues/10224)).
2024-05-21 19:17:29 +02:00
Piotr Osiewicz
0563472832 html: Bump to 0.1.1 (#12093)
Moves to using the npm package as installation method.

Release Notes:

- N/A
2024-05-21 18:36:47 +02:00
d1y
14436a75b1 project panel: Update file icon when editing filename (#12078)
Before:


![before](https://github.com/zed-industries/zed/assets/45585937/1590586d-9d42-4d44-85fc-8e79499408b3)

After:


![after](https://github.com/zed-industries/zed/assets/45585937/c0fd1b2a-1ecf-4403-b74a-25c3c700f00d)

Release Notes:

- Update file icons during editing in project panel

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2024-05-21 18:34:01 +03:00
Nate Butler
7b6f8c279d Tidy up user menu (#12084)
Minor cleanup

Release Notes:

- N/A
2024-05-21 10:34:35 -04:00
Piotr Osiewicz
7a90b1124f html: release 0.1.0 (#12083)
Add config for tag autoclosing: add following to lsp section of your
settings:
    "vscode-html-language-server": {
      "settings": {
        "html": { "tagAutoclosing": true }
      }
    }

It also accepts `css`, `js/ts` and `javascript` as options.

Disable HTML language server in JS/TS/TSX files for now. I decided to
disable it for now as it caused excessive edits in these types of files
(as reported by @mariansimecek in
https://github.com/zed-industries/zed/pull/11761#issuecomment-2122038107);
it looks like HTML language server tries to track language ranges (e.g.
whether a particular span is TS/HTML fragment etc) just like we do.
However in plain JS/TSX files it seems like it treats the whole file as
one big chunk of HTML, which is.. not right, to say the least.

No release note, as HTML extension goodies are not on Preview yet.

Release Notes:

- N/A
2024-05-21 14:04:02 +02:00
Thorsten Ball
a5b14de401 project panel: Add Duplicate action (#12081)
This fixes #5304 by adding a new Duplicate action to the project panel
context menu.

It really is implemented on top of copy&paste.



Release Notes:

- Added a Duplicate action to the project panel.
([#5304](https://github.com/zed-industries/zed/issues/5304)).



https://github.com/zed-industries/zed/assets/1185253/f0fa6a4b-f066-47df-84f0-257a049800d1
2024-05-21 09:58:10 +02:00
Anıl Şenay
ba1d28f160 Add .gql and .graphqls extensions for GraphQL icon (#12073)
There are `.gql` and `.graphqls` suffix support in [GraphQL VSCode
extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax).
I use those file extensions in my projects, hence I wanted them to be
graphql icons.

Release Notes:

- Added GraphQL icon for `.gql` and `.graphqls` files.

currently:

![resim](https://github.com/zed-industries/zed/assets/1047345/4c333129-00cc-401a-88e6-fd44f74caea3)

after this pr:

![resim](https://github.com/zed-industries/zed/assets/1047345/103a0b5a-1c8b-4dea-998c-e768940887c4)

in vscode:

![resim](https://github.com/zed-industries/zed/assets/1047345/29f438d6-ff9e-4a95-8ef2-e5d8d27c0fe9)
2024-05-20 21:47:34 -04:00
Marshall Bowers
2f3102672c ui: Don't break flex layout when using WithRemSize (#12076)
This PR fixes an issue where the flex hierarchy wasn't getting broken by
the use of `WithRemSize`.

Release Notes:

- N/A
2024-05-20 21:39:18 -04:00
Owen Law
315e45f543 Match the startup behavior of the CLI to the main app (#12044)
Currently the main binary will open an empty file if no previous
workspaces exist or, if it is the first startup, show the welcome page.
When starting via the CLI it will simply drop you in an empty workspace:
no empty file and no welcome page.

This changes the CLI startup to match the behavior of the non-CLI
startup, so they will both create an empty file or show the welcome page
if no path was given and no workspaces were opened in the past.

Release Notes:

- Matched startup behavior of the CLI to the behavior of the main app.
2024-05-20 19:33:19 -06:00
CharlesChen0823
1e18bcb949 vim: Fix %s replace not working more than twice (#12045)
close: #11981 

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-20 19:17:11 -06:00
versecafe
f2357c71e1 terminal: Add coloration to task icons based on status (#12066)
Release Notes:

- Fixes: ([#11968](https://github.com/zed-industries/zed/issues/11968)).

Adds colouration to task icons in terminal based off status


![image](https://github.com/zed-industries/zed/assets/147033096/32578358-3da8-4082-9212-637dcd346576)
2024-05-21 01:26:04 +02:00
Conrad Irwin
42ea2be1b4 Add "new window" option to the dock menu (#12067)
Fixes: #11651
Co-Authored-By: versecafe <147033096+versecafe@users.noreply.github.com>



Release Notes:

- Added a "New Window" item to the dock menu
([#11651](https://github.com/zed-industries/zed/issues/11651)).

---------

Co-authored-by: versecafe <147033096+versecafe@users.noreply.github.com>
2024-05-20 17:08:14 -06:00
Conrad Irwin
1732ea95c2 Better private file sharing for remote projects (#12002)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-20 16:48:24 -06:00
Antonio Scandurra
3a79aa85f4 Fuzzy-match lines when applying edits from the assistant (#12056)
This uses Jaro-Winkler similarity for now, which seemed to produce
pretty good results in my tests. We can easily swap it with something
else if needed.

Release Notes:

- N/A
2024-05-20 17:02:15 +02:00
Piotr Osiewicz
0b8c1680fb html: Add support for autoclosing of tags (#11761)
Fixes #5267 
TODO:
- [x] Publish our fork of vscode-langservers-extracted on GH and wire
that through as a language server of choice for HTML extension.
- [x] Figure out how to prevent edits made by remote participants from
moving the cursor of a host.

Release Notes:

- Added support for autoclosing of HTML tags in local projects.
2024-05-20 17:00:27 +02:00
Nate Butler
097032327d add PickerDelegate::selected_index_changed (#12059)
Adds the ability to have some effect run when a selection changes in a
picker.

If the `PickerDelegate` implements something other than `None` for
`selected_index_changed` then each time the selection changes it will
run that effect.

For example:

```rs
impl PickerDelegate for PromptManagerDelegate {
    //...

    fn selected_index_changed(
        &self,
        ix: usize,
        cx: &mut ViewContext<Picker<Self>>,
    ) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
        Some(self.prompt_manager.set_active_prompt(ix, cx))
    }

    //...
}
```

This isn't currently used in any picker, but I'm adding this to allow
the functionality we intended for the prompt library, we're changing
selections, activates a preview in the right column.

This will be useful for building any sort of UI where there's a picker
on the left and a preview on the right, such as a UI like them
telescope.

Release Notes:

- N/A
2024-05-20 10:52:04 -04:00
Piotr Osiewicz
7db85b0d2e golang: autoclose backticks (#12050)
Fixes #12025



Release Notes:
- Fixed backtick characters not getting autoclosed in Golang files
(#12025).
2024-05-20 10:18:12 +02:00
Joshua Farayola
ab7ce32888 Add glob support for custom file type language (#12043)
Release Notes:

- Added glob support for file_types configuration
([#10765](https://github.com/zed-industries/zed/issues/10765)).

`file_types` can now be written like this:

```json
"file_types": {
  "Dockerfile": [
    "Dockerfile",
    "Dockerfile.*",
  ]
}
```
2024-05-20 10:13:35 +02:00
Nipun Shukla
4e935f9f0f Remove F2 keybind for Rename on MacOS and Linux (#12037)
Fix [#11608](https://github.com/zed-industries/zed/issues/11608)

Release Notes:

- Changed rename keybind from F2 to Enter in right-click context menu
([#11608](https://github.com/zed-industries/zed/issues/11608)).

![image](https://github.com/zed-industries/zed/assets/30131536/5ebdbb04-ff4e-46ff-80fb-9e95b2b3d285)
2024-05-20 10:35:22 +03:00
Vitaly Slobodin
2f4890ae39 ruby: Pass initialization options to LSPs (#12012)
This pull request adds ability to pass `initialization_options` to both
`solargraph` and `ruby-lsp` language servers. Additionally it updates
the documentation to reflect that and the recently added `ruby-lsp`
server.

Release Notes:

- Pass `initialization_options` to Ruby LSP servers.
2024-05-20 10:18:32 +03:00
d1y
5ddd343b27 Update tree-sitter-go (#12020)
Release Notes:

- N/A
2024-05-19 21:06:40 +03:00
d1y
a9f35d2914 Suggest extension for .wit files (#12031)
Release Notes:

- Added an extension suggestion for `.wit` files.
2024-05-19 08:36:46 -04:00
Mikayla Maki
410c46a551 Trigger columnar selection behavior on middle mouse down (#12005)
fixes https://github.com/zed-industries/zed/issues/11990

Release Notes:

- Changed middle mouse down to trigger a columnar selection, creating a
rectangle of multi cursors over a dragged region.
([#11990](https://github.com/zed-industries/zed/issues/11990))
2024-05-17 17:57:00 -07:00
Conrad Irwin
1f611a9c90 Allow copy-pasting dev-server-token (#11992)
Release Notes:

- N/A
2024-05-17 16:41:46 -06:00
Max Brunsfeld
84affa96ff Allow the assistant to suggest edits to files in the project (#11993)
### Todo

* [x] tuck the new system prompt away somehow
* for now, we're treating it as built-in, and not editable. once we have
a way to fold away default prompts, let's make it a default prompt.
* [x] when applying edits, re-parse the edit from the latest content of
the assistant buffer (to allow for manual editing of edits)
* [x] automatically adjust the indentation of edits suggested by the
assistant
* [x] fix edit row highlights persisting even when assistant messages
with edits are deleted
* ~adjust the fuzzy search to allow for small errors in the old text,
using some string similarity routine~

We decided to defer the fuzzy searching thing to a separate PR, since
it's a little bit involved, and the current functionality works well
enough to be worth landing. A couple of notes on the fuzzy searching:
* sometimes the assistant accidentally omits line breaks from the text
that it wants to replace
* when the old text has hallucinations, the new text often contains the
same hallucinations. so we'll probably need to use a more fine-grained
editing strategy where we perform a character-wise diff of the old and
new text as reported by the assistant, and then adjust that diff so that
it can be applied to the actual buffer text

Release Notes:

- Added the ability to request edits to project files using the
assistant panel.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
2024-05-17 15:38:14 -07:00
Gus
4386268a94 Avoid extra completion requests (#11875)
Do not spawn a second completion request when completion menu is open and a new edit is made.

Release Notes:

- N/A
2024-05-18 00:27:40 +03:00
Joseph T. Lyons
e5a4421559 Reduce spamming of inline completion discard events (#11999)
I'm not a huge fan of passing around a boolean all around the place, but
this will tame the events for now until we have a better solution.

Release Notes:

- N/A
2024-05-17 16:37:17 -04:00
Marshall Bowers
99c6389ff8 gleam: Bump to v0.1.3 (#12000)
This PR bumps the Gleam extension to v0.1.3.

Changes:

- #11998

Release Notes:

- N/A
2024-05-17 16:31:01 -04:00
d1y
0325051629 gleam: Update tree-sitter-gleam (#11998)
#11996

Release Notes:

- N/A
2024-05-17 16:21:44 -04:00
Mikayla Maki
11c97a396e Implement 'Cmd+W with no open tabs closes the window', with a setting (#11989)
Follow up to: https://github.com/zed-industries/zed/pull/10986

However, I have set this to have a default behavior of 'auto': matching
the current platform's conventions, rather than a default value of
'off'.

fixes https://github.com/zed-industries/zed/issues/5322.

Release Notes:

- Changed the behavior of `workspace::CloseActiveItem`: when you're
using macOS and there are no open tabs, it now closes the window
([#5322](https://github.com/zed-industries/zed/issues/5322)). This can
be controlled with a new setting, `when_closing_with_no_tabs`, to
disable it on macOS, or enable it on other platforms.
2024-05-17 12:31:12 -07:00
Bosco
7fd736e23c docs: Update macOS development docs with dispatch.h error solution (#11986)
### Title
Update macOS Development Documentation with Dispatch.h Error Solution

### Description
This PR updates the macOS development documentation to include a
solution for the `dispatch/dispatch.h` file not found error. This error
is encountered during local development when using the `cargo run`
command. The documentation now includes steps to ensure the Xcode
command line tools are properly installed and set, and instructions to
set the `BINDGEN_EXTRA_CLANG_ARGS` environment variable.

### Changes
- Added troubleshooting section for `dispatch/dispatch.h` error in
`development/macos.md`.

### Related Issues
- Closes [#11963](https://github.com/zed-industries/zed/issues/11963)

### Testing Instructions
1. Follow the steps in the updated `development/macos.md` to configure
your environment.
2. Run `cargo clean` and `cargo run` to ensure the build completes
successfully.

Release Notes:

- N/A
2024-05-17 14:10:57 -04:00
Moritz Bitsch
4dd83da627 Fix hang when opening URL in first browser window (#11961)
If opening a url opens the first browser window the call does not return
completely blocking the ui until the browser window is closed. Using
spawn instead of status does not block, but we will loose the exitstatus
of the browser window.

Release Notes:

- N/A
2024-05-17 11:00:57 -07:00
yodatak
719e6e9777 linux: Add more missing dependencies on Fedora (#11868)
see https://docs.rs/openssl/latest/openssl/

Release Notes:

- N/A
2024-05-17 10:38:16 -07:00
Kuppjaerk
64ba08cced Add documentation for auto-switching theme (#11908)
Added documentation regarding auto-switching themes to the default
settings file, according to
([#9627](https://github.com/zed-industries/zed/issues/9627)).

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-17 13:29:59 -04:00
Marshall Bowers
b93e564a78 Format default settings (#11985)
This PR formats `default.json`, as it had gotten out-of-sync with
Prettier.

Release Notes:

- N/A
2024-05-17 13:13:58 -04:00
Conrad Irwin
483a735e03 Allow opening a single remote file (#11983)
Release Notes:

- N/A
2024-05-17 10:57:04 -06:00
Valentine Briese
a787be6c9f Clarify CodeLabel.filter_range doc (#11383)
Improves documentation for `CodeLabel.filter_range` in
`zed_extension_api` by clarifying that it's a range of only the text
displayed in the label, *not* the `code` field.

Release Notes:

- N/A
2024-05-17 12:09:35 -04:00
Conrad Irwin
b890fa71ff Report an error when trying to open ui in linux::headless (#11952)
Release Notes:

- N/A
2024-05-17 09:50:23 -06:00
Piotr Osiewicz
9d10969906 chore: Fix refining_impl_trait lint occurences (#11979)
These show up when compiling Zed with latest nightly, which means that
we'd have to do it one or two Rust releases down the line.
Release Notes:

- N/A
2024-05-17 16:58:22 +02:00
Marshall Bowers
79098671e6 theme: Remove default syntax colors (#11980)
This PR removes the default syntax colors from the theme.

With the changes in #11911 these colors could leak through if the theme
didn't provide a value for that syntax color.

Removing them gives themes a clean slate to work with.

Release Notes:

- N/A
2024-05-17 10:54:51 -04:00
Kirill Bulatov
8631280baa Support terminals with ssh in remote projects (#11913)
Release Notes:

- Added a way to create terminal tabs in remote projects, if an ssh
connection string is specified
2024-05-17 17:48:07 +03:00
张小白
70888cf3d6 Fix npm install command with a URI://localhost:port proxy setting (#11955)
NodeRuntime without environment information can not parse `localhost`
correctly.

Release Notes:

- N/A
2024-05-17 11:30:52 +03:00
Kirill Bulatov
5ad8e721db Change default Prettier's useTabs settings based on Zed settings (#11958)
Part of https://github.com/zed-industries/zed/issues/7656

When a project is formatted by Prettier that Zed installs, make it
respect Zed's `hard_tabs` settings by passing the value into Prettier
config as `useTabs`.


https://github.com/zed-industries/zed/assets/2690773/80345cdd-d4f8-40b2-ab56-dba6b9646c70

Release Notes:

- Fixed default Prettier not respecting Zed's `hard_tabs` settings
2024-05-17 11:05:46 +03:00
Max Brunsfeld
4ca6e0e387 Make autoscroll optional when highlighting editor rows (#11950)
Previously, when highlighting editor rows with a color, we always
auto-scrolled to the first highlighted row. This was useful in contexts
like go-to-line and the outline view. We had an explicit special case
for git diff highlights. Now, part of the `highlight_rows` API, you
specify whether or not you want the autoscroll behavior. This is needed
because we want to highlight rows in the assistant panel, and we don't
want the autoscroll.

Release Notes:

- N/A
2024-05-16 20:28:17 -07:00
Conrad Irwin
57b5bff299 Support very large channel membership lists (#11939)
Fixes the channel membership dialogue for the zed channel by not
downloading all 111k people in one go.

Release Notes:

- N/A
2024-05-16 20:02:25 -06:00
Max Brunsfeld
df3bd40c56 Speed up is_dirty and has_conflict (#11946)
I noticed that scrolling the assistant panel was very slow in debug
mode, after running a completion. From profiling, I saw that it was due
to the buffer's `is_dirty` and `has_conflict` checks, which use
`edits_since` to check if there are any non-undone edits since the saved
version.

I optimized this in two ways:
* I introduced a specialized `has_edits_since` method on text buffers,
which allows us to more cheaply check if the buffer has been edited
since a given version, without some of the overhead involved in
computing what the edits actually are.
* In the case of `has_conflict`, we don't even need to call that method
in the case where the buffer doesn't have a file (is untitled, as is the
case in the assistant panel). Buffers without files cannot be in
conflict.

Release Notes:

- Improved performance of editing the assistant panel and untitled
buffers with many edits.
2024-05-16 18:36:20 -07:00
Conrad Irwin
23315d214c Fix country code serialization (#11947)
Release Notes:

- N/A
2024-05-16 18:54:07 -06:00
Marshall Bowers
0dd5fe313b Revert "Fix aside affecting parent popover height (#11859)" (#11942)
This reverts commit d3dfa91254.

This change can cause weird behavior where the completion menu ends up
positioned away from the cursor location:

<img width="1062" alt="Screenshot 2024-05-16 at 6 43 17 PM"
src="https://github.com/zed-industries/zed/assets/1486634/0462a874-4fe3-4ca9-88ce-8d5d0b4009fe">

With the change reverted:

<img width="1026" alt="Screenshot 2024-05-16 at 6 43 35 PM"
src="https://github.com/zed-industries/zed/assets/1486634/9fc7b9a1-0cfb-4a84-8f6b-b481a785ceca">

Release Notes:

- Fixed an issue where the completion menu would sometimes appear
detached from the cursor location (preview only).
2024-05-16 18:53:08 -04:00
Marshall Bowers
b9ecca7524 Remove wiring for assistant2 (#11940)
This PR removes the wiring for `assistant2` that hooks it up to Zed.

Since we're focusing in on improving the current assistant, we don't
need this present in Zed.

I left the `assistant2` crate intact for now, to make it easier to
reference any code from it.

Release Notes:

- N/A
2024-05-16 18:32:53 -04:00
npmania
b60254feca x11: Add XIM support (#11657)
This pull request adds XIM (X Input Method) support to x11 platform.

The implementation utilizes [xim-rs](https://crates.io/crates/xim), a
XIM library written entirely in Rust, to provide asynchronous XIM
communication.
Preedit and candidate positioning are fully supported in the editor
interface, yet notably absent in the terminal environment.

This work is sponsored by [Rainlab Inc.](https://rainlab.co.jp/en/)

Release Notes:
- N/A

---------

Signed-off-by: npmania <np@mkv.li>
2024-05-16 15:13:51 -07:00
Marshall Bowers
97691c1def assistant: Remove unwraps in RecentBuffersContext (#11938)
This PR removes the `unwrap`s in the `RecentBuffersContext` when
building the message.

We can just make `build_message` return a `Result` to clean things up.

Release Notes:

- N/A
2024-05-16 17:57:52 -04:00
bbb651
746223427e wayland: Don't reinvert inverted scroll axes (#11937)
Release Notes:

- Wayland: Fixed Natural Scrolling Being Wrongly Reinverted
([#11874](https://github.com/zed-industries/zed/issues/11874)).
2024-05-16 14:43:46 -07:00
张小白
80caa74866 Support setting font feature values (#11898)
Now (on `macOS` and `Windows`) we can set font feature value:
```rust
  "buffer_font_features": {
    "cv01": true,
    "cv03": 3,
    "cv09": 1,
    "VSAH": 7,
    "VSAJ": 8
  }
```

And one can still use `"cv01": true`.



https://github.com/zed-industries/zed/assets/14981363/3e3fcf4f-abdb-4d9e-a0a6-71dc24a515c2




Release Notes:

- Added font feature values, now you can set font features like `"cv01":
7`.

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-16 14:27:55 -07:00
Joseph T. Lyons
b6189b05f9 Add telemetry for supermaven (#11821)
Data migration plan:

- [X] Make a duplicate table of `copilot_events`
    - Name: `inline_completion_events`
    - Omit `suggestion_id` column
- [X-reverted-skipping] In collab, continue to match on copilot_events,
but simply stuff their data into inline_completion_events, to forward it
to the new table
- [skipping] Once collab is deployed, ensure no events are being sent to
copilot_events, migrate `copilot_events` to new table via a transaction
- [skipping] Delete `copilot_events` table

---

- [X] Locally test that copilot events sent from old clients get put
into inline_completions_table
- [X] Locally test that copilot events and supermaven events sent from
new clients get put into inline_completions_table

---

- [X] Why are discard events being spammed?
- A:
8d4315712b/crates/editor/src/editor.rs (L2147)


![scr-20240514-pqmg](https://github.com/zed-industries/zed/assets/19867440/e51e7ae4-21b8-47a2-bfaa-f68fb355e409)

This will throw off the past results for accepted / dismissed that I was
wanting to use to evaluate Supermaven quality, by comparing its rate
with copilot's rate.

I'm not super thrilled with this fix, but I think it'll do. In the
`supermaven_completions_provider`, we check if there's a `completion_id`
before sending either an accepted or discard completion event. I don't
see a similar construct in the `copilot_completions_provider` to
piggyback off of, so I begrudgingly introduced
`should_allow_event_to_send` and had it follow the same pattern that
`completion_id` does. Maybe there's a better way?

---

Adds events to supermaven suggestions. Makes "CopilotEvents" generic ->
"InlineCompletionEvents".

Release Notes:

- N/A
2024-05-16 17:18:32 -04:00
Marshall Bowers
55f08c0511 assistant: Update current project context to work with Cargo workspaces (#11935)
This PR updates the current project context to work with Cargo
workspaces.

Release Notes:

- N/A
2024-05-16 16:59:57 -04:00
Nate Butler
f8672289fc Add prompt library (#11910)
This PR adds a Prompt Library to Zed, powering custom prompts and any
default prompts we want to package with the assistant.

These are useful for:

- Creating a "default prompt" - a super prompt that includes a
collection of things you want the assistant to know in every
conversation.
- Adding single prompts to your current context to help guide the
assistant's responses.
- (In the future) dynamically adding certain prompts to the assistant
based on the current context, such as the presence of Rust code or a
specific async runtime you want to work with.

These will also be useful for populating the assistant actions typeahead
we plan to build in the near future.

## Prompt Library

The prompt library is a registry of prompts. Initially by default when
opening the assistant, the prompt manager will load any custom prompts
present in your `~/.config/zed/prompts` directory.

Checked prompts are included in your "default prompt", which can be
inserted into the assitant by running `assistant: insert default prompt`
or clicking the `Insert Default Prompt` button in the assistant panel's
more menu.

When the app starts, no prompts are set to default. You can add prompts
to the default by checking them in the Prompt Library.

I plan to improve this UX in the future, allowing your default prompts
to be remembered, and allowing creating, editing and exporting prompts
from the Library.

### Creating a custom prompt

Prompts have a simple format:

```json
{
  // ~/.config/zed/prompts/no-comments.json
  "title": "No comments in code",
  "version": "1.0",
  "author": "Nate Butler <iamnbutler@gmail.com>",
  "languages": ["*"],
  "prompt": "Do not add inline or doc comments to any returned code. Avoid removing existing comments unless they are no longer accurate due to changes in the code."
}
```

Ensure you properly escape your prompt string when creating a new prompt
file.

Example:

```json
{
  // ...
  "prompt": "This project using the gpui crate as it's UI framework for building UI in Rust. When working in Rust files with gpui components, import it's dependencies using `use gpui::{*, prelude::*}`.\n\nWhen a struct has a `#[derive(IntoElement)]` attribute, it is a UI component that must implement `RenderOnce`. Example:\n\n```rust\n#[derive(IntoElement)]\nstruct MyComponent {\n    id: ElementId,\n}\n\nimpl MyComponent {\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self { id.into() }\n    }\n}\n\nimpl RenderOnce for MyComponent {\n    fn render(self, cx: &mut WindowContext) -> impl IntoElement {\n        div().id(self.id.clone()).child(text(\"Hello, world!\"))\n    }\n}\n```"
}
```


Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-16 16:55:54 -04:00
Conrad Irwin
6237e5eb50 Stop sending hangs to slack for a bit (#11933)
Release Notes:

- N/A
2024-05-16 14:11:00 -06:00
Conrad Irwin
44105e1f80 Upload panics via collab instead of zed.dev (#11932)
Release Notes:

- N/A
2024-05-16 14:10:49 -06:00
Kirill Bulatov
decfbc69a5 Disallow multiple save modals for the same pane item (#11931)
Fixes https://github.com/zed-industries/zed/issues/10192


Release Notes:

- Fixed multiple save modals appearing for the same file being closed
([10192](https://github.com/zed-industries/zed/issues/10192))
2024-05-16 22:55:05 +03:00
Marshall Bowers
6513886867 Don't scale context menus in editors with buffer font size (#11930)
With the changes in #11817, context menus within editors would get
scaled by the `buffer_font_size` instead of the `ui_font_size`.

This seems incorrect, as it results in context menus being sized
inconsistently depending on what context they originate from.

This PR makes it so that all context menus scale based on the
`ui_font_size`.

### Before

<img width="1474" alt="Screenshot 2024-05-16 at 2 43 19 PM"
src="https://github.com/zed-industries/zed/assets/1486634/a5be8113-ae24-44ad-a2e9-61105e1fcc9e">

### After

<img width="1095" alt="Screenshot 2024-05-16 at 2 43 01 PM"
src="https://github.com/zed-industries/zed/assets/1486634/3a8d51cf-fc91-4743-8f44-78344028e447">

Release Notes:

- Changed context menus in editors to no longer scale with
`buffer_font_size`.
2024-05-16 15:05:00 -04:00
Justy
53815af2d2 Fix small markdown typo in Windows docs (#11888)
Fixed a small issue in the windows docs where a note wasn't displaying
correctly

Release Notes:

- N/A
2024-05-16 11:44:48 -07:00
Fernando Tagawa
5596a34311 Wayland: Implement text_input_v3 and xkb compose (#11712)
Release Notes:

- N/A

Fixes #9207 
Known Issues:
- [ ] ~~After launching Zed and immediately trying to change input
method, the input panel will appear at Point{0, 0}~~
- [ ] ~~`ime_handle_preedit` should not trigger `write_to_primary`~~
Move to other PR
- [ ] ~~Cursor is visually stuck at the end.~~ Move to other PR
Currently tested with KDE & fcitx5.
2024-05-16 11:42:43 -07:00
Marshall Bowers
fdadbc7174 Add WithRemSize element (#11928)
This PR adds a new `WithRemSize` element to the `ui` crate.

This element can be used to create an element tree that has a different
rem size than the base window.

`WithRemSize` can be nested, allowing for subtrees that have a different
rem size than their parent and their children.

<img width="912" alt="Screenshot 2024-05-16 at 2 25 28 PM"
src="https://github.com/zed-industries/zed/assets/1486634/f599cd9f-c101-496b-93e8-06e570fbf74f">

Release Notes:

- N/A
2024-05-16 14:37:55 -04:00
Marshall Bowers
13bbaf1e18 Use UpdateGlobal accessors in more places (#11925)
This PR updates a number of instances that were previously using
`cx.update_global` to use `UpdateGlobal::update_global` instead.

Release Notes:

- N/A
2024-05-16 13:30:04 -04:00
Marshall Bowers
c1e291bc96 gpui: Improve Global ergonomics (#11923)
This PR adds some ergonomic improvements when working with GPUI
`Global`s.

Two new traits have been added—`ReadGlobal` and `UpdateGlobal`—that
provide associated functions on any type that implements `Global` for
accessing and updating the global without needing to call the methods on
the `cx` directly (which generally involves qualifying the type).

I looked into adding `ObserveGlobal` as well, but this seems a bit
trickier to implement as the signatures of `cx.observe_global` vary
slightly between the different contexts.

Release Notes:

- N/A
2024-05-16 12:47:43 -04:00
张小白
1b261608c6 Add basic proxy settings (#11852)
Adding `proxy` keyword to configure proxy while using zed. After setting
the proxy, restart Zed to acctually use the proxy.

Example setting: 
```rust
"proxy" = "socks5://localhost:10808"
"proxy" = "http://127.0.0.1:10809"
```

Closes #9424, closes #9422, closes #8650, closes #5032, closes #6701,
closes #11890

Release Notes:

- Added settings to configure proxy in Zed

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2024-05-16 19:43:26 +03:00
Thorsten Ball
90b631ff3e tailwind: Allow configuring custom tailwind server build (#11921)
This adds the ability to configure the `tailwindcss-language-server`
integration to use a custom build of the server.

Example configuration in Zed `settings.json`:

```json
{
  "lsp": {
    "tailwindcss-language-server": {
      "binary": {
        "arguments": [
          "/Users/username/tailwindcss-intellisense/packages/tailwindcss-language-server/bin/tailwindcss-language-server",
          "--stdio"
        ]
      }
    }
  }
}
```

This will cause Zed to use its own Node version and run it with the
given arguments.

**Note**: you need to provide `--stdio` as the second argument!

It's also possible to use a custom Node binary:

```json
{
  "lsp": {
    "tailwindcss-language-server": {
      "binary": {
        "path": "/Users/username/bin/my-node",
        "arguments": [
          "/Users/username/tailwindcss-intellisense/packages/tailwindcss-language-server/bin/tailwindcss-language-server",
          "--stdio"
        ]
      }
    }
  }
}
```

This is *super handy* when debugging the language server.

Release Notes:

- Added ability to configure own build of `tailwindcss-language-server`
in Zed settings. Example:
`{"lsp":{"tailwindcss-language-server":{"binary":{"arguments":["/absolute/path/to/tailwindcss-language-server/bin/tailwindcss-language-server",
"--stdio" ]}}}}`
2024-05-16 18:00:30 +02:00
Krzysztof Witkowski
a414b16754 python: Add highlighting to variables (#11851) 2024-05-16 11:43:48 -04:00
Conrad Irwin
9c02239afa chat: Only autocomplete active people (#11892)
Release Notes:

- chat: Updated name autocompletion to only consider active users

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-16 09:14:08 -06:00
Marshall Bowers
178ffabca6 theme: Properly merge SyntaxTheme styles to allow for partial overrides (#11911)
This PR improves the merging behavior for the `SyntaxTheme` such that
user-provided values get merged into the base theme.

This makes it possible to override individual styles without clobbering
the unspecified styles in the base theme.

Release Notes:

- Improved merging of `syntax` styles in the theme.
2024-05-16 09:58:47 -04:00
Marshall Bowers
4ba57d730b Make SyntaxTheme::new_test only available in tests (#11909)
This PR addresses a TODO comment by making `SyntaxTheme::new_test` only
available in tests.

We needed to make it available when the `test-support` feature was
enabled for it to be used in tests outside of the `theme` crate.

Release Notes:

- N/A
2024-05-16 09:30:29 -04:00
Vitaly Slobodin
f2e7c635ac editor: Add Cut, Copy, and Paste actions to the context menu (#11878)
Hi, I saw someone on Twitter mentioned that missing Cut, Copy and Paste
actions in the context menu in the editor block them from using Zed. It
turns out that resolving this issue is simply a matter of adding these
actions to the mouse context menu. To keep items in the context menu
grouped, I placed them at the top of the menu with a separator at the
end. Let me know if that's OK. Thanks!

Here is the screenshot:

![CleanShot 2024-05-16 at 07 04
44@2x](https://github.com/zed-industries/zed/assets/1894248/2ac84001-cdd7-4c01-b597-c5b1dc3e7fa3)

Release Notes:

- Added "Cut", "Copy", and "Paste" actions to the context menu
([#4280](https://github.com/zed-industries/zed/issues/4280)).
2024-05-16 08:59:17 -04:00
Thorsten Ball
8c681d0db3 lsp: Use itemDefaults if sent along with completion items (#11902)
This fixes #10532 by properly making use of `itemDefaults.data` when
that is sent along next to completion `items`.

With this line here we tell the language server that we support `data`
in `itemDefaults`, but we actually never checked for it and never used
it:


a0d7ec9f8e/crates/lsp/src/lsp.rs (L653)

In the case of `tailwindcss-language-server` that means that most of the
items it returns (more than 10k items!) were missing the `data`
attribute, since the language server thought it can send it along in the
`itemDefaults` (because we advertised our capability to use it.)

When we then did a `completionItem/resolve`, we would not send a `data`
attribute along, which lead to an error on the
`tailwindcss-language-server` side and thus no documentation.

This PR also adds support for the other `itemDefaults` that could be
sent along and that we say we support:


a0d7ec9f8e/crates/lsp/src/lsp.rs (L650-L653)

`editRange` we handle separately, so this PR only adds the other 3.

Release Notes:

- Fixed documentation not showing up for completion items coming from
`tailwindcss-language-server`.
([#10532](https://github.com/zed-industries/zed/issues/10532)).

Demo:


https://github.com/zed-industries/zed/assets/1185253/bc5ea0b3-7d83-499f-a908-b0d2a1db8a41
2024-05-16 13:26:07 +02:00
Toon Willems
9969d6c702 Fix: Missing token count for GPT-4o model. (bumps tiktoken-rs to v0.5.9) (#11893)
Fix: this makes sure we have token counts for the new GPT-4o model.

See: https://github.com/zurawiki/tiktoken-rs/releases/tag/v0.5.9 

Release Notes:

- Fix: Token count was missing for the new GPT-4o model.

(I believe this should go in a 0.136.x release)
2024-05-16 13:09:28 +02:00
Jason Lee
8c8c1769c7 docs: Fix quote in default.json (#11900)
Release Notes:

- N/A
2024-05-16 12:46:08 +02:00
Thorsten Ball
58919e9f04 eslint: Change default configuration to fix errors (#11896)
Without this, we'd get constant errors when typing something with ESLint
enabled:

[2024-05-16T10:32:30+02:00 WARN project] Generic lsp request to node
failed: Request textDocument/codeAction failed with message: Cannot read
properties of undefined (reading 'disableRuleComment')
[2024-05-16T10:32:30+02:00 ERROR util]
crates/project/src/project.rs:7023: Request textDocument/codeAction
failed with message: Cannot read properties of undefined (reading
'disableRuleComment')
[2024-05-16T10:32:31+02:00 WARN project] Generic lsp request to node
failed: Request textDocument/codeAction failed with message: Cannot read
properties of undefined (reading 'disableRuleComment')
[2024-05-16T10:32:31+02:00 ERROR util]
crates/project/src/project.rs:7023: Request textDocument/codeAction
failed with message: Cannot read properties of undefined (reading
'disableRuleComment')

This is fixed by changing the default settings for ESLint language
server to have those fields.

I don't think we need to make these configurable yet. These are defaults
that multiple other plugins also use:

- vscode-eslint:
https://sourcegraph.com/github.com/microsoft/vscode-eslint@4d9fc40e71c403d359beaccdd4a6f8d027031513/-/blob/client/src/client.ts?L702-703
- nvim-lspconfig:
https://sourcegraph.com/github.com/neovim/nvim-lspconfig@a27179f56c6f98a4cdcc79ee2971b514815a4940/-/blob/lua/lspconfig/server_configurations/eslint.lua?L94-101
- coc-eslitn:
https://sourcegraph.com/github.com/neoclide/coc-eslint@70eb10d294e068757743f9b580c724e92c5b977d/-/blob/src/index.ts?L698:17-698:35



Release Notes:

- Changed the default ESLint configuration to include the following in
order to silence warnings/errors: `{"codeAction": {
"disableRuleComment": { "enable": true, "location": "separateLine", },
"showDocumentation": { "enable": true } }}`
2024-05-16 10:41:57 +02:00
CharlesChen0823
a0d7ec9f8e Fix repeatedly docking project panel (#11884)
Close: #11808 , #9688

Release Notes:

- N/A
2024-05-15 21:32:03 -07:00
Conrad Irwin
ba8aba4d17 hotfix for collab crashes (#11885)
Release Notes:

- N/A
2024-05-15 21:04:37 -06:00
Marshall Bowers
66e873942d assistant: Factor RecentBuffersContext logic out of AssistantPanel (#11876)
This PR factors some more code related to the `RecentBuffersContext` out
of the `AssistantPanel` and into the corresponding module.

We're trying to strike a balance between keeping this code easy to
evolve as we work on the Assistant, while also having some semblance of
separation/structure.

This also adds the missing functionality of updating the remaining token
count when the `CurrentProjectContext` is enabled/disabled.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-15 16:16:10 -04:00
Kirill Bulatov
cb430fc3e4 Autodetect parser name with prettier by default (#11558)
Closes https://github.com/zed-industries/zed/issues/11517 

* Removes forced prettier parser name for languages, making `auto`
command to run prettier on every file by default.
* Moves prettier configs away from plugin language declarations into
language settings

Release Notes:

- N/A
2024-05-15 22:51:46 +03:00
Piotr Osiewicz
52c70c1082 emmet: release 0.0.3 (#11873)
Includes: #10779 
Release Notes:

- N/A
2024-05-15 21:27:37 +02:00
Vitaly Slobodin
1651cdf03c ruby: Use two spaces per indentation level (#11869)
Hello, this pull request changes the indentation level for Ruby language
from 2 spaces to the most used setting in the Ruby world: 2 spaces per
indentation level.
This setting is mentioned in the [Ruby style guide from the Rubocop
(Ruby linter and formatter)
team](https://rubystyle.guide/#spaces-indentation) and/or in another
popular Rubocop configuration tool -
[`standardrb`](https://github.com/standardrb/standard/blob/main/config/base.yml#L233)
Thanks!

Release Notes:

- N/A
2024-05-15 14:07:00 -04:00
张小白
a1e5f6bb7c windows: Use DwmFlush() to trigger vsync event (#11731)
Currently , on Windows 10, we used a `Timer` to trigger the vsync event,
but the `Timer`'s time precision is only about 15ms, which means a
maximum of 60FPS. This PR introduces a new function to allow for higher
frame rates on Windows 10.

And after reading the codes, I found that zed triggers a draw after
handling mouse or keyboard events, so we don't need to call draw again
when we handle `WM_*` messages. Therefore, I removed the
`invalidate_client_area` function.

Release Notes:

- N/A
2024-05-15 10:45:17 -07:00
张小白
4ae3396253 Make primary clipboard Linux only (#11843)
I guess only Linux supports the primary clipboard.

Release Notes:

- N/A
2024-05-15 10:44:47 -07:00
yodatak
1c62839295 Add missing linux dependencies for compiling openssl on Fedora (#11857)
Release Notes:

- N/A
2024-05-15 11:17:59 -06:00
Conrad Irwin
b7cf3040ef Remove 2 removal (#11867)
Release Notes:

- N/A
2024-05-15 11:06:05 -06:00
Conrad Irwin
247825bdd3 Deploy install.sh to cloudflare (#11866)
Release Notes:

- N/A
2024-05-15 10:35:30 -06:00
张小白
f7c5d70740 macOS: Support all OpenType font features (#11611)
This PR brings support for all `OpenType` font features to
`macOS(v10.10+)`. Now, both `Windows`(with #10756 ) and `macOS` support
all font features.

Due to my limited familiarity with the APIs on macOS, I believe I have
made sure to call `CFRelease` on all variables where it should be
called.

Close #11486 , and I think the official website's
[documentation](https://zed.dev/docs/configuring-zed) can be updated
after merging this PR.

> Zed supports a subset of OpenType features that can be enabled or
disabled for a given buffer or terminal font. The following OpenType
features can be enabled or disabled too: calt, case, cpsp, frac, liga,
onum, ordn, pnum, ss01, ss02, ss03, ss04, ss05, ss06, ss07, ss08, ss09,
ss10, ss11, ss12, ss13, ss14, ss15, ss16, ss17, ss18, ss19, ss20, subs,
sups, swsh, titl, tnum, zero.



https://github.com/zed-industries/zed/assets/14981363/44e503f9-1496-4746-bc7d-20878c6f8a93



Release Notes:

- Added support for **all** `OpenType` font features to macOS.
2024-05-15 18:26:50 +02:00
Joseph T. Lyons
f47bd32f15 v0.137.x dev 2024-05-15 11:47:42 -04:00
Congyu
c3c4e37940 Do not select target range going to definition (#11691)
Release Notes:

-Fixed #11347 , do not select target range going to definition. Just
place the cursor at the start of target range.
2024-05-15 09:13:32 -06:00
loczek
d3dfa91254 Fix aside affecting parent popover height (#11859)
Release Notes:

- Fixed the size of the completions menu changing based on the size of
the aside
([#11722](https://github.com/zed-industries/zed/issues/11722)).


https://github.com/zed-industries/zed/assets/30776250/c67e6fef-20f2-4dc5-92b3-09bb73f874a7


https://github.com/zed-industries/zed/assets/30776250/7467b8ee-6e66-42d7-a8cc-2df11df58c5e

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-15 11:08:24 -04:00
Piotr Osiewicz
4ff1ee126c tasks: minor fixes to docs (#11862)
Release Notes:

- N/A
2024-05-15 16:32:57 +02:00
Thorsten Ball
1b9014bca6 tailwind: Allow Tailwind LS to be used in Scala (#11858)
This fixes the issue mentioned here:
https://github.com/zed-industries/zed/issues/5830#issuecomment-2111947083

In order for other languages to work, we need to pass the following
settings along to the Tailwind language server.

With the following Zed settings, it then also works for Scala:

```json
{
  "languages": {
    "Scala": {
      "language_servers": ["tailwindcss-language-server"]
    },
  },
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "includeLanguages": {
          "scala": "html"
        },
        "experimental": {
          "classRegex": ["[cls|className]\\s\\:\\=\\s\"([^\"]*)"]
        }
      }
    }
  }
}
```

Release Notes:

- Added ability to configure settings for `tailwindcss-language-server`,
namely the `includeLanguages` and `experimental` objects.

**NOTE**: I have only tested that the language server boots up for Scala
files and that the settings are forwarded correctly. I don't have a
Scala+Tailwind project with which to test that the actual completions
also work.

cc @nguyenyou
2024-05-15 15:15:36 +02:00
Marshall Bowers
f42f4432ec Remove stray println! (#11855)
This PR removes a stray `println!` left over from #11844.

Release Notes:

- N/A
2024-05-15 08:39:52 -04:00
Piotr Osiewicz
a59a388c15 tasks: Wire through click handlers in new tasks modal (#11854)
🤦
Spotted by @SomeoneToIgnore 


Release Notes:

- N/A
2024-05-15 14:38:19 +02:00
Thorsten Ball
43d79af94a metal renderer: Increase instance buffer size dynamically (#11849)
Previously, we had an instance buffer pool that could only allocate
buffers with a fixed size (hardcoded to 2mb). This caused certain scenes
to render partially, e.g. when showing tens of thousands of glyphs on a
big screen.

With this commit, when `MetalRenderer` detects that a scene would be too
large to render using the current instance buffer size, it will:

- Clear the existing instance buffers
- Allocate new instance buffers that are twice as large
- Retry rendering the scene that failed with the newly-allocated buffers
during the same frame.

This fixes #11615.

Release Notes:

- Fixed rendering issues that could arise when having large amounts of
text displayed on a large display. Fixed by dynamically increasing the
size of the buffers used on the GPU.
([#11615](https://github.com/zed-industries/zed/issues/11615)).

Before:


https://github.com/zed-industries/zed/assets/1185253/464463be-b61c-4149-a417-01701699decb


After:



https://github.com/zed-industries/zed/assets/1185253/4feacf5a-d862-4a6b-90b8-317ac74e9851

Co-authored-by: Antonio <me@as-cii.com>
2024-05-15 13:43:06 +02:00
Bennet Bo Fenner
26ffdaffe2 tasks: Use unique id for run indicator (#11846)
This fixes a small visual issue with the run indicator. As all run
indicators use the same element id they all show up as pressed when
clicking on a single button. We can safely use a combination of
"run_indicator" and the actual row as the element id, as there can only
ever be one run indicator per line.

Before:

<img width="552" alt="Screenshot 2024-05-15 at 12 24 08"
src="https://github.com/zed-industries/zed/assets/53836821/18779f1a-0984-488f-83fd-4a6a561f223e">

After:

<img width="633" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/07ea26b5-06ad-4955-8250-d96d4704220c">


Release Notes:

- Fixed an issue where all run buttons would show up as pressed when
clicking on a single run button
2024-05-15 13:23:13 +02:00
Piotr Osiewicz
266643440c rust: reduce false positives in runnables query (#11845)
We were marking `#[cfg(test)]`ed function as a test, which is wrong.
Also allow for other attribute_items (such as #[should_panic]) between
test attribute and a function item.

Release Notes:

- N/A
2024-05-15 11:42:05 +02:00
Thorsten Ball
8bc41e150e Use editor's current font size to scale UI elements (#11844)
This is a follow-up to #11817 and fixes the case where the font size has
been changed with `cmd +/-` and not through the settings.

It now works with both: when the font size is adjusted in the settings
and when changing it via shortcuts.

Release Notes:

- N/A

Demo:


https://github.com/zed-industries/zed/assets/1185253/2e539bd3-f5cc-4aae-9f04-9ae014187959
2024-05-15 10:05:42 +02:00
Conrad Irwin
8629a076a7 Tighten up KeyBinding (#11839)
After #11795, the context menu was looking a little ridiculous on Mac in
vim mode (and the command palette has for a while).

<img width="258" alt="Screenshot 2024-05-14 at 20 35 50"
src="https://github.com/zed-industries/zed/assets/94272/cb0ec8b9-4da6-4ab4-9eec-c60d62f79eff">
<img width="581" alt="Screenshot 2024-05-14 at 20 56 28"
src="https://github.com/zed-industries/zed/assets/94272/d8fec440-17cc-4c20-80d9-c1d7f2f18315">

A future change would be to have a platform style for vim keybindings so
we can render `g A`, but for now this just removes a bunch of (to my
eyes at least) unnecessary space:

 
<img width="576" alt="Screenshot 2024-05-14 at 21 01 55"
src="https://github.com/zed-industries/zed/assets/94272/a39f4123-dc3b-4bb5-bb8d-5de6b37552e7">

cc @iamnbutler 


Release Notes:

- N/A
2024-05-14 21:12:17 -06:00
Gus
3cbac27117 Show buffer_search on vim::MoveToNextMatch (#11836)
This changes the vim::MoveToNextMatch event callback to open the
buffer_search toolbar. This fixes an issue where highlights would appear
which were only cancellable by opening then closing the toolbar.

Release Notes:

- the buffer search toolbar now opens on vim::MoveToNextMatch fixing the
issue where highlights were not cancellable

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-14 20:57:58 -06:00
CharlesChen0823
1a358e203e cleanup (#11835)
cleanup unneed code.

Release Notes:

- N/A
2024-05-14 18:25:54 -07:00
jansol
ba26acc1ed blade: Fix display of straight underlines (#11818)
Fixes: #11715

(also apply alpha of the color to wavy ones while we're at it)

Release Notes:

- Fixed display of straight underlines when using the blade renderer
(#11715)
2024-05-14 18:03:43 -07:00
Gus
edadc6f938 Fix bug with keymaps flickering in mouse menu (#11795)
Fixes a bug where Vim bindings would flash in the mouse context menu and
then be replaced by the default keybindings. Also fixes those bindings
not being usable while the mouse context menu was open.

Release Notes:

- Fixed bug where Vim bindings were not available when mouse context
menu was open

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-14 17:18:21 -06:00
Marshall Bowers
3da625e538 astro: Bump to v0.0.2 (#11834)
This PR bumps the Astro extension to v0.0.2.

Changes:

- #11830

Release Notes:

- N/A
2024-05-14 19:12:43 -04:00
Marshall Bowers
586f70852e ruby: Bump to v0.0.3 (#11833)
This PR bumps the Ruby extension to v0.0.3.

Changes:

- #11825

Release Notes:

- N/A
2024-05-14 19:08:39 -04:00
Marshall Bowers
3df144c88a php: Bump to v0.0.3 (#11832)
This PR bumps the PHP extension to v0.0.3.

Changes:

- #11695

Release Notes:

- N/A
2024-05-14 19:02:39 -04:00
d1y
af79e6b423 astro: Fix broken language injections (#11830)
Update upstream
4be180759e
This will solve #11827

Before:
<img width="644" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/f6b10667-9197-4e5d-8513-78ce3d22f9e7">
After:
<img width="700" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/7bd7b0e6-e73c-4d1d-abd6-d6b2d88e97a6">


Release Notes:

- N/A
2024-05-14 18:57:10 -04:00
Vitaly Slobodin
43be375c76 ruby: Fix solargraph completion highlighting (#11825)
Hi. This pull request fixes a small error with `solargraph` completions
to make them more detailed. It removes the nested match expression to
resolve the problem with highlighting the completion items and their
signatures with the return type as well. Thanks.

See screenshots below.

Release Notes:

- N/A

| Before  | After |
| ------------- | ------------- |
| ![CleanShot 2024-05-14 at 23 23
00@2x](https://github.com/zed-industries/zed/assets/1894248/4ea1fa41-1189-4607-8aea-547c27229a18)
| ![CleanShot 2024-05-14 at 23 29
30@2x](https://github.com/zed-industries/zed/assets/1894248/3c7be39a-2c7b-4662-8519-8c258c049cfa)
|
2024-05-14 18:54:19 -04:00
Marshall Bowers
26b5f34046 assistant: Add basic current project context (#11828)
This PR adds the beginnings of current project context to the Assistant.

Currently it supports reading a `Cargo.toml` file and using that to get
some basic information about the project, and its dependencies:

<img width="1264" alt="Screenshot 2024-05-14 at 6 17 03 PM"
src="https://github.com/zed-industries/zed/assets/1486634/cc8ed5ad-0ccb-45da-9c07-c96af84a14e3">

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
2024-05-14 18:39:52 -04:00
Conrad Irwin
5b2c019f83 cli: Support --foreground for debugging (#11819)
Release Notes:

- Added `--foreground` to the cli to allow running zed on the current
PTY.
2024-05-14 16:05:40 -06:00
Conrad Irwin
18b6ded8f0 Auto-open remote projects on creation (#11826)
Release Notes:

- N/A
2024-05-14 16:05:26 -06:00
Kirill Bulatov
67c9fc575f Fix a link to Zed configuring docs (#11739)
Based on https://github.com/zed-industries/zed/pull/11736 

Release Notes:

- N/A
2024-05-14 17:15:14 -04:00
Marshall Bowers
ba4d4c8e1c assistant: Restructure ambient context in preparation for adding more (#11822)
This PR restructures the ambient context in the `assistant` crate to
make it more amenable to adding more kinds of ambient context.

Release Notes:

- N/A
2024-05-14 17:03:39 -04:00
Conrad Irwin
bf4478703b Prevent remoting dialog from losing focus (#11820)
Release Notes:

- N/A
2024-05-14 14:32:37 -06:00
claytonrcarter
748cd38d77 php: Highlight PHPDoc comments (#11695)
This adds highlighting of phpdoc tags and PHP types to phpdoc comments,
using
[tree-sitter-phpdoc](https://github.com/claytonrcarter/tree-sitter-phpdoc)
(maintained by yours me, and also in use by neovim).

<table>
<tr>
<td>
<strong>Before</strong>
<img
src="https://github.com/zed-industries/zed/assets/1420419/bae4c502-8a2c-4399-893f-fcff4e5797b6">
</td>
<td>
<strong>After</strong>
<img
src="https://github.com/zed-industries/zed/assets/1420419/8848e9fb-61a0-4938-a118-7041da9589c0">
</td>
</tr>
</table>


Release Notes:

- N/A
2024-05-14 15:48:14 -04:00
Piotr Osiewicz
1db136ff65 tasks: Refresh available tasks in editor when tasks.json changes (#11811)
Release Notes:

- N/A
2024-05-14 21:26:35 +02:00
Thomas Aunvik
0ae0b08c38 linux: Add Keybinds Ctrl-Insert to Copy and Shift-Insert to Paste (#11799)
Release Notes:

- N/A
2024-05-14 12:03:21 -07:00
Marshall Bowers
5b8bb6237f Scale UI elements in the editor based on the buffer_font_size (#11817)
This PR adjusts how UI elements are rendered inside of full-size editors
to scale with the configured `buffer_font_size`.

This fixes some issues where UI elements (such as the `IconButton`s used
for code action and task run indicators) would not scale as the
`buffer_font_size` was changed.

We achieve this by changing the rem size when rendering the
`EditorElement`, with a rem size that is derived from the
`buffer_font_size`.

`WindowContext` now has a new `with_rem_size` method that can be used to
render an element with a given rem size. Note that this can only be
called during `request_layout`, `prepaint`, or `paint`, similar to
`with_text_style` or `with_content_mask`.

### Before

<img width="1264" alt="Screenshot 2024-05-14 at 2 15 39 PM"
src="https://github.com/zed-industries/zed/assets/1486634/05ad7f8d-c62f-4baa-bffd-38cace7f3710">

<img width="1264" alt="Screenshot 2024-05-14 at 2 15 49 PM"
src="https://github.com/zed-industries/zed/assets/1486634/254cd11c-3723-488f-ab3d-ed653169056c">

### After

<img width="1264" alt="Screenshot 2024-05-14 at 2 13 02 PM"
src="https://github.com/zed-industries/zed/assets/1486634/c8dad309-62a4-444f-bfeb-a0009dc08c03">

<img width="1264" alt="Screenshot 2024-05-14 at 2 13 06 PM"
src="https://github.com/zed-industries/zed/assets/1486634/4d9a3a52-9656-4768-b210-840b4884e381">

Note: This diff is best viewed with whitespace changes hidden:

<img width="245" alt="Screenshot 2024-05-14 at 2 22 45 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7cb9829f-9c1b-4224-95be-82182017ed90">

Release Notes:

- Changed UI elements within the editor to scale based on
`buffer_font_size` (e.g., code action indicators, task run indicators,
etc.).
2024-05-14 14:34:39 -04:00
Flafy
c8ddde27e1 Fix reveal_path blocks on linux (#11702)
If you go to the file tree and press "x" (which is
"project_panel::RevealInFinder"). It will open the default file
manager(in my case nautilus). But on Linux it makes Zed unresponsive.
This fixes that.

Release Notes:

- Fixed Zed blocked after opening file manager in the file tree on
Linux.
2024-05-14 11:18:45 -07:00
Mikayla Maki
bfc066a1ec Toss return value (#11815)
Release Notes:

- N/A
2024-05-14 11:17:10 -07:00
CharlesChen0823
fd8336c8cb linux: Handle modification events from file watcher (#11778)
Fixed #11595 


Release Notes:

- N/A
2024-05-14 11:00:26 -07:00
张小白
d0dd8bf059 windows: Improve handling of the WM_SETTINGCHANGE (#11738)
This event is broadcast to all windows, so we can handle this message in
the `WindowsWindow` ranther than in `WindowsPlatform`.

Release Notes:

- N/A
2024-05-14 10:57:46 -07:00
张小白
491c04e176 windows: Support multi-monitor (#11699)
Zed can detect changes in monitor connections and disconnections and
provide corresponding feedback. For example, if the current window's
display monitor is disconnected, the window will be moved to the primary
monitor. And now Zed always opens on the monitor specified in
`WindowParams`.

Release Notes:

- N/A
2024-05-14 10:54:18 -07:00
张小白
5154910c64 windows: Update crate Windows from 0.53 to 0.56 (#11662)
Version 0.56 has fixed many of the previous bugs, and one of the bugs
prevent me implementing some functions.

Release Notes:

- N/A
2024-05-14 09:59:55 -07:00
apricotbucket28
d1ee2d0749 wayland: Window controls and drag (#11525)
Based on https://github.com/zed-industries/zed/pull/11046

- Partially fixes #10346 
- Fixes https://github.com/zed-industries/zed/issues/9964

## Features
Window buttons

![image](https://github.com/zed-industries/zed/assets/71973804/1b7e0504-3925-45ba-90b5-5adb55e0d739)

Window drag

![image](https://github.com/zed-industries/zed/assets/71973804/9c509a37-e5a5-484c-9f80-c722aeee4380)

Native window context menu

![image](https://github.com/zed-industries/zed/assets/71973804/048ecf52-e277-49bb-a106-91cad226fd8a)

### Limitations

- No resizing
- Wayland only (though X11 always has window decorations)

### Technical

This PR adds three APIs to gpui.

1. `show_window_menu`: Triggers the native title bar context menu.
2. `start_system_move`: Tells the compositor to start dragging the
window.
3. `should_render_window_controls`: Whether the compositor doesn't
support server side decorations.

These APIs have only been implemented for Wayland, but they should be
portable to other platforms.

Release Notes:

- N/A

---------

Co-authored-by: Akilan Elango <akilan1997@gmail.com>
2024-05-14 09:44:55 -07:00
Thorsten Ball
db89353193 git: Support git repos with .git folder above project root (#11550)
TODOs:

- [x] Add assertions to the test to ensure that the git status is
propagated
- [x] Get collaboration working
- [x] Test opening a git repository inside another git repository
- [x] Test opening the sub-folder of a repository that itself contains
another git repository in a subfolder

Fixes:
- Fixes https://github.com/zed-industries/zed/issues/10154
- Fixes https://github.com/zed-industries/zed/issues/8418
- Fixes https://github.com/zed-industries/zed/issues/8275
- Fixes https://github.com/zed-industries/zed/issues/7816
- Fixes https://github.com/zed-industries/zed/issues/6762
- Fixes https://github.com/zed-industries/zed/issues/4419
- Fixes https://github.com/zed-industries/zed/issues/4672
- Fixes https://github.com/zed-industries/zed/issues/5161

Release Notes:

- Added support for opening subfolders of git repositories and treating
them as part of a repository (show git status in project panel, show git
diff in gutter, git blame works, ...)
([#4672](https://github.com/zed-industries/zed/issues/4672)).

Demo video:


https://github.com/zed-industries/zed/assets/1185253/afc1cdc3-372c-404e-99ea-15708589251c
2024-05-14 18:34:51 +02:00
Antonio Scandurra
9f0a20241b Report response latency and errors when using (inline) assistant (#11806)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: David <davidsp@anthropic.com>
2024-05-14 18:18:26 +02:00
Antonio Scandurra
de09409f01 Sanitize messages before sending them to Anthropic (#11810)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: David <davidsp@anthropic.com>
2024-05-14 17:47:33 +02:00
Conrad Irwin
69f9489aa9 Don't bundle libdl 🤦 (#11809)
Release Notes:

- N/A
2024-05-14 09:41:35 -06:00
Marshall Bowers
652748b0c9 gleam: Bump to v0.1.2 (#11803)
This PR bumps the Gleam extension to v0.1.2.

Changes:

- #11476
- #11801

Release Notes:

- N/A
2024-05-14 10:53:43 -04:00
Marshall Bowers
77f0d35684 gleam: Add gleam test task (#11801)
This PR adds a task for running `gleam test`.

Release Notes:

- N/A
2024-05-14 10:45:14 -04:00
Antonio Scandurra
5944caaa90 Add support for interacting with Claude in the assistant panel (#11798)
Release Notes:

- Added support for interacting with Claude in the assistant panel. You
can enable it by adding the following to your `settings.json`:

    ```json
    "assistant": {
        "version": "1",
        "provider": {
            "name": "anthropic"
        }
    }
    ```
2024-05-14 15:57:52 +02:00
Antonio Scandurra
019d98898e Add support for gpt-4o when using zed.dev as the model provider (#11794)
Release Notes:

- N/A
2024-05-14 13:55:47 +02:00
Antonio Scandurra
a13a92fbbf Introduce recent files ambient context for assistant (#11791)
<img width="1637" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/5aaec657-3499-42c9-9528-c83728f2a7a1">

Release Notes:

- Added a new ambient context feature that allows showing the model up
to three buffers (along with their diagnostics) that the user interacted
with recently.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-05-14 13:48:36 +02:00
Antonio Scandurra
e4c95b25bf Allow using the inline assistant within the assistant panel (#11754)
Release Notes:

- Added the ability to use the inline assistant within the assistant
panel.
2024-05-14 13:42:32 +02:00
Thorsten Ball
4766b41e96 docs: Document how to use custom api_url in Assistant (#11790)
This essentially documents the comment here:
https://github.com/zed-industries/zed/issues/4424#issuecomment-2053646583

Release Notes:


- N/A
2024-05-14 11:39:57 +02:00
Piotr Osiewicz
95e0d5ed74 tasks: Reorganize task modal (#11752)
![image](https://github.com/zed-industries/zed/assets/24362066/bc7cc3d3-d9fc-4be6-b9b6-e3d8edf5b533)

Release Notes:
- Improved tasks modal by highlighting a distinction between a task
template and concrete task instance and surfacing available keybindings
more prominently. Task templates are now always available in the modal,
even if there's already a history entry with the same label.
- Changed default key binding for "picker::UseSelectedQuery" to `opt-e`.
2024-05-14 11:22:09 +02:00
CharlesChen0823
0a096bf531 terminal: Fix Alacritty key bindings (#11782)
Close #10502 

Release Notes:

- Fixed `ctrl-space` not being forwarded correctly in the terminal view.
([#10502](https://github.com/zed-industries/zed/issues/10502))
2024-05-14 11:09:21 +02:00
Thorsten Ball
ec65035659 inline blame: Match icon size to font size in buffer (#11788)
This fixes #11311.


Release Notes:

- Fixed icon in inline git blame entry not changing with the buffer font
size. ([#11311](https://github.com/zed-industries/zed/issues/11311)).

Before:

![screenshot-2024-05-14-10 48
49@2x](https://github.com/zed-industries/zed/assets/1185253/4a288cae-a52b-4bee-8681-f1d9ba3b57f3)

After:

![screenshot-2024-05-14-10 50
06@2x](https://github.com/zed-industries/zed/assets/1185253/f7a6a608-8ecc-4642-adbd-80858dea75e9)
2024-05-14 11:06:16 +02:00
Toon Willems
9b74acc4f5 Add GPT-4o as possible model (#11764)
Resolves: #11766

Release Notes:

- Add GPT-4o support (see: https://openai.com/index/hello-gpt-4o/).
GPT-4o is better and faster than 4-turbo, at half the price.
2024-05-14 10:43:24 +02:00
Thorsten Ball
43da37b0ab shell: Load SHELL from passwd entry if launched as desktop app (#11758)
This fixes #8794 and other related problems.

The problem, in short, is this: `$SHELL` might be outdated. This code
ensures that we update `$SHELL` to what we can deem the newest version,
if we're started as a desktop application.

The background is that you can get the user's preferred shell in two
ways:

1. Read the `SHELL` env variable
2. Read the `/etc/passwd` file and check which shell is set

Most applications should and do prefer (1) over (2).

Why is it preferred? Reading `SHELL` means that processes can inherit
the variable from each other. And you can do something like
`SHELL=/bin/cool-shell ./my-cool-app`

But what happens if the application was launched from the desktop? Which
SHELL env does it inherit then?

It inherits the env from the process that launched it, which is
Finder.app or launchd or GNOME or something else — these are all
long-running processes that get their environment when the user logs in.

They do *not* get a new environment unless restarted (either process
restarted or computer restarted)

That means the `SHELL` env variable they have might be outdated.

That's a problem if you, for example, change your shell with `chsh` and
then launch the app from the desktop.

That change of the default shell is not reflected in the app if the app
only reads from SHELL. Because that hasn’t been updated. Instead it
should read from passwd file to get the newest value.



Release Notes:

- Fixed SHELL being outdated if Zed was launched via Finder or Raycast
or other desktop launchers.
([#8794](https://github.com/zed-industries/zed/issues/8794))
2024-05-14 10:16:55 +02:00
Conrad Irwin
15e1895159 Try some more linker magic to get it working on ubuntu 20 (#11784)
Release Notes:

- N/A
2024-05-13 22:08:10 -06:00
Conrad Irwin
aee00d41d8 Fix script/bundle-linux (#11783)
Release Notes:

- N/A
2024-05-13 20:39:27 -06:00
Marshall Bowers
172cb81e82 xtask: Check for licenses that are duplicated instead of being symlinked (#11777)
This PR updates `cargo xtask licenses` to also check for license files
that are not symlinks.

Release Notes:

- N/A
2024-05-13 19:13:09 -04:00
Marshall Bowers
b01878aadf Add xtask for finding crates with missing licenses (#11776)
This PR adds a new `cargo xtask licenses` command for finding crates
with missing license files.

A number of crates were uncovered that were missing a license file, and
have had the appropriate license file added.

Release Notes:

- N/A
2024-05-13 18:52:12 -04:00
Marshall Bowers
ff2eacead7 Add missing LICENSE file to http crate (#11773)
This PR adds a missing LICENSE file to the recently-extracted `http`
crate.

Release Notes:

- N/A
2024-05-13 18:26:12 -04:00
Kirill Bulatov
fcd5fa9257 Remove selection highlights from deleted diff editors on blur (#11772)
Follow-up of https://github.com/zed-industries/zed/pull/11710

Release Notes:

- Removed extra line highlights when deleted diff editors loose focus
2024-05-14 01:15:49 +03:00
Danilo Leal
cb34507ece docs: Fix typos on the Assistant Panel page (#11725)
Fix typos on the Assistant Panel page, also including removal of
unnecessary commas and standardization to US English.

Release Notes:

- N/A

PS: Assuming here US English is the preferred style (e.g., "canceled"
vs. "cancelled".) Happy to revert if that's not the case! :)
2024-05-13 18:03:06 -04:00
Nate Butler
1ab247756a Add Tool Strip (#11756)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-13 17:58:08 -04:00
Marshall Bowers
335636c42e ruby: Bump to v0.0.2 (#11769)
This PR bumps the Ruby extension to v0.0.2.

Changes:

- #11768

Release Notes:

- N/A
2024-05-13 17:32:43 -04:00
Vitaly Slobodin
24cc4c69f8 ruby: Add ruby-lsp as an experimental language server (#11768)
Adds [ruby-lsp](https://shopify.github.io/ruby-lsp/) as an alternative
LS for Ruby language.
While support for fully functional `ruby-lsp` is limited due to some
limitations (see https://github.com/zed-industries/zed/pull/8613) I
think it's OK to add it but disable by default. Thanks!

Resolves #4834.

Release Notes:

- N/A

### Some screenshots

Completion support
![CleanShot 2024-05-13 at 22 58
23@2x](https://github.com/zed-industries/zed/assets/1894248/d5047baa-c58f-465d-ae31-a7045aa56adf)

Symbol search
![CleanShot 2024-05-13 at 23 03
59@2x](https://github.com/zed-industries/zed/assets/1894248/0cb6320a-b000-4a0c-85eb-f8d1a8f6936e)

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-13 17:22:01 -04:00
Conrad Irwin
9af1298a7f Bundle linux deps (#11681)
Inlcude linux deps in the bundle

Release Notes:

- N/A
2024-05-13 14:10:03 -06:00
Andrew Lygin
8e92f19fed editor: Current line highlight options (#11710)
None:

<img width="717" alt="none"
src="https://github.com/zed-industries/zed/assets/2101250/b2a741db-c64a-4275-a612-5a0d15c9cab7">

Gutter:

<img width="715" alt="gutter"
src="https://github.com/zed-industries/zed/assets/2101250/f7a68a6e-6eba-41b4-9042-5a5fe2ee21a4">

Line:

<img width="717" alt="line"
src="https://github.com/zed-industries/zed/assets/2101250/117f5b00-abd7-425b-8047-1a6fab8293a7">

All:

<img width="715" alt="all"
src="https://github.com/zed-industries/zed/assets/2101250/ebccc0da-0fa0-44e5-903c-cc49d975db76">

This PR adds the `current_line_highlight` setting that defines how to
highlight the current line in the editor:

- `none`: Don't highlight the current line.
- `gutter`: Highlight the gutter area only.
- `line`: Highlight the editor area only.
- `all` (default): Highlight the whole line.

The options have been borrowed from VSCode.

Fixes #5222
Part of #4382

Release Notes:

- Added the `current_line_highlight` setting that defines how to
highlight the current line in the editor (#5222).
2024-05-13 22:02:12 +03:00
Conrad Irwin
cf97b995b2 Fix panic when accepting completions (#11762)
Release Notes:

- Fixed a panic caused by missing bounds check in completion handler
2024-05-13 13:24:54 -04:00
sebbeutler
bc292186cd Fix key-bindings.md example (#11415)
Fixed obselete command name in keymap example.
From "zed::OpenKeyMap" to "zed::OpenKeymap".



Release Notes:

- N/A
2024-05-13 09:09:59 -07:00
Danilo Leal
2e8197ce6e docs: Standardize "Note" blockquote usage (#11724)
Standardize the blockquote "Notes" usage, so all places are using the
`>` blockquote notation, as well as a consistent style for the "Note"
word.

PS: Thought that bolding the word "**Note**" would make for a higher
visual distinction, so went for it in all existing cases! No strong
feelings, though; happy to roll back to just "Note:" if that's
preferrable!

Release Notes:

- N/A
2024-05-13 09:02:44 -04:00
Marshall Bowers
76139330e3 vue: Bump to v0.0.2 (#11747)
This PR bumps the Vue extension to v0.0.2.

Changes:

- #11743

Release Notes:

- N/A
2024-05-13 08:49:40 -04:00
Thorsten Ball
c769d58b4c vue: Fix Vue.js language server not starting (#11743)
This fixes #10871.

The introduction of #11412 broke Vue.js language support, since it made
Zed rely more heavily on correct language name -> language ID mappings,
which the Vue.js extension didn't have.

Release Notes:

- N/A
2024-05-13 14:38:14 +02:00
Piotr Osiewicz
c90263d59b editor: Use proper rows for fold indicators in the gutter (#11741)
Follow-up to #11656

Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-05-13 12:03:13 +02:00
Thorsten Ball
1afcd12747 snippets: Fix <tab> not working when at end of snippet (#11740)
This fixes #10185 by not keeping snippet state around when already at
the end of the snippet and the tabstop is empty (i.e. it's not a
selection) and we're already on it.

The reason for the fix is outlined in the comments of #10185 but to
repeat:

1. `gopls` sends completions with type "snippet" even when suggesting
single word completions that don't contain tabstops
2. We use a default behavior and add an "end tabstop" by default so that
the cursor jumps to the end of the snippet when accepting it.
3. We'd then push the state of the snippet on the stack which is where
it would stay, with the cursor already at the end and the user unable to
get rid of the tabstop state.

This fixes the issue by not pushing snippet state when the tabstop we
accepted is the "end tabstop".

Release Notes:

- Fixed completions inside snippets breaking the jump-to-next-tabstop
behaviour when using Go/`gopls`
([#10185](https://github.com/zed-industries/zed/issues/10185)).

Demo:



https://github.com/zed-industries/zed/assets/1185253/35384e5e-45c6-46ab-870d-b48e56d8786b
2024-05-13 11:39:00 +02:00
Jason Lee
6df1bc85aa Fix runnable, code_actions button can not trigger when editor not focused (#11729)
## Before


https://github.com/zed-industries/zed/assets/5518/546450fc-ad2c-45d0-8bdb-7b15cfebe235

## After


https://github.com/zed-industries/zed/assets/5518/efc4f863-9db1-4846-83ae-c99ae4dcb3ed

Release Notes:

- Fixed code actions/runnable buttons not triggering when editor is not focused.
2024-05-13 11:02:15 +02:00
Thorsten Ball
91b9e4ee9f git blame: add "Open permalink" to right-click menu (#11734)
This adds a new option to the right click menu for git blame entries in
the gutter: "Open permalink". If there is a URL for the code host, then
this will open it.

Release Notes:

- Added "Open permalink" option to right-click menu of git blame entries
in gutter.

Demo:

![screenshot-2024-05-13-09 39
48@2x](https://github.com/zed-industries/zed/assets/1185253/656c177c-79f0-4a40-8838-7e963d099479)
2024-05-13 09:47:03 +02:00
Gu
8dd26553a3 docs: Fix broken link (#11685)
configuration link in `javascript.md`

https://zed.dev/docs/languages/javascript

Release Notes:

- N/A
2024-05-12 23:06:48 -04:00
João Miguel Nogueira
78f1482cd1 Add autosave with delay (#11325)
Implemented autosave functionality with a delay, which now refrains from
formatting the code upon triggering unless the user manually saves it.
Additionally, enhanced documentation for the `format_on_save` setting
has been added. This resolves the issue where autosave with delay would
inadvertently format the code, disrupting the user experience, as
reported in the corresponding issue.

Release Notes:

- Fixed a bug where autosave after_delay would auto-format the buffer
([#9787](https://github.com/zed-industries/zed/issues/9787)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-12 17:18:30 -04:00
Andrew Lygin
9fdfe5c813 Don't hide last symbol under the scrollbar (#11696)
This PR adds an extra scrollbar-wide margin to the right side of the
editor. This prevents hiding the last character under the scrollbar.

Fixes #7098

Release Notes:

- Fixed hiding of the last character under the scrollbar (#7098).
2024-05-12 22:04:20 +03:00
Danilo Leal
4446c38705 docs: Add link for "Configuring Zed" mention (#11723)
Release Notes:

- N/A
2024-05-12 21:57:38 +03:00
Kirill Bulatov
692afdca27 Remove deploy artifacts after uploads (#11726)
Release Notes:

- N/A
2024-05-12 21:53:27 +03:00
Erik Simmler
6657e301cd Update reference to keymap.json in tasks docs (#11711)
I assume this was an older file name or just a typo as I can't find any
other references to a `keybindings.json` file. Either way it was
confusing for a bit :)

Release Notes:

- N/A
2024-05-12 13:55:19 +02:00
Kyle Kelley
f990f70936 Bring the Tool Calling README up to date (#11683) 2024-05-12 04:47:19 -07:00
Conrad Irwin
f550f23b97 vim test redux (#11709)
This cleans up the neovim-backed vim tests:
- removed exempted tests (we'll rely on bug reports to find missing edge
cases)
- moved all assertions into non-async fn's so that failures are
reporting on the right file/line
- removed the NeovimBackedBindingTestContext
- renamed a few things to make them clearer
- reduced the number of permutations tested in some cases to reduce
slowest test from 60s to 5s

Release Notes:

- N/A
2024-05-11 14:04:05 -04:00
Kirill Bulatov
48cba328f2 Revert "Use sha in the names of Linux nightly archives (#11693)"
This reverts commit 6a64360ec8.
2024-05-11 12:04:49 +03:00
Kirill Bulatov
6a64360ec8 Use sha in the names of Linux nightly archives (#11693)
Release Notes:

- N/A
2024-05-11 11:14:49 +03:00
Piotr Osiewicz
fa04f7514e chore: Improve dev build startup time (#11692)
RustEmbed repeatedly compiled regexes for handling of
'include='/'exclude' statements in a hot loop, which caused each call to
Assets::iter() to take 600ms. Since it is being called twice on our
startup path, that alone contributed over a second to startup time in
debug builds. I've filed a PR with them
https://github.com/pyrossh/rust-embed/pull/244 which brings down the
time for a single iter() call to 6ms.

Release Notes:

- N/A
2024-05-11 10:10:13 +02:00
Maksim Bondarenkov
69676c9d33 update ring dependency (#11689)
this updates ring dependency to 0.17.x version, which has Windows on ARM
support

Release Notes:

- N/A
2024-05-11 09:44:58 +02:00
Kalle Ahlström
b8a83443ac editor: Support walking through overlapping diagnostics (#11139)
While looking into how to implement #4901, noticed that the current
`Goto next/previous diagnostic` behaved a bit weirdly. That is, when
there are multiple errors that have overlapping ranges, only the first
one can be chosen to be active by the `go_to_diagnostic_impl`.

### Previous behavior:


https://github.com/zed-industries/zed/assets/71292737/95897675-f5ee-40e5-869f-0a40066eb8e3

Doesn't go through all the diagnostics, and going backwards and forwards
doesn't show the same diagnostic always.

### New behavior:


https://github.com/zed-industries/zed/assets/71292737/81f7945a-7ad8-4a34-b286-cc2799b10500

Should always go through the diagnostics in a consistent manner.

Release Notes:
* Improved the behavioral consistency of "Go to Next/Previous
Diagnostic"
2024-05-11 00:32:49 +02:00
Kyle Kelley
c71cfd5da4 Change ToolOutput to ToolView (#11682)
Additionally, the internal `ToolView` trait used by the registry is now
called `InternalToolView`.

This should make it a bit easier to understand that the `ToolView` is
intended for a `gpui::View` (implementing `Render`). It does still feel
like more could be merged here but I think the built tools are now a bit
clearer.

Release Notes:

- N/A
2024-05-10 15:22:09 -07:00
Conrad Irwin
5515ba6043 Extract http from util (#11680)
This avoids the CLI linking libssl etc...

Release Notes:

- N/A
2024-05-10 15:50:20 -06:00
Kirill Bulatov
df41435d1a Introduce DisplayRow, MultiBufferRow newtypes and BufferRow type alias (#11656)
Part of https://github.com/zed-industries/zed/issues/8081

To avoid confusion and bugs when converting between various row `u32`'s,
use different types for each.
Further PRs should split `Point` into buffer and multi buffer variants
and make the code more readable.

Release Notes:

- N/A

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-05-11 00:06:51 +03:00
Kyle Kelley
38f110852f Improve prompts for tools (#11669)
Improves the descriptions for some of the tools. I wish we had metrics
to back up changes in how the model responds to tool schema changes so
anecdotally I'm just going to say this _seems_ improved.

Release Notes:

- N/A
2024-05-10 13:18:05 -07:00
Marshall Bowers
fc584017d1 ruby: Add embedded_template grammar (#11677)
This PR adds the `embedded_template` grammar to the Ruby extension, as
we need it present for ERB.

Release Notes:

- N/A
2024-05-10 16:08:46 -04:00
Conrad Irwin
451727d257 Create release archive in the target dir (#11675)
Release Notes:

- N/A
2024-05-10 13:48:48 -06:00
Conrad Irwin
c73d6502d6 Make block_with_timeout more robust (#11670)
The previous implementation relied on a background thread to wake up the
main thread,
which was prone to priority inversion under heavy load.

In a synthetic test, where we spawn 200 git processes while doing a 5ms
timeout, the old version blocked for 5-80ms, the new version blocks for
5.1-5.4ms.

Release Notes:

- Improved responsiveness of the main thread under high system load
2024-05-10 13:10:02 -06:00
Marshall Bowers
b34ab6f3a1 Remove references to submodules (#11673)
This PR removes the references to initializing Git submodules as part of
building Zed.

These are no longer needed, as our only submodule was removed in #11672.

Release Notes:

- N/A
2024-05-10 14:33:53 -04:00
Marshall Bowers
c9738a233e Vendor LiveKit protocol (#11672)
This PR vendors the protobuf files from the LiveKit protocol so that we
don't need to have that entire LiveKit protocol repo as a submodule.

---

Eventually I would like to replace this with the
[`livekit-protocol`](https://crates.io/crates/livekit-protocol) crate,
but there is some churn that needs to happen for that.

The main problem is that we're currently on a different version of
`prost` used by `livekit-protocol`, and upgrading our version of `prost`
means that we now need to source `protoc` ourselves (since it is no
longer available to be compiled from source as part of `prost-build`).

Release Notes:

- N/A
2024-05-10 14:18:40 -04:00
Robert Falkén
80d3eafa30 Alternate files with ctrl-6 (#11367)
This is my stab at #7709 

I realize the code is flawed. There's no test coverage, I'm using
`clone()` and there are probably better ways to hook into the events.
Also, I didn't know what context to use for the keybinding. But maybe
with some pointers from someone who actually know what they're doing, I
can get this shippable.

Release Notes:

- vim: Added ctrl-6 for
[alternate-file](https://vimhelp.org/editing.txt.html#CTRL-%5E) to
navigate back and forth between two buffers.



https://github.com/zed-industries/zed/assets/261929/2d10494e-5668-4988-b7b4-417c922d6c61

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-10 11:40:08 -06:00
Marshall Bowers
0d26beb91b Add configurable low-speed timeout for OpenAI provider (#11668)
This PR adds a setting to allow configuring the low-speed timeout for
the Assistant when using the OpenAI provider.

The `low_speed_timeout_in_seconds` accepts a number of seconds that the
HTTP client can go below a minimum speed limit (currently set to 100
bytes/second) before it times out.

```json
{
  "assistant": {
    "version": "1",
    "provider": { "name": "openai", "low_speed_timeout_in_seconds": 60 }
  },
}
```

This should help the case where the `openai` provider is being used with
a local model that requires higher timeouts.

Issue: https://github.com/zed-industries/zed/issues/9913

Release Notes:

- Added a `low_speed_timeout_in_seconds` setting to the Assistant's
OpenAI provider
([#9913](https://github.com/zed-industries/zed/issues/9913)).
2024-05-10 13:19:21 -04:00
Marshall Bowers
19994fc190 ruby: Move injections to extension (#11664)
This PR moves the Ruby injections added in #8796 to the right location,
since Ruby support was extracted into an extension in #11360.

Release Notes:

- N/A
2024-05-10 12:06:15 -04:00
Ulysse Buonomo
4f256c7577 Add Ruby language injections (#8796)
This adds support for Ruby heredoc's syntax highlighting. The injection
was directly taken from the tree-sitter
[documentation](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection).

It is quite simple, but has the drawback of only showing highlighting
once the heredoc is fully written and next line is started. This is due
to the fact that we use the last line of the heredoc to determine the
language. As using the first one would require some cleaning up that we
cannot do trivially. (I might have not fully understood the behaviour of
the `#match?` predicate, which could help us)

Fixes #4473



Release Notes:

- Added Ruby language injections
([#4473](https://github.com/zed-industries/zed/issues/4473)).

<img width="359" alt="image"
src="https://github.com/zed-industries/zed/assets/11378424/5115b875-a32d-4f28-b21f-471495169266">
2024-05-10 09:00:51 -07:00
Vitaly Slobodin
400e938997 Extract Ruby extension (#11360)
This PR extracts Ruby and ERB support into an extension and removes the
built-in Ruby and Ruby support from Zed.

As part of this, the new extension is prepared for adding support for
the `Ruby LSP` which has some blockers. See
https://github.com/zed-industries/zed/pull/8613 I was thinking of adding
an initial support for Ruby LSP but I think it would be better to start
with extracting the Ruby extension for now.

The implementation, as the 1st step, matches the bundled version but
with 3 differences:

1. Added signature output to the completion popup. See my comment below.
![CleanShot 2024-05-04 at 09 17
37@2x](https://github.com/zed-industries/zed/assets/1894248/486b7a48-ea0c-44ce-b0c9-9f8f5d3ad42d)

3. Use the shell environment for starting the `solargraph` executable.
See my comment below.
4. Bumped the tree sitter version for Ruby to the latest available
version.

Additionally, I plan to tweak this extension a bit in the future but I
think we should do this bit by bit. Thanks!

Release Notes:

- Removed built-in support for Ruby, in favor of making it available as
an extension.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-10 11:53:11 -04:00
Piotr Osiewicz
df00854bbc gpui: Bump taffy to 0.4.3 again (#11655)
We reverted bump to taffy 0.4.3 following an issue spotted by
@maxdeviant where chat text input was not being rendered correctly:

![image](https://github.com/zed-industries/zed/assets/24362066/9d7e6444-47b1-4ac2-808f-bf10404377c0)
This was an issue with the previous attempt to upgrade to taffy 0.4.0 as
well. We bail early in `compute_auto_height_layout` due to a missing
width:
df190ea846/crates/editor/src/element.rs (L5266)
The same issue is visible in story for auto-height editor (or rather,
the breakage is visible - the editor simply does not render at all
there).

I tracked down the breakage to
https://github.com/DioxusLabs/taffy/pull/573 ; it looks like it
specifically affects editors with auto-height. In taffy <0.4 which we
were using previously, we'd eventually get a proper width for
auto-height EditorElement after having initially computed the size. With
taffy 0.4 however (and specifically that PR mentioned earlier), we're
getting `Size::NONE` in layout phase [^1].
I've noticed though that even with taffy <0.3, the
`known_dimensions.width` was always equal to `available_space.width` in
layout phase. Hence, I went with falling back to `available_space.width`
when it is a definite value and we don't have a
`known_dimensions.width`.
Done this way, both chat input and auto-height story render correctly.
/cc @as-cii 
Related:
https://github.com/zed-industries/zed/pull/11606
https://github.com/zed-industries/zed/pull/11622
https://github.com/zed-industries/zed/pull/7868
https://github.com/zed-industries/zed/pull/7896

[^1]: This could possibly be related to change in what gets passed in
https://github.com/DioxusLabs/taffy/pull/573/files#diff-60c916e9b0c507925f032cecdde6ae163e41b84b8e4bc0a6c04f7d846b0aad9eR133
, though I'm not sure if editor is a leaf node in this case

Release Notes:

- N/A
2024-05-10 15:05:50 +02:00
Thorsten Ball
df190ea846 vcs menu: Use project's repositories, do not open directly (#11652)
I ran into this when trying to get #11550 working: the VCS menu would
open repositories on its owned, based on paths, instead of going through
the worktree on which we already store the git repositories.



Release Notes:

- N/A
2024-05-10 11:06:32 +02:00
Piotr Osiewicz
b3dc31d7c9 tasks: Filter out run indicators outside of excerpt bounds instead of using saturating_sub (#11634)
This way we'll display run indicators around excerpt boundaries
correctly.

Release Notes:

- N/A
2024-05-10 10:45:28 +02:00
Antonio Scandurra
358bc2d225 Replace rich_text with markdown in assistant2 (#11650)
We don't implement copy yet but it should be pretty straightforward to
add.


https://github.com/zed-industries/zed/assets/482957/6b4d7c34-de6b-4b07-aed9-608c771bbbdb

/cc: @rgbkrk @maxbrunsfeld @maxdeviant 

Release Notes:

- N/A
2024-05-10 10:22:14 +02:00
Conrad Irwin
0d760d8d19 Clarify key binding documentation (#11644)
Fixes #10762

Release Notes:

- N/A
2024-05-09 22:42:09 -06:00
Conrad Irwin
45f12b9426 vim cl (#11641)
Release Notes:

- vim: Added support for the changelist. `g;` and `g,` to the
previous/next change
- vim: Added support for the `'.` mark
- vim: Added support for `gi` to resume the previous insert
2024-05-09 21:18:56 -06:00
Conrad Irwin
4f9ba28a25 linux cli (#11585)
- [x] Build out cli on linux
- [x] Add support for --dev-server-token sent by the CLI
- [x] Package cli into the .tar.gz
- [x] Link the cli to ~/.local/bin in install.sh

Release Notes:

- linux: Add cli support for managing zed
2024-05-09 21:08:49 -06:00
Conrad Irwin
0c2d71f1ac Remove 'Destructive' prompts (#11631)
While these would match how macOS handles this scenario, they crash on
Catalina, and require mouse clicks to interact.

cc @bennetbo



Release Notes:

- N/A
2024-05-09 18:52:09 -06:00
Zachiah Sawyer
901cb8b3d2 vim: Add basic mark support (#11507)
Release Notes:
- vim: Added support for buffer-local marks (`'a-'z`) and some builtin
marks `'<`,`'>`,`'[`,`']`, `'{`, `'}` and `^`. Global marks (`'A-'Z`),
and other builtin marks (`'0-'9`, `'(`, `')`, `''`, `'.`, `'"`) are not
yet implemented. (#5122)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-09 18:51:19 -06:00
Kyle Kelley
9cef0ac869 Cleanup tool registry API surface (#11637)
Fast followups to #11629 

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-09 16:43:27 -07:00
Kirill Bulatov
79b5556267 Remove a stray eprintln (#11635)
Release Notes:

- N/A
2024-05-10 02:27:55 +03:00
Marshall Bowers
dd67bda595 Update .mailmap (#11633)
This PR updates the `.mailmap` file to merge some commit authors using
multiple emails.

Release Notes:

- N/A
2024-05-09 19:03:34 -04:00
Kirill Bulatov
4762e52d31 Properly calculate expanded git diff hunk highlight ranges (#11632)
Closes https://github.com/zed-industries/zed/issues/11576

Release Notes:

- Fixed expanded diff hunks highlighting an extra row as added
([11576](https://github.com/zed-industries/zed/issues/11576))
2024-05-10 02:02:56 +03:00
Kyle Kelley
50c45c7897 Streaming tools (#11629)
Stream characters in for tool calls to allow rendering partial input.


https://github.com/zed-industries/zed/assets/836375/0f023a4b-9c46-4449-ae69-8b6bcab41673

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-05-09 15:57:14 -07:00
Marshall Bowers
27ed0f4273 assistant2: Render saved conversations inline instead of in a modal (#11630)
This PR reworks how saved conversations are rendered in the new
assistant panel.

Instead of rendering them in a modal we now display them in the panel
itself:

<img width="402" alt="Screenshot 2024-05-09 at 6 18 40 PM"
src="https://github.com/zed-industries/zed/assets/1486634/82decc04-cb31-4d83-a942-7e8426e02679">

Release Notes:

- N/A
2024-05-09 18:29:08 -04:00
Conrad Irwin
a3e75540af Reduce serializability of project delete (#11628)
This may reduce locks when deleting projects.

Release Notes:

- N/A
2024-05-09 16:17:13 -06:00
Conrad Irwin
aa5113cd92 vim: Support paste with count (#11621)
Fixes: #10842



Release Notes:

- vim: Fix pasting with a count (#10842)
2024-05-09 16:12:59 -06:00
Kirill Bulatov
bca639bda9 Use larger runners for Linux CI steps (#11574)
To speed up Linux CI builds, use a set of self-hosted Linux machines and
use them to run all slow CI steps for Linux: "tests", bundling and
nightly builds.

Also adds a set of dev icons as Linux bundling script required them for
`run-bundling`-tagged builds from regular PRs.
Same icons as for Preview were used, but, ideally, something different
could be created.

Release Notes:

- N/A
2024-05-10 00:44:31 +03:00
Piotr Osiewicz
bff1d8b142 task: Allow obtaining custom task variables from tree-sitter queries (#11624)
From now on, only top-level captures are treated as runnable tags and
the rest is appended to task context as custom environmental variables
(unless the name is prefixed with _, in which case the capture is
ignored). This is most likely gonna help with Pest-like test runners.



Release Notes:

- N/A

---------

Co-authored-by: Remco <djsmits12@gmail.com>
2024-05-09 23:38:18 +02:00
张小白
95e246ac1c windows: Better dispatcher (#11485)
This PR leverages a more modern Windows API to implement
`WindowsDispatcher`, aligning its implementation more closely with that
of the `macOS` platform. The following improvements have been made:

1. Similar to `macOS`, there is no longer a need to use `sender` and
`receiver` to dispatch a `Runnable` on the main thread.
2. There is also no longer a need to use an `Event` for synchronization.
3. Consistent with #7506 and #11269, `Runnable` is now executed with
high priority.

However, this PR raises the minimum Windows version requirement of
`GPUI` to Windows 10, specifically Windows 10 Fall Creators Update
(10.0.16299). However, the `alacritty_terminal` dependency in Zed relies
on `conPTY` on Windows, an API introduced in the Windows 10 Fall
Creators Update. Therefore, the impact of this PR on Zed should be
minimal. I'd like to hear your voices about this PR, especially about
the minimum Windows version bumping.

Release Notes:

- N/A
2024-05-09 14:24:57 -07:00
Andrew Lygin
ba25e371be Fix scrollbar markers for folded code (#11625)
There're two errors in scrollbar markers in the presence of folded code:

1. Some markers are not displayed (when the marked row numbers are
greater than the total displayed rows count after folding).
2. Code folding / unfolding doesn't trigger markers repainting.

This PR fixes both problems.

Release Notes:

- Fixed scrollbar markers for folded code.

The second problem (markers are repainted after I move the cursor, not
after folding):


https://github.com/zed-industries/zed/assets/2101250/57ed563d-186d-4497-98ab-d4f946416726
2024-05-09 14:23:21 -07:00
Marshall Bowers
c73ef1a5f3 assistant2: List saved conversations from disk (#11627)
This PR updates the saved conversation picker to use a list of
conversations retrieved from disk instead of the static placeholder
values.

Release Notes:

- N/A
2024-05-09 16:17:07 -04:00
Conrad Irwin
8b5a0cff10 vim: Fix e/E with inlay hints (#11616)
Co-Authored-By: Sergey <sergey.b@hey.com>
Fixes: #7046

Release Notes:

- vim: Fixes e/E with inlay hints (#7046)

Co-authored-by: Sergey <sergey.b@hey.com>
2024-05-09 13:45:45 -06:00
Piotr Osiewicz
f0af508ae5 Revert "chore: Bump taffy version to 0.4.3" (#11622)
Reverts zed-industries/zed#11606
2024-05-09 19:11:37 +02:00
Marshall Bowers
5fe4070501 docs: Fix copying code blocks (#11617)
This PR fixes copying code blocks in the docs.

The problem was that some of the elements we removed from the base
mdBook template were causing errors in the script, which prevented the
right event listeners from being registered for the copy button.

To remedy this, the elements have been restored, but are using `display:
none` so that they don't appear in the UI.

Resolves #11592.

Release Notes:

- N/A
2024-05-09 11:52:26 -04:00
Marshall Bowers
981a143e9b copilot: Update root path on Windows (#11613)
This PR updates the root path used by Copilot to be a validate path when
running on Windows.

Release Notes:

- N/A

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2024-05-09 10:14:29 -04:00
Jason Lee
5e06ce4df3 Add zip extract support for Windows (#11156)
Release Notes:

- [x] Fixed install Node.js runtime and NPM lsp installation on Windows.
- [x] Update Node runtime command to execute on Windows with no window
popup.

Ref #9619, #9424

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-09 09:23:21 -04:00
Piotr Osiewicz
3bd53d0441 chore: Bump taffy version to 0.4.3 (#11606)
Taffy 0.4 has been released 2 months ago. We've been using an older
commit from their 0.4 development branch since November.
Compared to the commit we were pinned to, the following relevant changes
have been made:
-
563d5dcee7
-
64f8aa0fb1
-
70b35712a2

![image](https://github.com/zed-industries/zed/assets/24362066/ffdfae03-2743-496f-bb21-7aa38462178f)

Release Notes:

- N/A
2024-05-09 12:51:53 +02:00
Piotr Osiewicz
76535578e9 Task indicators in multibuffers (#11603)
Following #11487 the task indicators would no longer show up in
multibuffers.
Release Notes:

- N/A
2024-05-09 12:22:33 +02:00
Piotr Osiewicz
fdcedf15b7 editor: Do not show test indicators if a line is folded (#11599)
Originally reported by @RemcoSmitsDev



Release Notes:

- N/A
2024-05-09 11:43:50 +02:00
Piotr Osiewicz
bd6d385817 gpui: Pass Style by value to request_layout (#11597)
A minor thing I've spotted and decided to fix on the spot.
It was being cloned twice within the body of that function (one of which
was redundant even without this PR); now in most cases we go down from 2
clones to 0.
Release Notes:

- N/A
2024-05-09 11:38:53 +02:00
Antonio Scandurra
5df1481297 Introduce a new markdown crate (#11556)
This pull request introduces a new `markdown` crate which is capable of
parsing and rendering a Markdown source. One of the key additions is
that it enables text selection within a `Markdown` view. Eventually,
this will replace `RichText` but for now the goal is to use it in the
assistant revamped assistant in the spirit of making progress.

<img width="711" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/b56c777b-e57c-42f9-95c1-3ada22f63a69">

Note that this pull request doesn't yet use the new markdown renderer in
`assistant2`. This is because we need to modify the assistant before
slotting in the new renderer and I wanted to merge this independently of
those changes.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Alp <akeles@umd.edu>
Co-authored-by: Zachiah Sawyer <zachiah@proton.me>
2024-05-09 11:03:33 +02:00
Doy Bachtiar
ddaaaee973 docs: Fix a typo (#11588)
This PR fixes a typo in docs/src/development/debugging-crashes.md.

Release Notes:

- N/A
2024-05-08 20:16:02 -07:00
张小白
9772b7ac33 windows: Fix Zed freezing when resuming from sleep (#11589)
It seems that on the first frame after the system resumes from sleep,
`dcomp_vsync_fn` mistakenly detects the `timer_stop_event` triggering
and exits the loop.

Release Notes:

- N/A
2024-05-08 20:15:32 -07:00
CharlesChen0823
2e0811e113 windows: Improve platform clipboard (#11553)
I thought platform clipboard should share one ctx. and fixed in vim
mode, read from clipboard crash when using `unwrap`.

Release Notes:

- N/A
2024-05-08 16:09:13 -07:00
张小白
1b292d2fb3 Fix crash when the length of a line is greater than 1024 chars (#11536)
Close #11518 

Release Notes:

- N/A
2024-05-08 16:08:39 -07:00
Marshall Bowers
adecbd1815 Make Markdown default to "format_on_save": "off" (#11584)
This PR changes the Markdown language defaults to set `format_on_save`
to be `off`.

Prettier's Markdown formatting is a bit controversial for some people,
so we turn it off by default.

To restore the previous behavior, add the following to your settings:

```json
{
  "languages": {
    "Markdown": {
      "format_on_save": "on"
    }
  }
}
```


Release Notes:

- Changed the default `format_on_save` behavior for Markdown files to be
`off`.
2024-05-08 18:44:21 -04:00
Max Brunsfeld
a7aa2578e1 Implement serialization of assistant conversations, including tool calls and attachments (#11577)
Release Notes:

- N/A

---------

Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
2024-05-08 17:52:15 -04:00
Conrad Irwin
24ffa0fcf3 Don't panic on failure to allocate an AtlasTile (#11579)
Release Notes:

- Fixed a panic in graphics allocation
2024-05-08 15:47:15 -06:00
Conrad Irwin
b0494d1c05 Pass hover position as an anchor (#11578)
It's too easily to accidentally pass a point from one snapshot into
another

Release Notes:

- Fixed a panic in show hover
2024-05-08 15:39:37 -06:00
Dzmitry Malyshau
a89dc8c42e blade: Switch to linear color space (#11534)
Release Notes:

- N/A

## What

Addresses a long-standing issue of doing the blending operations in sRGB
space. Currently, the input HSL colors are provided in sRGB space and
converted to linear in the vertex shader. Conversion back to sRGB, which
is required on most platforms today, happens at the very end of the
pipeline when writing into sRGB render target.

Note-1: in the future we may consider doing HSL -> sRGB -> Linear
transform on CPU before feeding into shaders. However, I don't expect
any significant difference here given that we are likely bound by fill
rate and pixel shaders, anyway.

Note-2: the graphics stack is programmed to detect if the platform
supports presenting in linear color space and avoids converting to sRGB
at the end in this case. However, on my Z13 laptop this isn't supported
by the RADV driver.

Closes #7684 
Closes #11462
@jansol please confirm if you can!

## Comparison

Screenshot of the Glazier theme before the change:

![glazier-old](https://github.com/zed-industries/zed/assets/107301/6a9552e1-0819-4a4e-8121-8d62ec012bf4)
Same theme after the change:

![glazier-new](https://github.com/zed-industries/zed/assets/107301/4e61c422-4a4b-4c4b-84a3-55680626d681)
2024-05-08 12:47:29 -07:00
Nate Butler
d103903229 Style header for assistant2 (#11570)
Release Notes:

- N/A
2024-05-08 14:17:07 -04:00
CharlesChen0823
ec3aabe2c2 windows: Fix crash when saving files to disk (#11547)
closes #11544, sorry for introduce this issue by pre pr.
Release Notes:

- N/A
2024-05-08 11:12:07 -07:00
张小白
4b98c35d68 windows: Let IME early return in vim mode (#11551)
This PR follows up #11387 , slightly changes the IME window behavior to
match macOS implementation.

Release Notes:

- N/A
2024-05-08 11:01:48 -07:00
张小白
5103995c32 windows: Fix incorrect font rendering (#11545)
Previously, `DirectWrite` had been following the text system
implementation on `macOS`, using the font's postscript name to
differentiate between different font faces. However, I noticed
occasional rendering issues, such as fonts incorrectly rendering as
italics. Later, I discovered that on `Windows`, the postscript name is
**not** unique. Surprisingly, even the same font can have different
postscript names on macOS and Windows! It's hard to believe! The
postscript name of a font face should be obtained from the same font
table. Why would the same font face have different names?

For example, the postscript name of ZedMono on `macOS` is
`Zed-Mono-Bold-Extended-Italic`, while on `Windows`, it is
`Zed-Mono-Extended`, missing weight and style information, leading to
incorrect rendering.

This PR introduces a `FontIdentifier` struct to uniquely identify font
faces.

Release Notes:

- N/A
2024-05-08 10:58:31 -07:00
张小白
fb4c6dbaa7 windows: Implement ResizeColumn and ResizeRow cursor style (#11533)
This PR follows up #11406

Release Notes:

- N/A
2024-05-08 10:57:09 -07:00
LoganDark
91c1716858 Fix horizontal scrolling direction on Windows (#11520)
As per Microsoft documentation, positive values scroll right, not left.
GPUI was incorrectly assuming it perfectly mirrored vertical scrolling.

Fixes #11515

Release Notes:

- N/A
2024-05-08 10:56:31 -07:00
Andrew Lygin
0933426e63 Editor tab bar settings (#7356)
This PR is another step to tabless editing (#6424, #4963). It adds
support for tab bar settings that allow the user to change its placement
or to hide completely.

Configuraton:

```json
"tab_bar": {
  "show": true
}
```

Placemnet options are "top", "bottom" and "no".

This PR intentionally doesn't affect tab bars of other panes (Terminal
for instance) to keep code changes small. I guess we'll do the rest in
separate PRs.

Release Notes:

- Added support for configuring the editor tab bar (part of #6424,
#4963).

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-08 10:54:48 -07:00
Kyle Kelley
689e4aef2f Render messages as early as possible to show progress (#11569)
This shows "Researching..." as placeholder text as early as possible so
that the user can see the model is working on reading/researching/etc.

This also adds on an `Option<Value>` to the `render_running` function so
that tools can hopefully render based on partially completed JSON (still
to come).

Release Notes:

- N/A
2024-05-08 10:24:51 -07:00
Thorsten Ball
dbebb40956 linux: Store binary path before restart to handle deleted binary file (#11568)
This fixes restart after updates not working on Linux.

On Linux we can't reliably get the binary path after an update, because
the original binary was deleted and the path will contain ` (deleted)`.

See: https://github.com/rust-lang/rust/issues/69343

We *could* strip ` (deleted)` off, but that feels nasty. So instead we
save the original binary path, before we do the installation, then
restart.

Later on, we can also change this to be a _new_ binary path returned by
the installers, which we then have to start.


Release Notes:

- N/A
2024-05-08 19:13:28 +02:00
Max Brunsfeld
d2cec0221b Run windows CI on our own GH-hosted windows runner (#11567)
It's a 16-core runner.

Release Notes:

- N/A
2024-05-08 10:09:43 -07:00
Joseph T. Lyons
724acaab61 v0.136.x dev 2024-05-08 12:05:45 -04:00
551 changed files with 29115 additions and 16920 deletions

15
.cloudflare/README.md Normal file
View File

@@ -0,0 +1,15 @@
We have two cloudflare workers that let us serve some assets of this repo
from Cloudflare.
* `open-source-website-assets` is used for `install.sh`
* `docs-proxy` is used for `https://zed.dev/docs`
On push to `main`, both of these (and the files they depend on) are uploaded to Cloudflare.
### Deployment
These functions are deployed on push to main by the deploy_cloudflare.yml workflow. Worker Rules in Cloudflare intercept requests to zed.dev and proxy them to the appropriate workers.
### Testing
You can use [wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update) to test these workers locally, or to deploy custom versions.

View File

@@ -0,0 +1,14 @@
export default {
async fetch(request, _env, _ctx) {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
let res = await fetch(url, request);
if (res.status === 404) {
res = await fetch("https://zed.dev/404");
}
return res;
},
};

View File

@@ -0,0 +1,8 @@
name = "docs-proxy"
main = "src/worker.js"
compatibility_date = "2024-05-03"
workers_dev = true
[[routes]]
pattern = "zed.dev/docs*"
zone_name = "zed.dev"

View File

@@ -0,0 +1,19 @@
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
const object = await env.OPEN_SOURCE_WEBSITE_ASSETS_BUCKET.get(key);
if (!object) {
return await fetch("https://zed.dev/404");
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("etag", object.httpEtag);
return new Response(object.body, {
headers,
});
},
};

View File

@@ -0,0 +1,8 @@
name = "open-source-website-assets"
main = "src/worker.js"
compatibility_date = "2024-05-15"
workers_dev = true
[[r2_buckets]]
binding = 'OPEN_SOURCE_WEBSITE_ASSETS_BUCKET'
bucket_name = 'zed-open-source-website-assets'

View File

@@ -32,7 +32,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Remove untracked files
@@ -87,7 +86,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: cargo clippy
run: cargo xtask clippy
@@ -104,22 +102,17 @@ jobs:
# todo(linux): Actually run the tests
linux_tests:
name: (Linux) Run Clippy and tests
runs-on: ubuntu-latest
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: cargo clippy
run: cargo xtask clippy
@@ -130,13 +123,12 @@ jobs:
# todo(windows): Actually run the tests
windows_tests:
name: (Windows) Run Clippy and tests
runs-on: windows-latest
runs-on: hosted-windows-1
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
@@ -179,7 +171,6 @@ jobs:
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
submodules: "recursive"
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
@@ -262,26 +253,24 @@ jobs:
bundle-linux:
name: Create a Linux bundle
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
runs-on:
- self-hosted
- deploy
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}

56
.github/workflows/deploy_cloudflare.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy-docs:
name: Deploy Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.37"
- name: Build book
run: |
set -euo pipefail
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js

View File

@@ -21,7 +21,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
@@ -41,7 +40,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Install cargo nextest
@@ -76,7 +74,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Build docker image
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA

View File

@@ -1,35 +0,0 @@
name: Deploy Docs
on:
push:
branches:
- main
jobs:
deploy-docs:
name: Deploy Docs
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.37"
- name: Build book
run: |
set -euo pipefail
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs

View File

@@ -19,7 +19,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2

View File

@@ -31,7 +31,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Run randomized tests
run: script/randomized-test-ci

View File

@@ -25,7 +25,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
@@ -45,7 +44,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Run tests
uses: ./.github/actions/run_tests
@@ -75,7 +73,6 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Set release channel to nightly
run: |
@@ -96,7 +93,9 @@ jobs:
bundle-deb:
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
runs-on:
- self-hosted
- deploy
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
@@ -107,16 +106,9 @@ jobs:
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Set release channel to nightly
run: |

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ DerivedData/
.venv
.blob_store
.vscode
.wrangler

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "crates/live_kit_server/protocol"]
path = crates/live_kit_server/protocol
url = https://github.com/livekit/protocol

View File

@@ -15,8 +15,12 @@ Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ivan Žužak <izuzak@gmail.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com>
@@ -29,6 +33,9 @@ Kirill Bulatov <kirill@zed.dev>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marshall Bowers <elliott.codes@gmail.com>
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
Max Brunsfeld <maxbrunsfeld@gmail.com>
@@ -41,6 +48,8 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Nathan Sobo <nathan@zed.dev>
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Petros Amoiridis <petros@hey.com>
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Robert Clover <git@clo4.net>

View File

@@ -3,10 +3,5 @@
"label": "clippy",
"command": "cargo",
"args": ["xtask", "clippy"]
},
{
"label": "assistant2",
"command": "cargo",
"args": ["run", "-p", "assistant2", "--example", "assistant_example"]
}
]

749
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_tooling",
"crates/assistant2",
"crates/assistant_tooling",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -41,6 +41,7 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/http",
"crates/image_viewer",
"crates/inline_completion_button",
"crates/install_cli",
@@ -52,6 +53,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
@@ -126,6 +128,7 @@ members = [
"extensions/php",
"extensions/prisma",
"extensions/purescript",
"extensions/ruby",
"extensions/svelte",
"extensions/terraform",
"extensions/toml",
@@ -181,6 +184,7 @@ google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
http = { path = "crates/http" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
inline_completion_button = { path = "crates/inline_completion_button" }
@@ -192,6 +196,7 @@ languages = { path = "crates/languages" }
live_kit_client = { path = "crates/live_kit_client" }
live_kit_server = { path = "crates/live_kit_server" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
@@ -225,7 +230,7 @@ snippet = { path = "crates/snippet" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api"}
supermaven_api = { path = "crates/supermaven_api" }
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
sum_tree = { path = "crates/sum_tree" }
@@ -255,20 +260,24 @@ async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
ctor = "0.2.6"
ctrlc = "3.4.4"
signal-hook = "0.3.17"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
derive_more = "0.99.17"
emojis = "0.6.1"
env_logger = "0.9"
exec = "0.3.1"
fork = "0.1.23"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
@@ -287,10 +296,12 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
nanoid = "0.4"
nix = "0.28"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -304,8 +315,9 @@ pulldown-cmark = { version = "0.10.0", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.0", features = ["include-exclude"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = "0.8"
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -318,6 +330,7 @@ serde_json_lenient = { version = "0.1", features = [
serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -325,7 +338,7 @@ subtle = "2.5.0"
sysinfo = "0.30.7"
tempfile = "3.9.0"
thiserror = "1.0.29"
tiktoken-rs = "0.5.7"
tiktoken-rs = "0.5.9"
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -343,7 +356,7 @@ tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "b82ab803d887002a0af11f6ce63d72884580bf33" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
rustc-demangle = "0.1.23"
@@ -363,7 +376,7 @@ unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5"] }
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [
@@ -379,20 +392,22 @@ wit-component = "0.201"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.53.0"
version = "0.56.0"
features = [
"implement",
"Foundation_Numerics",
"System",
"System_Threading",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Media",
"Win32_Security",
"Win32_Security_Credentials",
"Win32_Storage_FileSystem",
@@ -406,6 +421,7 @@ features = [
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Time",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_Ime",

View File

@@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.15 7.49998C13.15 4.66458 10.9402 1.84998 7.50002 1.84998C4.7217 1.84998 3.34851 3.90636 2.76336 4.99997H4.5C4.77614 4.99997 5 5.22383 5 5.49997C5 5.77611 4.77614 5.99997 4.5 5.99997H1.5C1.22386 5.99997 1 5.77611 1 5.49997V2.49997C1 2.22383 1.22386 1.99997 1.5 1.99997C1.77614 1.99997 2 2.22383 2 2.49997V4.31318C2.70453 3.07126 4.33406 0.849976 7.50002 0.849976C11.5628 0.849976 14.15 4.18537 14.15 7.49998C14.15 10.8146 11.5628 14.15 7.50002 14.15C5.55618 14.15 3.93778 13.3808 2.78548 12.2084C2.16852 11.5806 1.68668 10.839 1.35816 10.0407C1.25306 9.78536 1.37488 9.49315 1.63024 9.38806C1.8856 9.28296 2.17781 9.40478 2.2829 9.66014C2.56374 10.3425 2.97495 10.9745 3.4987 11.5074C4.47052 12.4963 5.83496 13.15 7.50002 13.15C10.9402 13.15 13.15 10.3354 13.15 7.49998ZM7 10V5.00001H8V10H7Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 974 B

View File

@@ -57,7 +57,9 @@
"gitkeep": "vcs",
"gitmodules": "vcs",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
"graphqls": "graphql",
"h": "c",
"hpp": "cpp",
"handlebars": "code",

View File

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 6C1.5 6.89002 1.76392 7.76004 2.25839 8.50007C2.75285 9.24009 3.45566 9.81686 4.27792 10.1575C5.10019 10.4981 6.00499 10.5872 6.87791 10.4135C7.75082 10.2399 8.55264 9.81132 9.18198 9.18198C9.81132 8.55264 10.2399 7.75082 10.4135 6.87791C10.5872 6.00499 10.4981 5.10019 10.1575 4.27792C9.81686 3.45566 9.24009 2.75285 8.50007 2.25839C7.76004 1.76392 6.89002 1.5 6 1.5C4.74198 1.50473 3.53448 1.99561 2.63 2.87L1.5 4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 1.5V4H4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 3.5V6L8 7" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 778 B

1
assets/icons/library.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-library"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -53,7 +53,9 @@
// "alt-d": "editor::DeleteToNextWordEnd",
"ctrl-x": "editor::Cut",
"ctrl-c": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-v": "editor::Paste",
"shift-insert": "editor::Paste",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -189,6 +191,12 @@
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
"context": "Markdown",
"bindings": {
"ctrl-c": "markdown::Copy"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -493,6 +501,12 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -546,7 +560,9 @@
"alt-ctrl-n": "project_panel::NewDirectory",
"ctrl-x": "project_panel::Cut",
"ctrl-c": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-v": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-alt-c": "project_panel::CopyPath",
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
@@ -608,7 +624,9 @@
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"shift-ctrl-c": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"shift-ctrl-v": "terminal::Paste",
"shift-insert": "terminal::Paste",
"up": ["terminal::SendKeystroke", "up"],
"pageup": ["terminal::SendKeystroke", "pageup"],
"down": ["terminal::SendKeystroke", "down"],

View File

@@ -19,9 +19,6 @@
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"shift-enter": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-o": "workspace::Open",
@@ -211,11 +208,9 @@
}
},
{
"context": "AssistantChat > Editor", // Used in the assistant2 crate
"context": "Markdown",
"bindings": {
"enter": ["assistant2::Submit", "Simple"],
"cmd-enter": ["assistant2::Submit", "Codebase"],
"escape": "assistant2::Cancel"
"cmd-c": "markdown::Copy"
}
},
{
@@ -520,6 +515,12 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -576,7 +577,6 @@
"cmd-v": "project_panel::Paste",
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
@@ -630,6 +630,14 @@
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}
},
{
"context": "Picker",
"bindings": {
"alt-e": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
}
},
{
"context": "Terminal",
"bindings": {

View File

@@ -1,4 +1,10 @@
[
{
"context": "ProjectPanel || Editor",
"bindings": {
"ctrl-6": "pane::AlternateFile"
}
},
{
"context": "Editor && VimControl && !VimWaiting && !menu",
"bindings": {
@@ -117,6 +123,9 @@
}
}
],
"m": ["vim::PushOperator", "Mark"],
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
@@ -237,6 +246,9 @@
],
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g i": ["workspace::SendKeystrokes", "` ^ i"],
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
@@ -481,8 +493,8 @@
"shift-o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDelete",
"shift-x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"y": "vim::VisualYank",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",

View File

@@ -1,5 +1,18 @@
{
// The name of the Zed theme to use for the UI
// The name of the Zed theme to use for the UI.
//
// The theme can also be set to follow system preferences:
//
// "theme": {
// "mode": "system",
// "light": "One Light",
// "dark": "One Dark"
// }
//
// Where `mode` is one of:
// - "system": Use the theme that corresponds to the system's appearance
// - "light": Use the theme indicated by the "light" field
// - "dark": Use the theme indicated by the "dark" field
"theme": "One Dark",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
@@ -71,8 +84,28 @@
"restore_on_startup": "last_workspace",
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
// May take 3 values:
// 1. Use the current platform's convention
// "when_closing_with_no_tabs": "platform_default"
// 2. Always close the window:
// "when_closing_with_no_tabs": "close_window",
// 3. Never close the window
// "when_closing_with_no_tabs": "keep_window_open",
"when_closing_with_no_tabs": "platform_default",
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// How to highlight the current line in the editor.
//
// 1. Don't highlight the current line:
// "none"
// 2. Highlight the gutter area:
// "gutter"
// 3. Highlight the editor area:
// "line"
// 4. Highlight the full line (default):
// "all"
"current_line_highlight": "all",
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -283,13 +316,14 @@
// AI provider.
"provider": {
"name": "openai",
// The default model to use when starting new conversations. This
// The default model to use when creating new contexts. This
// setting can take three values:
//
// 1. "gpt-3.5-turbo"
// 2. "gpt-4"
// 3. "gpt-4-turbo-preview"
"default_model": "gpt-4-turbo-preview"
// 4. "gpt-4o"
"default_model": "gpt-4o"
}
},
// Whether the screen sharing icon is shown in the os status bar.
@@ -299,9 +333,7 @@
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
"language_servers": [
"..."
],
"language_servers": ["..."],
// When to automatically save edited buffers. This setting can
// take four values.
//
@@ -316,6 +348,8 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor
"show": true,
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},
@@ -347,6 +381,8 @@
// when saving it.
"ensure_final_newline_on_save": true,
// Whether or not to perform a buffer format before saving
//
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
"format_on_save": "on",
// How to perform a buffer format. This setting can take 4 values:
//
@@ -432,9 +468,7 @@
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [
".env"
]
"disabled_globs": [".env"]
},
// Settings specific to journaling
"journal": {
@@ -463,7 +497,7 @@
// }
// }
"shell": "system",
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
// Where to dock terminals panel. Can be `left`, `right`, `bottom`.
"dock": "bottom",
// Default width when the terminal is docked to the left or right.
"default_width": 640,
@@ -545,13 +579,8 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh', 'fish', and `nushell`
"directories": [".env", "env", ".venv", "venv"],
// Can also be `csh`, `fish`, and `nushell`
"activate_script": "default"
}
},
@@ -576,7 +605,7 @@
// use those languages.
//
// For example, to treat files like `foo.notjs` as JavaScript,
// and 'Embargo.lock' as TOML:
// and `Embargo.lock` as TOML:
//
// {
// "JavaScript": ["notjs"],
@@ -593,19 +622,30 @@
},
// Different settings for specific languages.
"languages": {
"C++": {
"format_on_save": "off"
"Astro": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-astro"]
}
},
"Blade": {
"prettier": {
"allowed": true
}
},
"C": {
"format_on_save": "off"
},
"C++": {
"format_on_save": "off"
},
"CSS": {
"prettier": {
"allowed": true
}
},
"Elixir": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
@@ -615,26 +655,120 @@
"source.organizeImports": true
}
},
"GraphQL": {
"prettier": {
"allowed": true
}
},
"HEEX": {
"language_servers": [
"elixir-ls",
"!next-ls",
"!lexical",
"..."
]
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
"allowed": true
}
},
"Java": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-java"]
}
},
"JavaScript": {
"prettier": {
"allowed": true
}
},
"JSON": {
"prettier": {
"allowed": true
}
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off",
"prettier": {
"allowed": true
}
},
"PHP": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"]
}
},
"Prisma": {
"tab_size": 2
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "..."]
},
"SCSS": {
"prettier": {
"allowed": true
}
},
"SQL": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-sql"]
}
},
"Svelte": {
"prettier": {
"allowed": true,
"plugins": ["prettier-plugin-svelte"]
}
},
"TSX": {
"prettier": {
"allowed": true
}
},
"Twig": {
"prettier": {
"allowed": true
}
},
"TypeScript": {
"prettier": {
"allowed": true
}
},
"Vue.js": {
"prettier": {
"allowed": true
}
},
"XML": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-xml"]
}
},
"YAML": {
"prettier": {
"allowed": true
}
}
},
// Zed's Prettier integration settings.
// If Prettier is enabled, Zed will use this for its Prettier instance for any applicable file, if
// project has no other Prettier installed.
// Allows to enable/disable formatting with Prettier
// and configure default Prettier, used when no project-level Prettier installation is found.
"prettier": {
// Use regular Prettier json configuration:
// // Whether to consider prettier formatter or not when attempting to format a file.
// "allowed": false,
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "plugins": [],
//
// // Use regular Prettier json configuration.
// // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
// // the project has no other Prettier installed.
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
@@ -692,5 +826,17 @@
// - `short`: "2 s, 15 l, 32 c"
// - `long`: "2 selections, 15 lines, 32 characters"
// Default: long
"line_indicator_format": "long"
"line_indicator_format": "long",
// Set a proxy to use. The proxy protocol is specified by the URI scheme.
//
// Supported URI scheme: `http`, `https`, `socks4`, `socks4a`, `socks5`,
// `socks5h`. `http` will be used when no scheme is specified.
//
// By default no proxy will be used, or Zed will try get proxy settings from
// environment variables.
//
// Examples:
// - "proxy": "socks5://localhost:10808"
// - "proxy": "http://127.0.0.1:10809"
"proxy": null
}

View File

@@ -281,11 +281,14 @@ impl ActivityIndicator {
message: "Installing Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Updated => Content {
AutoUpdateStatus::Updated { binary_path } => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
on_click: Some(Arc::new({
let restart = workspace::Restart {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::restart(&restart, cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -5,6 +5,10 @@ edition = "2021"
publish = false
license = "AGPL-3.0-or-later"
[features]
default = []
schemars = ["dep:schemars"]
[lints]
workspace = true
@@ -14,9 +18,11 @@ path = "src/anthropic.rs"
[dependencies]
anyhow.workspace = true
futures.workspace = true
http.workspace = true
isahc.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
util.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -1,17 +1,21 @@
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use isahc::config::Configurable;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, sync::Arc};
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use std::{convert::TryFrom, time::Duration};
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)]
pub enum Model {
#[default]
#[serde(rename = "claude-3-opus-20240229")]
#[serde(alias = "claude-3-opus", rename = "claude-3-opus-20240229")]
Claude3Opus,
#[serde(rename = "claude-3-sonnet-20240229")]
#[serde(alias = "claude-3-sonnet", rename = "claude-3-sonnet-20240229")]
Claude3Sonnet,
#[serde(rename = "claude-3-haiku-20240307")]
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
Claude3Haiku,
}
@@ -28,6 +32,14 @@ impl Model {
}
}
pub fn id(&self) -> &'static str {
match self {
Model::Claude3Opus => "claude-3-opus-20240229",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
Model::Claude3Haiku => "claude-3-opus-20240307",
}
}
pub fn display_name(&self) -> &'static str {
match self {
Self::Claude3Opus => "Claude 3 Opus",
@@ -141,20 +153,24 @@ pub enum TextDelta {
}
pub async fn stream_completion(
client: Arc<dyn HttpClient>,
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let uri = format!("{api_url}/v1/messages");
let request = HttpRequest::builder()
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", "messages-2023-12-15")
.header("Anthropic-Beta", "tools-2024-04-04")
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json")
.body(AsyncBody::from(serde_json::to_string(&request)?))?;
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.low_speed_timeout(100, low_speed_timeout);
}
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let reader = BufReader::new(response.into_body());
@@ -196,7 +212,7 @@ pub async fn stream_completion(
// #[cfg(test)]
// mod tests {
// use super::*;
// use util::http::IsahcHttpClient;
// use http::IsahcHttpClient;
// #[tokio::test]
// async fn stream_completion_success() {

View File

@@ -11,6 +11,8 @@ doctest = false
[dependencies]
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
collections.workspace = true
@@ -19,7 +21,9 @@ editor.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
http.workspace = true
indoc.workspace = true
language.workspace = true
log.workspace = true
@@ -30,19 +34,24 @@ ordered-float.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strsim = "0.11"
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
toml.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
gray_matter = "0.2.7"
[dev-dependencies]
ctor.workspace = true
@@ -51,3 +60,4 @@ env_logger.workspace = true
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
unindent.workspace = true

View File

@@ -1,3 +0,0 @@
Push content to a deeper layer.
A context can have multiple sublayers.
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.

View File

@@ -0,0 +1,30 @@
mod current_project;
mod recent_buffers;
pub use current_project::*;
pub use recent_buffers::*;
#[derive(Default)]
pub struct AmbientContext {
pub recent_buffers: RecentBuffersContext,
pub current_project: CurrentProjectContext,
}
impl AmbientContext {
pub fn snapshot(&self) -> AmbientContextSnapshot {
AmbientContextSnapshot {
recent_buffers: self.recent_buffers.snapshot.clone(),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct AmbientContextSnapshot {
pub recent_buffers: RecentBuffersSnapshot,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum ContextUpdated {
Updating,
Disabled,
}

View File

@@ -0,0 +1,180 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
use project::{Project, ProjectPath};
use util::ResultExt;
use crate::ambient_context::ContextUpdated;
use crate::assistant_panel::Conversation;
use crate::{LanguageModelRequestMessage, Role};
/// Ambient context about the current project.
pub struct CurrentProjectContext {
pub enabled: bool,
pub message: String,
pub pending_message: Option<Task<()>>,
}
#[allow(clippy::derivable_impls)]
impl Default for CurrentProjectContext {
fn default() -> Self {
Self {
enabled: false,
message: String::new(),
pending_message: None,
}
}
}
impl CurrentProjectContext {
/// Returns the [`CurrentProjectContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled
.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
.filter(|message| !message.content.is_empty())
}
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
pub fn update(
&mut self,
fs: Arc<dyn Fs>,
project: WeakModel<Project>,
cx: &mut ModelContext<Conversation>,
) -> ContextUpdated {
if !self.enabled {
self.message.clear();
self.pending_message = None;
cx.notify();
return ContextUpdated::Disabled;
}
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
else {
return;
};
let Some(path_to_cargo_toml) = path_to_cargo_toml
.ok_or_else(|| anyhow!("no Cargo.toml"))
.log_err()
else {
return;
};
let message_task = cx
.background_executor()
.spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
if let Some(message) = message_task.await.log_err() {
conversation
.update(&mut cx, |conversation, cx| {
conversation.ambient_context.current_project.message = message;
conversation.count_remaining_tokens(cx);
cx.notify();
})
.log_err();
}
}));
ContextUpdated::Updating
}
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
fn path_to_cargo_toml(
project: WeakModel<Project>,
cx: &mut AsyncAppContext,
) -> Result<Option<PathBuf>> {
cx.update(|cx| {
let worktree = project.update(cx, |project, _cx| {
project
.worktrees()
.next()
.ok_or_else(|| anyhow!("no worktree"))
})??;
let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
Some(ProjectPath {
worktree_id: worktree.id(),
path: cargo_toml.path.clone(),
})
});
let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
project
.update(cx, |project, cx| project.absolute_path(&path, cx))
.ok()
.flatten()
});
Ok(path_to_cargo_toml)
})?
}
}

View File

@@ -0,0 +1,147 @@
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
use gpui::{ModelContext, Subscription, Task, WeakModel};
use language::{Buffer, BufferSnapshot, Rope};
use std::{fmt::Write, path::PathBuf, time::Duration};
use super::ContextUpdated;
pub struct RecentBuffersContext {
pub enabled: bool,
pub buffers: Vec<RecentBuffer>,
pub snapshot: RecentBuffersSnapshot,
pub pending_message: Option<Task<()>>,
}
pub struct RecentBuffer {
pub buffer: WeakModel<Buffer>,
pub _subscription: Subscription,
}
impl Default for RecentBuffersContext {
fn default() -> Self {
Self {
enabled: true,
buffers: Vec::new(),
snapshot: RecentBuffersSnapshot::default(),
pending_message: None,
}
}
}
impl RecentBuffersContext {
pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
let source_buffers = self
.buffers
.iter()
.filter_map(|recent| {
let (full_path, snapshot) = recent
.buffer
.read_with(cx, |buffer, cx| {
(
buffer.file().map(|file| file.full_path(cx)),
buffer.snapshot(),
)
})
.ok()?;
Some(SourceBufferSnapshot {
full_path,
model: recent.buffer.clone(),
snapshot,
})
})
.collect::<Vec<_>>();
if !self.enabled || source_buffers.is_empty() {
self.snapshot.message = Default::default();
self.snapshot.source_buffers.clear();
self.pending_message = None;
cx.notify();
ContextUpdated::Disabled
} else {
self.pending_message = Some(cx.spawn(|this, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let message = if source_buffers.is_empty() {
Rope::new()
} else {
cx.background_executor()
.spawn({
let source_buffers = source_buffers.clone();
async move { message_for_recent_buffers(source_buffers) }
})
.await
};
this.update(&mut cx, |this, cx| {
this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
this.ambient_context.recent_buffers.snapshot.message = message;
this.count_remaining_tokens(cx);
cx.notify();
})
.ok();
}));
ContextUpdated::Updating
}
}
/// Returns the [`RecentBuffersContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled
.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.snapshot.message.to_string(),
})
.filter(|message| !message.content.is_empty())
}
}
#[derive(Clone, Default, Debug)]
pub struct RecentBuffersSnapshot {
pub message: Rope,
pub source_buffers: Vec<SourceBufferSnapshot>,
}
#[derive(Clone)]
pub struct SourceBufferSnapshot {
pub full_path: Option<PathBuf>,
pub model: WeakModel<Buffer>,
pub snapshot: BufferSnapshot,
}
impl std::fmt::Debug for SourceBufferSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceBufferSnapshot")
.field("full_path", &self.full_path)
.field("model (entity id)", &self.model.entity_id())
.field("snapshot (text)", &self.snapshot.text())
.finish()
}
}
fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
let mut message = String::new();
writeln!(
message,
"The following is a list of recent buffers that the user has opened."
)
.unwrap();
for buffer in buffers {
if let Some(path) = buffer.full_path {
writeln!(message, "```{}", path.display()).unwrap();
} else {
writeln!(message, "```untitled").unwrap();
}
for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
message.push_str(chunk.text);
}
if !message.ends_with('\n') {
message.push('\n');
}
message.push_str("```\n");
}
Rope::from(message.as_str())
}

View File

@@ -1,20 +1,23 @@
mod ambient_context;
pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod omit_ranges;
mod prompts;
mod saved_conversation;
mod search;
mod slash_command;
mod streaming_diff;
mod embedded_scope;
use ambient_context::AmbientContextSnapshot;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AssistantSettings, OpenAiModel, ZedDotDevModel};
use chrono::{DateTime, Local};
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, BorrowAppContext, Global, SharedString};
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use prompts::prompt_library::*;
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -26,7 +29,6 @@ use std::{
actions!(
assistant,
[
NewConversation,
Assist,
Split,
CycleMessageRole,
@@ -34,7 +36,10 @@ actions!(
ToggleFocus,
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
ApplyEdit
]
);
@@ -75,6 +80,7 @@ impl Display for Role {
pub enum LanguageModel {
ZedDotDev(ZedDotDevModel),
OpenAi(OpenAiModel),
Anthropic(AnthropicModel),
}
impl Default for LanguageModel {
@@ -87,20 +93,23 @@ impl LanguageModel {
pub fn telemetry_id(&self) -> String {
match self {
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
}
}
pub fn display_name(&self) -> String {
match self {
LanguageModel::OpenAi(model) => format!("openai/{}", model.display_name()),
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.display_name()),
LanguageModel::OpenAi(model) => model.display_name().into(),
LanguageModel::Anthropic(model) => model.display_name().into(),
LanguageModel::ZedDotDev(model) => model.display_name().into(),
}
}
pub fn max_token_count(&self) -> usize {
match self {
LanguageModel::OpenAi(model) => model.max_token_count(),
LanguageModel::Anthropic(model) => model.max_token_count(),
LanguageModel::ZedDotDev(model) => model.max_token_count(),
}
}
@@ -108,6 +117,7 @@ impl LanguageModel {
pub fn id(&self) -> &str {
match self {
LanguageModel::OpenAi(model) => model.id(),
LanguageModel::Anthropic(model) => model.id(),
LanguageModel::ZedDotDev(model) => model.id(),
}
}
@@ -178,8 +188,10 @@ pub struct LanguageModelChoiceDelta {
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
// todo!("delete this")
#[serde(skip)]
ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -231,13 +243,13 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
});
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
cx.observe_global::<SettingsStore>(|cx| {
cx.update_global(|assistant: &mut Assistant, cx: &mut AppContext| {
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
use std::fmt;
pub use anthropic::Model as AnthropicModel;
use gpui::Pixels;
pub use open_ai::Model as OpenAiModel;
use schemars::{
@@ -16,8 +17,9 @@ use settings::{Settings, SettingsSources};
pub enum ZedDotDevModel {
Gpt3Point5Turbo,
Gpt4,
#[default]
Gpt4Turbo,
#[default]
Gpt4Omni,
Claude3Opus,
Claude3Sonnet,
Claude3Haiku,
@@ -55,6 +57,7 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
"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())),
}
}
@@ -74,6 +77,7 @@ impl JsonSchema for ZedDotDevModel {
"gpt-3.5-turbo".to_owned(),
"gpt-4".to_owned(),
"gpt-4-turbo-preview".to_owned(),
"gpt-4o".to_owned(),
];
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
@@ -100,6 +104,7 @@ impl ZedDotDevModel {
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
Self::Gpt4 => "gpt-4",
Self::Gpt4Turbo => "gpt-4-turbo-preview",
Self::Gpt4Omni => "gpt-4o",
Self::Claude3Opus => "claude-3-opus",
Self::Claude3Sonnet => "claude-3-sonnet",
Self::Claude3Haiku => "claude-3-haiku",
@@ -112,6 +117,7 @@ impl ZedDotDevModel {
Self::Gpt3Point5Turbo => "GPT 3.5 Turbo",
Self::Gpt4 => "GPT 4",
Self::Gpt4Turbo => "GPT 4 Turbo",
Self::Gpt4Omni => "GPT 4 Omni",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
@@ -123,7 +129,7 @@ impl ZedDotDevModel {
match self {
Self::Gpt3Point5Turbo => 2048,
Self::Gpt4 => 4096,
Self::Gpt4Turbo => 128000,
Self::Gpt4Turbo | Self::Gpt4Omni => 128000,
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 200000,
Self::Custom(_) => 4096, // TODO: Make this configurable
}
@@ -153,6 +159,17 @@ pub enum AssistantProvider {
default_model: OpenAiModel,
#[serde(default = "open_ai_url")]
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")]
api_url: String,
#[serde(default)]
low_speed_timeout_in_seconds: Option<u64>,
},
}
@@ -165,7 +182,11 @@ impl Default for AssistantProvider {
}
fn open_ai_url() -> String {
"https://api.openai.com/v1".into()
open_ai::OPEN_AI_API_URL.to_string()
}
fn anthropic_api_url() -> String {
anthropic::ANTHROPIC_API_URL.to_string()
}
#[derive(Default, Debug, Deserialize, Serialize)]
@@ -222,12 +243,14 @@ impl AssistantSettingsContent {
Some(AssistantProvider::OpenAi {
default_model: settings.default_open_ai_model.clone().unwrap_or_default(),
api_url: 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(),
low_speed_timeout_in_seconds: None,
}
})
},
@@ -316,11 +339,11 @@ pub struct LegacyAssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when starting new conversations.
/// The default OpenAI model to use when creating new contexts.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when starting new conversations.
/// OpenAI API base URL to use when creating new contexts.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
@@ -364,14 +387,17 @@ impl Settings for AssistantSettings {
AssistantProvider::OpenAi {
default_model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProvider::OpenAi {
default_model: default_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;
}
(merged, provider_override) => {
*merged = provider_override;
@@ -392,7 +418,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
#[cfg(test)]
mod tests {
use gpui::{AppContext, BorrowAppContext};
use gpui::{AppContext, UpdateGlobal};
use settings::SettingsStore;
use super::*;
@@ -407,13 +433,14 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourTurbo,
api_url: open_ai_url()
default_model: OpenAiModel::FourOmni,
api_url: open_ai_url(),
low_speed_timeout_in_seconds: None,
}
);
// Ensure backward-compatibility.
cx.update_global::<SettingsStore, _>(|store, cx| {
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{
@@ -428,11 +455,12 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::FourTurbo,
api_url: "test-url".into()
default_model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
}
);
cx.update_global::<SettingsStore, _>(|store, cx| {
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{
@@ -448,12 +476,13 @@ mod tests {
AssistantSettings::get_global(cx).provider,
AssistantProvider::OpenAi {
default_model: OpenAiModel::Four,
api_url: open_ai_url()
api_url: open_ai_url(),
low_speed_timeout_in_seconds: None,
}
);
// The new version supports setting a custom model when using zed.dev.
cx.update_global::<SettingsStore, _>(|store, cx| {
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{

View File

@@ -3,11 +3,13 @@ use crate::{
CompletionProvider, LanguageModelRequest,
};
use anyhow::Result;
use client::telemetry::Telemetry;
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{EventEmitter, Model, ModelContext, Task};
use language::{Rope, TransactionId};
use std::{cmp, future, ops::Range};
use multi_buffer::MultiBufferRow;
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
pub enum Event {
Finished,
@@ -29,13 +31,19 @@ pub struct Codegen {
error: Option<anyhow::Error>,
generation: Task<()>,
idle: bool,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
}
impl EventEmitter<Event> for Codegen {}
impl Codegen {
pub fn new(buffer: Model<MultiBuffer>, kind: CodegenKind, cx: &mut ModelContext<Self>) -> Self {
pub fn new(
buffer: Model<MultiBuffer>,
kind: CodegenKind,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
Self {
buffer: buffer.clone(),
@@ -46,6 +54,7 @@ impl Codegen {
error: Default::default(),
idle: true,
generation: Task::ready(()),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
}
@@ -100,9 +109,11 @@ impl Codegen {
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
.into_values()
.next()
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let telemetry = self.telemetry.clone();
self.generation = cx.spawn(|this, mut cx| {
async move {
let generate = async {
@@ -110,68 +121,89 @@ impl Codegen {
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let diff = cx.background_executor().spawn(async move {
let chunks = strip_invalid_spans_from_codeblock(response.await?);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut response_latency = None;
let request_start = Instant::now();
let diff = async {
let chunks = strip_invalid_spans_from_codeblock(response.await?);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut new_text = String::new();
let mut base_indent = None;
let mut line_indent = None;
let mut first_line = true;
let mut new_text = String::new();
let mut base_indent = None;
let mut line_indent = None;
let mut first_line = true;
while let Some(chunk) = chunks.next().await {
let chunk = chunk?;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
if line_indent.is_none() {
if let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
{
line_indent = Some(non_whitespace_ch_ix);
base_indent = base_indent.or(line_indent);
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
if line_indent.is_none() {
if let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
{
line_indent = Some(non_whitespace_ch_ix);
base_indent = base_indent.or(line_indent);
let line_indent = line_indent.unwrap();
let base_indent = base_indent.unwrap();
let indent_delta = line_indent as i32 - base_indent as i32;
let mut corrected_indent_len = cmp::max(
0,
suggested_line_indent.len as i32 + indent_delta,
)
as usize;
if first_line {
corrected_indent_len = corrected_indent_len
.saturating_sub(selection_start.column as usize);
let line_indent = line_indent.unwrap();
let base_indent = base_indent.unwrap();
let indent_delta =
line_indent as i32 - base_indent as i32;
let mut corrected_indent_len = cmp::max(
0,
suggested_line_indent.len as i32 + indent_delta,
)
as usize;
if first_line {
corrected_indent_len = corrected_indent_len
.saturating_sub(
selection_start.column as usize,
);
}
let indent_char = suggested_line_indent.char();
let mut indent_buffer = [0; 4];
let indent_str =
indent_char.encode_utf8(&mut indent_buffer);
new_text.replace_range(
..line_indent,
&indent_str.repeat(corrected_indent_len),
);
}
}
let indent_char = suggested_line_indent.char();
let mut indent_buffer = [0; 4];
let indent_str =
indent_char.encode_utf8(&mut indent_buffer);
new_text.replace_range(
..line_indent,
&indent_str.repeat(corrected_indent_len),
);
if line_indent.is_some() {
hunks_tx.send(diff.push_new(&new_text)).await?;
new_text.clear();
}
if lines.peek().is_some() {
hunks_tx.send(diff.push_new("\n")).await?;
line_indent = None;
first_line = false;
}
}
if line_indent.is_some() {
hunks_tx.send(diff.push_new(&new_text)).await?;
new_text.clear();
}
if lines.peek().is_some() {
hunks_tx.send(diff.push_new("\n")).await?;
line_indent = None;
first_line = false;
}
}
}
hunks_tx.send(diff.push_new(&new_text)).await?;
hunks_tx.send(diff.finish()).await?;
hunks_tx.send(diff.push_new(&new_text)).await?;
hunks_tx.send(diff.finish()).await?;
anyhow::Ok(())
anyhow::Ok(())
};
let error_message = diff.await.err().map(|error| error.to_string());
if let Some(telemetry) = telemetry {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
model_telemetry_id,
response_latency,
error_message,
);
}
});
while let Some(hunks) = hunks_rx.next().await {
@@ -234,7 +266,8 @@ impl Codegen {
})?;
}
diff.await?;
diff.await;
anyhow::Ok(())
};
@@ -395,8 +428,9 @@ mod tests {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Transform { range }, cx));
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
@@ -453,8 +487,9 @@ mod tests {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 6))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
@@ -511,8 +546,9 @@ mod tests {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 2))
});
let codegen =
cx.new_model(|cx| Codegen::new(buffer.clone(), CodegenKind::Generate { position }, cx));
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));

View File

@@ -1,8 +1,10 @@
mod anthropic;
#[cfg(test)]
mod fake;
mod open_ai;
mod zed;
pub use anthropic::*;
#[cfg(test)]
pub use fake::*;
pub use open_ai::*;
@@ -18,6 +20,7 @@ use futures::{future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
@@ -33,10 +36,23 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
AssistantProvider::OpenAi {
default_model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Anthropic {
default_model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
};
@@ -51,9 +67,30 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
AssistantProvider::OpenAi {
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(default_model.clone(), api_url.clone(), settings_version);
provider.update(
default_model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
}
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
default_model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
}
(
CompletionProvider::ZedDotDev(provider),
@@ -61,7 +98,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
) => {
provider.update(default_model.clone(), settings_version);
}
(CompletionProvider::OpenAi(_), AssistantProvider::ZedDotDev { default_model }) => {
(_, AssistantProvider::ZedDotDev { default_model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
default_model.clone(),
client.clone(),
@@ -70,21 +107,37 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
));
}
(
CompletionProvider::ZedDotDev(_),
_,
AssistantProvider::OpenAi {
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
(
_,
AssistantProvider::Anthropic {
default_model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
default_model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
#[cfg(test)]
(CompletionProvider::Fake(_), _) => unimplemented!(),
}
})
})
@@ -93,6 +146,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
pub enum CompletionProvider {
OpenAi(OpenAiCompletionProvider),
Anthropic(AnthropicCompletionProvider),
ZedDotDev(ZedDotDevCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
@@ -108,6 +162,7 @@ impl CompletionProvider {
pub fn settings_version(&self) -> usize {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
@@ -117,6 +172,7 @@ impl CompletionProvider {
pub fn is_authenticated(&self) -> bool {
match self {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
@@ -126,6 +182,7 @@ impl CompletionProvider {
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
@@ -135,6 +192,7 @@ impl CompletionProvider {
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
match self {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
@@ -144,6 +202,7 @@ impl CompletionProvider {
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
@@ -153,6 +212,9 @@ impl CompletionProvider {
pub fn default_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())
}
@@ -168,9 +230,10 @@ impl CompletionProvider {
) -> BoxFuture<'static, Result<usize>> {
match self {
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
}
@@ -180,6 +243,7 @@ impl CompletionProvider {
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
match self {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),

View File

@@ -0,0 +1,327 @@
use crate::count_open_ai_tokens;
use crate::{
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
Role,
};
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use theme::ThemeSettings;
use ui::prelude::*;
use util::ResultExt;
pub struct AnthropicCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: AnthropicModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
}
impl AnthropicCompletionProvider {
pub fn new(
default_model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
api_key: None,
api_url,
default_model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(
&mut self,
default_model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
let api_url = self.api_url.clone();
cx.spawn(|mut cx| async move {
let api_key = if let Ok(api_key) = env::var("ANTHROPIC_API_KEY") {
api_key
} else {
let (_, api_key) = cx
.update(|cx| cx.read_credentials(&api_url))?
.await?
.ok_or_else(|| anyhow!("credentials not found"))?;
String::from_utf8(api_key)?
};
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = Some(api_key);
}
})
})
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
let delete_credentials = cx.delete_credentials(&self.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = None;
}
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
.into()
}
pub fn default_model(&self) -> AnthropicModel {
self.default_model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
count_open_ai_tokens(request, cx.background_executor())
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_anthropic_request(request);
let http_client = self.http_client.clone();
let api_key = self.api_key.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
let request = stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
low_speed_timeout,
);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(response) => match response {
anthropic::ResponseEvent::ContentBlockStart {
content_block, ..
} => match content_block {
anthropic::ContentBlock::Text { text } => Some(Ok(text)),
},
anthropic::ResponseEvent::ContentBlockDelta { delta, .. } => {
match delta {
anthropic::TextDelta::TextDelta { text } => Some(Ok(text)),
}
}
_ => None,
},
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.default_model(),
};
let mut system_message = String::new();
let mut messages: Vec<RequestMessage> = Vec::new();
for message in request.messages {
if message.content.is_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
let role = match message.role {
Role::User => AnthropicRole::User,
Role::Assistant => AnthropicRole::Assistant,
_ => unreachable!(),
};
if let Some(last_message) = messages.last_mut() {
if last_message.role == role {
last_message.content.push_str("\n\n");
last_message.content.push_str(&message.content);
continue;
}
}
messages.push(RequestMessage {
role,
content: message.content,
});
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
}
}
}
Request {
model,
messages,
stream: true,
system: system_message,
max_tokens: 4092,
}
}
}
struct AuthenticationPrompt {
api_key: View<Editor>,
api_url: String,
}
impl AuthenticationPrompt {
fn new(api_url: String, cx: &mut WindowContext) -> Self {
Self {
api_key: cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(
"sk-000000000000000000000000000000000000000000000000",
cx,
);
editor
}),
api_url,
}
}
fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
let api_key = self.api_key.read(cx).text(cx);
if api_key.is_empty() {
return;
}
let write_credentials = cx.write_credentials(&self.api_url, "Bearer", api_key.as_bytes());
cx.spawn(|_, mut cx| async move {
write_credentials.await?;
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.api_key = Some(api_key);
}
})
})
.detach_and_log_err(cx);
}
fn render_api_key_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.api_key,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
}
impl Render for AuthenticationPrompt {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const INSTRUCTIONS: [&str; 4] = [
"To use the assistant panel or inline assistant, you need to add your Anthropic API key.",
"You can create an API key at: https://console.anthropic.com/settings/keys",
"",
"Paste your Anthropic API key below and hit enter to use the assistant:",
];
v_flex()
.p_4()
.size_full()
.on_action(cx.listener(Self::save_api_key))
.children(
INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
)
.child(
h_flex()
.w_full()
.my_2()
.px_2()
.py_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(self.render_api_key_editor(cx)),
)
.child(
Label::new(
"You can also assign the ANTHROPIC_API_KEY environment variable and restart Zed.",
)
.size(LabelSize::Small),
)
.child(
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),
)
.into_any()
}
}

View File

@@ -1,3 +1,4 @@
use crate::assistant_settings::ZedDotDevModel;
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -5,18 +6,21 @@ use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
use settings::Settings;
use std::time::Duration;
use std::{env, sync::Arc};
use theme::ThemeSettings;
use ui::prelude::*;
use util::{http::HttpClient, ResultExt};
use util::ResultExt;
pub struct OpenAiCompletionProvider {
api_key: Option<String>,
api_url: String,
default_model: OpenAiModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
}
@@ -25,6 +29,7 @@ impl OpenAiCompletionProvider {
default_model: OpenAiModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
@@ -32,13 +37,21 @@ impl OpenAiCompletionProvider {
api_url,
default_model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(&mut self, default_model: OpenAiModel, api_url: String, settings_version: usize) {
pub fn update(
&mut self,
default_model: OpenAiModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.default_model = default_model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
@@ -112,9 +125,16 @@ impl OpenAiCompletionProvider {
let http_client = self.http_client.clone();
let api_key = self.api_key.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let api_key = api_key.ok_or_else(|| anyhow!("missing api key"))?;
let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let request = stream_completion(
http_client.as_ref(),
&api_url,
&api_key,
request,
low_speed_timeout,
);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
@@ -131,8 +151,8 @@ impl OpenAiCompletionProvider {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::ZedDotDev(_) => self.default_model(),
LanguageModel::OpenAi(model) => model,
_ => self.default_model(),
};
Request {
@@ -183,7 +203,17 @@ pub fn count_open_ai_tokens(
})
.collect::<Vec<_>>();
tiktoken_rs::num_tokens_from_messages(request.model.id(), &messages)
match request.model {
LanguageModel::Anthropic(_)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
// Tiktoken doesn't yet support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
}
_ => tiktoken_rs::num_tokens_from_messages(request.model.id(), &messages),
}
})
.boxed()
}

View File

@@ -78,9 +78,9 @@ impl ZedDotDevCompletionProvider {
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
match request.model {
LanguageModel::OpenAi(_) => future::ready(Err(anyhow!("invalid model"))).boxed(),
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
count_open_ai_tokens(request, cx.background_executor())
}
@@ -107,6 +107,7 @@ impl ZedDotDevCompletionProvider {
}
.boxed()
}
_ => future::ready(Err(anyhow!("invalid model"))).boxed(),
}
}

View File

@@ -1,91 +0,0 @@
use editor::MultiBuffer;
use gpui::{AppContext, Model, ModelContext, Subscription};
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
#[derive(Default)]
pub struct EmbeddedScope {
active_buffer: Option<Model<MultiBuffer>>,
active_buffer_enabled: bool,
active_buffer_subscription: Option<Subscription>,
}
impl EmbeddedScope {
pub fn new() -> Self {
Self {
active_buffer: None,
active_buffer_enabled: true,
active_buffer_subscription: None,
}
}
pub fn set_active_buffer(
&mut self,
buffer: Option<Model<MultiBuffer>>,
cx: &mut ModelContext<Conversation>,
) {
self.active_buffer_subscription.take();
if let Some(active_buffer) = buffer.clone() {
self.active_buffer_subscription =
Some(cx.subscribe(&active_buffer, |conversation, _, e, cx| {
if let multi_buffer::Event::Edited { .. } = e {
conversation.count_remaining_tokens(cx)
}
}));
}
self.active_buffer = buffer;
}
pub fn active_buffer(&self) -> Option<&Model<MultiBuffer>> {
self.active_buffer.as_ref()
}
pub fn active_buffer_enabled(&self) -> bool {
self.active_buffer_enabled
}
pub fn set_active_buffer_enabled(&mut self, enabled: bool) {
self.active_buffer_enabled = enabled;
}
/// Provide a message for the language model based on the active buffer.
pub fn message(&self, cx: &AppContext) -> Option<LanguageModelRequestMessage> {
if !self.active_buffer_enabled {
return None;
}
let active_buffer = self.active_buffer.as_ref()?;
let buffer = active_buffer.read(cx);
if let Some(singleton) = buffer.as_singleton() {
let singleton = singleton.read(cx);
let filename = singleton
.file()
.map(|file| file.path().to_string_lossy())
.unwrap_or("Untitled".into());
let text = singleton.text();
let language = singleton
.language()
.map(|l| {
let name = l.code_fence_block_name();
name.to_string()
})
.unwrap_or_default();
let markdown =
format!("User's active file `{filename}`:\n\n```{language}\n{text}```\n\n");
return Some(LanguageModelRequestMessage {
role: Role::System,
content: markdown,
});
}
None
}
}

View File

@@ -0,0 +1,101 @@
use rope::Rope;
use std::{cmp::Ordering, ops::Range};
pub(crate) fn text_in_range_omitting_ranges(
rope: &Rope,
range: Range<usize>,
omit_ranges: &[Range<usize>],
) -> String {
let mut content = String::with_capacity(range.len());
let mut omit_ranges = omit_ranges
.iter()
.skip_while(|omit_range| omit_range.end <= range.start)
.peekable();
let mut offset = range.start;
let mut chunks = rope.chunks_in_range(range.clone());
while let Some(chunk) = chunks.next() {
if let Some(omit_range) = omit_ranges.peek() {
match offset.cmp(&omit_range.start) {
Ordering::Less => {
let max_len = omit_range.start - offset;
if chunk.len() < max_len {
content.push_str(chunk);
offset += chunk.len();
} else {
content.push_str(&chunk[..max_len]);
chunks.seek(omit_range.end.min(range.end));
offset = omit_range.end;
omit_ranges.next();
}
}
Ordering::Equal | Ordering::Greater => {
chunks.seek(omit_range.end.min(range.end));
offset = omit_range.end;
omit_ranges.next();
}
}
} else {
content.push_str(chunk);
offset += chunk.len();
}
}
content
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{rngs::StdRng, Rng as _};
use util::RandomCharIter;
#[gpui::test(iterations = 100)]
fn test_text_in_range_omitting_ranges(mut rng: StdRng) {
let text = RandomCharIter::new(&mut rng).take(1024).collect::<String>();
let rope = Rope::from(text.as_str());
let mut start = rng.gen_range(0..=text.len() / 2);
let mut end = rng.gen_range(text.len() / 2..=text.len());
while !text.is_char_boundary(start) {
start -= 1;
}
while !text.is_char_boundary(end) {
end += 1;
}
let range = start..end;
let mut ix = 0;
let mut omit_ranges = Vec::new();
for _ in 0..rng.gen_range(0..10) {
let mut start = rng.gen_range(ix..=text.len());
while !text.is_char_boundary(start) {
start += 1;
}
let mut end = rng.gen_range(start..=text.len());
while !text.is_char_boundary(end) {
end += 1;
}
omit_ranges.push(start..end);
ix = end;
if ix == text.len() {
break;
}
}
let mut expected_text = text[range.clone()].to_string();
for omit_range in omit_ranges.iter().rev() {
let start = omit_range
.start
.saturating_sub(range.start)
.min(range.len());
let end = omit_range.end.saturating_sub(range.start).min(range.len());
expected_text.replace_range(start..end, "");
}
assert_eq!(
text_in_range_omitting_ranges(&rope, range.clone(), &omit_ranges),
expected_text,
"text: {text:?}\nrange: {range:?}\nomit_ranges: {omit_ranges:?}"
);
}
}

View File

@@ -1,95 +1,3 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}
pub mod prompt;
pub mod prompt_library;
pub mod prompt_manager;

View File

@@ -0,0 +1,278 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
use ui::SharedString;
use gray_matter::{engine::YAML, Matter};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPromptFrontmatter {
title: String,
version: String,
author: String,
#[serde(default)]
languages: Vec<String>,
#[serde(default)]
dependencies: Vec<String>,
}
impl Default for StaticPromptFrontmatter {
fn default() -> Self {
Self {
title: "New Prompt".to_string(),
version: "1.0".to_string(),
author: "No Author".to_string(),
languages: vec!["*".to_string()],
dependencies: vec![],
}
}
}
impl StaticPromptFrontmatter {
pub fn title(&self) -> SharedString {
self.title.clone().into()
}
// pub fn version(&self) -> SharedString {
// self.version.clone().into()
// }
// pub fn author(&self) -> SharedString {
// self.author.clone().into()
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.languages
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.dependencies
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
}
/// A statuc prompt that can be loaded into the prompt library
/// from Markdown with a frontmatter header
///
/// Examples:
///
/// ### Globally available prompt
///
/// ```markdown
/// ---
/// title: Foo
/// version: 1.0
/// author: Jane Kim <jane@kim.com
/// languages: ["*"]
/// dependencies: []
/// ---
///
/// Foo and bar are terms used in programming to describe generic concepts.
/// ```
///
/// ### Language-specific prompt
///
/// ```markdown
/// ---
/// title: UI with GPUI
/// version: 1.0
/// author: Nate Butler <iamnbutler@gmail.com>
/// languages: ["rust"]
/// dependencies: ["gpui"]
/// ---
///
/// When building a UI with GPUI, ensure you...
/// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt {
content: String,
file_name: Option<String>,
}
impl StaticPrompt {
pub fn new(content: String) -> Self {
StaticPrompt {
content,
file_name: None,
}
}
pub fn title(&self) -> Option<SharedString> {
self.metadata().map(|m| m.title())
}
// pub fn version(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.version())
// }
// pub fn author(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.author())
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.metadata().map(|m| m.languages()).unwrap_or_default()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.metadata()
// .map(|m| m.dependencies())
// .unwrap_or_default()
// }
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
// todo!()
// }
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
}
impl StaticPrompt {
// pub fn update(&mut self, contents: String) -> &mut Self {
// self.content = contents;
// self
// }
/// Sets the file name of the prompt
pub fn file_name(&mut self, file_name: String) -> &mut Self {
self.file_name = Some(file_name);
self
}
/// Sets the file name of the prompt based on the title
// pub fn file_name_from_title(&mut self) -> &mut Self {
// if let Some(title) = self.title() {
// let file_name = title.to_lowercase().replace(" ", "_");
// if !file_name.is_empty() {
// self.file_name = Some(file_name);
// }
// }
// self
// }
/// Returns the prompt's content
pub fn content(&self) -> &String {
&self.content
}
fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> {
let matter = Matter::<YAML>::new();
let result = matter.parse(self.content.as_str());
match result.data {
Some(data) => {
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
let body = result.content;
Ok((front_matter, body))
}
None => Err(anyhow::anyhow!("Failed to parse frontmatter")),
}
}
pub fn metadata(&self) -> Option<StaticPromptFrontmatter> {
self.parse().ok().map(|(front_matter, _)| front_matter)
}
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View File

@@ -0,0 +1,152 @@
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::sync::Arc;
use util::paths::PROMPTS_DIR;
use uuid::Uuid;
use super::prompt::StaticPrompt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PromptId(pub Uuid);
#[allow(unused)]
impl PromptId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct PromptLibraryState {
/// A set of prompts that all assistant contexts will start with
default_prompt: Vec<PromptId>,
/// All [Prompt]s loaded into the library
prompts: HashMap<PromptId, StaticPrompt>,
/// Prompts that have been changed but haven't been
/// saved back to the file system
dirty_prompts: Vec<PromptId>,
version: usize,
}
pub struct PromptLibrary {
state: RwLock<PromptLibraryState>,
}
impl Default for PromptLibrary {
fn default() -> Self {
Self::new()
}
}
impl PromptLibrary {
fn new() -> Self {
Self {
state: RwLock::new(PromptLibraryState::default()),
}
}
pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> {
let state = self.state.read();
state
.prompts
.iter()
.map(|(id, prompt)| (*id, prompt.clone()))
.collect()
}
pub fn first_prompt_id(&self) -> Option<PromptId> {
let state = self.state.read();
state.prompts.keys().next().cloned()
}
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
let state = self.state.read();
state.prompts.get(&id).cloned()
}
/// Save the current state of the prompt library to the
/// file system as a JSON file
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
fs.create_dir(&PROMPTS_DIR).await?;
let path = PROMPTS_DIR.join("index.json");
let json = {
let state = self.state.read();
serde_json::to_string(&*state)?
};
fs.atomic_write(path, json).await?;
Ok(())
}
/// Load the state of the prompt library from the file system
/// or create a new one if it doesn't exist
pub async fn load(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let path = PROMPTS_DIR.join("index.json");
let state = if fs.is_file(&path).await {
let json = fs.load(&path).await?;
serde_json::from_str(&json)?
} else {
PromptLibraryState::default()
};
let mut prompt_library = Self {
state: RwLock::new(state),
};
prompt_library.load_prompts(fs).await?;
Ok(prompt_library)
}
/// Load all prompts from the file system
/// adding them to the library if they don't already exist
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
// let current_prompts = self.all_prompt_contents().clone();
// For now, we'll just clear the prompts and reload them all
self.state.get_mut().prompts.clear();
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
while let Some(prompt_path) = prompt_paths.next().await {
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
if !fs.is_file(&prompt_path).await
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
{
continue;
}
let json = fs
.load(&prompt_path)
.await
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
let mut static_prompt = StaticPrompt::new(json);
if let Some(file_name) = prompt_path.file_name() {
let file_name = file_name.to_string_lossy().into_owned();
static_prompt.file_name(file_name);
}
let state = self.state.get_mut();
let id = Uuid::new_v4();
state.prompts.insert(PromptId(id), static_prompt);
state.version += 1;
}
// Write any changes back to the file system
self.save(fs.clone()).await?;
Ok(())
}
}

View File

@@ -0,0 +1,327 @@
use collections::HashMap;
use editor::Editor;
use fs::Fs;
use gpui::{prelude::FluentBuilder, *};
use language::{language_settings, Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing};
use util::{ResultExt, TryFutureExt};
use workspace::ModalView;
use super::prompt_library::{PromptId, PromptLibrary};
use crate::prompts::prompt::StaticPrompt;
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
#[allow(dead_code)]
fs: Arc<dyn Fs>,
picker: View<Picker<PromptManagerDelegate>>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
}
impl PromptManager {
pub fn new(
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_manager = cx.view().downgrade();
let picker = cx.new_view(|cx| {
Picker::uniform_list(
PromptManagerDelegate {
prompt_manager,
matching_prompts: vec![],
matching_prompt_ids: vec![],
prompt_library: prompt_library.clone(),
selected_index: 0,
},
cx,
)
.max_height(rems(35.75))
.modal(false)
});
let focus_handle = picker.focus_handle(cx);
let mut manager = Self {
focus_handle,
prompt_library,
language_registry,
fs,
picker,
prompt_editors: HashMap::default(),
active_prompt_id: None,
};
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
manager
}
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
self.active_prompt_id = prompt_id;
cx.notify();
}
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
let focus_handle = editor.focus_handle(cx);
cx.focus(&focus_handle)
}
}
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let picker = self.picker.clone();
v_flex()
.id("prompt-list")
.bg(cx.theme().colors().surface_background)
.h_full()
.w_2_5()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h(rems(1.75))
.w_full()
.flex_none()
.justify_between()
.child(Label::new("Prompt Library").size(LabelSize::Small))
.child(IconButton::new("new-prompt", IconName::Plus).disabled(true)),
)
.child(
v_flex()
.h(rems(38.25))
.flex_grow()
.justify_start()
.child(picker),
)
}
fn set_editor_for_prompt(
&mut self,
prompt_id: PromptId,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
cx.new_view(|cx| {
let text = if let Some(prompt) = prompt_library.prompt(prompt_id) {
prompt.content().to_owned()
} else {
"".to_string()
};
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(text, cx);
let markdown = self.language_registry.language_for_name("Markdown");
cx.spawn(|buffer, mut cx| async move {
if let Some(markdown) = markdown.await.log_err() {
_ = buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
});
}
})
.detach();
buffer.set_language_registry(self.language_registry.clone());
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor
})
});
editor_for_prompt.clone()
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.key_context("PromptManager")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
// .on_action(cx.listener(Self::save_active_prompt))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(64.))
.h(rems(40.))
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(
div().w_3_5().h_full().child(
v_flex()
.id("prompt-editor")
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.size_full()
.flex_none()
.min_w_64()
.h_full()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h_7()
.w_full()
.justify_between()
.child(div())
.child(
IconButton::new("dismiss", IconName::Close)
.shape(IconButtonShape::Square)
.on_click(|_, cx| {
cx.dispatch_action(menu::Cancel.boxed_clone());
}),
),
)
.when_some(self.active_prompt_id, |this, active_prompt_id| {
this.child(
h_flex()
.flex_1()
.w_full()
.py(Spacing::Large.rems(cx))
.px(Spacing::XLarge.rems(cx))
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
)
}),
),
)
}
}
impl EventEmitter<DismissEvent> for PromptManager {}
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
pub struct PromptManagerDelegate {
prompt_manager: WeakView<PromptManager>,
matching_prompts: Vec<Arc<StaticPrompt>>,
matching_prompt_ids: Vec<PromptId>,
prompt_library: Arc<PromptLibrary>,
selected_index: usize,
}
impl PickerDelegate for PromptManagerDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Find a prompt…".into()
}
fn match_count(&self) -> usize {
self.matching_prompt_ids.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn selected_index_changed(
&self,
ix: usize,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
let prompt_manager = self.prompt_manager.upgrade()?;
Some(Box::new(move |cx| {
prompt_manager.update(cx, |manager, cx| {
manager.set_active_prompt(Some(prompt_id), cx);
})
}))
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let prompt_library = self.prompt_library.clone();
cx.spawn(|picker, mut cx| async move {
async {
let prompts = prompt_library.prompts();
let matching_prompts = prompts
.into_iter()
.filter(|(_, prompt)| {
prompt
.content()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect::<Vec<_>>();
picker.update(&mut cx, |picker, cx| {
picker.delegate.matching_prompt_ids =
matching_prompts.iter().map(|(id, _)| *id).collect();
picker.delegate.matching_prompts = matching_prompts
.into_iter()
.map(|(_, prompt)| Arc::new(prompt))
.collect();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
let prompt_manager = self.prompt_manager.upgrade().unwrap();
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
}
fn should_dismiss(&self) -> bool {
false
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.prompt_manager
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let matching_prompt = self.matching_prompts.get(ix)?;
let prompt = matching_prompt.clone();
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(prompt.title().unwrap_or_default().clone())),
)
}
}

View File

@@ -0,0 +1,171 @@
use language::Rope;
use std::ops::Range;
/// Search the given buffer for the given substring, ignoring any differences
/// in line indentation between the query and the buffer.
///
/// Returns a vector of ranges of byte offsets in the buffer corresponding
/// to the entire lines of the buffer.
pub fn fuzzy_search_lines(haystack: &Rope, needle: &str) -> Option<Range<usize>> {
const SIMILARITY_THRESHOLD: f64 = 0.8;
let mut best_match: Option<(Range<usize>, f64)> = None; // (range, score)
let mut haystack_lines = haystack.chunks().lines();
let mut haystack_line_start = 0;
while let Some(mut haystack_line) = haystack_lines.next() {
let next_haystack_line_start = haystack_line_start + haystack_line.len() + 1;
let mut advanced_to_next_haystack_line = false;
let mut matched = true;
let match_start = haystack_line_start;
let mut match_end = next_haystack_line_start;
let mut match_score = 0.0;
let mut needle_lines = needle.lines().peekable();
while let Some(needle_line) = needle_lines.next() {
let similarity = line_similarity(haystack_line, needle_line);
if similarity >= SIMILARITY_THRESHOLD {
match_end = haystack_lines.offset();
match_score += similarity;
if needle_lines.peek().is_some() {
if let Some(next_haystack_line) = haystack_lines.next() {
advanced_to_next_haystack_line = true;
haystack_line = next_haystack_line;
} else {
matched = false;
break;
}
} else {
break;
}
} else {
matched = false;
break;
}
}
if matched
&& best_match
.as_ref()
.map(|(_, best_score)| match_score > *best_score)
.unwrap_or(true)
{
best_match = Some((match_start..match_end, match_score));
}
if advanced_to_next_haystack_line {
haystack_lines.seek(next_haystack_line_start);
}
haystack_line_start = next_haystack_line_start;
}
best_match.map(|(range, _)| range)
}
/// Calculates the similarity between two lines, ignoring leading and trailing whitespace,
/// using the Jaro-Winkler distance.
///
/// Returns a value between 0.0 and 1.0, where 1.0 indicates an exact match.
fn line_similarity(line1: &str, line2: &str) -> f64 {
strsim::jaro_winkler(line1.trim(), line2.trim())
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{AppContext, Context as _};
use language::Buffer;
use unindent::Unindent as _;
use util::test::marked_text_ranges;
#[gpui::test]
fn test_fuzzy_search_lines(cx: &mut AppContext) {
let (text, expected_ranges) = marked_text_ranges(
&r#"
fn main() {
if a() {
assert_eq!(
1 + 2,
does_not_match,
);
}
println!("hi");
assert_eq!(
1 + 2,
3,
); // this last line does not match
« assert_eq!(
1 + 2,
3,
);
»
« assert_eq!(
"something",
"else",
);
»
}
"#
.unindent(),
false,
);
let buffer = cx.new_model(|cx| Buffer::local(&text, cx));
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
1 + 2,
3,
);
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[0]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
1 + 2,
3,
);
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[0]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
asst_eq!(
\"something\",
\"els\"
)
"
.unindent(),
)
.unwrap();
assert_eq!(actual_range, expected_ranges[1]);
let actual_range = fuzzy_search_lines(
snapshot.as_rope(),
&"
assert_eq!(
2 + 1,
3,
);
"
.unindent(),
);
assert_eq!(actual_range, None);
}
}

View File

@@ -0,0 +1,319 @@
use anyhow::Result;
use collections::HashMap;
use editor::{CompletionProvider, Editor};
use futures::channel::oneshot;
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WindowHandle};
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use project::Project;
use rope::Point;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use workspace::Workspace;
use crate::PromptLibrary;
mod current_file_command;
mod file_command;
mod prompt_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
}
#[derive(Default)]
pub(crate) struct SlashCommandRegistry {
commands: HashMap<String, Box<dyn SlashCommand>>,
}
pub(crate) trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn description(&self) -> String;
fn complete_argument(
&self,
query: String,
cancel: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>>;
fn requires_argument(&self) -> bool;
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation;
}
pub(crate) struct SlashCommandInvocation {
pub output: Task<Result<String>>,
pub invalidated: oneshot::Receiver<()>,
pub cleanup: SlashCommandCleanup,
}
#[derive(Default)]
pub(crate) struct SlashCommandCleanup(Option<Box<dyn FnOnce()>>);
impl SlashCommandCleanup {
pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
Self(Some(Box::new(cleanup)))
}
}
impl Drop for SlashCommandCleanup {
fn drop(&mut self) {
if let Some(cleanup) = self.0.take() {
cleanup();
}
}
}
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
}
impl SlashCommandRegistry {
pub fn new(
project: Model<Project>,
prompt_library: Arc<PromptLibrary>,
window: Option<WindowHandle<Workspace>>,
) -> Arc<Self> {
let mut this = Self {
commands: HashMap::default(),
};
this.register_command(file_command::FileSlashCommand::new(project));
this.register_command(prompt_command::PromptSlashCommand::new(prompt_library));
if let Some(window) = window {
this.register_command(current_file_command::CurrentFileSlashCommand::new(window));
}
Arc::new(this)
}
fn register_command(&mut self, command: impl SlashCommand) {
self.commands.insert(command.name(), Box::new(command));
}
fn command_names(&self) -> impl Iterator<Item = &String> {
self.commands.keys()
}
pub(crate) fn command(&self, name: &str) -> Option<&dyn SlashCommand> {
self.commands.get(name).map(|b| &**b)
}
}
impl SlashCommandCompletionProvider {
pub fn new(commands: Arc<SlashCommandRegistry>) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
commands,
}
}
fn complete_command_name(
&self,
command_name: &str,
range: Range<Anchor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
.command_names()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.clone(),
char_bag: def.as_str().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let executor = cx.background_executor().clone();
executor.clone().spawn(async move {
let matches = match_strings(
&candidates,
&command_name,
true,
usize::MAX,
&Default::default(),
executor,
)
.await;
Ok(matches
.into_iter()
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
if command.requires_argument() {
new_text.push(' ');
}
Some(project::Completion {
old_range: range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
new_text,
label: CodeLabel::plain(mat.string, None),
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
})
})
.collect())
})
}
fn complete_command_argument(
&self,
command_name: &str,
argument: String,
range: Range<Anchor>,
cx: &mut AppContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let completions = command.complete_argument(argument, new_cancel_flag.clone(), cx);
cx.background_executor().spawn(async move {
Ok(completions
.await?
.into_iter()
.map(|arg| project::Completion {
old_range: range.clone(),
label: CodeLabel::plain(arg.clone(), None),
new_text: arg.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
})
.collect())
})
} else {
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
}
}
}
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let task = buffer.update(cx, |buffer, cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
let call = SlashCommandLine::parse(line)?;
let name = &line[call.name.clone()];
if let Some(argument) = call.argument {
let start = buffer.anchor_after(Point::new(position.row, argument.start as u32));
let argument = line[argument.clone()].to_string();
Some(self.complete_command_argument(name, argument, start..buffer_position, cx))
} else {
let start = buffer.anchor_after(Point::new(position.row, call.name.start as u32));
Some(self.complete_command_name(name, start..buffer_position, cx))
}
});
task.unwrap_or_else(|| Task::ready(Ok(Vec::new())))
}
fn resolve_completions(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
SlashCommandLine::parse(line).is_some()
} else {
false
}
}
}
impl SlashCommandLine {
pub(crate) fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.argument = Some(next_ix..next_ix);
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
argument: None,
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}

View File

@@ -0,0 +1,135 @@
use std::{borrow::Cow, cell::Cell, rc::Rc};
use anyhow::{anyhow, Result};
use collections::HashMap;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{AppContext, Entity, Subscription, Task, WindowHandle};
use workspace::{Event as WorkspaceEvent, Workspace};
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
pub(crate) struct CurrentFileSlashCommand {
workspace: WindowHandle<Workspace>,
}
impl CurrentFileSlashCommand {
pub fn new(workspace: WindowHandle<Workspace>) -> Self {
Self { workspace }
}
}
impl SlashCommand for CurrentFileSlashCommand {
fn name(&self) -> String {
"current_file".into()
}
fn description(&self) -> String {
"insert the current file".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(&self, _argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let (invalidate_tx, invalidate_rx) = oneshot::channel();
let invalidate_tx = Rc::new(Cell::new(Some(invalidate_tx)));
let mut subscriptions: Vec<Subscription> = Vec::new();
let output = self.workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
let mut most_recent_buffer = None;
for editor in workspace.items_of_type::<Editor>(cx) {
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
continue;
};
let timestamp = timestamps_by_entity_id
.get(&editor.entity_id())
.copied()
.unwrap_or_default();
if most_recent_buffer
.as_ref()
.map_or(true, |(_, prev_timestamp)| timestamp > *prev_timestamp)
{
most_recent_buffer = Some((buffer, timestamp));
}
}
subscriptions.push({
let workspace_view = cx.view().clone();
let invalidate_tx = invalidate_tx.clone();
cx.window_context()
.subscribe(&workspace_view, move |_workspace, event, _cx| match event {
WorkspaceEvent::ActiveItemChanged
| WorkspaceEvent::ItemAdded
| WorkspaceEvent::ItemRemoved
| WorkspaceEvent::PaneAdded(_)
| WorkspaceEvent::PaneRemoved => {
if let Some(invalidate_tx) = invalidate_tx.take() {
_ = invalidate_tx.send(());
}
}
_ => {}
})
});
if let Some((buffer, _)) = most_recent_buffer {
subscriptions.push({
let invalidate_tx = invalidate_tx.clone();
cx.window_context().observe(&buffer, move |_buffer, _cx| {
if let Some(invalidate_tx) = invalidate_tx.take() {
_ = invalidate_tx.send(());
}
})
});
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
cx.background_executor().spawn(async move {
let path = path
.as_ref()
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("untitled"));
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
output.push_str("```");
output.push_str(&path);
output.push('\n');
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
Ok(output)
})
} else {
Task::ready(Err(anyhow!("no recent buffer found")))
}
});
SlashCommandInvocation {
output: output.unwrap_or_else(|error| Task::ready(Err(error))),
invalidated: invalidate_rx,
cleanup: SlashCommandCleanup::new(move || drop(subscriptions)),
}
}
}

View File

@@ -0,0 +1,145 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use anyhow::Result;
use futures::channel::oneshot;
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task};
use project::{PathMatchCandidateSet, Project};
use std::{
path::Path,
sync::{atomic::AtomicBool, Arc},
};
pub(crate) struct FileSlashCommand {
project: Model<Project>,
}
impl FileSlashCommand {
pub fn new(project: Model<Project>) -> Self {
Self { project }
}
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
let worktrees = self
.project
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
directories_only: false,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
impl SlashCommand for FileSlashCommand {
fn name(&self) -> String {
"file".into()
}
fn description(&self) -> String {
"insert an entire file".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> gpui::Task<Result<Vec<String>>> {
let paths = self.search_paths(query, cancellation_flag, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect())
})
}
fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let project = self.project.read(cx);
let Some(argument) = argument else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let path = Path::new(argument);
let abs_path = project.worktrees().find_map(|worktree| {
let worktree = worktree.read(cx);
worktree.entry_for_path(path)?;
worktree.absolutize(path).ok()
});
let Some(abs_path) = abs_path else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow::anyhow!("missing path"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let fs = project.fs().clone();
let argument = argument.to_string();
let output = cx.background_executor().spawn(async move {
let content = fs.load(&abs_path).await?;
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
output.push_str(&argument);
output.push('\n');
output.push_str(&content);
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
Ok(output)
});
SlashCommandInvocation {
output,
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
}
}
}

View File

@@ -0,0 +1,95 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use crate::prompts::prompt_library::PromptLibrary;
use anyhow::{anyhow, Context, Result};
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Task};
use std::sync::{atomic::AtomicBool, Arc};
pub(crate) struct PromptSlashCommand {
library: Arc<PromptLibrary>,
}
impl PromptSlashCommand {
pub fn new(library: Arc<PromptLibrary>) -> Self {
Self { library }
}
}
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
"prompt".into()
}
fn description(&self) -> String {
"insert a prompt from the library".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let library = self.library.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
let candidates = library
.prompts()
.into_iter()
.enumerate()
.filter_map(|(ix, prompt)| {
prompt
.1
.title()
.map(|title| StringMatchCandidate::new(ix, title.into()))
})
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&cancellation_flag,
executor,
)
.await;
Ok(matches
.into_iter()
.map(|mat| candidates[mat.candidate_id].string.clone())
.collect())
})
}
fn run(&self, title: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
let Some(title) = title else {
return SlashCommandInvocation {
output: Task::ready(Err(anyhow!("missing prompt name"))),
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
};
};
let library = self.library.clone();
let title = title.to_string();
let output = cx.background_executor().spawn(async move {
let prompt = library
.prompts()
.into_iter()
.filter_map(|prompt| prompt.1.title().map(|title| (title, prompt)))
.find(|(t, _)| t == &title)
.with_context(|| format!("no prompt found with title {:?}", title))?
.1;
Ok(prompt.1.content().to_owned())
});
SlashCommandInvocation {
output,
invalidated: oneshot::channel().1,
cleanup: SlashCommandCleanup::default(),
}
}
}

View File

@@ -0,0 +1,86 @@
When the user asks you to suggest edits for a buffer, use a strict template consisting of:
* A markdown code block with the file path as the language identifier.
* The original code that should be replaced
* A separator line (`---`)
* The new text that should replace the original lines
Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
## Example
If you have a buffer with the following lines:
```path/to/file.rs
fn quicksort(arr: &mut [i32]) {
if arr.len() <= 1 {
return;
}
let pivot_index = partition(arr);
let (left, right) = arr.split_at_mut(pivot_index);
quicksort(left);
quicksort(&mut right[1..]);
}
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
```
And you want to replace the for loop inside `partition`, output the following.
```edit path/to/file.rs
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
---
let mut j = 0;
while j < last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
j += 1;
}
```
If you wanted to insert comments above the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
---
// A helper function used for quicksort.
fn partition(arr: &mut [i32]) -> usize {
```
If you wanted to delete the partition function, output the following:
```edit path/to/file.rs
fn partition(arr: &mut [i32]) -> usize {
let last_index = arr.len() - 1;
let pivot = arr[last_index];
let mut i = 0;
for j in 0..last_index {
if arr[j] <= pivot {
arr.swap(i, j);
i += 1;
}
}
arr.swap(i, last_index);
i
}
---
```

View File

@@ -19,20 +19,22 @@ stories = ["dep:story"]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
markdown.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
rich_text.workspace = true
regex.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
@@ -42,6 +44,7 @@ story = { workspace = true, optional = true }
theme.workspace = true
ui.workspace = true
util.workspace = true
unindent.workspace = true
workspace.workspace = true
[dev-dependencies]
@@ -51,6 +54,7 @@ env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages.workspace = true
markdown = { workspace = true, features = ["test-support"] }
node_runtime.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
@@ -58,4 +62,5 @@ release_channel.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -2,41 +2,41 @@ mod assistant_settings;
mod attachments;
mod completion_provider;
mod saved_conversation;
mod saved_conversation_picker;
mod saved_conversations;
mod tools;
pub mod ui;
use crate::saved_conversation::{SavedConversation, SavedMessage, SavedMessageRole};
use crate::saved_conversation_picker::SavedConversationPicker;
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
ui::UserOrAssistant,
};
use crate::saved_conversation::SavedConversationMetadata;
use crate::ui::UserOrAssistant;
use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
};
use attachments::ActiveEditorAttachmentTool;
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use file_icons::FileIcons;
use fs::Fs;
use futures::{future::join_all, StreamExt};
use gpui::{
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
FocusableView, ListAlignment, ListState, Model, ReadGlobal, Render, Task, UpdateGlobal, View,
WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
use saved_conversations::SavedConversations;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use tools::AnnotationTool;
use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool};
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
use util::paths::CONVERSATIONS_DIR;
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
@@ -63,15 +63,7 @@ pub enum SubmitMode {
Codebase,
}
gpui::actions!(
assistant2,
[
Cancel,
ToggleFocus,
DebugProjectIndex,
ToggleSavedConversations
]
);
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -111,8 +103,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
},
)
.detach();
cx.observe_new_views(SavedConversationPicker::register)
.detach();
}
pub fn enabled(cx: &AppContext) -> bool {
@@ -135,22 +125,25 @@ impl AssistantPanel {
})?;
cx.new_view(|cx| {
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
let project_index = SemanticIndex::update_global(cx, |semantic_index, cx| {
semantic_index.project_index(project.clone(), cx)
});
// Used in tools to render file icons
cx.observe_global::<FileIcons>(|_, cx| {
cx.notify();
})
.detach();
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone()), cx)
.register(ProjectIndexTool::new(project_index.clone()))
.unwrap();
tool_registry
.register(
CreateBufferTool::new(workspace.clone(), project.clone()),
cx,
)
.register(CreateBufferTool::new(workspace.clone(), project.clone()))
.unwrap();
tool_registry
.register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
.register(AnnotationTool::new(workspace.clone(), project.clone()))
.unwrap();
let mut attachment_registry = AttachmentRegistry::new();
@@ -264,6 +257,8 @@ pub struct AssistantChat {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
saved_conversations: View<SavedConversations>,
saved_conversations_open: bool,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
user_store: Model<UserStore>,
@@ -274,11 +269,11 @@ pub struct AssistantChat {
tool_registry: Arc<ToolRegistry>,
attachment_registry: Arc<AttachmentRegistry>,
project_index: Model<ProjectIndex>,
markdown_style: MarkdownStyle,
}
struct EditingMessage {
id: MessageId,
old_body: Arc<str>,
body: View<Editor>,
}
@@ -294,7 +289,7 @@ impl AssistantChat {
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
let model = CompletionProvider::global(cx).default_model();
let view = cx.view().downgrade();
let list_state = ListState::new(
0,
@@ -319,6 +314,24 @@ impl AssistantChat {
_ => None,
};
let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx));
cx.spawn({
let fs = fs.clone();
let saved_conversations = saved_conversations.downgrade();
|_assistant_chat, mut cx| async move {
let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?;
cx.update(|cx| {
saved_conversations.update(cx, |this, cx| {
this.init(saved_conversation_metadata, cx);
})
})??;
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
Self {
model,
messages: Vec::new(),
@@ -328,6 +341,8 @@ impl AssistantChat {
editor.set_placeholder_text("Send a message…", cx);
editor
}),
saved_conversations,
saved_conversations_open: false,
list_state,
user_store,
fs,
@@ -341,30 +356,60 @@ impl AssistantChat {
pending_completion: None,
attachment_registry,
tool_registry,
markdown_style: MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
// @nate: Could we add inline-code specific styles to the theme?
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
},
}
}
fn editing_message_id(&self) -> Option<MessageId> {
self.editing_message.as_ref().map(|message| message.id)
fn message_for_id(&self, id: MessageId) -> Option<&ChatMessage> {
self.messages.iter().find(|message| match message {
ChatMessage::User(message) => message.id == id,
ChatMessage::Assistant(message) => message.id == id,
})
}
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
self.messages.iter().find_map(|message| match message {
ChatMessage::User(message) => message
.body
.focus_handle(cx)
.contains_focused(cx)
.then_some(message.id),
ChatMessage::Assistant(_) => None,
})
fn toggle_saved_conversations(&mut self) {
self.saved_conversations_open = !self.saved_conversations_open;
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit.
if let Some(editing_message) = self.editing_message.take() {
editing_message
.body
.update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
if self.editing_message.take().is_some() {
cx.notify();
return;
}
@@ -381,14 +426,7 @@ impl AssistantChat {
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
if let Some(focused_message_id) = self.focused_message_id(cx) {
self.truncate_messages(focused_message_id, cx);
self.pending_completion.take();
self.composer_editor.focus_handle(cx).focus(cx);
if self.editing_message_id() == Some(focused_message_id) {
self.editing_message.take();
}
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
if self.composer_editor.focus_handle(cx).is_focused(cx) {
// Don't allow multiple concurrent completions.
if self.pending_completion.is_some() {
cx.propagate();
@@ -399,10 +437,12 @@ impl AssistantChat {
let text = composer_editor.text(cx);
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_text(text, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
Markdown::new(
text,
self.markdown_style.clone(),
Some(self.language_registry.clone()),
cx,
)
});
composer_editor.clear(cx);
@@ -413,6 +453,26 @@ impl AssistantChat {
})
});
self.push_message(message, cx);
} else if let Some(editing_message) = self.editing_message.as_ref() {
let focus_handle = editing_message.body.focus_handle(cx);
if focus_handle.contains_focused(cx) {
if let Some(ChatMessage::User(user_message)) =
self.message_for_id(editing_message.id)
{
user_message.body.update(cx, |body, cx| {
body.reset(editing_message.body.read(cx).text(cx), cx);
});
}
self.truncate_messages(editing_message.id, cx);
self.pending_completion.take();
self.composer_editor.focus_handle(cx).focus(cx);
self.editing_message.take();
} else {
log::error!("unexpected state: no user message editor is focused.");
return;
}
} else {
log::error!("unexpected state: no user message editor is focused.");
return;
@@ -491,7 +551,7 @@ impl AssistantChat {
let messages = messages.await?;
let completion = cx.update(|cx| {
CompletionProvider::get(cx).complete(
CompletionProvider::global(cx).complete(
model_name,
messages,
Vec::new(),
@@ -501,18 +561,22 @@ impl AssistantChat {
});
let mut stream = completion?.await?;
let mut body = String::new();
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
messages,
..
})) = this.messages.last_mut()
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
this.messages.last_mut()
{
if messages.is_empty() {
messages.push(AssistantMessage {
body: RichText::default(),
messages.push(AssistantMessagePart {
body: cx.new_view(|cx| {
Markdown::new(
"".into(),
this.markdown_style.clone(),
Some(this.language_registry.clone()),
cx,
)
}),
tool_calls: Vec::new(),
})
}
@@ -520,35 +584,37 @@ impl AssistantChat {
let message = messages.last_mut().unwrap();
if let Some(content) = &delta.content {
body.push_str(content);
message
.body
.update(cx, |message, cx| message.append(&content, cx));
}
for tool_call in delta.tool_calls {
let index = tool_call.index as usize;
for tool_call_delta in delta.tool_calls {
let index = tool_call_delta.index as usize;
if index >= message.tool_calls.len() {
message.tool_calls.resize_with(index + 1, Default::default);
}
let call = &mut message.tool_calls[index];
let tool_call = &mut message.tool_calls[index];
if let Some(id) = &tool_call.id {
call.id.push_str(id);
if let Some(id) = &tool_call_delta.id {
tool_call.id.push_str(id);
}
match tool_call.variant {
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
if let Some(name) = &tool_call.name {
call.name.push_str(name);
}
if let Some(arguments) = &tool_call.arguments {
call.arguments.push_str(arguments);
}
match tool_call_delta.variant {
Some(proto::tool_call_delta::Variant::Function(
tool_call_delta,
)) => {
this.tool_registry.update_tool_call(
tool_call,
tool_call_delta.name.as_deref(),
tool_call_delta.arguments.as_deref(),
cx,
);
}
None => {}
}
}
message.body =
RichText::new(body.clone(), &[], &this.language_registry);
cx.notify();
} else {
unreachable!()
@@ -562,7 +628,7 @@ impl AssistantChat {
let mut tool_tasks = Vec::new();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
if let Some(ChatMessage::Assistant(AssistantMessage {
error: message_error,
messages,
..
@@ -573,54 +639,54 @@ impl AssistantChat {
cx.notify();
} else {
if let Some(current_message) = messages.last_mut() {
for tool_call in current_message.tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
for tool_call in current_message.tool_calls.iter_mut() {
tool_tasks
.extend(this.tool_registry.execute_tool_call(tool_call, cx));
}
}
}
}
})?;
// This ends recursion on calling for responses after tools
if tool_tasks.is_empty() {
return Ok(());
}
let tools = join_all(tool_tasks.into_iter()).await;
// If the WindowContext went away for any tool's view we don't include it
// especially since the below call would fail for the same reason.
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
this.messages.last_mut()
{
if let Some(current_message) = messages.last_mut() {
current_message.tool_calls = tools;
cx.notify();
} else {
unreachable!()
}
}
})?;
join_all(tool_tasks.into_iter()).await;
}
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
// If the last message is a grouped assistant message, add to the grouped message
if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
self.messages.last_mut()
{
messages.push(AssistantMessage {
body: RichText::default(),
messages.push(AssistantMessagePart {
body: cx.new_view(|cx| {
Markdown::new(
"".into(),
self.markdown_style.clone(),
Some(self.language_registry.clone()),
cx,
)
}),
tool_calls: Vec::new(),
});
return;
}
let message = ChatMessage::Assistant(GroupedAssistantMessage {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
messages: vec![AssistantMessage {
body: RichText::default(),
messages: vec![AssistantMessagePart {
body: cx.new_view(|cx| {
Markdown::new(
"".into(),
self.markdown_style.clone(),
Some(self.language_registry.clone()),
cx,
)
}),
tool_calls: Vec::new(),
}],
error: None,
@@ -668,40 +734,30 @@ impl AssistantChat {
*entry = !*entry;
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
let messages = self
.messages
.drain(..)
.map(|message| {
let text = match &message {
ChatMessage::User(message) => message.body.read(cx).text(cx),
ChatMessage::Assistant(message) => message
.messages
.iter()
.map(|message| message.body.text.to_string())
.collect::<Vec<_>>()
.join("\n\n"),
};
SavedMessage {
id: message.id(),
role: match message {
ChatMessage::User(_) => SavedMessageRole::User,
ChatMessage::Assistant(_) => SavedMessageRole::Assistant,
},
text,
}
})
.collect::<Vec<_>>();
// Reset the chat for the new conversation.
fn reset(&mut self) {
self.messages.clear();
self.list_state.reset(0);
self.editing_message.take();
self.collapsed_messages.clear();
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
let messages = std::mem::take(&mut self.messages)
.into_iter()
.map(|message| self.serialize_message(message, cx))
.collect::<Vec<_>>();
self.reset();
let title = messages
.first()
.map(|message| message.text.clone())
.map(|message| match message {
SavedChatMessage::User { body, .. } => body.clone(),
SavedChatMessage::Assistant { messages, .. } => messages
.first()
.map(|message| message.body.to_string())
.unwrap_or_default(),
})
.unwrap_or_else(|| "A conversation with the assistant.".to_string());
let saved_conversation = SavedConversation {
@@ -773,69 +829,72 @@ impl AssistantChat {
.id(SharedString::from(format!("message-{}-container", id.0)))
.when(is_first, |this| this.pt(padding))
.map(|element| {
if self.editing_message_id() == Some(*id) {
element.child(Composer::new(
body.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
)
.into_any_element(),
))
} else {
element
.on_click(cx.listener({
let id = *id;
let body = body.clone();
move |assistant_chat, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
assistant_chat.editing_message = Some(EditingMessage {
id,
body: body.clone(),
old_body: body.read(cx).text(cx).into(),
});
body.focus_handle(cx).focus(cx);
}
}
}))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
// todo!(): clean up the vec usage
vec![
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
],
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
if let Some(editing_message) = self.editing_message.as_ref() {
if editing_message.id == *id {
return element.child(Composer::new(
editing_message.body.clone(),
self.project_index_button.clone(),
self.active_file_button.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
)
// TODO: Wire up selections.
.selected(is_last),
)
.into_any_element(),
));
}
}
element
.on_click(cx.listener({
let id = *id;
let body = body.clone();
move |assistant_chat, event: &ClickEvent, cx| {
if event.up.click_count == 2 {
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
let source = Arc::from(body.read(cx).source());
editor.set_text(source, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
assistant_chat.editing_message = Some(EditingMessage {
id,
body: body.clone(),
});
body.focus_handle(cx).focus(cx);
}
}
}))
.child(
crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
// todo!(): clean up the vec usage
vec![
body.clone().into_any_element(),
h_flex()
.gap_2()
.children(
attachments
.iter()
.map(|attachment| attachment.view.clone()),
)
.into_any_element(),
],
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
)
// TODO: Wire up selections.
.selected(is_last),
)
})
.into_any(),
ChatMessage::Assistant(GroupedAssistantMessage {
ChatMessage::Assistant(AssistantMessage {
id,
messages,
error,
@@ -844,26 +903,25 @@ impl AssistantChat {
let mut message_elements = Vec::new();
for message in messages {
if !message.body.text.is_empty() {
message_elements.push(
div()
// todo!(): The element Id will need to be a combo of the base ID and the index within the grouping
.child(message.body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
if !message.body.read(cx).source().is_empty() {
message_elements.push(div().child(message.body.clone()).into_any())
}
let tools = message
.tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.filter_map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
if !tools.is_empty() {
message_elements.push(div().children(tools).into_any_element())
message_elements.push(div().children(tools).into_any())
}
}
if message_elements.is_empty() {
message_elements.push(::ui::Label::new("Researching...").into_any_element())
}
div()
.when(is_first, |this| this.pt(padding))
.child(
@@ -909,14 +967,14 @@ impl AssistantChat {
// Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User {
content: body.read(cx).text(cx),
content: body.read(cx).source().to_string(),
});
}
ChatMessage::Assistant(GroupedAssistantMessage { messages, .. }) => {
ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
for message in messages {
let body = message.body.clone();
if body.text.is_empty() && message.tool_calls.is_empty() {
if body.read(cx).source().is_empty() && message.tool_calls.is_empty() {
continue;
}
@@ -935,19 +993,17 @@ impl AssistantChat {
.collect();
completion_messages.push(CompletionMessage::Assistant {
content: Some(body.text.to_string()),
content: Some(body.read(cx).source().to_string()),
tool_calls: tool_calls_from_assistant,
});
for tool_call in &message.tool_calls {
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
let content = self.tool_registry.content_for_tool_call(
tool_call,
&mut project_context,
cx,
);
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
@@ -966,10 +1022,53 @@ impl AssistantChat {
Ok(completion_messages)
})
}
fn serialize_message(
&self,
message: ChatMessage,
cx: &mut ViewContext<AssistantChat>,
) -> SavedChatMessage {
match message {
ChatMessage::User(message) => SavedChatMessage::User {
id: message.id,
body: message.body.read(cx).source().into(),
attachments: message
.attachments
.iter()
.map(|attachment| {
self.attachment_registry
.serialize_user_attachment(attachment)
})
.collect(),
},
ChatMessage::Assistant(message) => SavedChatMessage::Assistant {
id: message.id,
error: message.error,
messages: message
.messages
.iter()
.map(|message| SavedAssistantMessagePart {
body: message.body.read(cx).source().to_string().into(),
tool_calls: message
.tool_calls
.iter()
.filter_map(|tool_call| {
self.tool_registry
.serialize_tool_call(tool_call, cx)
.log_err()
})
.collect(),
})
.collect(),
},
}
}
}
impl Render for AssistantChat {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems();
div()
.relative()
.flex_1()
@@ -978,23 +1077,60 @@ impl Render for AssistantChat {
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1().pt(header_height))
.child(
h_flex()
.gap_2()
.absolute()
.top_0()
.justify_between()
.w_full()
.h(header_height)
.p(Spacing::Small.rems(cx))
.child(
Button::new("open-saved-conversations", "Saved Conversations").on_click(
|_event, cx| cx.dispatch_action(Box::new(ToggleSavedConversations)),
),
IconButton::new(
"toggle-saved-conversations",
if self.saved_conversations_open {
IconName::ChevronRight
} else {
IconName::ChevronLeft
},
)
.on_click(cx.listener(|this, _event, _cx| {
this.toggle_saved_conversations();
}))
.tooltip(move |cx| Tooltip::text("Switch Conversations", cx)),
)
.child(
IconButton::new("new-conversation", IconName::Plus)
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
h_flex()
.gap(Spacing::Large.rems(cx))
.child(
IconButton::new("new-conversation", IconName::Plus)
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Context", cx)),
)
.child(
IconButton::new("assistant-menu", IconName::Menu)
.disabled(true)
.tooltip(move |cx| {
Tooltip::text(
"Coming soon Assistant settings & controls",
cx,
)
}),
),
),
)
.child(list(self.list_state.clone()).flex_1())
.when(self.saved_conversations_open, |element| {
element.child(
h_flex()
.absolute()
.top(header_height)
.w_full()
.child(self.saved_conversations.clone()),
)
})
.child(Composer::new(
self.composer_editor.clone(),
self.project_index_button.clone(),
@@ -1018,38 +1154,31 @@ impl MessageId {
enum ChatMessage {
User(UserMessage),
Assistant(GroupedAssistantMessage),
Assistant(AssistantMessage),
}
impl ChatMessage {
pub fn id(&self) -> MessageId {
match self {
ChatMessage::User(message) => message.id,
ChatMessage::Assistant(message) => message.id,
}
}
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self {
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
ChatMessage::User(message) => Some(message.body.focus_handle(cx)),
ChatMessage::Assistant(_) => None,
}
}
}
struct UserMessage {
id: MessageId,
body: View<Editor>,
attachments: Vec<UserAttachment>,
pub id: MessageId,
pub body: View<Markdown>,
pub attachments: Vec<UserAttachment>,
}
struct AssistantMessagePart {
pub body: View<Markdown>,
pub tool_calls: Vec<ToolFunctionCall>,
}
struct AssistantMessage {
body: RichText,
tool_calls: Vec<ToolFunctionCall>,
}
struct GroupedAssistantMessage {
id: MessageId,
messages: Vec<AssistantMessage>,
error: Option<SharedString>,
pub id: MessageId,
pub messages: Vec<AssistantMessagePart>,
pub error: Option<SharedString>,
}

View File

@@ -1,64 +1,68 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
#[derive(Serialize, Deserialize)]
pub struct ActiveEditorAttachment {
buffer: WeakModel<Buffer>,
path: Option<ProjectPath>,
#[serde(skip)]
buffer: Option<WeakModel<Buffer>>,
path: Option<PathBuf>,
}
pub struct FileAttachmentView {
output: Result<ActiveEditorAttachment>,
project_path: Option<ProjectPath>,
buffer: Option<WeakModel<Buffer>>,
error: Option<anyhow::Error>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(attachment) => {
let filename: SharedString = attachment
.path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
// todo!(): make the button link to the actual file to open
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip({
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
})
.into_any_element()
}
Err(err) => div().child(err.to_string()).into_any_element(),
if let Some(error) = &self.error {
return div().child(error.to_string()).into_any_element();
}
let filename: SharedString = self
.project_path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
.into_any_element()
}
}
impl ToolOutput for FileAttachmentView {
impl AttachmentOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Ok(result) = &self.output {
if let Some(path) = &result.path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
} else if let Some(buffer) = result.buffer.upgrade() {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
if let Some(path) = &self.project_path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
}
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
String::new()
}
}
@@ -77,6 +81,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn name(&self) -> Arc<str> {
"active-editor-attachment".into()
}
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
@@ -91,13 +99,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path =
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let path = project::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
return Ok(ActiveEditorAttachment {
buffer: buffer.downgrade(),
buffer: Some(buffer.downgrade()),
path,
});
} else {
@@ -106,7 +111,34 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
}))
}
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| FileAttachmentView { output })
fn view(
&self,
output: Result<ActiveEditorAttachment>,
cx: &mut WindowContext,
) -> View<Self::View> {
let error;
let project_path;
let buffer;
match output {
Ok(output) => {
error = None;
let workspace = self.workspace.upgrade().unwrap();
let project = workspace.read(cx).project();
project_path = output
.path
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
buffer = output.buffer;
}
Err(err) => {
error = Some(err);
buffer = None;
project_path = None;
}
}
cx.new_view(|_cx| FileAttachmentView {
project_path,
buffer,
error,
})
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AppContext, Global};
use gpui::Global;
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
@@ -11,10 +11,6 @@ pub use open_ai::RequestMessage as CompletionMessage;
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl CompletionProvider {
pub fn get(cx: &AppContext) -> &Self {
cx.global::<CompletionProvider>()
}
pub fn new(backend: impl CompletionProviderBackend) -> Self {
Self(Arc::new(backend))
}

View File

@@ -1,4 +1,16 @@
use std::cmp::Reverse;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
use fs::Fs;
use futures::StreamExt;
use gpui::SharedString;
use regex::Regex;
use serde::{Deserialize, Serialize};
use util::paths::CONVERSATIONS_DIR;
use crate::MessageId;
@@ -8,42 +20,71 @@ pub struct SavedConversation {
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedMessage>,
pub messages: Vec<SavedChatMessage>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SavedMessageRole {
User,
Assistant,
pub enum SavedChatMessage {
User {
id: MessageId,
body: String,
attachments: Vec<SavedUserAttachment>,
},
Assistant {
id: MessageId,
messages: Vec<SavedAssistantMessagePart>,
error: Option<SharedString>,
},
}
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub role: SavedMessageRole,
pub text: String,
pub struct SavedAssistantMessagePart {
pub body: SharedString,
pub tool_calls: Vec<SavedToolFunctionCall>,
}
/// Returns a list of placeholder conversations for mocking the UI.
///
/// Once we have real saved conversations to pull from we can use those instead.
pub fn placeholder_conversations() -> Vec<SavedConversation> {
vec![
SavedConversation {
version: "0.3.0".to_string(),
title: "How to get a list of exported functions in an Erlang module".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "7 wonders of the ancient world".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
messages: vec![],
},
]
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.\d.\d.\d.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the old assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -5,57 +5,66 @@ use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, V
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::saved_conversation::{self, SavedConversation};
use crate::ToggleSavedConversations;
use crate::saved_conversation::SavedConversationMetadata;
pub struct SavedConversationPicker {
picker: View<Picker<SavedConversationPickerDelegate>>,
pub struct SavedConversations {
focus_handle: FocusHandle,
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
}
impl EventEmitter<DismissEvent> for SavedConversationPicker {}
impl EventEmitter<DismissEvent> for SavedConversations {}
impl ModalView for SavedConversationPicker {}
impl FocusableView for SavedConversationPicker {
impl FocusableView for SavedConversations {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
if let Some(picker) = self.picker.as_ref() {
picker.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl SavedConversationPicker {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
workspace.toggle_modal(cx, move |cx| {
let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
Self::new(delegate, cx)
});
});
impl SavedConversations {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
picker: None,
}
}
pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
pub fn init(
&mut self,
saved_conversations: Vec<SavedConversationMetadata>,
cx: &mut ViewContext<Self>,
) {
let delegate =
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
}
}
impl Render for SavedConversationPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
impl Render for SavedConversations {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_full()
.bg(cx.theme().colors().panel_background)
.children(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversationPicker>,
saved_conversations: Vec<SavedConversation>,
view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
let saved_conversations = saved_conversation::placeholder_conversations();
pub fn new(
weak_view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
) -> Self {
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
@@ -176,7 +185,6 @@ impl PickerDelegate for SavedConversationPickerDelegate {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(

View File

@@ -1,12 +1,13 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use gpui::{prelude::*, AnyElement, Model, Task, View, WeakView};
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
use language::ToPoint;
use project::{Project, ProjectPath};
use project::{search::SearchQuery, Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
@@ -25,26 +26,30 @@ impl AnnotationTool {
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
#[serde(default = "default_title")]
title: String,
annotations: Vec<Annotation>,
/// Excerpts from the file to show to the user.
excerpts: Vec<Excerpt>,
}
fn default_title() -> String {
"Untitled".to_string()
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Annotation {
struct Excerpt {
/// Path to the file
path: String,
/// Name of a symbol in the code
symbol_name: String,
/// Text to display near the symbol definition
text: String,
/// A short, distinctive string that appears in the file, used to define a location in the file.
text_passage: String,
/// Text to display above the code excerpt
annotation: String,
}
impl LanguageModelTool for AnnotationTool {
type Input = AnnotationInput;
type Output = String;
type View = AnnotationResultView;
fn name(&self) -> String {
@@ -55,105 +60,137 @@ impl LanguageModelTool for AnnotationTool {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let workspace = self.workspace.clone();
let project = self.project.clone();
let excerpts = input.annotations.clone();
let title = input.title.clone();
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|cx| {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
cx.spawn(|view, mut cx| async move {
while let Some(excerpt) = rx.next().await {
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
}
anyhow::Ok(())
})
.detach();
AnnotationResultView {
project: self.project.clone(),
workspace: self.workspace.clone(),
tx,
pending_excerpt: None,
added_editor_to_workspace: false,
editor: None,
error: None,
rendered_excerpt_count: 0,
}
})
}
}
pub struct AnnotationResultView {
workspace: WeakView<Workspace>,
project: Model<Project>,
pending_excerpt: Option<Excerpt>,
added_editor_to_workspace: bool,
editor: Option<View<Editor>>,
tx: UnboundedSender<Excerpt>,
error: Option<anyhow::Error>,
rendered_excerpt_count: usize,
}
impl AnnotationResultView {
async fn add_excerpt(
this: WeakView<Self>,
excerpt: Excerpt,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _cx| this.project.clone())?;
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
});
})?;
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Task::ready(Err(anyhow::anyhow!("No worktree found")));
return Err(anyhow::anyhow!("No worktree found"));
};
let buffer_tasks = project.update(cx, |project, cx| {
let excerpts = excerpts.clone();
excerpts
.iter()
.map(|excerpt| {
let project_path = ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
};
project.open_buffer(project_path.clone(), cx)
let buffer_task = project.update(cx, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
},
cx,
)
})?;
let buffer = match buffer_task.await {
Ok(buffer) => buffer,
Err(error) => {
return this.update(cx, |this, cx| {
this.error = Some(error);
cx.notify();
})
.collect::<Vec<_>>()
});
}
};
cx.spawn(move |mut cx| async move {
let buffers = futures::future::try_join_all(buffer_tasks).await?;
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
let matches = query.search(&snapshot, None).await;
let Some(first_match) = matches.first() else {
log::warn!(
"text {:?} does not appear in '{}'",
excerpt.text_passage,
excerpt.path
);
return Ok(());
};
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(title)
})?;
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))?;
this.update(cx, |this, cx| {
let mut start = first_match.start.to_point(&snapshot);
start.column = 0;
for (excerpt, buffer) in excerpts.iter().zip(buffers.iter()) {
let snapshot = buffer.update(&mut cx, |buffer, _cx| buffer.snapshot())?;
if let Some(editor) = &this.editor {
editor.update(cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
if let Some(outline) = snapshot.outline(None) {
let matches = outline
.search(&excerpt.symbol_name, cx.background_executor().clone())
.await;
if let Some(mat) = matches.first() {
let item = &outline.items[mat.candidate_id];
let start = item.range.start.to_point(&snapshot);
editor.update(&mut cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let explanation = SharedString::from(excerpt.text.clone());
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: 2,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
Self::render_note_block(&explanation, cx)
}),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
})?;
}
let annotation = SharedString::from(excerpt.annotation);
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: annotation.split('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
});
if !this.added_editor_to_workspace {
this.added_editor_to_workspace = true;
this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
}
}
})?;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
anyhow::Ok("showed comments to users in a new view".into())
})
Ok(())
}
fn output_view(
_: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| AnnotationResultView { output })
}
}
impl AnnotationTool {
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
@@ -179,24 +216,89 @@ impl AnnotationTool {
}
}
pub struct AnnotationResultView {
output: Result<String>,
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(output) => div().child(output.clone().into_any_element()),
Err(error) => div().child(format!("failed to open path: {:?}", error)),
if let Some(error) = &self.error {
ui::Label::new(error.to_string()).into_any_element()
} else {
ui::Label::new(SharedString::from(format!(
"Opened a buffer with {} excerpts",
self.rendered_excerpt_count
)))
.into_any_element()
}
}
}
impl ToolOutput for AnnotationResultView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(output) => output.clone(),
Err(err) => format!("Failed to create buffer: {err:?}"),
impl ToolView for AnnotationResultView {
type Input = AnnotationInput;
type SerializedState = Option<String>;
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
if let Some(error) = &self.error {
format!("Failed to create buffer: {error:?}")
} else {
format!(
"opened {} excerpts in a buffer",
self.rendered_excerpt_count
)
}
}
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
let editor = if let Some(editor) = &self.editor {
editor.clone()
} else {
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
});
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
});
self.editor = Some(editor.clone());
editor
};
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multibuffer, cx| {
if multibuffer.title(cx) != input.title {
multibuffer.set_title(input.title.clone(), cx);
}
});
self.pending_excerpt = input.excerpts.pop();
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.rendered_excerpt_count = input.excerpts.len();
});
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(excerpt) = self.pending_excerpt.take() {
self.rendered_excerpt_count += 1;
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.tx.close_channel();
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.error.as_ref().map(|error| error.to_string())
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
if let Some(error_message) = output {
self.error = Some(anyhow::anyhow!("{}", error_message));
}
Ok(())
}
}

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
@@ -20,7 +20,7 @@ impl CreateBufferTool {
}
}
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
@@ -32,25 +32,70 @@ pub struct CreateBufferInput {
}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = ();
type View = CreateBufferView;
fn name(&self) -> String {
"create_buffer".to_string()
"create_file".to_string()
}
fn description(&self) -> String {
"Create a new buffer in the current codebase".to_string()
"Create a new untitled file in the current codebase. Side effect: opens it in a new pane/tab for the user to edit.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
workspace: self.workspace.clone(),
project: self.project.clone(),
input: None,
error: None,
})
}
}
pub struct CreateBufferView {
workspace: WeakView<Workspace>,
project: Model<Project>,
input: Option<CreateBufferInput>,
error: Option<anyhow::Error>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ui::Label::new("Opening a buffer")
}
}
impl ToolView for CreateBufferView {
type Input = CreateBufferInput;
type SerializedState = ();
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
let Some(input) = self.input.as_ref() else {
return "No input".to_string();
};
match &self.error {
None => format!("Created a new {} buffer", input.language),
Some(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let text = input.text.clone();
let language_name = input.language.clone();
|mut cx| async move {
let input = self.input.clone();
|_this, mut cx| async move {
let input = input.ok_or_else(|| anyhow!("no input"))?;
let text = input.text.clone();
let language_name = input.language.clone();
let language = cx
.update(|cx| {
project
@@ -86,34 +131,15 @@ impl LanguageModelTool for CreateBufferTool {
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
language: input.language,
output,
})
}
}
pub struct CreateBufferView {
language: String,
output: Result<()>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}
impl ToolOutput for CreateBufferView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(_) => format!("Created a new {} buffer", self.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
()
}
fn deserialize(
&mut self,
_output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
Ok(())
}
}

View File

@@ -1,13 +1,20 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolOutput};
use assistant_tooling::{LanguageModelTool, ToolView};
use collections::BTreeMap;
use gpui::{prelude::*, Model, Task};
use file_icons::FileIcons;
use gpui::{prelude::*, AnyElement, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
use serde::{Deserialize, Serialize};
use std::{
fmt::Write as _,
ops::Range,
path::{Path, PathBuf},
str::FromStr as _,
sync::Arc,
};
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
@@ -15,126 +22,373 @@ pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
#[derive(Default)]
enum ProjectIndexToolState {
#[default]
CollectingQuery,
Searching,
Error(anyhow::Error),
Finished {
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
index_status: Status,
},
}
pub struct ProjectIndexView {
project_index: Model<ProjectIndex>,
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
element_id: ElementId,
expanded_header: bool,
state: ProjectIndexToolState,
}
pub struct ProjectIndexOutput {
status: Status,
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
#[derive(Default, Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Criteria to include results
includes: Option<SearchFilter>,
/// Criteria to exclude results
excludes: Option<SearchFilter>,
}
impl ProjectIndexView {
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
let element_id = ElementId::Name(nanoid::nanoid!().into());
#[derive(Deserialize, JsonSchema, Clone, Default)]
pub struct SearchFilter {
/// Filter by file path prefix
prefix_path: Option<String>,
/// Filter by file extension
extension: Option<String>,
// Note: we possibly can't do content filtering very easily given the project context handling
// the final results, so we're leaving out direct string matches for now
}
Self {
input,
output,
element_id,
expanded_header: false,
fn project_starts_with(prefix_path: Option<String>, project_path: ProjectPath) -> bool {
if let Some(path) = &prefix_path {
if let Some(path) = PathBuf::from_str(path).ok() {
return project_path.path.starts_with(path);
}
}
return false;
}
impl SearchFilter {
fn matches(&self, project_path: &ProjectPath) -> bool {
let path_match = project_starts_with(self.prefix_path.clone(), project_path.clone());
path_match
&& (if let Some(extension) = &self.extension {
project_path
.path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == extension)
.unwrap_or(false)
} else {
true
})
}
}
#[derive(Serialize, Deserialize)]
pub struct SerializedState {
index_status: Status,
error_message: Option<String>,
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
}
#[derive(Default, Serialize, Deserialize)]
struct WorktreeIndexOutput {
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
}
fn render_filter_section(
&mut self,
heading: &str,
filter: Option<SearchFilter>,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let filter = match filter {
Some(filter) => filter,
None => return None,
};
// Any of the filter fields can be empty. We'll show nothing if they're all empty.
let path = filter.prefix_path.as_ref().map(|path| {
let icon_path = FileIcons::get_icon(Path::new(path), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Paths: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(path.clone()).color(Color::Muted))
});
let extension = filter.extension.as_ref().map(|extension| {
let icon_path = FileIcons::get_icon(Path::new(extension), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Extensions: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(extension.clone()).color(Color::Muted))
});
if path.is_none() && extension.is_none() {
return None;
}
Some(
v_flex()
.child(ui::Label::new(heading.to_string()))
.gap_1()
.children(path)
.children(extension)
.into_any_element(),
)
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
let (header_text, content) = match &self.state {
ProjectIndexToolState::Error(error) => {
return format!("failed to search: {error:?}").into_any_element()
}
Ok(output) => output,
};
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
("Searching...".to_string(), div())
}
ProjectIndexToolState::Finished { excerpts, .. } => {
let file_count = excerpts.len();
let file_count = output.excerpts.len();
if excerpts.is_empty() {
("No results found".to_string(), div())
} else {
let header_text = format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
);
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
)
}));
(header_text, el)
}
}
};
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
));
.child(header_text);
v_flex().gap_3().child(
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(
v_flex()
.gap_2()
.children(output.excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
})),
),
),
)
v_flex()
.gap_3()
.child(
CollapsibleContainer::new("collapsible-container", self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.children(self.render_filter_section(
"Includes",
self.input.includes.clone(),
cx,
))
.children(self.render_filter_section(
"Excludes",
self.input.excludes.clone(),
cx,
))
.child(content),
),
)
.into_any_element()
}
}
impl ToolOutput for ProjectIndexView {
impl ToolView for ProjectIndexView {
type Input = CodebaseQuery;
type SerializedState = SerializedState;
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut WindowContext,
_: &mut ViewContext<Self>,
) -> String {
match &self.output {
Ok(output) => {
match &self.state {
ProjectIndexToolState::CollectingQuery => String::new(),
ProjectIndexToolState::Searching => String::new(),
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in &output.excerpts {
for (project_path, ranges) in excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if output.status != Status::Idle {
if *index_status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = input;
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.state = ProjectIndexToolState::Searching;
cx.notify();
let project_index = self.project_index.read(cx);
let index_status = project_index.status();
// TODO: wire the filters into the search here instead of processing after.
// Otherwise we'll get zero results sometimes.
let search = project_index.search(self.input.query.clone(), DEFAULT_SEARCH_LIMIT, cx);
let includes = self.input.includes.clone();
let excludes = self.input.excludes.clone();
cx.spawn(|this, mut cx| async move {
let search_result = search.await;
this.update(&mut cx, |this, cx| {
match search_result {
Ok(search_results) => {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
for search_result in search_results {
let project_path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path,
};
if let Some(includes) = &includes {
if !includes.matches(&project_path) {
continue;
}
} else if let Some(excludes) = &excludes {
if excludes.matches(&project_path) {
continue;
}
}
excerpts
.entry(project_path)
.or_default()
.push(search_result.range);
}
this.state = ProjectIndexToolState::Finished {
excerpts,
index_status,
};
}
Err(error) => {
this.state = ProjectIndexToolState::Error(error);
}
}
cx.notify();
})
})
}
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
let mut serialized = SerializedState {
error_message: None,
index_status: Status::Idle,
worktrees: Default::default(),
};
match &self.state {
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
serialized.index_status = *index_status;
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (project_path, excerpts) in excerpts {
if let Some(worktree) =
project.worktree_for_id(project_path.worktree_id, cx)
{
let worktree_path = worktree.read(cx).abs_path();
serialized
.worktrees
.entry(worktree_path)
.or_default()
.excerpts
.insert(project_path.path.clone(), excerpts.clone());
}
}
}
}
_ => {}
}
serialized
}
fn deserialize(
&mut self,
serialized: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()> {
if !serialized.worktrees.is_empty() {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (worktree_path, worktree_state) in serialized.worktrees {
if let Some(worktree) = project
.worktrees()
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
{
let worktree_id = worktree.read(cx).id();
for (path, serialized_excerpts) in worktree_state.excerpts {
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
}
}
}
}
self.state = ProjectIndexToolState::Finished {
excerpts,
index_status: serialized.index_status,
};
}
cx.notify();
Ok(())
}
}
impl ProjectIndexTool {
@@ -144,66 +398,31 @@ impl ProjectIndexTool {
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
"semantic_search_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base and an embedding of the query.".to_string()
unindent::unindent(
r#"This search tool uses a semantic index to perform search queries across your codebase, identifying and returning excerpts of text and code possibly related to the query.
Ideal for:
- Discovering implementations of similar logic within the project
- Finding usage examples of functions, classes/structures, libraries, and other code elements
- Developing understanding of the codebase's architecture and design
Note: The search's effectiveness is directly related to the current state of the codebase and the specificity of your query. It is recommended that you use snippets of code that are similar to the code you wish to find."#,
)
}
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let status = project_index.status();
let search = project_index.search(
query.query.clone(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
cx.spawn(|mut cx| async move {
let search_results = search.await?;
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
excerpts: Default::default(),
};
for search_result in search_results {
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.clone(),
};
let excerpts_for_path = output.excerpts.entry(path).or_default();
let ix = match excerpts_for_path
.binary_search_by_key(&search_result.range.start, |r| r.start)
{
Ok(ix) | Err(ix) => ix,
};
excerpts_for_path.insert(ix, search_result.range);
}
output
})
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
cx.new_view(|_| ProjectIndexView {
state: ProjectIndexToolState::CollectingQuery,
input: Default::default(),
expanded_header: false,
project_index: self.project_index.clone(),
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView::new(input, output))
}
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -119,7 +119,7 @@ impl RenderOnce for ChatMessage {
)
.when(self.messages.len() > 0, |el| {
el.child(
h_flex().child(
h_flex().w_full().child(
v_flex()
.relative()
.overflow_hidden()

View File

@@ -3,7 +3,7 @@ use crate::{
AssistantChat, CompletionProvider,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use gpui::{AnyElement, FontStyle, FontWeight, ReadGlobal, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings;
use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
@@ -48,68 +48,73 @@ impl RenderOnce for Composer {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let mut editor_border = cx.theme().colors().text;
editor_border.fade_out(0.90);
// Remove the extra 1px added by the border
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
h_flex()
.p(Spacing::Small.rems(cx))
.w_full()
.items_start()
.child(
v_flex().size_full().gap_1().child(
v_flex()
.w_full()
.p_3()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
v_flex()
.w_full()
.rounded_lg()
.p(padding)
.border_1()
.border_color(editor_border)
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
),
.child(h_flex().gap_1().child(self.model_selector)),
),
),
)
}
}
@@ -134,7 +139,7 @@ impl RenderOnce for ModelSelector {
popover_menu("model-switcher")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::get(cx).available_models() {
for model in CompletionProvider::global(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();

View File

@@ -16,11 +16,14 @@ anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true
project.workspace = true
repair_json.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
sum_tree.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]

View File

@@ -1,16 +1,16 @@
# Assistant Tooling
Bringing OpenAI compatible tool calling to GPUI.
Bringing Language Model tool calling to GPUI.
This unlocks:
- **Structured Extraction** of model responses
- **Validation** of model inputs
- **Execution** of chosen toolsn
- **Execution** of chosen tools
## Overview
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When make a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
Language Models can produce structured outputs that are perfect for calling functions. The most famous of these is OpenAI's tool calling. When making a chat completion you can pass a list of tools available to the model. The model will choose `0..n` tools to help them complete a user's task. It's up to _you_ to create the tools that the model can call.
> **User**: "Hey I need help with implementing a collapsible panel in GPUI"
>
@@ -22,187 +22,64 @@ Language Models can produce structured outputs that are perfect for calling func
>
> **Assistant**: "Here are some excerpts from the GPUI codebase that might help you."
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with a simple trait, `LanguageModelTool`.
This library is designed to facilitate this interaction mode by allowing you to go from `struct` to `tool` with two simple traits, `LanguageModelTool` and `ToolView`.
## Example
Let's expose querying a semantic index directly by the model. First, we'll set up some _necessary_ imports
## Using the Tool Registry
```rust
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use gpui::{App, AppContext, Task};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
```
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(WeatherTool { api_client },
})
.unwrap(); // You can only register one tool per name
Then we'll define the query structure the model must fill in. This _must_ derive `Deserialize` from `serde` and `JsonSchema` from the `schemars` crate.
```rust
#[derive(Deserialize, JsonSchema)]
struct CodebaseQuery {
query: String,
}
```
After that we can define our tool, with the expectation that it will need a `ProjectIndex` to search against. For this example, the index uses the same interface as `semantic_index::ProjectIndex`.
```rust
struct ProjectIndex {}
impl ProjectIndex {
fn new() -> Self {
ProjectIndex {}
}
fn search(&self, _query: &str, _limit: usize, _cx: &AppContext) -> Task<Result<Vec<String>>> {
// Instead of hooking up a real index, we're going to fake it
if _query.contains("gpui") {
return Task::ready(Ok(vec![r#"// crates/gpui/src/gpui.rs
//! # Welcome to GPUI!
//!
//! GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
//! for Rust, designed to support a wide variety of applications
"#
.to_string()]));
}
return Task::ready(Ok(vec![]));
}
}
struct ProjectIndexTool {
project_index: ProjectIndex,
}
```
Now we can implement the `LanguageModelTool` trait for our tool by:
- Defining the `Input` from the model, which is `CodebaseQuery`
- Defining the `Output`
- Implementing the `name` and `description` functions to provide the model information when it's choosing a tool
- Implementing the `execute` function to run the tool
```rust
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = String;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Executes a query against the codebase, returning excerpts related to the query".to_string()
}
fn execute(&self, query: Self::Input, cx: &AppContext) -> Task<Result<Self::Output>> {
let results = self.project_index.search(query.query.as_str(), 10, cx);
cx.spawn(|_cx| async move {
let results = results.await?;
if !results.is_empty() {
Ok(results.join("\n"))
} else {
Ok("No results".to_string())
}
})
}
}
```
For the sake of this example, let's look at the types that OpenAI will be passing to us
```rust
// OpenAI definitions, shown here for demonstration
#[derive(Deserialize)]
struct FunctionCall {
name: String,
args: String,
}
#[derive(Deserialize, Eq, PartialEq)]
enum ToolCallType {
#[serde(rename = "function")]
Function,
Other,
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
struct ToolCallId(String);
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum ToolCall {
Function {
#[allow(dead_code)]
id: ToolCallId,
function: FunctionCall,
},
Other {
#[allow(dead_code)]
id: ToolCallId,
},
}
#[derive(Deserialize)]
struct AssistantMessage {
role: String,
content: Option<String>,
tool_calls: Option<Vec<ToolCall>>,
}
```
When the model wants to call tools, it will pass a list of `ToolCall`s. When those are `function`s that we can handle, we'll pass them to our `ToolRegistry` to get a future that we can await.
```rust
// Inside `fn main()`
App::new().run(|cx: &mut AppContext| {
let tool = ProjectIndexTool {
project_index: ProjectIndex::new(),
};
let mut registry = ToolRegistry::new();
let registered = registry.register(tool);
assert!(registered.is_ok());
```
Let's pretend the model sent us back a message requesting
```rust
let model_response = json!({
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"function": {
"name": "query_codebase",
"args": r#"{"query":"GPUI Task background_executor"}"#
},
"type": "function"
}
]
let completion = cx.update(|cx| {
CompletionProvider::get(cx).complete(
model_name,
messages,
Vec::new(),
1.0,
// The definitions get passed directly to OpenAI when you want
// the model to be able to call your tool
tool_registry.definitions(),
)
});
let message: AssistantMessage = serde_json::from_value(model_response).unwrap();
let mut stream = completion?.await?;
// We know there's a tool call, so let's skip straight to it for this example
let tool_calls = message.tool_calls.as_ref().unwrap();
let tool_call = tool_calls.get(0).unwrap();
let mut message = AssistantMessage::new();
while let Some(delta) = stream.next().await {
// As messages stream in, you'll get both assistant content
if let Some(content) = &delta.content {
message
.body
.update(cx, |message, cx| message.append(&content, cx));
}
// And tool calls!
for tool_call_delta in delta.tool_calls {
let index = tool_call_delta.index as usize;
if index >= message.tool_calls.len() {
message.tool_calls.resize_with(index + 1, Default::default);
}
let tool_call = &mut message.tool_calls[index];
// Build up an ID
if let Some(id) = &tool_call_delta.id {
tool_call.id.push_str(id);
}
tool_registry.update_tool_call(
tool_call,
tool_call_delta.name.as_deref(),
tool_call_delta.arguments.as_deref(),
cx,
);
}
}
```
We can now use our registry to call the tool.
Once the stream of tokens is complete, you can exexute the tool call by calling `tool_registry.execute_tool_call(tool_call, cx)`, which returns a `Task<Result<()>>`.
```rust
let task = registry.call(
tool_call.name,
tool_call.args,
);
cx.spawn(|_cx| async move {
let result = task.await?;
println!("{}", result.unwrap());
Ok(())
})
```
As the tokens stream in and tool calls are executed, your `ToolView` will get updates. Render each tool call by passing that `tool_call` in to `tool_registry.render_tool_call(tool_call, cx)`. The final message for the model can be pulled by calling `self.tool_registry.content_for_tool_call( tool_call, &mut project_context, cx, )`.

View File

@@ -2,8 +2,12 @@ mod attachment_registry;
mod project_context;
mod tool_registry;
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
pub use attachment_registry::{
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
UserAttachment,
};
pub use project_context::ProjectContext;
pub use tool_registry::{
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition,
ToolRegistry, ToolView,
};

View File

@@ -1,8 +1,10 @@
use crate::{ProjectContext, ToolOutput};
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use collections::HashMap;
use futures::future::join_all;
use gpui::{AnyView, Render, Task, View, WindowContext};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
sync::{
@@ -16,25 +18,39 @@ pub struct AttachmentRegistry {
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
}
pub trait AttachmentOutput {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
}
pub trait LanguageModelAttachment {
type Output: 'static;
type View: Render + ToolOutput;
type Output: DeserializeOwned + Serialize + 'static;
type View: Render + AttachmentOutput;
fn name(&self) -> Arc<str>;
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
}
/// A collected attachment from running an attachment tool
pub struct UserAttachment {
pub view: AnyView,
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
}
#[derive(Serialize, Deserialize)]
pub struct SavedUserAttachment {
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
}
/// Internal representation of an attachment tool to allow us to treat them dynamically
struct RegisteredAttachment {
name: Arc<str>,
enabled: AtomicBool,
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
}
impl AttachmentRegistry {
@@ -45,24 +61,65 @@ impl AttachmentRegistry {
}
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
let call = Box::new(move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = Arc::new(attachment);
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let view = cx.update(|cx| A::view(result, cx))?;
let call = Box::new({
let attachment = attachment.clone();
move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = attachment.clone();
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let serialized_output =
result
.as_ref()
.map_err(ToString::to_string)
.and_then(|output| {
Ok(RawValue::from_string(
serde_json::to_string(output).map_err(|e| e.to_string())?,
)
.unwrap())
});
let view = cx.update(|cx| attachment.view(result, cx))?;
Ok(UserAttachment {
name: attachment.name(),
view: view.into(),
generate_fn: generate::<A>,
serialized_output,
})
})
}
});
let deserialize = Box::new({
let attachment = attachment.clone();
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
let serialized_output = saved_attachment.serialized_output.clone();
let output = match &serialized_output {
Ok(serialized_output) => {
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
}
Err(error) => Err(anyhow!("{error}")),
};
let view = attachment.view(output, cx).into();
Ok(UserAttachment {
view: view.into(),
name: saved_attachment.name.clone(),
view,
serialized_output,
generate_fn: generate::<A>,
})
})
}
});
self.registered_attachments.insert(
TypeId::of::<A>(),
RegisteredAttachment {
name: attachment.name(),
call,
deserialize,
enabled: AtomicBool::new(true),
},
);
@@ -134,6 +191,35 @@ impl AttachmentRegistry {
.collect())
})
}
pub fn serialize_user_attachment(
&self,
user_attachment: &UserAttachment,
) -> SavedUserAttachment {
SavedUserAttachment {
name: user_attachment.name.clone(),
serialized_output: user_attachment.serialized_output.clone(),
}
}
pub fn deserialize_user_attachment(
&self,
saved_user_attachment: SavedUserAttachment,
cx: &mut WindowContext,
) -> Result<UserAttachment> {
if let Some(registered_attachment) = self
.registered_attachments
.values()
.find(|attachment| attachment.name == saved_user_attachment.name)
{
(registered_attachment.deserialize)(&saved_user_attachment, cx)
} else {
Err(anyhow!(
"no attachment tool for name {}",
saved_user_attachment.name
))
}
}
}
impl UserAttachment {

View File

@@ -1,41 +1,67 @@
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use gpui::{
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
};
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
use repair_json::repair;
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Deserialize;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
collections::HashMap,
fmt::Display,
mem,
sync::atomic::{AtomicBool, Ordering::SeqCst},
};
use crate::ProjectContext;
use ui::ViewContext;
pub struct ToolRegistry {
registered_tools: HashMap<String, RegisteredTool>,
}
#[derive(Default, Deserialize)]
#[derive(Default)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(skip)]
pub result: Option<ToolFunctionCallResult>,
state: ToolFunctionCallState,
}
pub enum ToolFunctionCallResult {
#[derive(Default)]
enum ToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
ParsingFailed,
Finished {
view: AnyView,
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
},
KnownTool(Box<dyn InternalToolView>),
ExecutedTool(Box<dyn InternalToolView>),
}
#[derive(Clone)]
trait InternalToolView {
fn view(&self) -> AnyView;
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
}
#[derive(Default, Serialize, Deserialize)]
pub struct SavedToolFunctionCall {
id: String,
name: String,
arguments: String,
state: SavedToolFunctionCallState,
}
#[derive(Default, Serialize, Deserialize)]
enum SavedToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
KnownTool,
ExecutedTool(Box<RawValue>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolFunctionDefinition {
pub name: String,
pub description: String,
@@ -43,14 +69,7 @@ pub struct ToolFunctionDefinition {
}
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: for<'de> Deserialize<'de> + JsonSchema;
/// The output returned by executing the tool.
type Output: 'static;
type View: Render + ToolOutput;
type View: ToolView;
/// Returns the name of the tool.
///
@@ -66,7 +85,7 @@ pub trait LanguageModelTool {
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
fn definition(&self) -> ToolFunctionDefinition {
let root_schema = schema_for!(Self::Input);
let root_schema = schema_for!(<Self::View as ToolView>::Input);
ToolFunctionDefinition {
name: self.name(),
@@ -75,29 +94,34 @@ pub trait LanguageModelTool {
}
}
/// Executes the tool with the given input.
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View>;
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
div()
}
/// A view of the output of running the tool, for displaying to the user.
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
}
pub trait ToolOutput: Sized {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
pub trait ToolView: Render {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: DeserializeOwned + JsonSchema;
/// The output returned by executing the tool.
type SerializedState: DeserializeOwned + Serialize;
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
fn deserialize(
&mut self,
output: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()>;
}
struct RegisteredTool {
enabled: AtomicBool,
type_id: TypeId,
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn InternalToolView>>,
definition: ToolFunctionDefinition,
}
@@ -134,68 +158,141 @@ impl ToolRegistry {
.collect()
}
pub fn render_tool_call(
pub fn update_tool_call(
&self,
tool_call: &ToolFunctionCall,
call: &mut ToolFunctionCall,
name: Option<&str>,
arguments: Option<&str>,
cx: &mut WindowContext,
) -> AnyElement {
match &tool_call.result {
Some(result) => div()
.p_2()
.child(result.into_any_element(&tool_call.name))
.into_any_element(),
None => self
.registered_tools
.get(&tool_call.name)
.map(|tool| (tool.render_running)(cx))
.unwrap_or_else(|| div().into_any_element()),
) {
if let Some(name) = name {
call.name.push_str(name);
}
if let Some(arguments) = arguments {
if call.arguments.is_empty() {
if let Some(tool) = self.registered_tools.get(&call.name) {
let view = (tool.build_view)(cx);
call.state = ToolFunctionCallState::KnownTool(view);
} else {
call.state = ToolFunctionCallState::NoSuchTool;
}
}
call.arguments.push_str(arguments);
if let ToolFunctionCallState::KnownTool(view) = &call.state {
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
view.try_set_input(&repaired_arguments, cx)
}
}
}
}
pub fn register<T: 'static + LanguageModelTool>(
&mut self,
tool: T,
pub fn execute_tool_call(
&self,
tool_call: &mut ToolFunctionCall,
cx: &mut WindowContext,
) -> Option<Task<Result<()>>> {
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
let task = view.execute(cx);
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
Some(task)
} else {
None
}
}
pub fn render_tool_call(
&self,
tool_call: &ToolFunctionCall,
_cx: &mut WindowContext,
) -> Result<()> {
) -> Option<AnyElement> {
match &tool_call.state {
ToolFunctionCallState::NoSuchTool => {
Some(ui::Label::new("No such tool").into_any_element())
}
ToolFunctionCallState::Initializing => None,
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
Some(view.view().into_any_element())
}
}
}
pub fn content_for_tool_call(
&self,
tool_call: &ToolFunctionCall,
project_context: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match &tool_call.state {
ToolFunctionCallState::Initializing => String::new(),
ToolFunctionCallState::NoSuchTool => {
format!("No such tool: {}", tool_call.name)
}
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
view.generate(project_context, cx)
}
}
}
pub fn serialize_tool_call(
&self,
call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Result<SavedToolFunctionCall> {
Ok(SavedToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
ToolFunctionCallState::ExecutedTool(view) => {
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
}
},
})
}
pub fn deserialize_tool_call(
&self,
call: &SavedToolFunctionCall,
cx: &mut WindowContext,
) -> Result<ToolFunctionCall> {
let Some(tool) = self.registered_tools.get(&call.name) else {
return Err(anyhow!("no such tool {}", call.name));
};
Ok(ToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
SavedToolFunctionCallState::KnownTool => {
log::error!("Deserialized tool that had not executed");
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
ToolFunctionCallState::KnownTool(view)
}
SavedToolFunctionCallState::ExecutedTool(output) => {
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
view.deserialize_output(output, cx)?;
ToolFunctionCallState::ExecutedTool(view)
}
},
})
}
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
let name = tool.name();
let registered_tool = RegisteredTool {
type_id: TypeId::of::<T>(),
definition: tool.definition(),
enabled: AtomicBool::new(true),
call: Box::new(
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ParsingFailed),
}));
};
let result = tool.execute(&input, cx);
cx.spawn(move |mut cx| async move {
let result: Result<T::Output> = result.await;
let view = cx.update(|cx| T::output_view(input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
view: view.into(),
generate_fn: generate::<T>,
}),
})
})
},
),
render_running: render_running::<T>,
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
};
let previous = self.registered_tools.insert(name.clone(), registered_tool);
@@ -204,77 +301,40 @@ impl ToolRegistry {
}
return Ok(());
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
T::render_running(cx).into_any_element()
}
fn generate<T: LanguageModelTool>(
view: AnyView,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
view.downcast::<T::View>()
.unwrap()
.update(cx, |view, cx| T::View::generate(view, project, cx))
}
}
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
pub fn call(
&self,
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Task<Result<ToolFunctionCall>> {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let tool = match self.registered_tools.get(&name) {
Some(tool) => tool,
None => {
let name = name.clone();
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::NoSuchTool),
}));
}
};
(tool.call)(tool_call, cx)
}
}
impl ToolFunctionCallResult {
pub fn generate(
&self,
name: &String,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match self {
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::Finished { generate_fn, view } => {
(generate_fn)(view.clone(), project, cx)
}
impl<T: ToolView> InternalToolView for View<T> {
fn view(&self) -> AnyView {
self.clone().into()
}
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
self.update(cx, |view, cx| view.generate(project, cx))
}
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
self.update(cx, |view, cx| {
view.set_input(input, cx);
cx.notify();
});
}
}
fn into_any_element(&self, name: &String) -> AnyElement {
match self {
ToolFunctionCallResult::NoSuchTool => {
format!("Language Model attempted to call {name}").into_any_element()
}
ToolFunctionCallResult::ParsingFailed => {
format!("Language Model called {name} with bad arguments").into_any_element()
}
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
}
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |view, cx| view.execute(cx))
}
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
let output = self.update(cx, |view, cx| view.serialize(cx));
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
}
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
self.update(cx, |view, cx| view.deserialize(state, cx))?;
Ok(())
}
}
@@ -293,7 +353,6 @@ mod test {
use super::*;
use gpui::{div, prelude::*, Render, TestAppContext};
use gpui::{EmptyView, View};
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -304,10 +363,6 @@ mod test {
unit: String,
}
struct WeatherTool {
current_weather: WeatherResult,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
@@ -316,24 +371,81 @@ mod test {
}
struct WeatherView {
result: WeatherResult,
input: Option<WeatherQuery>,
result: Option<WeatherResult>,
// Fake API call
current_weather: WeatherResult,
}
#[derive(Clone, Serialize)]
struct WeatherTool {
current_weather: WeatherResult,
}
impl WeatherView {
fn new(current_weather: WeatherResult) -> Self {
Self {
input: None,
result: None,
current_weather,
}
}
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
match self.result {
Some(ref result) => div()
.child(format!("temperature: {}", result.temperature))
.into_any_element(),
None => div().child("Calculating weather...").into_any_element(),
}
}
}
impl ToolOutput for WeatherView {
fn generate(&self, _output: &mut ProjectContext, _cx: &mut WindowContext) -> String {
impl ToolView for WeatherView {
type Input = WeatherQuery;
type SerializedState = WeatherResult;
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
serde_json::to_string(&self.result).unwrap()
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
let input = self.input.as_ref().unwrap();
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
self.result = Some(weather);
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.current_weather.clone()
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
self.current_weather = output;
Ok(())
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
@@ -344,88 +456,71 @@ mod test {
"Fetches the current weather for a given location.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &mut WindowContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
Task::ready(Ok(weather))
}
fn output_view(
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| {
let result = result.unwrap();
WeatherView { result }
})
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
}
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
let tools = vec![tool.definition()];
assert_eq!(tools.len(), 1);
let expected = ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: schema_for!(WeatherQuery),
};
assert_eq!(tools[0].name, expected.name);
assert_eq!(tools[0].description, expected.description);
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
assert_eq!(
expected_schema,
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
let mut registry = ToolRegistry::new();
registry
.register(WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
"required": ["location", "unit"]
})
.unwrap();
let definitions = registry.definitions();
assert_eq!(
definitions,
[ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: serde_json::from_value(json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
}))
.unwrap(),
}]
);
let args = json!({
"location": "San Francisco",
"unit": "Celsius"
let mut call = ToolFunctionCall {
id: "the-id".to_string(),
name: "get_cur".to_string(),
..Default::default()
};
let task = cx.update(|cx| {
registry.update_tool_call(
&mut call,
Some("rent_weather"),
Some(r#"{"location": "San Francisco","#),
cx,
);
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
registry.execute_tool_call(&mut call, cx).unwrap()
});
task.await.unwrap();
let query: WeatherQuery = serde_json::from_value(args).unwrap();
let result = cx.update(|cx| tool.execute(&query, cx)).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tool.current_weather);
match &call.state {
ToolFunctionCallState::ExecutedTool(_view) => {}
_ => panic!(),
}
}
}

View File

@@ -18,6 +18,7 @@ client.workspace = true
db.workspace = true
editor.workspace = true
gpui.workspace = true
http.workspace = true
isahc.workspace = true
log.workspace = true
markdown_preview.workspace = true

View File

@@ -20,6 +20,7 @@ use smol::{fs, io::AsyncReadExt};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs::File, process::Command};
use http::{HttpClient, HttpClientWithUrl};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{
env::consts::{ARCH, OS},
@@ -29,10 +30,7 @@ use std::{
time::Duration,
};
use update_notification::UpdateNotification;
use util::{
http::{HttpClient, HttpClientWithUrl},
ResultExt,
};
use util::ResultExt;
use workspace::notifications::NotificationId;
use workspace::Workspace;
@@ -56,16 +54,22 @@ struct UpdateRequestBody {
telemetry: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Installing,
Updated,
Updated { binary_path: PathBuf },
Errored,
}
impl AutoUpdateStatus {
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated { .. })
}
}
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
@@ -306,7 +310,7 @@ impl AutoUpdater {
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
if self.pending_poll.is_some() || self.status.is_updated() {
return;
}
@@ -328,7 +332,7 @@ impl AutoUpdater {
}
pub fn status(&self) -> AutoUpdateStatus {
self.status
self.status.clone()
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
@@ -404,6 +408,11 @@ impl AutoUpdater {
cx.notify();
})?;
// We store the path of our current binary, before we install, since installation might
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
// So we cache it here, which allows us to then restart later on.
let binary_path = cx.update(|cx| cx.app_path())??;
match OS {
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
@@ -413,7 +422,7 @@ impl AutoUpdater {
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated;
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
})?;

View File

@@ -50,3 +50,4 @@ language = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }

View File

@@ -40,3 +40,4 @@ rpc = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }

View File

@@ -123,6 +123,7 @@ impl Channel {
}
}
#[derive(Debug)]
pub struct ChannelMembership {
pub user: Arc<User>,
pub kind: proto::channel_member::Kind,
@@ -815,9 +816,11 @@ impl ChannelStore {
Ok(())
})
}
pub fn get_channel_member_details(
pub fn fuzzy_search_members(
&self,
channel_id: ChannelId,
query: String,
limit: u16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<ChannelMembership>>> {
let client = self.client.clone();
@@ -826,26 +829,24 @@ impl ChannelStore {
let response = client
.request(proto::GetChannelMembers {
channel_id: channel_id.0,
query,
limit: limit as u64,
})
.await?;
let user_ids = response.members.iter().map(|m| m.user_id).collect();
let user_store = user_store
.upgrade()
.ok_or_else(|| anyhow!("user store dropped"))?;
let users = user_store
.update(&mut cx, |user_store, cx| user_store.get_users(user_ids, cx))?
.await?;
Ok(users
.into_iter()
.zip(response.members)
.map(|(user, member)| ChannelMembership {
user,
role: member.role(),
kind: member.kind(),
})
.collect())
user_store.update(&mut cx, |user_store, _| {
user_store.insert(response.users);
response
.members
.into_iter()
.filter_map(|member| {
Some(ChannelMembership {
user: user_store.get_cached_user(member.user_id)?,
role: member.role(),
kind: member.kind(),
})
})
.collect()
})
})
}

View File

@@ -4,9 +4,9 @@ use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, TestAppContext};
use http::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_update_channels(cx: &mut AppContext) {

View File

@@ -19,11 +19,17 @@ path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
libc.workspace = true
ipc-channel = "0.18"
once_cell.workspace = true
release_channel.workspace = true
serde.workspace = true
util.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
exec.workspace = true
fork.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-services = "0.2"

View File

@@ -13,6 +13,7 @@ pub enum CliRequest {
paths: Vec<String>,
wait: bool,
open_new_workspace: Option<bool>,
dev_server_token: Option<String>,
},
}

View File

@@ -1,17 +1,24 @@
#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse};
use serde::Deserialize;
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
use std::{
env,
ffi::OsStr,
fs,
env, fs, io,
path::{Path, PathBuf},
process::ExitStatus,
thread::{self, JoinHandle},
};
use util::paths::PathLikeWithPosition;
struct Detect;
trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
}
#[derive(Parser, Debug)]
#[command(name = "zed", disable_version_flag = true)]
struct Args {
@@ -33,9 +40,12 @@ struct Args {
/// Print Zed's version and the app path.
#[arg(short, long)]
version: bool,
/// Custom Zed.app path
#[arg(short, long)]
bundle_path: Option<PathBuf>,
/// Run zed in the foreground (useful for debugging)
#[arg(long)]
foreground: bool,
/// Custom path to Zed.app or the zed binary
#[arg(long)]
zed: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
@@ -49,12 +59,6 @@ fn parse_path_with_position(
})
}
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
fn main() -> Result<()> {
// Intercept version designators
#[cfg(target_os = "macos")]
@@ -68,14 +72,10 @@ fn main() -> Result<()> {
}
let args = Args::parse();
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if let Some(dev_server_token) = args.dev_server_token {
return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
}
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
println!("{}", bundle.zed_version_string());
println!("{}", app.zed_version_string());
return Ok(());
}
@@ -101,7 +101,10 @@ fn main() -> Result<()> {
paths.push(canonicalized.to_string(|path| path.display().to_string()))
}
let (tx, rx) = bundle.launch()?;
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
@@ -110,78 +113,164 @@ fn main() -> Result<()> {
None
};
tx.send(CliRequest::Open {
paths,
wait: args.wait,
open_new_workspace,
})?;
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
tx.send(CliRequest::Open {
paths,
wait: args.wait,
open_new_workspace,
dev_server_token: args.dev_server_token,
})?;
while let Ok(response) = rx.recv() {
match response {
CliResponse::Ping => {}
CliResponse::Stdout { message } => println!("{message}"),
CliResponse::Stderr { message } => eprintln!("{message}"),
CliResponse::Exit { status } => std::process::exit(status),
while let Ok(response) = rx.recv() {
match response {
CliResponse::Ping => {}
CliResponse::Stdout { message } => println!("{message}"),
CliResponse::Stderr { message } => eprintln!("{message}"),
CliResponse::Exit { status } => std::process::exit(status),
}
}
Ok(())
});
if args.foreground {
app.run_foreground(url)?;
} else {
app.launch(url)?;
sender.join().unwrap()?;
}
Ok(())
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
#[cfg(target_os = "linux")]
mod linux {
use std::path::Path;
use std::{
env,
ffi::OsString,
io,
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixDatagram},
},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
time::Duration,
};
use cli::{CliRequest, CliResponse};
use ipc_channel::ipc::{IpcReceiver, IpcSender};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Bundle, InfoPlist};
use crate::{Detect, InstalledApp};
impl Bundle {
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
unimplemented!()
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
match dir.join("zed").canonicalize() {
Ok(path) => Ok(path),
// development builds have Zed capitalized
Err(e) => match dir.join("Zed").canonicalize() {
Ok(path) => Ok(path),
Err(_) => Err(e),
},
}
}?;
Ok(App(path))
}
}
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{} {}",
if *RELEASE_CHANNEL == "stable" {
"".to_string()
} else {
format!(" {} ", *RELEASE_CHANNEL)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
self.0.display(),
)
}
pub fn plist(&self) -> &InfoPlist {
unimplemented!()
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let uid: u32 = unsafe { libc::getuid() };
let sock_addr =
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
let sock = UnixDatagram::unbound()?;
if sock.connect_addr(&sock_addr).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
}
Ok(())
}
pub fn path(&self) -> &Path {
unimplemented!()
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.status()
}
}
impl App {
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
let path = &self.0;
match fork::fork() {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
if let Err(_) = fork::setsid() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if std::env::var("ZED_KEEP_FD").is_err() {
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
// if exec succeeded, we never get here.
eprintln!("failed to exec {:?}: {}", path, error);
process::exit(1)
}
Err(_) => Err(anyhow!(io::Error::last_os_error())),
}
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
fn wait_for_socket(
&self,
sock_addr: &SocketAddr,
sock: &mut UnixDatagram,
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
if sock.connect_addr(&sock_addr).is_ok() {
return Ok(());
}
}
sock.connect_addr(&sock_addr)
}
}
}
@@ -189,59 +278,84 @@ mod linux {
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use crate::{Detect, InstalledApp};
use std::io;
use std::path::Path;
use std::process::ExitStatus;
use cli::{CliRequest, CliResponse};
use ipc_channel::ipc::{IpcReceiver, IpcSender};
use crate::{Bundle, InfoPlist};
impl Bundle {
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
unimplemented!()
}
pub fn plist(&self) -> &InfoPlist {
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
pub fn path(&self) -> &Path {
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
unimplemented!()
}
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use std::{fs, path::Path, process::Command, ptr};
use serde::Deserialize;
use std::{
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
process::{Command, ExitStatus},
ptr,
};
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use crate::{locate_bundle, Bundle, InfoPlist};
use crate::{Detect, InstalledApp};
impl Bundle {
pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
let bundle_path = if let Some(bundle_path) = args_bundle_path {
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let bundle_path = if let Some(bundle_path) = path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
@@ -256,7 +370,7 @@ mod mac_os {
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Self::App {
Ok(Bundle::App {
app_bundle: bundle_path,
plist,
})
@@ -271,42 +385,27 @@ mod mac_os {
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Self::LocalPath {
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
pub fn spawn(&self, args: Vec<String>) -> Result<()> {
let path = match self {
Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Self::LocalPath { executable, .. } => executable.clone(),
};
Command::new(path).args(args).status()?;
Ok(())
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
fn launch(&self, url: String) -> anyhow::Result<()> {
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
@@ -368,18 +467,32 @@ mod mac_os {
}
}
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
Ok((handshake.requests, handshake.responses))
Ok(())
}
pub fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
let path = match self {
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Bundle::LocalPath { executable, .. } => executable.clone(),
};
std::process::Command::new(path).arg(ipc_url).status()
}
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
}

View File

@@ -19,15 +19,17 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
async-native-tls = { version = "0.5.0", features = ["vendored"] }
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
once_cell.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
@@ -56,3 +58,11 @@ gpui = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "linux")'.dependencies]
async-native-tls = {"version" = "0.5.0", features = ["vendored"]}
# This is an indirect dependency of async-tungstenite that is included
# here so we can vendor libssl with the feature flag.
[package.metadata.cargo-machete]
ignored = ["async-native-tls"]

View File

@@ -17,9 +17,9 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, BorrowAppContext, Global, Model,
Task, WeakModel,
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use http::{HttpClient, HttpClientWithUrl};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
@@ -28,7 +28,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use settings::{Settings, SettingsSources};
use std::fmt;
use std::pin::Pin;
use std::{
@@ -47,7 +47,6 @@ use std::{
use telemetry::Telemetry;
use thiserror::Error;
use url::Url;
use util::http::{HttpClient, HttpClientWithUrl};
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
@@ -86,7 +85,7 @@ lazy_static! {
}
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!(client, [SignIn, SignOut, Reconnect]);
@@ -114,11 +113,35 @@ impl Settings for ClientSettings {
}
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ProxySettingsContent {
proxy: Option<String>,
}
#[derive(Deserialize, Default)]
pub struct ProxySettings {
pub proxy: Option<String>,
}
impl Settings for ProxySettings {
const KEY: Option<&'static str> = None;
type FileContent = ProxySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self {
proxy: sources
.user
.and_then(|value| value.proxy.clone())
.or(sources.default.proxy.clone()),
})
}
}
pub fn init_settings(cx: &mut AppContext) {
TelemetrySettings::register(cx);
cx.update_global(|store: &mut SettingsStore, cx| {
store.register_setting::<ClientSettings>(cx);
});
ClientSettings::register(cx);
ProxySettings::register(cx);
}
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
@@ -204,7 +227,7 @@ pub enum EstablishConnectionError {
#[error("{0}")]
Other(#[from] anyhow::Error),
#[error("{0}")]
Http(#[from] util::http::Error),
Http(#[from] http::Error),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
@@ -512,6 +535,7 @@ impl Client {
let clock = Arc::new(clock::RealSystemClock);
let http = Arc::new(HttpClientWithUrl::new(
&ClientSettings::get_global(cx).server_url,
ProxySettings::get_global(cx).proxy.clone(),
));
Self::new(clock, http.clone(), cx)
}
@@ -1679,10 +1703,10 @@ mod tests {
use clock::FakeSystemClock;
use gpui::{BackgroundExecutor, Context, TestAppContext};
use http::FakeHttpClient;
use parking_lot::Mutex;
use settings::SettingsStore;
use std::future;
use util::http::FakeHttpClient;
#[gpui::test(iterations = 10)]
async fn test_reconnection(cx: &mut TestAppContext) {

View File

@@ -0,0 +1 @@

View File

@@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
use clock::SystemClock;
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use http::{self, HttpClient, HttpClientWithUrl, Method};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
@@ -14,12 +15,11 @@ use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent,
SettingEvent,
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent,
MemoryEvent, SettingEvent,
};
use tempfile::NamedTempFile;
use util::http::{self, HttpClient, HttpClientWithUrl, Method};
#[cfg(not(debug_assertions))]
use util::ResultExt;
use util::TryFutureExt;
@@ -241,14 +241,14 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_copilot_event(
pub fn report_inline_completion_event(
self: &Arc<Self>,
suggestion_id: Option<String>,
provider: String,
suggestion_accepted: bool,
file_extension: Option<String>,
) {
let event = Event::Copilot(CopilotEvent {
suggestion_id,
let event = Event::InlineCompletion(InlineCompletionEvent {
provider,
suggestion_accepted,
file_extension,
});
@@ -261,11 +261,15 @@ impl Telemetry {
conversation_id: Option<String>,
kind: AssistantKind,
model: String,
response_latency: Option<Duration>,
error_message: Option<String>,
) {
let event = Event::Assistant(AssistantEvent {
conversation_id,
kind,
model: model.to_string(),
response_latency,
error_message,
});
self.report_event(event)
@@ -497,7 +501,7 @@ mod tests {
use chrono::TimeZone;
use clock::FakeSystemClock;
use gpui::TestAppContext;
use util::http::FakeHttpClient;
use http::FakeHttpClient;
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {

View File

@@ -89,6 +89,7 @@ pub enum ContactRequestStatus {
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
by_github_login: HashMap<String, u64>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_user: watch::Receiver<Option<Arc<User>>>,
@@ -144,6 +145,7 @@ impl UserStore {
];
Self {
users: Default::default(),
by_github_login: Default::default(),
current_user: current_user_rx,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
@@ -231,6 +233,7 @@ impl UserStore {
#[cfg(feature = "test-support")]
pub fn clear_cache(&mut self) {
self.users.clear();
self.by_github_login.clear();
}
async fn handle_update_invite_info(
@@ -644,6 +647,12 @@ impl UserStore {
})
}
pub fn cached_user_by_github_login(&self, github_login: &str) -> Option<Arc<User>> {
self.by_github_login
.get(github_login)
.and_then(|id| self.users.get(id).cloned())
}
pub fn current_user(&self) -> Option<Arc<User>> {
self.current_user.borrow().clone()
}
@@ -661,26 +670,31 @@ impl UserStore {
cx.spawn(|this, mut cx| async move {
if let Some(rpc) = client.upgrade() {
let response = rpc.request(request).await.context("error loading users")?;
let users = response
.users
.into_iter()
.map(User::new)
.collect::<Vec<_>>();
let users = response.users;
this.update(&mut cx, |this, _| {
for user in &users {
this.users.insert(user.id, user.clone());
}
})
.ok();
Ok(users)
this.update(&mut cx, |this, _| this.insert(users))
} else {
Ok(Vec::new())
}
})
}
pub fn insert(&mut self, users: Vec<proto::User>) -> Vec<Arc<User>> {
let mut ret = Vec::with_capacity(users.len());
for user in users {
let user = User::new(user);
if let Some(old) = self.users.insert(user.id, user.clone()) {
if old.github_login != user.github_login {
self.by_github_login.remove(&old.github_login);
}
}
self.by_github_login
.insert(user.github_login.clone(), user.id);
ret.push(user)
}
ret
}
pub fn set_participant_indices(
&mut self,
participant_indices: HashMap<u64, ParticipantIndex>,

View File

@@ -35,6 +35,7 @@ envy = "0.4.2"
futures.workspace = true
google_ai.workspace = true
hex.workspace = true
http.workspace = true
live_kit_server.workspace = true
log.workspace = true
nanoid.workspace = true
@@ -90,6 +91,7 @@ language = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
menu.workspace = true
multi_buffer = { workspace = true, features = ["test-support"] }
node_runtime.workspace = true
notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true

View File

@@ -407,6 +407,7 @@ CREATE TABLE dev_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
ssh_connection_string TEXT,
hashed_token TEXT NOT NULL
);

View File

@@ -0,0 +1 @@
ALTER TABLE dev_servers ADD COLUMN ssh_connection_string TEXT;

View File

@@ -15,8 +15,9 @@ use serde::{Serialize, Serializer};
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent,
SettingEvent,
};
use uuid::Uuid;
@@ -26,6 +27,7 @@ pub fn router() -> Router {
Router::new()
.route("/telemetry/events", post(post_events))
.route("/telemetry/crashes", post(post_crash))
.route("/telemetry/panics", post(post_panic))
.route("/telemetry/hangs", post(post_hang))
}
@@ -280,30 +282,77 @@ pub async fn post_hang(
backtrace = %backtrace,
"hang report");
Ok(())
}
pub async fn post_panic(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::Http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::Http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let panic = report.panic;
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
installation_id = %panic.installation_id.unwrap_or_default(),
description = %panic.payload,
backtrace = %panic.backtrace.join("\n"),
"panic report");
let backtrace = if panic.backtrace.len() > 25 {
let total = panic.backtrace.len();
format!(
"{}\n and {} more",
panic
.backtrace
.iter()
.take(20)
.cloned()
.collect::<Vec<_>>()
.join("\n"),
total - 20
)
} else {
panic.backtrace.join("\n")
};
let backtrace_with_summary = panic.payload + "\n" + &backtrace;
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown("Possible Hang".to_string())))
w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string())))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!(
"*Version:*\n {} ",
report.app_version.unwrap_or_default()
panic.app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*Incident:*\n<https://{}.{}/{}.hang.json|{}…>",
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
"*OS:*\n{} {}",
panic.os_name,
panic.os_version.unwrap_or_default()
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace)))
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
@@ -376,13 +425,19 @@ pub async fn post_events(
first_event_at,
country_code.clone(),
)),
Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
)),
// Needed for clients sending old copilot_event types
Event::Copilot(_) => {}
Event::InlineCompletion(event) => {
to_upload
.inline_completion_events
.push(InlineCompletionEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
))
}
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
event.clone(),
&wrapper,
@@ -464,7 +519,7 @@ pub async fn post_events(
#[derive(Default)]
struct ToUpload {
editor_events: Vec<EditorEventRow>,
copilot_events: Vec<CopilotEventRow>,
inline_completion_events: Vec<InlineCompletionEventRow>,
assistant_events: Vec<AssistantEventRow>,
call_events: Vec<CallEventRow>,
cpu_events: Vec<CpuEventRow>,
@@ -483,14 +538,14 @@ impl ToUpload {
.await
.with_context(|| format!("failed to upload to table '{EDITOR_EVENTS_TABLE}'"))?;
const COPILOT_EVENTS_TABLE: &str = "copilot_events";
const INLINE_COMPLETION_EVENTS_TABLE: &str = "inline_completion_events";
Self::upload_to_table(
COPILOT_EVENTS_TABLE,
&self.copilot_events,
INLINE_COMPLETION_EVENTS_TABLE,
&self.inline_completion_events,
clickhouse_client,
)
.await
.with_context(|| format!("failed to upload to table '{COPILOT_EVENTS_TABLE}'"))?;
.with_context(|| format!("failed to upload to table '{INLINE_COMPLETION_EVENTS_TABLE}'"))?;
const ASSISTANT_EVENTS_TABLE: &str = "assistant_events";
Self::upload_to_table(
@@ -590,7 +645,7 @@ where
let country_code = country_code.as_bytes();
serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16)
serializer.serialize_u16(((country_code[1] as u16) << 8) + country_code[0] as u16)
}
#[derive(Serialize, Debug, clickhouse::Row)]
@@ -660,9 +715,9 @@ impl EditorEventRow {
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct CopilotEventRow {
pub struct InlineCompletionEventRow {
pub installation_id: String,
pub suggestion_id: String,
pub provider: String,
pub suggestion_accepted: bool,
pub app_version: String,
pub file_extension: String,
@@ -682,9 +737,9 @@ pub struct CopilotEventRow {
pub patch: Option<i32>,
}
impl CopilotEventRow {
impl InlineCompletionEventRow {
fn from_event(
event: CopilotEvent,
event: InlineCompletionEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
@@ -711,7 +766,7 @@ impl CopilotEventRow {
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
suggestion_id: event.suggestion_id.unwrap_or_default(),
provider: event.provider,
suggestion_accepted: event.suggestion_accepted,
}
}
@@ -785,6 +840,8 @@ pub struct AssistantEventRow {
conversation_id: String,
kind: String,
model: String,
response_latency_in_ms: Option<i64>,
error_message: Option<String>,
}
impl AssistantEventRow {
@@ -811,6 +868,10 @@ impl AssistantEventRow {
conversation_id: event.conversation_id.unwrap_or_default(),
kind: event.kind.to_string(),
model: event.model,
response_latency_in_ms: event
.response_latency
.map(|latency| latency.as_millis() as i64),
error_message: event.error_message,
}
}
}

View File

@@ -415,7 +415,7 @@ impl Database {
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
let base_delay = SLEEPS[prev_attempt_count];
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!(
log::warn!(
"retrying transaction after serialization error. delay: {} ms.",
randomized_delay
);
@@ -509,8 +509,7 @@ pub type NotificationBatch = Vec<(UserId, proto::Notification)>;
pub struct CreatedChannelMessage {
pub message_id: MessageId,
pub participant_connection_ids: Vec<ConnectionId>,
pub channel_members: Vec<UserId>,
pub participant_connection_ids: HashSet<ConnectionId>,
pub notifications: NotificationBatch,
}

View File

@@ -440,12 +440,7 @@ impl Database {
channel_id: ChannelId,
user: UserId,
operations: &[proto::Operation],
) -> Result<(
Vec<ConnectionId>,
Vec<UserId>,
i32,
Vec<proto::VectorClockEntry>,
)> {
) -> Result<(HashSet<ConnectionId>, i32, Vec<proto::VectorClockEntry>)> {
self.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
@@ -479,7 +474,6 @@ impl Database {
.filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
.collect::<Vec<_>>();
let mut channel_members;
let max_version;
if !operations.is_empty() {
@@ -504,12 +498,6 @@ impl Database {
)
.await?;
channel_members = self.get_channel_participants(&channel, &tx).await?;
let collaborators = self
.get_channel_buffer_collaborators_internal(channel_id, &tx)
.await?;
channel_members.retain(|member| !collaborators.contains(member));
buffer_operation::Entity::insert_many(operations)
.on_conflict(
OnConflict::columns([
@@ -524,11 +512,10 @@ impl Database {
.exec(&*tx)
.await?;
} else {
channel_members = Vec::new();
max_version = Vec::new();
}
let mut connections = Vec::new();
let mut connections = HashSet::default();
let mut rows = channel_buffer_collaborator::Entity::find()
.filter(
Condition::all()
@@ -538,13 +525,13 @@ impl Database {
.await?;
while let Some(row) = rows.next().await {
let row = row?;
connections.push(ConnectionId {
connections.insert(ConnectionId {
id: row.connection_id as u32,
owner_id: row.connection_server_id.0 as u32,
});
}
Ok((connections, channel_members, buffer.epoch, max_version))
Ok((connections, buffer.epoch, max_version))
})
.await
}

View File

@@ -3,7 +3,7 @@ use rpc::{
proto::{channel_member::Kind, ChannelBufferVersion, VectorClockEntry},
ErrorCode, ErrorCodeExt,
};
use sea_orm::TryGetableMany;
use sea_orm::{DbBackend, TryGetableMany};
impl Database {
#[cfg(test)]
@@ -700,77 +700,73 @@ impl Database {
pub async fn get_channel_participant_details(
&self,
channel_id: ChannelId,
filter: &str,
limit: u64,
user_id: UserId,
) -> Result<Vec<proto::ChannelMember>> {
let (role, members) = self
) -> Result<(Vec<proto::ChannelMember>, Vec<proto::User>)> {
let members = self
.transaction(move |tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
let role = self
.check_user_is_channel_participant(&channel, user_id, &tx)
self.check_user_is_channel_participant(&channel, user_id, &tx)
.await?;
Ok((
role,
self.get_channel_participant_details_internal(&channel, &tx)
.await?,
))
let mut query = channel_member::Entity::find()
.find_also_related(user::Entity)
.filter(channel_member::Column::ChannelId.eq(channel.root_id()));
if cfg!(any(test, sqlite)) && self.pool.get_database_backend() == DbBackend::Sqlite {
query = query.filter(Expr::cust_with_values(
"UPPER(github_login) LIKE ?",
[Self::fuzzy_like_string(&filter.to_uppercase())],
))
} else {
query = query.filter(Expr::cust_with_values(
"github_login ILIKE $1",
[Self::fuzzy_like_string(filter)],
))
}
let members = query.order_by(
Expr::cust(
"not role = 'admin', not role = 'member', not role = 'guest', not accepted, github_login",
),
sea_orm::Order::Asc,
)
.limit(limit)
.all(&*tx)
.await?;
Ok(members)
})
.await?;
if role == ChannelRole::Admin {
Ok(members
.into_iter()
.map(|channel_member| proto::ChannelMember {
role: channel_member.role.into(),
user_id: channel_member.user_id.to_proto(),
kind: if channel_member.accepted {
let mut users: Vec<proto::User> = Vec::with_capacity(members.len());
let members = members
.into_iter()
.map(|(member, user)| {
if let Some(user) = user {
users.push(proto::User {
id: user.id.to_proto(),
avatar_url: format!(
"https://github.com/{}.png?size=128",
user.github_login
),
github_login: user.github_login,
})
}
proto::ChannelMember {
role: member.role.into(),
user_id: member.user_id.to_proto(),
kind: if member.accepted {
Kind::Member
} else {
Kind::Invitee
}
.into(),
})
.collect())
} else {
return Ok(members
.into_iter()
.filter_map(|member| {
if !member.accepted {
return None;
}
Some(proto::ChannelMember {
role: member.role.into(),
user_id: member.user_id.to_proto(),
kind: Kind::Member.into(),
})
})
.collect());
}
}
}
})
.collect();
async fn get_channel_participant_details_internal(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<channel_member::Model>> {
Ok(channel_member::Entity::find()
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
.all(tx)
.await?)
}
/// Returns the participants in the given channel.
pub async fn get_channel_participants(
&self,
channel: &channel::Model,
tx: &DatabaseTransaction,
) -> Result<Vec<UserId>> {
let participants = self
.get_channel_participant_details_internal(channel, tx)
.await?;
Ok(participants
.into_iter()
.map(|member| member.user_id)
.collect())
Ok((members, users))
}
/// Returns whether the given user is an admin in the specified channel.

View File

@@ -73,6 +73,7 @@ impl Database {
pub async fn create_dev_server(
&self,
name: &str,
ssh_connection_string: Option<&str>,
hashed_access_token: &str,
user_id: UserId,
) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> {
@@ -86,6 +87,9 @@ impl Database {
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
name: ActiveValue::Set(name.trim().to_string()),
user_id: ActiveValue::Set(user_id),
ssh_connection_string: ActiveValue::Set(
ssh_connection_string.map(ToOwned::to_owned),
),
})
.exec_with_returning(&*tx)
.await?;
@@ -133,6 +137,7 @@ impl Database {
&self,
id: DevServerId,
name: &str,
ssh_connection_string: Option<&str>,
user_id: UserId,
) -> crate::Result<proto::DevServerProjectsUpdate> {
self.transaction(|tx| async move {
@@ -145,6 +150,9 @@ impl Database {
dev_server::Entity::update(dev_server::ActiveModel {
name: ActiveValue::Set(name.trim().to_string()),
ssh_connection_string: ActiveValue::Set(
ssh_connection_string.map(ToOwned::to_owned),
),
..dev_server.clone().into_active_model()
})
.exec(&*tx)

View File

@@ -251,7 +251,7 @@ impl Database {
.await?;
let mut is_participant = false;
let mut participant_connection_ids = Vec::new();
let mut participant_connection_ids = HashSet::default();
let mut participant_user_ids = Vec::new();
while let Some(row) = rows.next().await {
let row = row?;
@@ -259,7 +259,7 @@ impl Database {
is_participant = true;
}
participant_user_ids.push(row.user_id);
participant_connection_ids.push(row.connection());
participant_connection_ids.insert(row.connection());
}
drop(rows);
@@ -336,13 +336,9 @@ impl Database {
}
}
let mut channel_members = self.get_channel_participants(&channel, &tx).await?;
channel_members.retain(|member| !participant_user_ids.contains(member));
Ok(CreatedChannelMessage {
message_id,
participant_connection_ids,
channel_members,
notifications,
})
})

View File

@@ -130,13 +130,21 @@ impl Database {
.await
}
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
self.weak_transaction(|tx| async move {
project::Entity::delete_by_id(project_id).exec(&*tx).await?;
Ok(())
})
.await
}
/// Unshares the given project.
pub async fn unshare_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
user_id: Option<UserId>,
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
let project = project::Entity::find_by_id(project_id)
@@ -149,10 +157,7 @@ impl Database {
None
};
if project.host_connection()? == connection {
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
return Ok((true, room, guest_connection_ids));
}
if let Some(dev_server_project_id) = project.dev_server_project_id {
if let Some(user_id) = user_id {
@@ -169,7 +174,7 @@ impl Database {
})
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
return Ok((false, room, guest_connection_ids));
}
}

View File

@@ -48,6 +48,9 @@ impl Database {
/// Returns all users by ID. There are no access checks here, so this should only be used internally.
pub async fn get_users_by_ids(&self, ids: Vec<UserId>) -> Result<Vec<user::Model>> {
if ids.len() >= 10000_usize {
return Err(anyhow!("too many users"))?;
}
self.transaction(|tx| async {
let tx = tx;
Ok(user::Entity::find()

View File

@@ -10,6 +10,7 @@ pub struct Model {
pub name: String,
pub user_id: UserId,
pub hashed_token: String,
pub ssh_connection_string: Option<String>,
}
impl ActiveModelBehavior for ActiveModel {}
@@ -32,6 +33,7 @@ impl Model {
dev_server_id: self.id.to_proto(),
name: self.name.clone(),
status: status as i32,
ssh_connection_string: self.ssh_connection_string.clone(),
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::{
db::{
tests::{channel_tree, new_test_connection, new_test_user},
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId,
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
},
test_both_dbs,
};
@@ -40,15 +40,15 @@ async fn test_channels(db: &Arc<Database>) {
.await
.unwrap();
let mut members = db
.transaction(|tx| async move {
let channel = db.get_channel_internal(replace_id, &tx).await?;
db.get_channel_participants(&channel, &tx).await
})
let (members, _) = db
.get_channel_participant_details(replace_id, "", 10, a_id)
.await
.unwrap();
members.sort();
assert_eq!(members, &[a_id, b_id]);
let ids = members
.into_iter()
.map(|m| UserId::from_proto(m.user_id))
.collect::<Vec<_>>();
assert_eq!(ids, &[a_id, b_id]);
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
let cargo_id = db.create_sub_channel("cargo", rust_id, a_id).await.unwrap();
@@ -195,8 +195,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
assert_eq!(user_3_invites, &[channel_1_1]);
let mut members = db
.get_channel_participant_details(channel_1_1, user_1)
let (mut members, _) = db
.get_channel_participant_details(channel_1_1, "", 100, user_1)
.await
.unwrap();
@@ -231,8 +231,8 @@ async fn test_channel_invites(db: &Arc<Database>) {
.await
.unwrap();
let members = db
.get_channel_participant_details(channel_1_3, user_1)
let (members, _) = db
.get_channel_participant_details(channel_1_3, "", 100, user_1)
.await
.unwrap();
assert_eq!(
@@ -243,16 +243,16 @@ async fn test_channel_invites(db: &Arc<Database>) {
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
proto::ChannelMember {
user_id: user_3.to_proto(),
kind: proto::channel_member::Kind::Invitee.into(),
role: proto::ChannelRole::Admin.into(),
},
proto::ChannelMember {
user_id: user_2.to_proto(),
kind: proto::channel_member::Kind::Member.into(),
role: proto::ChannelRole::Member.into(),
},
]
);
}
@@ -482,8 +482,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.await
.unwrap();
let mut members = db
.get_channel_participant_details(public_channel_id, admin)
let (mut members, _) = db
.get_channel_participant_details(public_channel_id, "", 100, admin)
.await
.unwrap();
@@ -557,8 +557,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.await
.is_err());
let mut members = db
.get_channel_participant_details(public_channel_id, admin)
let (mut members, _) = db
.get_channel_participant_details(public_channel_id, "", 100, admin)
.await
.unwrap();
@@ -594,8 +594,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.unwrap();
// currently people invited to parent channels are not shown here
let mut members = db
.get_channel_participant_details(public_channel_id, admin)
let (mut members, _) = db
.get_channel_participant_details(public_channel_id, "", 100, admin)
.await
.unwrap();
@@ -663,8 +663,8 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
.await
.unwrap();
let mut members = db
.get_channel_participant_details(public_channel_id, admin)
let (mut members, _) = db
.get_channel_participant_details(public_channel_id, "", 100, admin)
.await
.unwrap();

View File

@@ -42,6 +42,7 @@ use futures::{
stream::FuturesUnordered,
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use http::IsahcHttpClient;
use prometheus::{register_int_gauge, IntGauge};
use rpc::{
proto::{
@@ -73,7 +74,6 @@ use tracing::{
field::{self},
info_span, instrument, Instrument,
};
use util::http::IsahcHttpClient;
use self::connection_pool::VersionedMessage;
@@ -2032,23 +2032,34 @@ async fn unshare_project_internal(
user_id: Option<UserId>,
session: &Session,
) -> Result<()> {
let (room, guest_connection_ids) = &*session
.db()
.await
.unshare_project(project_id, connection_id, user_id)
.await?;
let delete = {
let room_guard = session
.db()
.await
.unshare_project(project_id, connection_id, user_id)
.await?;
let message = proto::UnshareProject {
project_id: project_id.to_proto(),
let (delete, room, guest_connection_ids) = &*room_guard;
let message = proto::UnshareProject {
project_id: project_id.to_proto(),
};
broadcast(
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
if let Some(room) = room {
room_updated(room, &session.peer);
}
*delete
};
broadcast(
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
if let Some(room) = room {
room_updated(room, &session.peer);
if delete {
let db = session.db().await;
db.delete_project(project_id).await?;
}
Ok(())
@@ -2354,7 +2365,12 @@ async fn create_dev_server(
let (dev_server, status) = session
.db()
.await
.create_dev_server(&request.name, &hashed_access_token, session.user_id())
.create_dev_server(
&request.name,
request.ssh_connection_string.as_deref(),
&hashed_access_token,
session.user_id(),
)
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
@@ -2362,7 +2378,7 @@ async fn create_dev_server(
response.send(proto::CreateDevServerResponse {
dev_server_id: dev_server.id.0 as u64,
access_token: auth::generate_dev_server_token(dev_server.id.0 as usize, access_token),
name: request.name.clone(),
name: request.name,
})?;
Ok(())
}
@@ -2423,7 +2439,12 @@ async fn rename_dev_server(
let status = session
.db()
.await
.rename_dev_server(dev_server_id, &request.name, session.user_id())
.rename_dev_server(
dev_server_id,
&request.name,
request.ssh_connection_string.as_deref(),
session.user_id(),
)
.await?;
send_dev_server_projects_update(session.user_id(), status, &session).await;
@@ -3672,10 +3693,15 @@ async fn get_channel_members(
) -> Result<()> {
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let members = db
.get_channel_participant_details(channel_id, session.user_id())
let limit = if request.limit == 0 {
u16::MAX as u64
} else {
request.limit
};
let (members, users) = db
.get_channel_participant_details(channel_id, &request.query, limit, session.user_id())
.await?;
response.send(proto::GetChannelMembersResponse { members })?;
response.send(proto::GetChannelMembersResponse { members, users })?;
Ok(())
}
@@ -3875,13 +3901,13 @@ async fn update_channel_buffer(
let db = session.db().await;
let channel_id = ChannelId::from_proto(request.channel_id);
let (collaborators, non_collaborators, epoch, version) = db
let (collaborators, epoch, version) = db
.update_channel_buffer(channel_id, session.user_id(), &request.operations)
.await?;
channel_buffer_updated(
session.connection_id,
collaborators,
collaborators.clone(),
&proto::UpdateChannelBuffer {
channel_id: channel_id.to_proto(),
operations: request.operations,
@@ -3891,25 +3917,29 @@ async fn update_channel_buffer(
let pool = &*session.connection_pool().await;
broadcast(
None,
non_collaborators
.iter()
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|peer_id| {
session.peer.send(
peer_id,
proto::UpdateChannels {
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
channel_id: channel_id.to_proto(),
epoch: epoch as u64,
version: version.clone(),
}],
..Default::default()
},
)
},
);
let non_collaborators =
pool.channel_connection_ids(channel_id)
.filter_map(|(connection_id, _)| {
if collaborators.contains(&connection_id) {
None
} else {
Some(connection_id)
}
});
broadcast(None, non_collaborators, |peer_id| {
session.peer.send(
peer_id,
proto::UpdateChannels {
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
channel_id: channel_id.to_proto(),
epoch: epoch as u64,
version: version.clone(),
}],
..Default::default()
},
)
});
Ok(())
}
@@ -4037,7 +4067,6 @@ async fn send_channel_message(
let CreatedChannelMessage {
message_id,
participant_connection_ids,
channel_members,
notifications,
} = session
.db()
@@ -4068,7 +4097,7 @@ async fn send_channel_message(
};
broadcast(
Some(session.connection_id),
participant_connection_ids,
participant_connection_ids.clone(),
|connection| {
session.peer.send(
connection,
@@ -4084,24 +4113,27 @@ async fn send_channel_message(
})?;
let pool = &*session.connection_pool().await;
broadcast(
None,
channel_members
.iter()
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|peer_id| {
session.peer.send(
peer_id,
proto::UpdateChannels {
latest_channel_message_ids: vec![proto::ChannelMessageId {
channel_id: channel_id.to_proto(),
message_id: message_id.to_proto(),
}],
..Default::default()
},
)
},
);
let non_participants =
pool.channel_connection_ids(channel_id)
.filter_map(|(connection_id, _)| {
if participant_connection_ids.contains(&connection_id) {
None
} else {
Some(connection_id)
}
});
broadcast(None, non_participants, |peer_id| {
session.peer.send(
peer_id,
proto::UpdateChannels {
latest_channel_message_ids: vec![proto::ChannelMessageId {
channel_id: channel_id.to_proto(),
message_id: message_id.to_proto(),
}],
..Default::default()
},
)
});
send_notifications(pool, &session.peer, notifications);
Ok(())
@@ -4333,6 +4365,7 @@ async fn complete_with_open_ai(
OPEN_AI_API_URL,
&api_key,
crate::ai::language_model_request_to_open_ai(request)?,
None,
)
.await
.context("open_ai::stream_completion request failed within collab")?;
@@ -4477,8 +4510,8 @@ async fn complete_with_anthropic(
.collect();
let mut stream = anthropic::stream_completion(
session.http_client.clone(),
"https://api.anthropic.com",
session.http_client.as_ref(),
anthropic::ANTHROPIC_API_URL,
&api_key,
anthropic::Request {
model,
@@ -4487,6 +4520,7 @@ async fn complete_with_anthropic(
system: system_message,
max_tokens: 4092,
},
None,
)
.await?;

View File

@@ -99,7 +99,7 @@ async fn test_core_channels(
.channel_store()
.update(cx_a, |store, cx| {
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
store.get_channel_member_details(channel_a_id, cx)
store.fuzzy_search_members(channel_a_id, "".to_string(), 10, cx)
})
.await
.unwrap();

View File

@@ -20,7 +20,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
let resp = store
.update(cx, |store, cx| {
store.create_dev_server("server-1".to_string(), cx)
store.create_dev_server("server-1".to_string(), None, cx)
})
.await
.unwrap();
@@ -167,7 +167,7 @@ async fn create_dev_server_project(
let resp = store
.update(cx, |store, cx| {
store.create_dev_server("server-1".to_string(), cx)
store.create_dev_server("server-1".to_string(), None, cx)
})
.await
.unwrap();
@@ -352,6 +352,7 @@ async fn test_dev_server_rename(
store.rename_dev_server(
store.dev_servers().first().unwrap().id,
"name-edited".to_string(),
None,
cx,
)
})
@@ -521,7 +522,7 @@ async fn test_create_dev_server_project_path_validation(
let resp = store
.update(cx1, |store, cx| {
store.create_dev_server("server-2".to_string(), cx)
store.create_dev_server("server-2".to_string(), None, cx)
})
.await
.unwrap();

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