Compare commits

...

125 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
294 changed files with 12763 additions and 4988 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'

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

@@ -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

1
.gitignore vendored
View File

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

View File

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

336
Cargo.lock generated
View File

@@ -70,6 +70,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
dependencies = [
"cfg-if",
"const-random",
"getrandom 0.2.10",
"once_cell",
"version_check",
@@ -240,9 +241,9 @@ checksum = "e78f17bacc1bc7b91fef7b1885c10772eb2b9e4e989356f6f0f6a972240f97cd"
[[package]]
name = "anyhow"
version = "1.0.75"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
[[package]]
name = "approx"
@@ -347,7 +348,9 @@ dependencies = [
"file_icons",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"gray_matter",
"http 0.1.0",
"indoc",
"language",
@@ -357,20 +360,24 @@ dependencies = [
"open_ai",
"ordered-float 2.10.0",
"parking_lot",
"picker",
"project",
"rand 0.8.5",
"regex",
"rope",
"schemars",
"search",
"serde",
"serde_json",
"settings",
"smol",
"strsim 0.11.1",
"telemetry_events",
"theme",
"tiktoken-rs",
"toml 0.8.10",
"ui",
"unindent",
"util",
"uuid",
"workspace",
@@ -1301,7 +1308,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
"base64 0.21.4",
"base64 0.21.7",
"bitflags 1.3.2",
"bytes 1.5.0",
"futures-util",
@@ -1396,9 +1403,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.4"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
@@ -1681,7 +1688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a"
dependencies = [
"memchr",
"regex-automata 0.3.8",
"regex-automata 0.3.9",
"serde",
]
@@ -2091,7 +2098,7 @@ dependencies = [
"bitflags 1.3.2",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"strsim",
"strsim 0.10.0",
"termcolor",
"textwrap",
]
@@ -2115,7 +2122,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex 0.5.1",
"strsim",
"strsim 0.10.0",
]
[[package]]
@@ -2412,7 +2419,6 @@ dependencies = [
"call",
"channel",
"client",
"clock",
"collections",
"db",
"dev_server_projects",
@@ -2561,6 +2567,26 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.10",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -2801,7 +2827,7 @@ dependencies = [
"cranelift-entity",
"cranelift-isle",
"gimli",
"hashbrown 0.14.0",
"hashbrown 0.14.5",
"log",
"regalloc2",
"smallvec",
@@ -3121,7 +3147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.0",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
@@ -3613,11 +3639,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.3.31"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
dependencies = [
"serde",
"typeid",
]
[[package]]
@@ -3763,6 +3790,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"project",
"release_channel",
"schemars",
"semantic_version",
"serde",
@@ -3817,6 +3845,7 @@ dependencies = [
"language",
"picker",
"project",
"release_channel",
"semantic_version",
"serde",
"settings",
@@ -3836,9 +3865,9 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fancy-regex"
version = "0.11.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
dependencies = [
"bit-set",
"regex",
@@ -3950,6 +3979,7 @@ dependencies = [
"menu",
"picker",
"project",
"serde",
"serde_json",
"settings",
"text",
@@ -4497,7 +4527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
dependencies = [
"fallible-iterator",
"indexmap 2.0.0",
"indexmap 2.2.6",
"stable_deref_trait",
]
@@ -4610,7 +4640,6 @@ name = "go_to_line"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"gpui",
"indoc",
@@ -4753,6 +4782,7 @@ dependencies = [
"windows 0.56.0",
"windows-core 0.56.0",
"x11rb",
"xim",
"xkbcommon",
]
@@ -4765,6 +4795,18 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "gray_matter"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
dependencies = [
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "grid"
version = "0.13.0"
@@ -4831,9 +4873,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.14.0"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.8",
"allocator-api2",
@@ -4845,7 +4887,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.0",
"hashbrown 0.14.5",
]
[[package]]
@@ -4854,7 +4896,7 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270"
dependencies = [
"base64 0.21.4",
"base64 0.21.7",
"bytes 1.5.0",
"headers-core",
"http 0.2.9",
@@ -5263,12 +5305,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.0.0"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
"hashbrown 0.14.5",
"serde",
]
@@ -5498,9 +5540,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.9"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jni"
@@ -5902,6 +5944,12 @@ dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -6005,9 +6053,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
dependencies = [
"serde",
"value-bag",
@@ -6038,8 +6086,8 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.94.1"
source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?branch=apply-snippet-edit#853c7881d200777e20799026651ca36727144646"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -6367,7 +6415,7 @@ dependencies = [
"bitflags 2.4.2",
"codespan-reporting",
"hexf-parse",
"indexmap 2.0.0",
"indexmap 2.2.6",
"log",
"num-traits",
"rustc-hash",
@@ -6797,8 +6845,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
dependencies = [
"crc32fast",
"hashbrown 0.14.0",
"indexmap 2.0.0",
"hashbrown 0.14.5",
"indexmap 2.2.6",
"memchr",
]
@@ -7012,7 +7060,6 @@ dependencies = [
name = "outline"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"fuzzy",
"gpui",
@@ -7231,7 +7278,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
dependencies = [
"fixedbitset",
"indexmap 2.0.0",
"indexmap 2.2.6",
]
[[package]]
@@ -7384,7 +7431,7 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
dependencies = [
"base64 0.21.4",
"base64 0.21.7",
"indexmap 1.9.3",
"line-wrap",
"quick-xml 0.30.0",
@@ -7632,6 +7679,7 @@ dependencies = [
"client",
"clock",
"collections",
"dev_server_projects",
"env_logger",
"fs",
"futures 0.3.28",
@@ -7659,9 +7707,12 @@ dependencies = [
"serde_json",
"settings",
"sha2 0.10.7",
"shlex",
"similar",
"smol",
"snippet",
"task",
"tempfile",
"terminal",
"text",
"unindent",
@@ -8016,12 +8067,14 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"dev_server_projects",
"editor",
"feature_flags",
"fuzzy",
"gpui",
"language",
"markdown",
"menu",
"ordered-float 2.10.0",
"picker",
@@ -8029,9 +8082,9 @@ dependencies = [
"rpc",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"task",
"terminal_view",
"ui",
"ui_text_field",
"util",
@@ -8119,9 +8172,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
[[package]]
name = "regex-automata"
@@ -8184,7 +8237,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64 0.21.4",
"base64 0.21.7",
"bytes 1.5.0",
"encoding_rs",
"futures-core",
@@ -8566,7 +8619,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
"base64 0.21.4",
"base64 0.21.7",
]
[[package]]
@@ -8604,9 +8657,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.15"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "safemem"
@@ -8935,18 +8988,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@@ -8975,11 +9028,11 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.107"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.2.6",
"itoa",
"ryu",
"serde",
@@ -8991,7 +9044,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.2.6",
"itoa",
"ryu",
"serde",
@@ -9507,7 +9560,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap 2.0.0",
"indexmap 2.2.6",
"log",
"memchr",
"once_cell",
@@ -9577,7 +9630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db"
dependencies = [
"atoi",
"base64 0.21.4",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.4.2",
"byteorder",
@@ -9624,7 +9677,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624"
dependencies = [
"atoi",
"base64 0.21.4",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.4.2",
"byteorder",
@@ -9713,7 +9766,6 @@ name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant2",
"clap 4.4.4",
"collab_ui",
"ctrlc",
@@ -9762,6 +9814,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.25.0"
@@ -10095,6 +10153,7 @@ dependencies = [
"gpui",
"language",
"menu",
"parking_lot",
"picker",
"project",
"schemars",
@@ -10102,6 +10161,7 @@ dependencies = [
"serde_json",
"settings",
"task",
"text",
"tree-sitter-rust",
"tree-sitter-typescript",
"ui",
@@ -10346,12 +10406,12 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.5.7"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb"
checksum = "c314e7ce51440f9e8f5a497394682a57b7c323d0f4d0a6b1b13c429056e0e234"
dependencies = [
"anyhow",
"base64 0.21.4",
"base64 0.21.7",
"bstr",
"fancy-regex",
"lazy_static",
@@ -10397,6 +10457,15 @@ dependencies = [
"time",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
@@ -10608,7 +10677,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
@@ -10621,7 +10690,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.2.6",
"toml_datetime",
"winnow 0.5.15",
]
@@ -10632,7 +10701,7 @@ version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
dependencies = [
"indexmap 2.0.0",
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
@@ -10857,8 +10926,8 @@ dependencies = [
[[package]]
name = "tree-sitter-go"
version = "0.19.1"
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=aeb2f33b366fd78d5789ff104956ce23508b85db#aeb2f33b366fd78d5789ff104956ce23508b85db"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-go?rev=b82ab803d887002a0af11f6ce63d72884580bf33#b82ab803d887002a0af11f6ce63d72884580bf33"
dependencies = [
"cc",
"tree-sitter",
@@ -11056,6 +11125,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
[[package]]
name = "typenum"
version = "1.17.0"
@@ -11292,9 +11367,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.4.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
dependencies = [
"value-bag-serde1",
"value-bag-sval2",
@@ -11302,9 +11377,9 @@ dependencies = [
[[package]]
name = "value-bag-serde1"
version = "1.4.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b"
dependencies = [
"erased-serde",
"serde",
@@ -11313,9 +11388,9 @@ dependencies = [
[[package]]
name = "value-bag-sval2"
version = "1.4.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b"
dependencies = [
"sval",
"sval_buffer",
@@ -11566,7 +11641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
dependencies = [
"anyhow",
"indexmap 2.0.0",
"indexmap 2.2.6",
"serde",
"serde_derive",
"serde_json",
@@ -11582,7 +11657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.4.2",
"indexmap 2.0.0",
"indexmap 2.2.6",
"semver",
]
@@ -11609,7 +11684,7 @@ dependencies = [
"cfg-if",
"encoding_rs",
"gimli",
"indexmap 2.0.0",
"indexmap 2.2.6",
"libc",
"log",
"object",
@@ -11740,7 +11815,7 @@ dependencies = [
"cpp_demangle",
"cranelift-entity",
"gimli",
"indexmap 2.0.0",
"indexmap 2.2.6",
"log",
"object",
"rustc-demangle",
@@ -11791,7 +11866,7 @@ dependencies = [
"cc",
"cfg-if",
"encoding_rs",
"indexmap 2.0.0",
"indexmap 2.2.6",
"libc",
"log",
"mach",
@@ -11896,7 +11971,7 @@ checksum = "96326c9800fb6c099f50d1bd2126d636fc2f96950e1675acf358c0f52516cd38"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.0.0",
"indexmap 2.2.6",
"wit-parser",
]
@@ -12591,7 +12666,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.0.0",
"indexmap 2.2.6",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -12619,7 +12694,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"indexmap 2.0.0",
"indexmap 2.2.6",
"log",
"serde",
"serde_derive",
@@ -12638,7 +12713,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.0.0",
"indexmap 2.2.6",
"log",
"semver",
"serde",
@@ -12799,6 +12874,35 @@ dependencies = [
"winapi",
]
[[package]]
name = "xim"
version = "0.4.0"
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
dependencies = [
"ahash 0.8.8",
"hashbrown 0.14.5",
"log",
"x11rb",
"xim-ctext",
"xim-parser",
]
[[package]]
name = "xim-ctext"
version = "0.3.0"
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
dependencies = [
"encoding_rs",
]
[[package]]
name = "xim-parser"
version = "0.2.1"
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
dependencies = [
"bitflags 2.4.2",
]
[[package]]
name = "xkbcommon"
version = "0.7.0"
@@ -12839,6 +12943,15 @@ dependencies = [
"toml 0.8.10",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -12929,13 +13042,12 @@ dependencies = [
[[package]]
name = "zed"
version = "0.136.0"
version = "0.138.0"
dependencies = [
"activity_indicator",
"anyhow",
"assets",
"assistant",
"assistant2",
"audio",
"auto_update",
"backtrace",
@@ -13053,33 +13165,33 @@ dependencies = [
name = "zed_dart"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_deno"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_elixir"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_elm"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_emmet"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13100,15 +13212,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.6"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.6"
@@ -13121,50 +13224,59 @@ dependencies = [
]
[[package]]
name = "zed_gleam"
version = "0.1.2"
name = "zed_extension_api"
version = "0.0.7"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_gleam"
version = "0.1.3"
dependencies = [
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_haskell"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_html"
version = "0.0.1"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.4",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_lua"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_ocaml"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_php"
version = "0.0.3"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13185,30 +13297,30 @@ dependencies = [
[[package]]
name = "zed_ruby"
version = "0.0.3"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_svelte"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_terraform"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_toml"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
@@ -13222,14 +13334,14 @@ dependencies = [
name = "zed_vue"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_zig"
version = "0.1.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"zed_extension_api 0.0.7",
]
[[package]]

View File

@@ -330,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"] }
@@ -337,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",
@@ -355,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"
@@ -375,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 = [
@@ -402,11 +403,11 @@ features = [
"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",

View File

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

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

@@ -191,6 +191,12 @@
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
"context": "Markdown",
"bindings": {
"ctrl-c": "markdown::Copy"
}
},
{
"context": "AssistantPanel",
"bindings": {
@@ -495,6 +501,12 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {

View File

@@ -208,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"
}
},
{
@@ -517,6 +515,12 @@
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
@@ -573,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",

View File

@@ -493,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,6 +84,15 @@
"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.
@@ -294,7 +316,7 @@
// 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"
@@ -311,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.
//
@@ -448,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": {
@@ -479,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,
@@ -561,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"
}
},
@@ -592,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"],
@@ -609,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
@@ -631,32 +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"
"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,
@@ -714,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

@@ -21,6 +21,7 @@ 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
@@ -33,12 +34,14 @@ 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
@@ -47,6 +50,8 @@ 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
@@ -55,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

@@ -9,3 +9,22 @@ 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

@@ -1,3 +1,4 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@@ -8,6 +9,7 @@ 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};
@@ -32,10 +34,12 @@ impl Default for CurrentProjectContext {
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(),
})
self.enabled
.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
.filter(|message| !message.content.is_empty())
}
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
@@ -44,12 +48,12 @@ impl CurrentProjectContext {
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;
return ContextUpdated::Disabled;
}
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
@@ -74,12 +78,16 @@ impl CurrentProjectContext {
if let Some(message) = message_task.await.log_err() {
conversation
.update(&mut cx, |conversation, _cx| {
.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> {
@@ -87,32 +95,54 @@ impl CurrentProjectContext {
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
let name = cargo_toml
.package
.as_ref()
.map(|package| package.name.as_str());
if let Some(name) = name {
message.push_str(&format!(" named \"{name}\""));
}
message.push_str(". ");
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}")?;
}
let description = cargo_toml
.package
.as_ref()
.and_then(|package| package.description.as_ref())
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
message.push_str("It describes itself as ");
message.push_str(&format!("\"{description}\""));
message.push_str(". ");
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
let dependencies = cargo_toml.dependencies.keys().cloned().collect::<Vec<_>>();
if !dependencies.is_empty() {
message.push_str("The following dependencies are installed: ");
message.push_str(&dependencies.join(", "));
message.push_str(". ");
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)

View File

@@ -1,12 +1,14 @@
use gpui::{Subscription, Task, WeakModel};
use language::Buffer;
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 crate::{LanguageModelRequestMessage, Role};
use super::ContextUpdated;
pub struct RecentBuffersContext {
pub enabled: bool,
pub buffers: Vec<RecentBuffer>,
pub message: String,
pub snapshot: RecentBuffersSnapshot,
pub pending_message: Option<Task<()>>,
}
@@ -20,18 +22,126 @@ impl Default for RecentBuffersContext {
Self {
enabled: true,
buffers: Vec::new(),
message: String::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.message.clone(),
})
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

@@ -3,16 +3,21 @@ 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;
use ambient_context::AmbientContextSnapshot;
pub use assistant_panel::AssistantPanel;
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};
@@ -31,8 +36,10 @@ actions!(
ToggleFocus,
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
ApplyEdit
]
);
@@ -182,6 +189,9 @@ pub struct LanguageModelChoiceDelta {
struct MessageMetadata {
role: Role,
status: MessageStatus,
// todo!("delete this")
#[serde(skip)]
ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -233,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

@@ -339,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>,
@@ -418,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::*;
@@ -440,7 +440,7 @@ mod tests {
);
// Ensure backward-compatibility.
cx.update_global::<SettingsStore, _>(|store, cx| {
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{
@@ -460,7 +460,7 @@ mod tests {
low_speed_timeout_in_seconds: None,
}
);
cx.update_global::<SettingsStore, _>(|store, cx| {
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{
@@ -482,7 +482,7 @@ mod tests {
);
// 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

@@ -233,7 +233,7 @@ impl CompletionProvider {
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))),
}
}

View File

@@ -204,9 +204,7 @@ pub fn count_open_ai_tokens(
.collect::<Vec<_>>();
match request.model {
LanguageModel::OpenAi(OpenAiModel::FourOmni)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
| LanguageModel::Anthropic(_)
LanguageModel::Anthropic(_)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {

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

@@ -24,7 +24,8 @@ 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};
@@ -124,7 +125,7 @@ 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)
});
@@ -288,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,
@@ -439,7 +440,7 @@ impl AssistantChat {
Markdown::new(
text,
self.markdown_style.clone(),
self.language_registry.clone(),
Some(self.language_registry.clone()),
cx,
)
});
@@ -550,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(),
@@ -572,7 +573,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
this.markdown_style.clone(),
this.language_registry.clone(),
Some(this.language_registry.clone()),
cx,
)
}),
@@ -666,7 +667,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
self.markdown_style.clone(),
self.language_registry.clone(),
Some(self.language_registry.clone()),
cx,
)
}),
@@ -682,7 +683,7 @@ impl AssistantChat {
Markdown::new(
"".into(),
self.markdown_style.clone(),
self.language_registry.clone(),
Some(self.language_registry.clone()),
cx,
)
}),
@@ -1107,7 +1108,7 @@ impl Render for AssistantChat {
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
.tooltip(move |cx| Tooltip::text("New Context", cx)),
)
.child(
IconButton::new("assistant-menu", IconName::Menu)

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

@@ -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};
@@ -139,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

@@ -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

@@ -17,8 +17,7 @@ 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;
@@ -29,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::{
@@ -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) {
@@ -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)
}

View File

@@ -15,9 +15,9 @@ 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;
#[cfg(not(debug_assertions))]
@@ -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,
});

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

@@ -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,
}
}

View File

@@ -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

@@ -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

@@ -2365,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;
@@ -2373,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(())
}
@@ -2434,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;
@@ -3683,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(())
}
@@ -3886,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,
@@ -3902,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(())
}
@@ -4048,7 +4067,6 @@ async fn send_channel_message(
let CreatedChannelMessage {
message_id,
participant_connection_ids,
channel_members,
notifications,
} = session
.db()
@@ -4079,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,
@@ -4095,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(())

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();

View File

@@ -19,7 +19,7 @@ use editor::{
};
use futures::StreamExt;
use git::diff::DiffHunkStatus;
use gpui::{BorrowAppContext, TestAppContext, VisualContext, VisualTestContext};
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
@@ -1517,7 +1517,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
cx_b.update(editor::init);
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
@@ -1531,7 +1531,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
@@ -1779,7 +1779,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
cx_b.update(editor::init);
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false,
@@ -1793,7 +1793,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
@@ -2269,14 +2269,14 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
min_column: None,
});
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});

View File

@@ -13,11 +13,11 @@ use fs::{FakeFs, Fs as _, RemoveOptions};
use futures::{channel::mpsc, StreamExt as _};
use git::repository::GitFileStatus;
use gpui::{
px, size, AppContext, BackgroundExecutor, BorrowAppContext, Model, Modifiers, MouseButton,
MouseDownEvent, TestAppContext,
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
TestAppContext, UpdateGlobal,
};
use language::{
language_settings::{AllLanguageSettings, Formatter},
language_settings::{AllLanguageSettings, Formatter, PrettierSettings},
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
@@ -4401,7 +4401,7 @@ async fn test_formatting_buffer(
// Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::External {
command: "awk".into(),
@@ -4445,18 +4445,17 @@ async fn test_prettier_formatting_buffer(
client_a.language_registry().add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
},
Some(tree_sitter_rust::language()),
)));
let mut fake_language_servers = client_a.language_registry().register_fake_lsp_adapter(
"Rust",
"TypeScript",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
@@ -4470,11 +4469,11 @@ async fn test_prettier_formatting_buffer(
let buffer_text = "let one = \"two\"";
client_a
.fs()
.insert_tree(&directory, json!({ "a.rs": buffer_text }))
.insert_tree(&directory, json!({ "a.ts": buffer_text }))
.await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let open_buffer = project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
let buffer_a = cx_a.executor().spawn(open_buffer).await.unwrap();
let project_id = active_call_a
@@ -4482,20 +4481,28 @@ async fn test_prettier_formatting_buffer(
.await
.unwrap();
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::Auto);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::LanguageServer);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});

View File

@@ -34,7 +34,6 @@ auto_update.workspace = true
call.workspace = true
channel.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true

View File

@@ -78,12 +78,14 @@ impl ChatPanel {
let fs = workspace.app_state().fs.clone();
let client = workspace.app_state().client.clone();
let channel_store = ChannelStore::global(cx);
let user_store = workspace.app_state().user_store.clone();
let languages = workspace.app_state().languages.clone();
let input_editor = cx.new_view(|cx| {
MessageEditor::new(
languages.clone(),
channel_store.clone(),
user_store.clone(),
None,
cx.new_view(|cx| Editor::auto_height(4, cx)),
cx,
)
@@ -231,19 +233,12 @@ impl ChatPanel {
fn set_active_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
let channel_id = chat.read(cx).channel_id;
{
self.markdown_data.clear();
let chat = chat.read(cx);
let channel_name = chat.channel(cx).map(|channel| channel.name.clone());
let message_count = chat.message_count();
self.message_list.reset(message_count);
self.message_editor.update(cx, |editor, cx| {
editor.set_channel(channel_id, channel_name, cx);
editor.clear_reply_to_message_id();
});
};
self.markdown_data.clear();
self.message_list.reset(chat.read(cx).message_count());
self.message_editor.update(cx, |editor, cx| {
editor.set_channel_chat(chat.clone(), cx);
editor.clear_reply_to_message_id();
});
let subscription = cx.subscribe(&chat, Self::channel_did_change);
self.active_chat = Some((chat, subscription));
self.acknowledge_last_message(cx);

View File

@@ -1,12 +1,12 @@
use anyhow::Result;
use channel::{ChannelMembership, ChannelStore, MessageParams};
use client::{ChannelId, UserId};
use collections::{HashMap, HashSet};
use channel::{ChannelChat, ChannelStore, MessageParams};
use client::{UserId, UserStore};
use collections::HashSet;
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
Render, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
};
use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
@@ -31,11 +31,10 @@ lazy_static! {
pub struct MessageEditor {
pub editor: View<Editor>,
channel_store: Model<ChannelStore>,
channel_members: HashMap<String, UserId>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
mentions: Vec<UserId>,
mentions_task: Option<Task<()>>,
channel_id: Option<ChannelId>,
reply_to_message_id: Option<u64>,
edit_message_id: Option<u64>,
}
@@ -76,12 +75,24 @@ impl CompletionProvider for MessageEditorCompletionProvider {
) -> 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 {
text == "@"
}
}
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
channel_store: Model<ChannelStore>,
user_store: Model<UserStore>,
channel_chat: Option<Model<ChannelChat>>,
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
@@ -127,9 +138,8 @@ impl MessageEditor {
Self {
editor,
channel_store,
channel_members: HashMap::default(),
channel_id: None,
user_store,
channel_chat,
mentions: Vec::new(),
mentions_task: None,
reply_to_message_id: None,
@@ -161,12 +171,13 @@ impl MessageEditor {
self.edit_message_id = None;
}
pub fn set_channel(
&mut self,
channel_id: ChannelId,
channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>,
) {
pub fn set_channel_chat(&mut self, chat: Model<ChannelChat>, cx: &mut ViewContext<Self>) {
let channel_id = chat.read(cx).channel_id;
self.channel_chat = Some(chat);
let channel_name = ChannelStore::global(cx)
.read(cx)
.channel_for_id(channel_id)
.map(|channel| channel.name.clone());
self.editor.update(cx, |editor, cx| {
if let Some(channel_name) = channel_name {
editor.set_placeholder_text(format!("Message #{channel_name}"), cx);
@@ -174,31 +185,6 @@ impl MessageEditor {
editor.set_placeholder_text("Message Channel", cx);
}
});
self.channel_id = Some(channel_id);
self.refresh_users(cx);
}
pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
if let Some(channel_id) = self.channel_id {
let members = self.channel_store.update(cx, |store, cx| {
store.get_channel_member_details(channel_id, cx)
});
cx.spawn(|this, mut cx| async move {
let members = members.await?;
this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
self.channel_members.clear();
self.channel_members.extend(
members
.into_iter()
.map(|member| (member.user.github_login.clone(), member.user.id)),
);
}
pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
@@ -368,13 +354,19 @@ impl MessageEditor {
let start_anchor = buffer.read(cx).anchor_before(start_offset);
let mut names = HashSet::default();
for (github_login, _) in self.channel_members.iter() {
names.insert(github_login.clone());
}
if let Some(channel_id) = self.channel_id {
for participant in self.channel_store.read(cx).channel_participants(channel_id) {
if let Some(chat) = self.channel_chat.as_ref() {
let chat = chat.read(cx);
for participant in ChannelStore::global(cx)
.read(cx)
.channel_participants(chat.channel_id)
{
names.insert(participant.github_login.clone());
}
for message in chat
.messages_in_range(chat.message_count().saturating_sub(100)..chat.message_count())
{
names.insert(message.sender.github_login.clone());
}
}
let candidates = names
@@ -481,11 +473,15 @@ impl MessageEditor {
text.clear();
text.extend(buffer.text_for_range(range.clone()));
if let Some(username) = text.strip_prefix('@') {
if let Some(user_id) = this.channel_members.get(username) {
if let Some(user) = this
.user_store
.read(cx)
.cached_user_by_github_login(username)
{
let start = multi_buffer.anchor_after(range.start);
let end = multi_buffer.anchor_after(range.end);
mentioned_user_ids.push(*user_id);
mentioned_user_ids.push(user.id);
anchor_ranges.push(start..end);
}
}
@@ -550,106 +546,3 @@ impl Render for MessageEditor {
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Client, User, UserStore};
use clock::FakeSystemClock;
use gpui::TestAppContext;
use http::FakeHttpClient;
use language::{Language, LanguageConfig};
use project::Project;
use rpc::proto;
use settings::SettingsStore;
use util::test::marked_text_ranges;
#[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) {
let language_registry = init_test(cx);
let (editor, cx) = cx.add_window_view(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.new_view(|cx| Editor::auto_height(4, cx)),
cx,
)
});
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
editor.set_members(
vec![
ChannelMembership {
user: Arc::new(User {
github_login: "a-b".into(),
id: 101,
avatar_uri: "avatar_a-b".into(),
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
ChannelMembership {
user: Arc::new(User {
github_login: "C_D".into(),
id: 102,
avatar_uri: "avatar_C_D".into(),
}),
kind: proto::channel_member::Kind::Member,
role: proto::ChannelRole::Member,
},
],
cx,
);
editor.editor.update(cx, |editor, cx| {
editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
});
});
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
assert_eq!(
editor.take_message(cx),
MessageParams {
text,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
reply_to_message_id: None
}
);
});
}
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);
channel::init(&client, user_store, cx);
MessageEditorSettings::register(cx);
});
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
..Default::default()
},
Some(tree_sitter_markdown::language()),
)));
language_registry
}
}

View File

@@ -1569,11 +1569,28 @@ impl CollabPanel {
*pending_name = Some(channel_name.clone());
self.channel_store
.update(cx, |channel_store, cx| {
channel_store.create_channel(&channel_name, *location, cx)
let create = self.channel_store.update(cx, |channel_store, cx| {
channel_store.create_channel(&channel_name, *location, cx)
});
if location.is_none() {
cx.spawn(|this, mut cx| async move {
let channel_id = create.await?;
this.update(&mut cx, |this, cx| {
this.show_channel_modal(
channel_id,
channel_modal::Mode::InviteMembers,
cx,
)
})
})
.detach();
.detach_and_prompt_err(
"Failed to create channel",
cx,
|_, _| None,
);
} else {
create.detach_and_prompt_err("Failed to create channel", cx, |_, _| None);
}
cx.notify();
}
ChannelEditingState::Rename {
@@ -1859,12 +1876,8 @@ impl CollabPanel {
let workspace = self.workspace.clone();
let user_store = self.user_store.clone();
let channel_store = self.channel_store.clone();
let members = self.channel_store.update(cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
});
cx.spawn(|_, mut cx| async move {
let members = members.await?;
workspace.update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
ChannelModal::new(
@@ -1872,7 +1885,6 @@ impl CollabPanel {
channel_store.clone(),
channel_id,
mode,
members,
cx,
)
});
@@ -2949,7 +2961,7 @@ struct DraggedChannelView {
}
impl Render for DraggedChannelView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
h_flex()
.font_family(ui_font)

View File

@@ -37,7 +37,6 @@ impl ChannelModal {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
@@ -54,7 +53,8 @@ impl ChannelModal {
channel_id,
match_candidates: Vec::new(),
context_menu: None,
members,
members: Vec::new(),
has_all_members: false,
mode,
},
cx,
@@ -78,37 +78,15 @@ impl ChannelModal {
}
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let channel_id = self.channel_id;
cx.spawn(|this, mut cx| async move {
if mode == Mode::ManageMembers {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
})?
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
.update(cx, |picker, _| picker.delegate.members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
})
})
.detach();
self.picker.update(cx, |picker, cx| {
let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
picker.update_matches(picker.query(cx), cx);
cx.notify()
});
cx.notify()
}
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
@@ -260,6 +238,7 @@ pub struct ChannelModalDelegate {
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
has_all_members: bool,
context_menu: Option<(View<ContextMenu>, Subscription)>,
}
@@ -288,37 +267,59 @@ impl PickerDelegate for ChannelModalDelegate {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
match self.mode {
Mode::ManageMembers => {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
if self.has_all_members {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
}));
let matches = cx.background_executor().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = &mut picker.delegate;
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
} else {
let search_members = self.channel_store.update(cx, |store, cx| {
store.fuzzy_search_members(self.channel_id, query.clone(), 100, cx)
});
cx.spawn(|picker, mut cx| async move {
async {
let members = search_members.await?;
picker.update(&mut cx, |picker, cx| {
picker.delegate.has_all_members =
query == "" && members.len() < 100;
picker.delegate.matching_member_indices =
(0..members.len()).collect();
picker.delegate.members = members;
cx.notify();
})?;
anyhow::Ok(())
}
}));
let matches = cx.background_executor().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
let delegate = &mut picker.delegate;
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
.extend(matches.into_iter().map(|m| m.candidate_id));
cx.notify();
})
.ok();
})
.log_err()
.await;
})
}
}
Mode::InviteMembers => {
let search_users = self

View File

@@ -731,7 +731,7 @@ impl CollabTitlebarItem {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.action("Themes...", theme_selector::Toggle::default().boxed_clone())
.action("Themes", theme_selector::Toggle::default().boxed_clone())
.separator()
.action("Sign Out", client::SignOut.boxed_clone())
})
@@ -743,7 +743,11 @@ impl CollabTitlebarItem {
h_flex()
.gap_0p5()
.child(Avatar::new(user.avatar_uri.clone()))
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
.child(
Icon::new(IconName::ChevronDown)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@@ -755,16 +759,18 @@ impl CollabTitlebarItem {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.action("Themes...", theme_selector::Toggle::default().boxed_clone())
.action("Themes", theme_selector::Toggle::default().boxed_clone())
})
.into()
})
.trigger(
ButtonLike::new("user-menu")
.child(
h_flex()
.gap_0p5()
.child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
h_flex().gap_0p5().child(
Icon::new(IconName::ChevronDown)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),

View File

@@ -12,7 +12,7 @@ use command_palette_hooks::{
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@@ -362,7 +362,7 @@ impl PickerDelegate for CommandPaletteDelegate {
self.matches.clear();
self.commands.clear();
cx.update_global(|hit_counts: &mut HitCounts, _| {
HitCounts::update_global(cx, |hit_counts, _cx| {
*hit_counts.0.entry(command.name).or_default() += 1;
});
let action = command.action;

View File

@@ -59,6 +59,10 @@ impl CopilotCompletionProvider {
}
impl InlineCompletionProvider for CopilotCompletionProvider {
fn name() -> &'static str {
"copilot"
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -186,17 +190,23 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_copilot_event(
Some(completion.uuid.clone()),
true,
self.file_extension.clone(),
);
if self.active_completion().is_some() {
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_inline_completion_event(
Self::name().to_string(),
true,
self.file_extension.clone(),
);
}
}
}
}
fn discard(&mut self, cx: &mut ModelContext<Self>) {
fn discard(
&mut self,
should_report_inline_completion_event: bool,
cx: &mut ModelContext<Self>,
) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None);
@@ -210,8 +220,17 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
copilot.discard_completions(&self.completions, cx)
})
.detach_and_log_err(cx);
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_copilot_event(None, false, self.file_extension.clone());
if should_report_inline_completion_event {
if self.active_completion().is_some() {
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_inline_completion_event(
Self::name().to_string(),
false,
self.file_extension.clone(),
);
}
}
}
}
@@ -273,7 +292,7 @@ mod tests {
};
use fs::FakeFs;
use futures::StreamExt;
use gpui::{BackgroundExecutor, BorrowAppContext, Context, TestAppContext};
use gpui::{BackgroundExecutor, Context, TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
@@ -473,8 +492,8 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// Tabbing when there is an active suggestion inserts it.
editor.tab(&Default::default(), cx);
// AcceptInlineCompletion when there is an active suggestion inserts it.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
@@ -531,8 +550,8 @@ mod tests {
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Tabbing again accepts the suggestion.
editor.tab(&Default::default(), cx);
// Using AcceptInlineCompletion again accepts the suggestion.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
@@ -1138,7 +1157,7 @@ mod tests {
editor::init_settings(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
});

View File

@@ -39,6 +39,7 @@ impl From<proto::DevServerProject> for DevServerProject {
pub struct DevServer {
pub id: DevServerId,
pub name: SharedString,
pub ssh_connection_string: Option<SharedString>,
pub status: DevServerStatus,
}
@@ -48,6 +49,7 @@ impl From<proto::DevServer> for DevServer {
id: DevServerId(dev_server.dev_server_id),
status: dev_server.status(),
name: dev_server.name.into(),
ssh_connection_string: dev_server.ssh_connection_string.map(|s| s.into()),
}
}
}
@@ -164,11 +166,17 @@ impl Store {
pub fn create_dev_server(
&mut self,
name: String,
ssh_connection_string: Option<String>,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::CreateDevServerResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
let result = client.request(proto::CreateDevServer { name }).await?;
let result = client
.request(proto::CreateDevServer {
name,
ssh_connection_string,
})
.await?;
Ok(result)
})
}
@@ -177,6 +185,7 @@ impl Store {
&mut self,
dev_server_id: DevServerId,
name: String,
ssh_connection_string: Option<String>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let client = self.client.clone();
@@ -185,6 +194,7 @@ impl Store {
.request(proto::RenameDevServer {
dev_server_id: dev_server_id.0,
name,
ssh_connection_string,
})
.await?;
Ok(())

View File

@@ -87,7 +87,7 @@ struct DiagnosticGroupState {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let child = if self.path_states.is_empty() {
div()
.bg(cx.theme().colors().editor_background)

View File

@@ -19,6 +19,7 @@ test-support = [
"gpui/test-support",
"multi_buffer/test-support",
"project/test-support",
"theme/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
@@ -86,6 +87,7 @@ release_channel.workspace = true
rand.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true

View File

@@ -143,6 +143,7 @@ gpui::actions!(
editor,
[
AcceptPartialCopilotSuggestion,
AcceptInlineCompletion,
AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,

View File

@@ -18,6 +18,7 @@
//! [EditorElement]: crate::element::EditorElement
mod block_map;
mod flap_map;
mod fold_map;
mod inlay_map;
mod tab_map;
@@ -28,7 +29,9 @@ use crate::{EditorStyle, RowExt};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle};
use gpui::{
AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
use inlay_map::InlayMap;
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
@@ -42,6 +45,7 @@ use serde::Deserialize;
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use ui::WindowContext;
use wrap_map::WrapMap;
@@ -49,10 +53,15 @@ pub use block_map::{
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
pub use flap_map::*;
use self::block_map::BlockRow;
use self::block_map::{BlockRow, BlockSnapshot};
use self::fold_map::FoldSnapshot;
pub use self::fold_map::{Fold, FoldId, FoldPoint};
use self::inlay_map::InlaySnapshot;
pub use self::inlay_map::{InlayOffset, InlayPoint};
use self::tab_map::TabSnapshot;
use self::wrap_map::WrapSnapshot;
pub(crate) use inlay_map::Inlay;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -61,6 +70,8 @@ pub enum FoldStatus {
Foldable,
}
pub type RenderFoldToggle = Arc<dyn Fn(FoldStatus, &mut WindowContext) -> AnyElement>;
const UNNECESSARY_CODE_FADE: f32 = 0.3;
pub trait ToDisplayPoint {
@@ -92,6 +103,8 @@ pub struct DisplayMap {
text_highlights: TextHighlights,
/// Regions of inlays that should be highlighted.
inlay_highlights: InlayHighlights,
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
flap_map: FlapMap,
pub clip_at_line_ends: bool,
}
@@ -113,7 +126,9 @@ impl DisplayMap {
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
let flap_map = FlapMap::default();
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
buffer_subscription,
@@ -122,6 +137,7 @@ impl DisplayMap {
tab_map,
wrap_map,
block_map,
flap_map,
text_highlights: Default::default(),
inlay_highlights: Default::default(),
clip_at_line_ends: false,
@@ -147,6 +163,7 @@ impl DisplayMap {
tab_snapshot,
wrap_snapshot,
block_snapshot,
flap_snapshot: self.flap_map.snapshot(),
text_highlights: self.text_highlights.clone(),
inlay_highlights: self.inlay_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
@@ -157,14 +174,14 @@ impl DisplayMap {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| fold.range.to_offset(&other.buffer_snapshot)),
.map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)),
cx,
);
}
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -209,6 +226,24 @@ impl DisplayMap {
self.block_map.read(snapshot, edits);
}
pub fn insert_flaps(
&mut self,
flaps: impl IntoIterator<Item = Flap>,
cx: &mut ModelContext<Self>,
) -> Vec<FlapId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.flap_map.insert(flaps, &snapshot)
}
pub fn remove_flaps(
&mut self,
flap_ids: impl IntoIterator<Item = FlapId>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
self.flap_map.remove(flap_ids, &snapshot)
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -367,11 +402,12 @@ pub struct HighlightedChunk<'a> {
#[derive(Clone)]
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: fold_map::FoldSnapshot,
inlay_snapshot: inlay_map::InlaySnapshot,
tab_snapshot: tab_map::TabSnapshot,
wrap_snapshot: wrap_map::WrapSnapshot,
block_snapshot: block_map::BlockSnapshot,
pub fold_snapshot: FoldSnapshot,
pub flap_snapshot: FlapSnapshot,
inlay_snapshot: InlaySnapshot,
tab_snapshot: TabSnapshot,
wrap_snapshot: WrapSnapshot,
block_snapshot: BlockSnapshot,
text_highlights: TextHighlights,
inlay_highlights: InlayHighlights,
clip_at_line_ends: bool,
@@ -833,17 +869,7 @@ impl DisplaySnapshot {
DisplayRow(self.block_snapshot.longest_row())
}
pub fn fold_for_line(&self, buffer_row: MultiBufferRow) -> Option<FoldStatus> {
if self.is_line_folded(buffer_row) {
Some(FoldStatus::Folded)
} else if self.is_foldable(buffer_row) {
Some(FoldStatus::Foldable)
} else {
None
}
}
pub fn is_foldable(&self, buffer_row: MultiBufferRow) -> bool {
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_buffer_row();
if buffer_row >= max_row {
return false;
@@ -867,9 +893,17 @@ impl DisplaySnapshot {
false
}
pub fn foldable_range(&self, buffer_row: MultiBufferRow) -> Option<Range<Point>> {
pub fn foldable_range(
&self,
buffer_row: MultiBufferRow,
) -> Option<(Range<Point>, &'static str)> {
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
if self.is_foldable(MultiBufferRow(start.row))
if let Some(flap) = self
.flap_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
Some((flap.range.to_point(&self.buffer_snapshot), ""))
} else if self.starts_indent(MultiBufferRow(start.row))
&& !self.is_line_folded(MultiBufferRow(start.row))
{
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
@@ -888,7 +922,7 @@ impl DisplaySnapshot {
}
}
let end = end.unwrap_or(max_point);
Some(start..end)
Some((start..end, ""))
} else {
None
}
@@ -1165,7 +1199,7 @@ pub mod tests {
} else {
log::info!("folding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.fold(ranges, cx);
map.fold(ranges.into_iter().map(|range| (range, "")), cx);
});
}
}
@@ -1513,7 +1547,10 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
)],
cx,
)
});
@@ -1595,7 +1632,10 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)],
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
)],
cx,
)
});
@@ -1754,6 +1794,33 @@ pub mod tests {
assert("αˇ", cx);
}
#[gpui::test]
fn test_flaps(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
let buffer = MultiBuffer::build_simple(text, cx);
let font_size = px(14.0);
cx.new_model(|cx| {
let mut map =
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx);
let snapshot = map.buffer.read(cx).snapshot(cx);
let range =
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
map.flap_map.insert(
[Flap::new(
range,
|_row, _status, _toggle, _cx| div(),
|_row, _status, _cx| div(),
)],
&map.buffer.read(cx).snapshot(cx),
);
map
});
}
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
init_test(cx, |_| {});

View File

@@ -0,0 +1,292 @@
use collections::HashMap;
use gpui::{AnyElement, IntoElement};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point;
use ui::WindowContext;
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct FlapId(usize);
#[derive(Default)]
pub struct FlapMap {
snapshot: FlapSnapshot,
next_id: FlapId,
id_to_range: HashMap<FlapId, Range<Anchor>>,
}
#[derive(Clone, Default)]
pub struct FlapSnapshot {
flaps: SumTree<FlapItem>,
}
impl FlapSnapshot {
/// Returns the first Flap starting on the specified buffer row.
pub fn query_row<'a>(
&'a self,
row: MultiBufferRow,
snapshot: &'a MultiBufferSnapshot,
) -> Option<&'a Flap> {
let start = snapshot.anchor_before(Point::new(row.0, 0));
let mut cursor = self.flaps.cursor::<ItemSummary>();
cursor.seek(&start, Bias::Left, snapshot);
while let Some(item) = cursor.item() {
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
Ordering::Equal => return Some(&item.flap),
Ordering::Greater => break,
}
}
return None;
}
pub fn flap_items_with_offsets(
&self,
snapshot: &MultiBufferSnapshot,
) -> Vec<(FlapId, Range<Point>)> {
let mut cursor = self.flaps.cursor::<ItemSummary>();
let mut results = Vec::new();
cursor.next(snapshot);
while let Some(item) = cursor.item() {
let start_point = item.flap.range.start.to_point(snapshot);
let end_point = item.flap.range.end.to_point(snapshot);
results.push((item.id, start_point..end_point));
cursor.next(snapshot);
}
results
}
}
type RenderToggleFn = Arc<
dyn Send
+ Sync
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
) -> AnyElement,
>;
type RenderTrailerFn =
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
#[derive(Clone)]
pub struct Flap {
pub range: Range<Anchor>,
pub render_toggle: RenderToggleFn,
pub render_trailer: RenderTrailerFn,
}
impl Flap {
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
range: Range<Anchor>,
render_toggle: RenderToggle,
render_trailer: RenderTrailer,
) -> Self
where
RenderToggle: 'static
+ Send
+ Sync
+ Fn(
MultiBufferRow,
bool,
Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
&mut WindowContext,
) -> ToggleElement
+ 'static,
ToggleElement: IntoElement,
RenderTrailer: 'static
+ Send
+ Sync
+ Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement
+ 'static,
TrailerElement: IntoElement,
{
Flap {
range,
render_toggle: Arc::new(move |row, folded, toggle, cx| {
render_toggle(row, folded, toggle, cx).into_any_element()
}),
render_trailer: Arc::new(move |row, folded, cx| {
render_trailer(row, folded, cx).into_any_element()
}),
}
}
}
impl std::fmt::Debug for Flap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Flap").field("range", &self.range).finish()
}
}
#[derive(Clone, Debug)]
struct FlapItem {
id: FlapId,
flap: Flap,
}
impl FlapMap {
pub fn snapshot(&self) -> FlapSnapshot {
self.snapshot.clone()
}
pub fn insert(
&mut self,
flaps: impl IntoIterator<Item = Flap>,
snapshot: &MultiBufferSnapshot,
) -> Vec<FlapId> {
let mut new_ids = Vec::new();
self.snapshot.flaps = {
let mut new_flaps = SumTree::new();
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
for flap in flaps {
new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot);
let id = self.next_id;
self.next_id.0 += 1;
self.id_to_range.insert(id, flap.range.clone());
new_flaps.push(FlapItem { flap, id }, snapshot);
new_ids.push(id);
}
new_flaps.append(cursor.suffix(snapshot), snapshot);
new_flaps
};
new_ids
}
pub fn remove(
&mut self,
ids: impl IntoIterator<Item = FlapId>,
snapshot: &MultiBufferSnapshot,
) {
let mut removals = Vec::new();
for id in ids {
if let Some(range) = self.id_to_range.remove(&id) {
removals.push((id, range.clone()));
}
}
removals.sort_unstable_by(|(a_id, a_range), (b_id, b_range)| {
AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id))
});
self.snapshot.flaps = {
let mut new_flaps = SumTree::new();
let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
for (id, range) in removals {
new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot);
while let Some(item) = cursor.item() {
cursor.next(snapshot);
if item.id == id {
break;
} else {
new_flaps.push(item.clone(), snapshot);
}
}
}
new_flaps.append(cursor.suffix(snapshot), snapshot);
new_flaps
};
}
}
#[derive(Debug, Clone)]
pub struct ItemSummary {
range: Range<Anchor>,
}
impl Default for ItemSummary {
fn default() -> Self {
Self {
range: Anchor::min()..Anchor::min(),
}
}
}
impl sum_tree::Summary for ItemSummary {
type Context = MultiBufferSnapshot;
fn add_summary(&mut self, other: &Self, _snapshot: &MultiBufferSnapshot) {
self.range = other.range.clone();
}
}
impl sum_tree::Item for FlapItem {
type Summary = ItemSummary;
fn summary(&self) -> Self::Summary {
ItemSummary {
range: self.flap.range.clone(),
}
}
}
/// Implements `SeekTarget` for `Range<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
AnchorRangeExt::cmp(self, &cursor_location.range, snapshot)
}
}
impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor {
fn cmp(&self, other: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering {
self.cmp(&other.range.start, snapshot)
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{div, AppContext};
use multi_buffer::MultiBuffer;
#[gpui::test]
fn test_insert_and_remove_flaps(cx: &mut AppContext) {
let text = "line1\nline2\nline3\nline4\nline5";
let buffer = MultiBuffer::build_simple(text, cx);
let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let mut flap_map = FlapMap::default();
// Insert flaps
let flaps = [
Flap::new(
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Flap::new(
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
];
let flap_ids = flap_map.insert(flaps, &snapshot);
assert_eq!(flap_ids.len(), 2);
// Verify flaps are inserted
let flap_snapshot = flap_map.snapshot();
assert!(flap_snapshot
.query_row(MultiBufferRow(1), &snapshot)
.is_some());
assert!(flap_snapshot
.query_row(MultiBufferRow(3), &snapshot)
.is_some());
// Remove flaps
flap_map.remove(flap_ids, &snapshot);
// Verify flaps are removed
let flap_snapshot = flap_map.snapshot();
assert!(flap_snapshot
.query_row(MultiBufferRow(1), &snapshot)
.is_none());
assert!(flap_snapshot
.query_row(MultiBufferRow(3), &snapshot)
.is_none());
}
}

View File

@@ -75,12 +75,12 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> {
pub(crate) fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
for range in ranges.into_iter() {
for (range, fold_text) in ranges.into_iter() {
let buffer = &snapshot.buffer;
let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
@@ -99,6 +99,7 @@ impl<'a> FoldMapWriter<'a> {
folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
text: fold_text,
});
let inlay_range =
@@ -324,11 +325,14 @@ impl FoldMap {
let mut folds = iter::from_fn({
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|f| {
let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer);
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end)
let item = folds_cursor.item().map(|fold| {
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
(
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end),
fold.text,
)
});
folds_cursor.next(&inlay_snapshot.buffer);
item
@@ -336,25 +340,27 @@ impl FoldMap {
})
.peekable();
while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
let mut fold = folds.next().unwrap();
while folds
.peek()
.map_or(false, |(fold_range, _)| fold_range.start < edit.new.end)
{
let (mut fold_range, fold_text) = folds.next().unwrap();
let sum = new_transforms.summary();
assert!(fold.start.0 >= sum.input.len);
assert!(fold_range.start.0 >= sum.input.len);
while folds
.peek()
.map_or(false, |next_fold| next_fold.start <= fold.end)
{
let next_fold = folds.next().unwrap();
if next_fold.end > fold.end {
fold.end = next_fold.end;
while folds.peek().map_or(false, |(next_fold_range, _)| {
next_fold_range.start <= fold_range.end
}) {
let (next_fold_range, _) = folds.next().unwrap();
if next_fold_range.end > fold_range.end {
fold_range.end = next_fold_range.end;
}
}
if fold.start.0 > sum.input.len {
if fold_range.start.0 > sum.input.len {
let text_summary = inlay_snapshot
.text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
.text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start);
new_transforms.push(
Transform {
summary: TransformSummary {
@@ -367,16 +373,15 @@ impl FoldMap {
);
}
if fold.end > fold.start {
let output_text = "";
if fold_range.end > fold_range.start {
new_transforms.push(
Transform {
summary: TransformSummary {
output: TextSummary::from(output_text),
output: TextSummary::from(fold_text),
input: inlay_snapshot
.text_summary_for_range(fold.start..fold.end),
.text_summary_for_range(fold_range.start..fold_range.end),
},
output_text: Some(output_text),
output_text: Some(fold_text),
},
&(),
);
@@ -853,6 +858,7 @@ impl Into<ElementId> for FoldId {
pub struct Fold {
pub id: FoldId,
pub range: FoldRange,
pub text: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -948,7 +954,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange {
impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange {
fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
self.0.cmp(&other.0, buffer)
AnchorRangeExt::cmp(&self.0, &other.0, buffer)
}
}
@@ -1159,8 +1165,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
let (snapshot2, edits) = writer.fold(vec![
Point::new(0, 2)..Point::new(2, 2),
Point::new(2, 4)..Point::new(4, 1),
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(2, 4)..Point::new(4, 1), ""),
]);
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
assert_eq!(
@@ -1239,19 +1245,19 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![5..8]);
writer.fold(vec![(5..8, "")]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "abcde⋯ijkl");
// Create an fold adjacent to the start of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![0..1, 2..5]);
writer.fold(vec![(0..1, ""), (2..5, "")]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
// Create an fold adjacent to the end of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![11..11, 8..10]);
writer.fold(vec![(11..11, ""), (8..10, "")]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯kl");
}
@@ -1261,7 +1267,7 @@ mod tests {
// Create two adjacent folds.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![0..2, 2..5]);
writer.fold(vec![(0..2, ""), (2..5, "")]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "⋯fghijkl");
@@ -1285,10 +1291,10 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
Point::new(0, 2)..Point::new(2, 2),
Point::new(0, 4)..Point::new(1, 0),
Point::new(1, 2)..Point::new(3, 2),
Point::new(3, 1)..Point::new(4, 1),
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯eeeee");
@@ -1305,8 +1311,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
Point::new(0, 2)..Point::new(2, 2),
Point::new(3, 1)..Point::new(4, 1),
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
@@ -1330,10 +1336,10 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
Point::new(0, 2)..Point::new(2, 2),
Point::new(0, 4)..Point::new(1, 0),
Point::new(1, 2)..Point::new(3, 2),
Point::new(3, 1)..Point::new(4, 1),
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot
@@ -1408,10 +1414,10 @@ mod tests {
snapshot_edits.push((snapshot.clone(), edits));
let mut expected_text: String = inlay_snapshot.text().to_string();
for fold_range in map.merged_fold_ranges().into_iter().rev() {
for (fold_range, fold_text) in map.merged_folds().into_iter().rev() {
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "");
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text);
}
assert_eq!(snapshot.text(), expected_text);
@@ -1423,7 +1429,7 @@ mod tests {
let mut prev_row = 0;
let mut expected_buffer_rows = Vec::new();
for fold_range in map.merged_fold_ranges().into_iter() {
for (fold_range, _fold_text) in map.merged_folds().into_iter() {
let fold_start = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
.row();
@@ -1535,11 +1541,11 @@ mod tests {
}
let folded_buffer_rows = map
.merged_fold_ranges()
.merged_folds()
.iter()
.flat_map(|range| {
let start_row = range.start.to_point(&buffer_snapshot).row;
let end = range.end.to_point(&buffer_snapshot);
.flat_map(|(fold_range, _)| {
let start_row = fold_range.start.to_point(&buffer_snapshot).row;
let end = fold_range.end.to_point(&buffer_snapshot);
if end.column == 0 {
start_row..end.row
} else {
@@ -1634,8 +1640,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
Point::new(0, 2)..Point::new(2, 2),
Point::new(3, 1)..Point::new(4, 1),
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
@@ -1653,34 +1659,39 @@ mod tests {
}
impl FoldMap {
fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
fn merged_folds(&self) -> Vec<(Range<usize>, &'static str)> {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
let mut fold_ranges = folds
let mut folds = folds
.iter()
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
.map(|fold| {
(
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer),
fold.text,
)
})
.peekable();
let mut merged_ranges = Vec::new();
while let Some(mut fold_range) = fold_ranges.next() {
while let Some(next_range) = fold_ranges.peek() {
let mut merged_folds = Vec::new();
while let Some((mut fold_range, fold_text)) = folds.next() {
while let Some((next_range, _)) = folds.peek() {
if fold_range.end >= next_range.start {
if next_range.end > fold_range.end {
fold_range.end = next_range.end;
}
fold_ranges.next();
folds.next();
} else {
break;
}
}
if fold_range.end > fold_range.start {
merged_ranges.push(fold_range);
merged_folds.push((fold_range, fold_text));
}
}
merged_ranges
merged_folds
}
pub fn randomly_mutate(
@@ -1698,10 +1709,11 @@ mod tests {
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
to_unfold.push(start..end);
}
log::info!("unfolding {:?}", to_unfold);
let inclusive = rng.gen();
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
snapshot_edits.push((snapshot, edits));
let (snapshot, edits) = writer.fold(to_unfold);
let (snapshot, edits) = writer.unfold(to_unfold, inclusive);
snapshot_edits.push((snapshot, edits));
}
_ => {
@@ -1711,7 +1723,8 @@ mod tests {
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
to_fold.push(start..end);
let text = if rng.gen() { "" } else { "" };
to_fold.push((start..end, text));
}
log::info!("folding {:?}", to_fold);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,12 @@ use crate::{
JoinLines,
};
use futures::StreamExt;
use gpui::{div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions};
use gpui::{div, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
},
BracketPairConfig,
Capability::ReadWrite,
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
@@ -492,8 +494,8 @@ fn test_clone(cx: &mut TestAppContext) {
editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
editor.fold_ranges(
[
Point::new(1, 0)..Point::new(2, 0),
Point::new(3, 0)..Point::new(4, 0),
(Point::new(1, 0)..Point::new(2, 0), ""),
(Point::new(3, 0)..Point::new(4, 0), ""),
],
true,
cx,
@@ -901,9 +903,9 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 6)..Point::new(0, 12),
Point::new(1, 2)..Point::new(1, 4),
Point::new(2, 4)..Point::new(2, 8),
(Point::new(0, 6)..Point::new(0, 12), ""),
(Point::new(1, 2)..Point::new(1, 4), ""),
(Point::new(2, 4)..Point::new(2, 8), ""),
],
true,
cx,
@@ -3405,9 +3407,9 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 2)..Point::new(1, 2),
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
(Point::new(0, 2)..Point::new(1, 2), ""),
(Point::new(2, 3)..Point::new(4, 1), ""),
(Point::new(7, 0)..Point::new(8, 4), ""),
],
true,
cx,
@@ -3889,9 +3891,9 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 2)..Point::new(1, 2),
Point::new(2, 3)..Point::new(4, 1),
Point::new(7, 0)..Point::new(8, 4),
(Point::new(0, 2)..Point::new(1, 2), ""),
(Point::new(2, 3)..Point::new(4, 1), ""),
(Point::new(7, 0)..Point::new(8, 4), ""),
],
true,
cx,
@@ -4546,8 +4548,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
_ = view.update(cx, |view, cx| {
view.fold_ranges(
vec![
Point::new(0, 21)..Point::new(0, 24),
Point::new(3, 20)..Point::new(3, 22),
(Point::new(0, 21)..Point::new(0, 24), ""),
(Point::new(3, 20)..Point::new(3, 22), ""),
],
true,
cx,
@@ -6254,13 +6256,18 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
// Enable Prettier formatting for the same buffer, and ensure
// LSP is called instead of Prettier.
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
..LanguageConfig::default()
},
Some(tree_sitter_rust::language()),
)));
update_test_language_settings(cx, |settings| {
// Enable Prettier formatting for the same buffer, and ensure
// LSP is called instead of Prettier.
settings.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
let mut fake_servers = language_registry.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
@@ -6524,6 +6531,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx,
)
.await;
let counter = Arc::new(AtomicUsize::new(0));
cx.set_state(indoc! {"
oneˇ
@@ -6539,10 +6547,13 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
three
"},
vec!["first_completion", "second_completion"],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
editor
@@ -6613,10 +6624,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
cx.simulate_keystroke("i");
@@ -6629,10 +6642,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor
@@ -6667,9 +6682,17 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions, cx);
});
handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
handle_completion_request(
&mut cx,
"editor.<clo|>",
vec!["close", "clobber"],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
let apply_additional_edits = cx.update_editor(|editor, cx| {
editor
.confirm_completion(&ConfirmCompletion::default(), cx)
@@ -6680,6 +6703,103 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_no_duplicated_completion_requests(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let completion_item = lsp::CompletionItem {
label: "Some".into(),
kind: Some(lsp::CompletionItemKind::SNIPPET),
detail: Some("Wrap the expression in an `Option::Some`".to_string()),
documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "```rust\nSome(2)\n```".to_string(),
})),
deprecated: Some(false),
sort_text: Some("Some".to_string()),
filter_text: Some("Some".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 22,
},
end: lsp::Position {
line: 0,
character: 22,
},
},
new_text: "Some(2)".to_string(),
})),
additional_text_edits: Some(vec![lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 22,
},
},
new_text: "".to_string(),
}]),
..Default::default()
};
let closure_completion_item = completion_item.clone();
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let task_completion_item = closure_completion_item.clone();
counter_clone.fetch_add(1, atomic::Ordering::Release);
async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
task_completion_item,
])))
}
});
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.assert_editor_state(indoc! {"fn main() { let a = 2.ˇ; }"});
assert!(request.next().await.is_some());
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.simulate_keystroke("S");
cx.simulate_keystroke("o");
cx.simulate_keystroke("m");
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.assert_editor_state(indoc! {"fn main() { let a = 2.Somˇ; }"});
assert!(request.next().await.is_some());
assert!(request.next().await.is_some());
assert!(request.next().await.is_some());
request.close();
assert!(request.next().await.is_none());
assert_eq!(
counter.load(atomic::Ordering::Acquire),
4,
"With the completions menu open, only one LSP request should happen per input"
);
}
#[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -8599,27 +8719,32 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
});
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
fs.insert_file("/file.ts", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let project = Project::test(fs, ["/file.ts".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
path_suffixes: vec!["ts".to_string()],
..Default::default()
},
prettier_parser_name: Some("test_parser".to_string()),
..Default::default()
},
Some(tree_sitter_rust::language()),
)));
update_test_language_settings(cx, |settings| {
settings.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
let test_plugin = "test_plugin";
let _ = language_registry.register_fake_lsp_adapter(
"Rust",
"TypeScript",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
@@ -8628,7 +8753,7 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.update(cx, |project, cx| project.open_local_buffer("/file.ts", cx))
.await
.unwrap();
@@ -11323,6 +11448,67 @@ async fn test_multiple_expanded_hunks_merge(
);
}
#[gpui::test]
fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
build_editor(buffer, cx)
});
let render_args = Arc::new(Mutex::new(None));
let snapshot = editor
.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let range =
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
struct RenderArgs {
row: MultiBufferRow,
folded: bool,
callback: Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
}
let flap = Flap::new(
range,
{
let toggle_callback = render_args.clone();
move |row, folded, callback, _cx| {
*toggle_callback.lock() = Some(RenderArgs {
row,
folded,
callback,
});
div()
}
},
|_row, _folded, _cx| div(),
);
editor.insert_flaps(Some(flap), cx);
let snapshot = editor.snapshot(cx);
let _div = snapshot.render_fold_toggle(MultiBufferRow(1), false, cx.view().clone(), cx);
snapshot
})
.unwrap();
let render_args = render_args.lock().take().unwrap();
assert_eq!(render_args.row, MultiBufferRow(1));
assert_eq!(render_args.folded, false);
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
cx.update_window(*editor, |_, cx| (render_args.callback)(true, cx))
.unwrap();
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
assert!(snapshot.is_line_folded(MultiBufferRow(1)));
cx.update_window(*editor, |_, cx| (render_args.callback)(false, cx))
.unwrap();
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap();
assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -11346,6 +11532,7 @@ pub fn handle_completion_request(
cx: &mut EditorLspTestContext,
marked_string: &str,
completions: Vec<&'static str>,
counter: Arc<AtomicUsize>,
) -> impl Future<Output = ()> {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
@@ -11361,6 +11548,7 @@ pub fn handle_completion_request(
let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
counter.fetch_add(1, atomic::Ordering::Release);
async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
@@ -11424,7 +11612,7 @@ pub(crate) fn update_test_language_settings(
f: impl Fn(&mut AllLanguageSettingsContent),
) {
_ = cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
});
@@ -11435,7 +11623,7 @@ pub(crate) fn update_test_project_settings(
f: impl Fn(&mut ProjectSettings),
) {
_ = cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<ProjectSettings>(cx, f);
});
});

View File

@@ -1,8 +1,7 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
display_map::{
BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint,
TransformBlock,
BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock,
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar,
@@ -26,7 +25,7 @@ use crate::{
};
use anyhow::Result;
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
use collections::{BTreeMap, HashMap};
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
@@ -51,7 +50,7 @@ use smallvec::SmallVec;
use std::{
any::TypeId,
borrow::Cow,
cmp::{self, max, Ordering},
cmp::{self, Ordering},
fmt::Write,
iter, mem,
ops::{Deref, Range},
@@ -384,6 +383,7 @@ impl EditorElement {
register_action(view, cx, Editor::unique_lines_case_insensitive);
register_action(view, cx, Editor::unique_lines_case_sensitive);
register_action(view, cx, Editor::accept_partial_inline_completion);
register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal)
}
@@ -477,6 +477,7 @@ impl EditorElement {
editor.select(
SelectPhase::BeginColumnar {
position,
reset: false,
goal_column: point_for_position.exact_unclipped.column(),
},
cx,
@@ -537,25 +538,22 @@ impl EditorElement {
text_hitbox: &Hitbox,
cx: &mut ViewContext<Editor>,
) {
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
if cx.default_prevented() {
return;
}
if let Some(item) = cx.read_from_primary() {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(item.text(), cx);
}
editor.select(
SelectPhase::BeginColumnar {
position,
reset: true,
goal_column: point_for_position.exact_unclipped.column(),
},
cx,
);
}
fn mouse_up(
@@ -585,6 +583,28 @@ impl EditorElement {
cx.stop_propagation();
} else if end_selection {
cx.stop_propagation();
} else if cfg!(target_os = "linux") && event.button == MouseButton::Middle {
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
return;
}
#[cfg(target_os = "linux")]
if let Some(item) = cx.read_from_clipboard() {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
editor.select(
SelectPhase::Begin {
position,
add: false,
click_count: 1,
},
cx,
);
editor.insert(item.text(), cx);
}
cx.stop_propagation()
}
}
@@ -849,6 +869,11 @@ impl EditorElement {
snapshot
.folds_in_range(visible_anchor_range.clone())
.filter_map(|fold| {
// Skip folds that have no text.
if fold.text.is_empty() {
return None;
}
let fold_range = fold.range.clone();
let display_range = fold.range.start.to_display_point(&snapshot)
..fold.range.end.to_display_point(&snapshot);
@@ -1143,28 +1168,17 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
fn layout_gutter_fold_indicators(
fn prepaint_gutter_fold_toggles(
&self,
fold_statuses: Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
toggles: &mut [Option<AnyElement>],
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
gutter_settings: crate::editor_settings::Gutter,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
) -> Vec<Option<AnyElement>> {
let mut indicators = self.editor.update(cx, |editor, cx| {
editor.render_fold_indicators(
fold_statuses,
&self.style,
editor.gutter_hovered,
line_height,
gutter_dimensions.margin,
cx,
)
});
for (ix, fold_indicator) in indicators.iter_mut().enumerate() {
) {
for (ix, fold_indicator) in toggles.iter_mut().enumerate() {
if let Some(fold_indicator) = fold_indicator {
debug_assert!(gutter_settings.folds);
let available_space = size(
@@ -1187,8 +1201,49 @@ impl EditorElement {
fold_indicator.prepaint_as_root(origin, available_space, cx);
}
}
}
indicators
#[allow(clippy::too_many_arguments)]
fn prepaint_flap_trailers(
&self,
trailers: Vec<Option<AnyElement>>,
lines: &[LineWithInvisibles],
line_height: Pixels,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
em_width: Pixels,
cx: &mut WindowContext,
) -> Vec<Option<FlapTrailerLayout>> {
trailers
.into_iter()
.enumerate()
.map(|(ix, element)| {
let mut element = element?;
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let size = element.layout_as_root(available_space, cx);
let line = &lines[ix].line;
let padding = if line.width == Pixels::ZERO {
Pixels::ZERO
} else {
4. * em_width
};
let position = point(
scroll_pixel_position.x + line.width + padding,
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
);
let centering_offset = point(px(0.), (line_height - size.height) / 2.);
let origin = content_origin + position + centering_offset;
element.prepaint_as_root(origin, available_space, cx);
Some(FlapTrailerLayout {
element,
bounds: Bounds::new(origin, size),
})
})
.collect()
}
// Folds contained in a hunk are ignored apart from shrinking visual size
@@ -1272,6 +1327,7 @@ impl EditorElement {
display_row: DisplayRow,
display_snapshot: &DisplaySnapshot,
line_layout: &LineWithInvisibles,
flap_trailer: Option<&FlapTrailerLayout>,
em_width: Pixels,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
@@ -1311,17 +1367,22 @@ impl EditorElement {
let start_x = {
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
let padded_line_width =
line_layout.line.width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS);
let line_end = if let Some(flap_trailer) = flap_trailer {
flap_trailer.bounds.right()
} else {
content_origin.x - scroll_pixel_position.x + line_layout.line.width
};
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
let min_column = ProjectSettings::get_global(cx)
let min_column_in_pixels = ProjectSettings::get_global(cx)
.git
.inline_blame
.and_then(|settings| settings.min_column)
.map(|col| self.column_pixels(col as usize, cx))
.unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
(content_origin.x - scroll_pixel_position.x) + max(padded_line_width, min_column)
cmp::max(padded_line_end, min_start)
};
let absolute_offset = point(start_x, start_y);
@@ -1560,13 +1621,16 @@ impl EditorElement {
active_rows: &BTreeMap<DisplayRow, bool>,
newest_selection_head: Option<DisplayPoint>,
snapshot: &EditorSnapshot,
cx: &WindowContext,
) -> (
Vec<Option<ShapedLine>>,
Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
) {
cx: &mut WindowContext,
) -> Vec<Option<ShapedLine>> {
let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| {
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full
});
if !include_line_numbers {
return Vec::new();
}
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor.selections.newest::<Point>(cx);
SelectionLayout::new(
@@ -1581,69 +1645,100 @@ impl EditorElement {
.head
});
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let include_line_numbers =
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
let include_fold_statuses =
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
let mut fold_statuses = Vec::with_capacity(rows.len());
let mut line_number = String::new();
let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
let mut line_number = String::new();
buffer_rows
.into_iter()
.enumerate()
.map(|(ix, multibuffer_row)| {
let multibuffer_row = multibuffer_row?;
let display_row = DisplayRow(rows.start.0 + ix as u32);
let color = if active_rows.contains_key(&display_row) {
cx.theme().colors().editor_active_line_number
} else {
cx.theme().colors().editor_line_number
};
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
Some(shaped_line)
})
.collect()
}
for (ix, row) in buffer_rows.into_iter().enumerate() {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let (active, color) = if active_rows.contains_key(&display_row) {
(true, cx.theme().colors().editor_active_line_number)
} else {
(false, cx.theme().colors().editor_line_number)
};
if let Some(multibuffer_row) = row {
if include_line_numbers {
line_number.clear();
let default_number = multibuffer_row.0 + 1;
let number = relative_rows
.get(&DisplayRow(ix as u32 + rows.start.0))
.unwrap_or(&default_number);
write!(&mut line_number, "{number}").unwrap();
let run = TextRun {
len: line_number.len(),
font: self.style.text.font(),
color,
background_color: None,
underline: None,
strikethrough: None,
};
let shaped_line = cx
.text_system()
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
shaped_line_numbers.push(Some(shaped_line));
}
if include_fold_statuses {
fold_statuses.push(
is_singleton
.then(|| {
snapshot
.fold_for_line(multibuffer_row)
.map(|fold_status| (fold_status, multibuffer_row, active))
})
.flatten(),
)
}
} else {
fold_statuses.push(None);
shaped_line_numbers.push(None);
}
fn layout_gutter_fold_toggles(
&self,
rows: Range<DisplayRow>,
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
active_rows: &BTreeMap<DisplayRow, bool>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<AnyElement>> {
let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds
&& snapshot.mode == EditorMode::Full
&& self.editor.read(cx).is_singleton(cx);
if include_fold_statuses {
buffer_rows
.into_iter()
.enumerate()
.map(|(ix, row)| {
if let Some(multibuffer_row) = row {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let active = active_rows.contains_key(&display_row);
snapshot.render_fold_toggle(
multibuffer_row,
active,
self.editor.clone(),
cx,
)
} else {
None
}
})
.collect()
} else {
Vec::new()
}
}
(shaped_line_numbers, fold_statuses)
fn layout_flap_trailers(
&self,
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
snapshot: &EditorSnapshot,
cx: &mut WindowContext,
) -> Vec<Option<AnyElement>> {
buffer_rows
.into_iter()
.map(|row| {
if let Some(multibuffer_row) = row {
snapshot.render_flap_trailer(multibuffer_row, cx)
} else {
None
}
})
.collect()
}
fn layout_lines(
@@ -2418,10 +2513,16 @@ impl EditorElement {
}
}
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
let show_git_gutter = layout
.position_map
.snapshot
.show_git_diff_gutter
.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
)
});
if show_git_gutter {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
@@ -2445,8 +2546,8 @@ impl EditorElement {
}
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
cx.with_element_namespace("gutter_fold_indicators", |cx| {
for fold_indicator in layout.fold_indicators.iter_mut().flatten() {
cx.with_element_namespace("gutter_fold_toggles", |cx| {
for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() {
fold_indicator.paint(cx);
}
});
@@ -2626,6 +2727,11 @@ impl EditorElement {
self.paint_redactions(layout, cx);
self.paint_cursors(layout, cx);
self.paint_inline_blame(layout, cx);
cx.with_element_namespace("flap_trailers", |cx| {
for trailer in layout.flap_trailers.iter_mut().flatten() {
trailer.element.paint(cx);
}
});
},
)
}
@@ -3271,7 +3377,9 @@ impl EditorElement {
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
editor.update(cx, |editor, cx| {
if event.pressed_button == Some(MouseButton::Left) {
if event.pressed_button == Some(MouseButton::Left)
|| event.pressed_button == Some(MouseButton::Middle)
{
Self::mouse_dragged(
editor,
event,
@@ -3946,9 +4054,9 @@ impl Element for EditorElement {
)
};
let highlighted_rows = self.editor.update(cx, |editor, cx| {
editor.highlighted_display_rows(HashSet::default(), cx)
});
let highlighted_rows = self
.editor
.update(cx, |editor, cx| editor.highlighted_display_rows(cx));
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
@@ -3970,15 +4078,29 @@ impl Element for EditorElement {
cx,
);
let (line_numbers, fold_statuses) = self.layout_line_numbers(
let line_numbers = self.layout_line_numbers(
start_row..end_row,
buffer_rows.clone().into_iter(),
buffer_rows.iter().copied(),
&active_rows,
newest_selection_head,
&snapshot,
cx,
);
let mut gutter_fold_toggles =
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.layout_gutter_fold_toggles(
start_row..end_row,
buffer_rows.iter().copied(),
&active_rows,
&snapshot,
cx,
)
});
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
self.layout_flap_trailers(buffer_rows.iter().copied(), &snapshot, cx)
});
let display_hunks = self.layout_git_gutters(
line_height,
&gutter_hitbox,
@@ -4024,15 +4146,30 @@ impl Element for EditorElement {
scroll_position.y * line_height,
);
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
self.prepaint_flap_trailers(
flap_trailers,
&line_layouts,
line_height,
content_origin,
scroll_pixel_position,
em_width,
cx,
)
});
let mut inline_blame = None;
if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row();
if (start_row..end_row).contains(&display_row) {
let line_layout = &line_layouts[display_row.minus(start_row) as usize];
let line_ix = display_row.minus(start_row) as usize;
let line_layout = &line_layouts[line_ix];
let flap_trailer_layout = flap_trailers[line_ix].as_ref();
inline_blame = self.layout_inline_blame(
display_row,
&snapshot.display_snapshot,
line_layout,
flap_trailer_layout,
em_width,
content_origin,
scroll_pixel_position,
@@ -4150,7 +4287,11 @@ impl Element for EditorElement {
gutter_dimensions.width - gutter_dimensions.left_padding,
cx,
);
if gutter_settings.code_actions {
let show_code_actions = snapshot
.show_code_actions
.unwrap_or_else(|| gutter_settings.code_actions);
if show_code_actions {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
@@ -4204,21 +4345,17 @@ impl Element for EditorElement {
let mouse_context_menu = self.layout_mouse_context_menu(cx);
let fold_indicators = if gutter_settings.folds {
cx.with_element_namespace("gutter_fold_indicators", |cx| {
self.layout_gutter_fold_indicators(
fold_statuses,
line_height,
&gutter_dimensions,
gutter_settings,
scroll_pixel_position,
&gutter_hitbox,
cx,
)
})
} else {
Vec::new()
};
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.prepaint_gutter_fold_toggles(
&mut gutter_fold_toggles,
line_height,
&gutter_dimensions,
gutter_settings,
scroll_pixel_position,
&gutter_hitbox,
cx,
)
});
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = cx
@@ -4288,7 +4425,8 @@ impl Element for EditorElement {
mouse_context_menu,
test_indicators,
code_actions_indicator,
fold_indicators,
gutter_fold_toggles,
flap_trailers,
tab_invisible,
space_invisible,
}
@@ -4408,7 +4546,8 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
fold_indicators: Vec<Option<AnyElement>>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
flap_trailers: Vec<Option<FlapTrailerLayout>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
@@ -4532,6 +4671,11 @@ impl ScrollbarLayout {
}
}
struct FlapTrailerLayout {
element: AnyElement,
bounds: Bounds<Pixels>,
}
struct FoldLayout {
display_range: Range<DisplayPoint>,
hover_element: AnyElement,
@@ -4950,16 +5094,14 @@ mod tests {
let layouts = cx
.update_window(*window, |_, cx| {
element
.layout_line_numbers(
DisplayRow(0)..DisplayRow(6),
(0..6).map(MultiBufferRow).map(Some),
&Default::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
cx,
)
.0
element.layout_line_numbers(
DisplayRow(0)..DisplayRow(6),
(0..6).map(MultiBufferRow).map(Some),
&Default::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
cx,
)
})
.unwrap();
assert_eq!(layouts.len(), 6);

View File

@@ -194,6 +194,7 @@ impl Editor {
editor.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(removed_rows, &snapshot),
None,
false,
cx,
);
}
@@ -269,6 +270,7 @@ impl Editor {
self.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
Some(added_hunk_color(cx)),
false,
cx,
);
None
@@ -277,6 +279,7 @@ impl Editor {
self.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(hunk_start..hunk_end, &snapshot),
Some(added_hunk_color(cx)),
false,
cx,
);
self.insert_deleted_text_block(diff_base_buffer, deleted_text_lines, &hunk, cx)
@@ -476,6 +479,7 @@ impl Editor {
editor.highlight_rows::<DiffRowHighlight>(
to_inclusive_row_range(removed_rows, &snapshot),
None,
false,
cx,
);
}
@@ -581,7 +585,7 @@ fn editor_with_deleted_text(
.buffer_snapshot
.anchor_after(editor.buffer.read(cx).len(cx));
editor.highlight_rows::<DiffRowHighlight>(start..=end, Some(deleted_color), cx);
editor.highlight_rows::<DiffRowHighlight>(start..=end, Some(deleted_color), false, cx);
let subscription_editor = parent_editor.clone();
editor._subscriptions.extend([

View File

@@ -3,6 +3,7 @@ use gpui::{AppContext, Model, ModelContext};
use language::Buffer;
pub trait InlineCompletionProvider: 'static + Sized {
fn name() -> &'static str;
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -24,7 +25,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
cx: &mut ModelContext<Self>,
);
fn accept(&mut self, cx: &mut ModelContext<Self>);
fn discard(&mut self, cx: &mut ModelContext<Self>);
fn discard(&mut self, should_report_inline_completion_event: bool, cx: &mut ModelContext<Self>);
fn active_completion_text<'a>(
&'a self,
buffer: &Model<Buffer>,
@@ -55,7 +56,7 @@ pub trait InlineCompletionProviderHandle {
cx: &mut AppContext,
);
fn accept(&self, cx: &mut AppContext);
fn discard(&self, cx: &mut AppContext);
fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext);
fn active_completion_text<'a>(
&'a self,
buffer: &Model<Buffer>,
@@ -105,8 +106,10 @@ where
self.update(cx, |this, cx| this.accept(cx))
}
fn discard(&self, cx: &mut AppContext) {
self.update(cx, |this, cx| this.discard(cx))
fn discard(&self, should_report_inline_completion_event: bool, cx: &mut AppContext) {
self.update(cx, |this, cx| {
this.discard(should_report_inline_completion_event, cx)
})
}
fn active_completion_text<'a>(

View File

@@ -1,6 +1,7 @@
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
Copy, Cut, DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition,
GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFinder, SelectMode,
ToggleCodeActions,
};
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use workspace::OpenInTerminal;
@@ -85,6 +86,10 @@ pub fn deploy_context_menu(
}),
)
.separator()
.action("Cut", Box::new(Cut))
.action("Copy", Box::new(Copy))
.action("Paste", Box::new(Paste))
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
.action("Open in Terminal", Box::new(OpenInTerminal));
match focus {

View File

@@ -1,13 +1,9 @@
use std::{any::TypeId, cmp, f32};
use collections::HashSet;
use crate::{
display_map::ToDisplayPoint, DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt,
};
use gpui::{px, Bounds, Pixels, ViewContext};
use language::Point;
use crate::{
display_map::ToDisplayPoint, DiffRowHighlight, DisplayRow, Editor, EditorMode,
LineWithInvisibles, RowExt,
};
use std::{cmp, f32};
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum Autoscroll {
@@ -107,14 +103,10 @@ impl Editor {
let mut target_top;
let mut target_bottom;
if let Some(first_highlighted_row) = &self
.highlighted_display_rows(
HashSet::from_iter(Some(TypeId::of::<DiffRowHighlight>())),
cx,
)
.first_entry()
if let Some(first_highlighted_row) =
self.highlighted_display_row_for_autoscroll(&display_map)
{
target_top = first_highlighted_row.key().as_f32();
target_top = first_highlighted_row.as_f32();
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(cx);
@@ -244,7 +236,10 @@ impl Editor {
let mut target_left;
let mut target_right;
if self.highlighted_rows.is_empty() {
if self
.highlighted_display_row_for_autoscroll(&display_map)
.is_none()
{
target_left = px(f32::INFINITY);
target_right = px(0.);
for selection in selections {

View File

@@ -1,16 +1,16 @@
use crate::Editor;
use std::{path::Path, sync::Arc};
use anyhow::Context;
use gpui::WindowContext;
use language::{BasicContextProvider, ContextProvider};
use project::{Location, WorktreeId};
use gpui::{Model, WindowContext};
use language::ContextProvider;
use project::{BasicContextProvider, Location, Project};
use task::{TaskContext, TaskVariables, VariableName};
use text::Point;
use util::ResultExt;
use workspace::Workspace;
pub(crate) fn task_context_for_location(
captured_variables: TaskVariables,
workspace: &Workspace,
location: Location,
cx: &mut WindowContext<'_>,
@@ -19,44 +19,33 @@ pub(crate) fn task_context_for_location(
.log_err()
.flatten();
let buffer = location.buffer.clone();
let language_context_provider = buffer
.read(cx)
.language()
.and_then(|language| language.context_provider())
.unwrap_or_else(|| Arc::new(BasicContextProvider));
let worktree_abs_path = buffer
.read(cx)
.file()
.map(|file| WorktreeId::from_usize(file.worktree_id()))
.and_then(|worktree_id| {
workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
});
let task_variables = combine_task_variables(
worktree_abs_path.as_deref(),
let mut task_variables = combine_task_variables(
captured_variables,
location,
language_context_provider.as_ref(),
workspace.project().clone(),
cx,
)
.log_err()?;
// Remove all custom entries starting with _, as they're not intended for use by the end user.
task_variables.sweep();
Some(TaskContext {
cwd,
task_variables,
})
}
pub(crate) fn task_context_with_editor(
fn task_context_with_editor(
workspace: &Workspace,
editor: &mut Editor,
cx: &mut WindowContext<'_>,
) -> Option<TaskContext> {
let (selection, buffer, editor_snapshot) = {
let selection = editor.selections.newest::<usize>(cx);
let mut selection = editor.selections.newest::<Point>(cx);
if editor.selections.line_mode {
selection.start = Point::new(selection.start.row, 0);
selection.end = Point::new(selection.end.row + 1, 0);
}
let (buffer, _, _) = editor
.buffer()
.read(cx)
@@ -79,21 +68,21 @@ pub(crate) fn task_context_with_editor(
buffer,
range: start..end,
};
task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| {
let captured_variables = {
let mut variables = TaskVariables::default();
for range in location
.buffer
.read(cx)
.snapshot()
.runnable_ranges(location.range)
.runnable_ranges(location.range.clone())
{
for (capture_name, value) in range.extra_captures {
task_context
.task_variables
.insert(VariableName::Custom(capture_name.into()), value);
variables.insert(VariableName::Custom(capture_name.into()), value);
}
}
task_context
})
variables
};
task_context_for_location(captured_variables, workspace, location.clone(), cx)
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
@@ -109,24 +98,26 @@ pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskCo
}
fn combine_task_variables(
worktree_abs_path: Option<&Path>,
mut captured_variables: TaskVariables,
location: Location,
context_provider: &dyn ContextProvider,
project: Model<Project>,
cx: &mut WindowContext<'_>,
) -> anyhow::Result<TaskVariables> {
if context_provider.is_basic() {
context_provider
.build_context(worktree_abs_path, &location, cx)
.context("building basic provider context")
} else {
let mut basic_context = BasicContextProvider
.build_context(worktree_abs_path, &location, cx)
.context("building basic default context")?;
basic_context.extend(
context_provider
.build_context(worktree_abs_path, &location, cx)
let language_context_provider = location
.buffer
.read(cx)
.language()
.and_then(|language| language.context_provider());
let baseline = BasicContextProvider::new(project)
.build_context(&captured_variables, &location, cx)
.context("building basic default context")?;
captured_variables.extend(baseline);
if let Some(provider) = language_context_provider {
captured_variables.extend(
provider
.build_context(&captured_variables, &location, cx)
.context("building provider context ")?,
);
Ok(basic_context)
}
Ok(captured_variables)
}

View File

@@ -168,8 +168,7 @@ pub fn expanded_hunks_background_highlights(
let mut range_start = 0;
let mut previous_highlighted_row = None;
for (highlighted_row, _) in editor.highlighted_display_rows(collections::HashSet::default(), cx)
{
for (highlighted_row, _) in editor.highlighted_display_rows(cx) {
match previous_highlighted_row {
Some(previous_row) => {
if previous_row + 1 != highlighted_row.0 {

View File

@@ -30,6 +30,7 @@ log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
project.workspace = true
release_channel.workspace = true
schemars.workspace = true
semantic_version.workspace = true
serde.workspace = true

View File

@@ -67,7 +67,7 @@ impl ExtensionBuilder {
pub fn new(cache_dir: PathBuf) -> Self {
Self {
cache_dir,
http: http::client(),
http: http::client(None),
}
}

View File

@@ -30,10 +30,11 @@ use gpui::{
};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
ContextProviderWithTasks, LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,
LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -70,7 +71,10 @@ pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
}
/// Returns whether the given extension version is compatible with this version of Zed.
pub fn is_version_compatible(extension_version: &ExtensionMetadata) -> bool {
pub fn is_version_compatible(
release_channel: ReleaseChannel,
extension_version: &ExtensionMetadata,
) -> bool {
let schema_version = extension_version.manifest.schema_version.unwrap_or(0);
if CURRENT_SCHEMA_VERSION.0 < schema_version {
return false;
@@ -82,7 +86,7 @@ pub fn is_version_compatible(extension_version: &ExtensionMetadata) -> bool {
.as_ref()
.and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
{
if !is_supported_wasm_api_version(wasm_api_version) {
if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
return false;
}
}
@@ -428,7 +432,7 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<ExtensionMetadata>>> {
let schema_versions = schema_version_range();
let wasm_api_versions = wasm_api_version_range();
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
let extension_settings = ExtensionSettings::get_global(cx);
let extension_ids = self
.extension_index
@@ -681,7 +685,7 @@ impl ExtensionStore {
log::info!("installing extension {extension_id} latest version");
let schema_versions = schema_version_range();
let wasm_api_versions = wasm_api_version_range();
let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
let Some(url) = self
.http_client

View File

@@ -714,6 +714,7 @@ fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
release_channel::init("0.0.0", cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
ExtensionSettings::register(cx);

View File

@@ -16,6 +16,7 @@ use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use http::HttpClient;
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use std::{
path::{Path, PathBuf},
@@ -30,6 +31,7 @@ use wit::Extension;
pub(crate) struct WasmHost {
engine: Engine,
release_channel: ReleaseChannel,
http_client: Arc<dyn HttpClient>,
node_runtime: Arc<dyn NodeRuntime>,
pub(crate) language_registry: Arc<LanguageRegistry>,
@@ -96,6 +98,7 @@ impl WasmHost {
http_client,
node_runtime,
language_registry,
release_channel: ReleaseChannel::global(cx),
_main_thread_message_task: task,
main_thread_message_tx: tx,
})
@@ -124,8 +127,13 @@ impl WasmHost {
},
);
let (mut extension, instance) =
Extension::instantiate_async(&mut store, zed_api_version, &component).await?;
let (mut extension, instance) = Extension::instantiate_async(
&mut store,
this.release_channel,
zed_api_version,
&component,
)
.await?;
extension
.call_init_extension(&mut store)

View File

@@ -1,7 +1,9 @@
mod since_v0_0_1;
mod since_v0_0_4;
mod since_v0_0_6;
use since_v0_0_6 as latest;
mod since_v0_0_7;
use release_channel::ReleaseChannel;
use since_v0_0_7 as latest;
use super::{wasm_engine, WasmState};
use anyhow::{Context, Result};
@@ -35,17 +37,27 @@ fn wasi_view(state: &mut WasmState) -> &mut WasmState {
}
/// Returns whether the given Wasm API version is supported by the Wasm host.
pub fn is_supported_wasm_api_version(version: SemanticVersion) -> bool {
wasm_api_version_range().contains(&version)
pub fn is_supported_wasm_api_version(
release_channel: ReleaseChannel,
version: SemanticVersion,
) -> bool {
wasm_api_version_range(release_channel).contains(&version)
}
/// Returns the Wasm API version range that is supported by the Wasm host.
#[inline(always)]
pub fn wasm_api_version_range() -> RangeInclusive<SemanticVersion> {
since_v0_0_1::MIN_VERSION..=latest::MAX_VERSION
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
let max_version = if release_channel == ReleaseChannel::Dev {
latest::MAX_VERSION
} else {
since_v0_0_6::MAX_VERSION
};
since_v0_0_1::MIN_VERSION..=max_version
}
pub enum Extension {
V007(since_v0_0_7::Extension),
V006(since_v0_0_6::Extension),
V004(since_v0_0_4::Extension),
V001(since_v0_0_1::Extension),
@@ -54,14 +66,24 @@ pub enum Extension {
impl Extension {
pub async fn instantiate_async(
store: &mut Store<WasmState>,
release_channel: ReleaseChannel,
version: SemanticVersion,
component: &Component,
) -> Result<(Self, Instance)> {
if version >= latest::MIN_VERSION {
if release_channel == ReleaseChannel::Dev && version >= latest::MIN_VERSION {
let (extension, instance) =
latest::Extension::instantiate_async(store, &component, latest::linker())
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V007(extension), instance))
} else if version >= since_v0_0_6::MIN_VERSION {
let (extension, instance) = since_v0_0_6::Extension::instantiate_async(
store,
&component,
since_v0_0_6::linker(),
)
.await
.context("failed to instantiate wasm extension")?;
Ok((Self::V006(extension), instance))
} else if version >= since_v0_0_4::MIN_VERSION {
let (extension, instance) = since_v0_0_4::Extension::instantiate_async(
@@ -86,6 +108,7 @@ impl Extension {
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V007(ext) => ext.call_init_extension(store).await,
Extension::V006(ext) => ext.call_init_extension(store).await,
Extension::V004(ext) => ext.call_init_extension(store).await,
Extension::V001(ext) => ext.call_init_extension(store).await,
@@ -100,10 +123,14 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V006(ext) => {
Extension::V007(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V006(ext) => Ok(ext
.call_language_server_command(store, &language_server_id.0, resource)
.await?
.map(|command| command.into())),
Extension::V004(ext) => Ok(ext
.call_language_server_command(store, config, resource)
.await?
@@ -123,6 +150,14 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V007(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V006(ext) => {
ext.call_language_server_initialization_options(
store,
@@ -153,6 +188,14 @@ impl Extension {
resource: Resource<Arc<dyn LspAdapterDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V007(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V006(ext) => {
ext.call_language_server_workspace_configuration(
store,
@@ -172,11 +215,20 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V006(ext) => {
Extension::V007(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V006(ext) => Ok(ext
.call_labels_for_completions(store, &language_server_id.0, &completions)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
@@ -187,11 +239,20 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
Extension::V006(ext) => {
Extension::V007(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V006(ext) => Ok(ext
.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
}
}
}

View File

@@ -1,21 +1,10 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::settings::Settings;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use project::project_settings::ProjectSettings;
use language::LspAdapterDelegate;
use semantic_version::SemanticVersion;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 6);
@@ -26,11 +15,13 @@ wasmtime::component::bindgen!({
path: "../extension_api/wit/since_v0.0.6",
with: {
"worktree": ExtensionWorktree,
"zed:extension/github": latest::zed::extension::github,
"zed:extension/lsp": latest::zed::extension::lsp,
"zed:extension/nodejs": latest::zed::extension::nodejs,
"zed:extension/platform": latest::zed::extension::platform,
},
});
pub use self::zed::extension::*;
mod settings {
include!("../../../../extension_api/wit/since_v0.0.6/settings.rs");
}
@@ -39,7 +30,93 @@ pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
LINKER.get_or_init(|| {
super::new_linker(|linker, f| {
Extension::add_to_linker(linker, f)?;
latest::zed::extension::github::add_to_linker(linker, f)?;
latest::zed::extension::nodejs::add_to_linker(linker, f)?;
latest::zed::extension::platform::add_to_linker(linker, f)?;
Ok(())
})
})
}
impl From<Command> for latest::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<SettingsLocation> for latest::SettingsLocation {
fn from(value: SettingsLocation) -> Self {
Self {
worktree_id: value.worktree_id,
path: value.path,
}
}
}
impl From<LanguageServerInstallationStatus> for latest::LanguageServerInstallationStatus {
fn from(value: LanguageServerInstallationStatus) -> Self {
match value {
LanguageServerInstallationStatus::None => Self::None,
LanguageServerInstallationStatus::Downloading => Self::Downloading,
LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate,
LanguageServerInstallationStatus::Failed(message) => Self::Failed(message),
}
}
}
impl From<DownloadedFileType> for latest::DownloadedFileType {
fn from(value: DownloadedFileType) -> Self {
match value {
DownloadedFileType::Gzip => Self::Gzip,
DownloadedFileType::GzipTar => Self::GzipTar,
DownloadedFileType::Zip => Self::Zip,
DownloadedFileType::Uncompressed => Self::Uncompressed,
}
}
}
impl From<Range> for latest::Range {
fn from(value: Range) -> Self {
Self {
start: value.start,
end: value.end,
}
}
}
impl From<CodeLabelSpan> for latest::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for latest::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<CodeLabel> for latest::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
}
#[async_trait]
@@ -48,16 +125,14 @@ impl HostWorktree for WasmState {
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id())
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
@@ -65,19 +140,14 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
@@ -85,11 +155,7 @@ impl HostWorktree for WasmState {
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
@@ -98,107 +164,6 @@ impl HostWorktree for WasmState {
}
}
#[async_trait]
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
#[async_trait]
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = http::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(github::GithubRelease {
version: release.tag_name,
assets: release
.assets
.into_iter()
.map(|asset| github::GithubReleaseAsset {
name: asset.name,
download_url: asset.browser_download_url,
})
.collect(),
})
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
@@ -207,50 +172,13 @@ impl ExtensionImports for WasmState {
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: location.worktree_id as usize,
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let settings =
AllLanguageSettings::get(location, cx).language(key.as_deref());
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&Arc::<str>::from(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
latest::ExtensionImports::get_settings(
self,
location.map(|location| location.into()),
category,
key,
)
.await
}
async fn set_language_server_installation_status(
@@ -258,23 +186,12 @@ impl ExtensionImports for WasmState {
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
latest::ExtensionImports::set_language_server_installation_status(
self,
server_name,
status.into(),
)
.await
}
async fn download_file(
@@ -283,103 +200,10 @@ impl ExtensionImports for WasmState {
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg("-d")
.arg(&destination_path)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
})
.await
.to_wasmtime_result()
latest::ExtensionImports::download_file(self, url, path, file_type.into()).await
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
latest::ExtensionImports::make_file_executable(self, path).await
}
}

View File

@@ -0,0 +1,408 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::settings::Settings;
use anyhow::{anyhow, bail, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, OnceLock},
};
use util::maybe;
use wasmtime::component::{Linker, Resource};
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 0, 7);
wasmtime::component::bindgen!({
async: true,
path: "../extension_api/wit/since_v0.0.7",
with: {
"worktree": ExtensionWorktree,
},
});
pub use self::zed::extension::*;
mod settings {
include!("../../../../extension_api/wit/since_v0.0.7/settings.rs");
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {
// We only ever hand out borrows of worktrees.
Ok(())
}
}
#[async_trait]
impl nodejs::Host for WasmState {
async fn node_binary_path(&mut self) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.binary_path()
.await
.map(|path| path.to_string_lossy().to_string())
.to_wasmtime_result()
}
async fn npm_package_latest_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<String, String>> {
self.host
.node_runtime
.npm_package_latest_version(&package_name)
.await
.to_wasmtime_result()
}
async fn npm_package_installed_version(
&mut self,
package_name: String,
) -> wasmtime::Result<Result<Option<String>, String>> {
self.host
.node_runtime
.npm_package_installed_version(&self.work_dir(), &package_name)
.await
.to_wasmtime_result()
}
async fn npm_install_package(
&mut self,
package_name: String,
version: String,
) -> wasmtime::Result<Result<(), String>> {
self.host
.node_runtime
.npm_install_packages(&self.work_dir(), &[(&package_name, &version)])
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl lsp::Host for WasmState {}
impl From<http::github::GithubRelease> for github::GithubRelease {
fn from(value: http::github::GithubRelease) -> Self {
Self {
version: value.tag_name,
assets: value.assets.into_iter().map(Into::into).collect(),
}
}
}
impl From<http::github::GithubReleaseAsset> for github::GithubReleaseAsset {
fn from(value: http::github::GithubReleaseAsset) -> Self {
Self {
name: value.name,
download_url: value.browser_download_url,
}
}
}
#[async_trait]
impl github::Host for WasmState {
async fn latest_github_release(
&mut self,
repo: String,
options: github::GithubReleaseOptions,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release = http::github::latest_github_release(
&repo,
options.require_assets,
options.pre_release,
self.host.http_client.clone(),
)
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
async fn github_release_by_tag_name(
&mut self,
repo: String,
tag: String,
) -> wasmtime::Result<Result<github::GithubRelease, String>> {
maybe!(async {
let release =
http::github::get_release_by_tag_name(&repo, &tag, self.host.http_client.clone())
.await?;
Ok(release.into())
})
.await
.to_wasmtime_result()
}
}
#[async_trait]
impl platform::Host for WasmState {
async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> {
Ok((
match env::consts::OS {
"macos" => platform::Os::Mac,
"linux" => platform::Os::Linux,
"windows" => platform::Os::Windows,
_ => panic!("unsupported os"),
},
match env::consts::ARCH {
"aarch64" => platform::Architecture::Aarch64,
"x86" => platform::Architecture::X86,
"x86_64" => platform::Architecture::X8664,
_ => panic!("unsupported architecture"),
},
))
}
}
#[async_trait]
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
location: Option<self::SettingsLocation>,
category: String,
key: Option<String>,
) -> wasmtime::Result<Result<String, String>> {
self.on_main_thread(|cx| {
async move {
let location = location
.as_ref()
.map(|location| ::settings::SettingsLocation {
worktree_id: location.worktree_id as usize,
path: Path::new(&location.path),
});
cx.update(|cx| match category.as_str() {
"language" => {
let settings =
AllLanguageSettings::get(location, cx).language(key.as_deref());
Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size,
})?)
}
"lsp" => {
let settings = key
.and_then(|key| {
ProjectSettings::get(location, cx)
.lsp
.get(&Arc::<str>::from(key))
})
.cloned()
.unwrap_or_default();
Ok(serde_json::to_string(&settings::LspSettings {
binary: settings.binary.map(|binary| settings::BinarySettings {
path: binary.path,
arguments: binary.arguments,
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
})?)
}
_ => {
bail!("Unknown settings category: {}", category);
}
})
}
.boxed_local()
})
.await?
.to_wasmtime_result()
}
async fn set_language_server_installation_status(
&mut self,
server_name: String,
status: LanguageServerInstallationStatus,
) -> wasmtime::Result<()> {
let status = match status {
LanguageServerInstallationStatus::CheckingForUpdate => {
LanguageServerBinaryStatus::CheckingForUpdate
}
LanguageServerInstallationStatus::Downloading => {
LanguageServerBinaryStatus::Downloading
}
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
LanguageServerInstallationStatus::Failed(error) => {
LanguageServerBinaryStatus::Failed { error }
}
};
self.host
.language_registry
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}
async fn download_file(
&mut self,
url: String,
path: String,
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());
self.host.fs.create_dir(&extension_work_dir).await?;
let destination_path = self
.host
.writeable_path_from_extension(&self.manifest.id, &path)?;
let mut response = self
.host
.http_client
.get(&url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
let body = BufReader::new(response.body_mut());
match file_type {
DownloadedFileType::Uncompressed => {
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::Gzip => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.create_file_with(&destination_path, body)
.await?;
}
DownloadedFileType::GzipTar => {
let body = GzipDecoder::new(body);
futures::pin_mut!(body);
self.host
.fs
.extract_tar_file(&destination_path, Archive::new(body))
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg("-d")
.arg(&destination_path)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
}
}
Ok(())
})
.await
.to_wasmtime_result()
}
async fn make_file_executable(&mut self, path: String) -> wasmtime::Result<Result<(), String>> {
#[allow(unused)]
let path = self
.host
.writeable_path_from_extension(&self.manifest.id, Path::new(&path))?;
#[cfg(unix)]
{
use std::fs::{self, Permissions};
use std::os::unix::fs::PermissionsExt;
return fs::set_permissions(&path, Permissions::from_mode(0o755))
.map_err(|error| anyhow!("failed to set permissions for path {path:?}: {error}"))
.to_wasmtime_result();
}
#[cfg(not(unix))]
Ok(Ok(()))
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_extension_api"
version = "0.0.6"
version = "0.0.7"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"

View File

@@ -16,7 +16,8 @@ pub use serde_json;
pub use wit::{
download_file, make_file_executable,
zed::extension::github::{
latest_github_release, GithubRelease, GithubReleaseAsset, GithubReleaseOptions,
github_release_by_tag_name, latest_github_release, GithubRelease, GithubReleaseAsset,
GithubReleaseOptions,
},
zed::extension::nodejs::{
node_binary_path, npm_install_package, npm_package_installed_version,
@@ -140,7 +141,7 @@ pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "
mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.0.6",
path: "./wit/since_v0.0.7",
});
}

View File

@@ -1,4 +1,4 @@
#[path = "../wit/since_v0.0.6/settings.rs"]
#[path = "../wit/since_v0.0.7/settings.rs"]
mod types;
use crate::{wit, Result, SettingsLocation, Worktree};

View File

@@ -97,7 +97,7 @@ world extension {
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the code to include when filtering.
/// The range of the displayed label to include when filtering.
filter-range: range,
}

View File

@@ -0,0 +1,130 @@
package zed:extension;
world extension {
import github;
import platform;
import nodejs;
use lsp.{completion, symbol};
/// Initializes the extension.
export init-extension: func();
/// The type of a downloaded file.
enum downloaded-file-type {
/// A gzipped file (`.gz`).
gzip,
/// A gzipped tar archive (`.tar.gz`).
gzip-tar,
/// A ZIP file (`.zip`).
zip,
/// An uncompressed file.
uncompressed,
}
/// The installation status for a language server.
variant language-server-installation-status {
/// The language server has no installation status.
none,
/// The language server is being downloaded.
downloading,
/// The language server is checking for updates.
checking-for-update,
/// The language server installation failed for specified reason.
failed(string),
}
record settings-location {
worktree-id: u64,
path: string,
}
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
/// Downloads a file from the given URL and saves it to the given path within the extension's
/// working directory.
///
/// The file will be extracted according to the given file type.
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
/// Makes the file at the given path executable.
import make-file-executable: func(filepath: string) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
/// A list of environment variables.
type env-vars = list<tuple<string, string>>;
/// A command.
record command {
/// The command to execute.
command: string,
/// The arguments to pass to the command.
args: list<string>,
/// The environment variables to set for the command.
env: env-vars,
}
/// A Zed worktree.
resource worktree {
/// Returns the ID of the worktree.
id: func() -> u64;
/// Returns the root path of the worktree.
root-path: func() -> string;
/// Returns the textual contents of the specified file in the worktree.
read-text-file: func(path: string) -> result<string, string>;
/// Returns the path to the given binary name, if one is present on the `$PATH`.
which: func(binary-name: string) -> option<string>;
/// Returns the current shell environment.
shell-env: func() -> env-vars;
}
/// Returns the command used to start up the language server.
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
/// Returns the initialization options to pass to the language server on startup.
///
/// The initialization options are represented as a JSON string.
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the language server.
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// A label containing some code.
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the displayed label to include when filtering.
filter-range: range,
}
/// A span within a code label.
variant code-label-span {
/// A range into the parsed code.
code-range(range),
/// A span containing a code literal.
literal(code-label-span-literal),
}
/// A span containing a code literal.
record code-label-span-literal {
/// The literal text.
text: string,
/// The name of the highlight to use for this literal.
highlight-name: option<string>,
}
/// A (half-open) range (`[start, end)`).
record range {
/// The start of the range (inclusive).
start: u32,
/// The end of the range (exclusive).
end: u32,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
}

View File

@@ -0,0 +1,33 @@
interface github {
/// A GitHub release.
record github-release {
/// The version of the release.
version: string,
/// The list of assets attached to the release.
assets: list<github-release-asset>,
}
/// An asset from a GitHub release.
record github-release-asset {
/// The name of the asset.
name: string,
/// The download URL for the asset.
download-url: string,
}
/// The options used to filter down GitHub releases.
record github-release-options {
/// Whether releases without assets should be included.
require-assets: bool,
/// Whether pre-releases should be included.
pre-release: bool,
}
/// Returns the latest release for the given GitHub repository.
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
///
/// Returns an error if a release with the given tag name does not exist.
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
}

View File

@@ -0,0 +1,83 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
/// The kind of an LSP completion.
variant completion-kind {
text,
method,
function,
%constructor,
field,
variable,
class,
%interface,
module,
property,
unit,
value,
%enum,
keyword,
snippet,
color,
file,
reference,
folder,
enum-member,
constant,
struct,
event,
operator,
type-parameter,
other(s32),
}
/// Defines how to interpret the insert text in a completion item.
variant insert-text-format {
plain-text,
snippet,
other(s32),
}
/// An LSP symbol.
record symbol {
kind: symbol-kind,
name: string,
}
/// The kind of an LSP symbol.
variant symbol-kind {
file,
module,
namespace,
%package,
class,
method,
property,
field,
%constructor,
%enum,
%interface,
function,
variable,
constant,
%string,
number,
boolean,
array,
object,
key,
null,
enum-member,
struct,
event,
operator,
type-parameter,
other(s32),
}
}

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