Compare commits

...

127 Commits

Author SHA1 Message Date
mgsloan@gmail.com
06f7eb84be Merge branch 'completion-docs-prefetch' into fix-completion-fuzzy-match 2024-12-13 20:26:11 -07:00
mgsloan@gmail.com
653484ee56 Progress! 2024-12-13 19:28:12 -07:00
mgsloan@gmail.com
ce367ef305 WIP
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 18:43:36 -07:00
mgsloan@gmail.com
b76237b230 Remove use of mutation for completion resolution in lsp_store
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 18:43:36 -07:00
mgsloan@gmail.com
8bdf90787a Change editor context_menu from RwLock to RefCell 2024-12-13 18:41:57 -07:00
Conrad Irwin
2f2e7f0317 Revert "Resolve documentation for visible completions (#21705)" (#21985)
This reverts commit ab595b0d55.

Release Notes:

- (preview only) Fixed a panic in completions
2024-12-13 12:22:26 -07:00
Michael Sloan
2b699053e6 Log invariant violations in fuzzy string match iterator (#21983)
Seeing frequent inscrutable panics here

Release Notes:

- N/A
2024-12-13 11:16:30 -07:00
Antonio Scandurra
01e5ac0a49 Maintain inline completion order, simplifying how we track pending completions (#21977)
Release Notes:

- N/A
2024-12-13 17:24:07 +01:00
Thorsten Ball
306f1c6838 zeta: Increase context lines to 32 (#21968)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-13 15:41:43 +01:00
Thorsten Ball
2f722e63a1 Highlight whitespace-only inline completions with background (#21954)
Noticed that whitespace-only insertions are really hard to make out, so
this changes it to make them visible by giving them a green background.

![screenshot-2024-12-13-10 49
09@2x](https://github.com/user-attachments/assets/10d83067-46f2-4cb5-97fa-0f44d254890d)


Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-13 13:40:34 +01:00
Jaagup Averin
6838b6203a python: Refine highlighting (#21389)
Fixes:
* Types in binary unions as per [PEP
604](https://peps.python.org/pep-0604/) not highlighted;
   * `except*` keyword not highlighted;
* Classes beginning with `_` not recognized as such, however `_` is a
valid first character for private classes; additionally the regex for
parsing constant/class names appeared inconsistent and incomplete so was
adjusted;
   * Builtin types such as `float`, `dict`, etc not recognized as types;
   * **Update:** decorators with arguments not recognized as decorators;
* **Update:** docstrings after type alias assignments not recognized as
docstrings;
* **Update:** `and/in/is/not/or/is not/not in` not capturable as
keywords;
* **Update:** decorators with "nesting" (@x.y.z) not recognized as
decorators;

Before:

![new_before](https://github.com/user-attachments/assets/6f05262e-be3b-41bf-aee6-26438c2bf254)

After:

![new_after](https://github.com/user-attachments/assets/408c481c-5eb9-40c9-8e18-52ebf5a121d3)

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-13 12:40:16 +01:00
tims
5318f529de Improve editor open URL command to open the selected portion of URL (#21825)
Closes #21718

Just like in Vim, if a URL is selected, it opens exactly that portion of
the URL. Otherwise, if only the cursor is on a URL, it opens the entire
URL.

Zed currently does the latter. This PR also adds support for the former.


https://github.com/user-attachments/assets/8bdd2952-ceec-487c-b27a-5cea4258eb03

Release Notes:

- Updated the `editor: open url` to also handle the selected portion of
a URL.
2024-12-12 22:15:21 -08:00
Danilo Leal
096bbfead5 zeta: Adjust reviewing UI (#21932)
Most notably, adding a title bar-ish in the left column as so to add the
"from most recent to oldest" info, which is supposed to make scanning
the list of completions easier to do (at least it would've helped me
figure out that was sorted that way when I was wondering about it!).

<img width="800" alt="Screenshot 2024-12-12 at 16 24 36"
src="https://github.com/user-attachments/assets/1acc9951-3df0-4cd2-96ff-94ed555ecae5"
/>

Release Notes:

- N/A
2024-12-13 00:52:23 -03:00
Danilo Leal
0b4495a920 zeta: Adjust the "Jump To Edit" button visuals (#21933)
| One Dark | One Light |
|--------|--------|
| <img width="1495" alt="Screenshot 2024-12-12 at 16 27 12"
src="https://github.com/user-attachments/assets/897ee786-a6f7-4d4e-8722-301ac13e6d8c"
/> | <img width="1495" alt="Screenshot 2024-12-12 at 16 27 18"
src="https://github.com/user-attachments/assets/a78aa5e4-f327-41da-bc9c-6e102bc67fe2"
/> |

| One Dark | One Light |
|--------|--------|
| <img width="1495" alt="Screenshot 2024-12-12 at 16 26 54"
src="https://github.com/user-attachments/assets/0357468e-7b5f-4f92-bcdb-5f94e353d8b2"
/> | <img width="1495" alt="Screenshot 2024-12-12 at 16 26 59"
src="https://github.com/user-attachments/assets/20e0f47e-e20f-46a7-b053-8e528b0975d7"
/> |


Release Notes:

- N/A
2024-12-13 00:52:12 -03:00
Bennet Bo Fenner
636c28b652 project panel: Reintroduce project panel knockout color (#21926)
Reintroduces #20760 after it was reverted in #21807

Closes #20572

/cc @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-13 00:52:03 -03:00
Danilo Leal
6ceec5d9a2 Fix project and buffer search input width (#21949)
Closes https://github.com/zed-industries/zed/issues/21922

Now, both the project and buffer search inputs have a min-width set so
that text inside it, as well as the additional controls, are always
visible even at the window's smallest possible size, which looks like
this:

<img width="407" alt="Screenshot 2024-12-13 at 00 35 46"
src="https://github.com/user-attachments/assets/e6e2c4c6-4f75-4663-8c65-590e02141a5d"
/>


Release Notes:

- N/A
2024-12-13 00:51:51 -03:00
Nate Butler
9f0f63f92b Git panel refinements 2 (#21947)
Add entry list, scrollbar

Release Notes:

- N/A
2024-12-12 22:30:00 -05:00
0x2CA
b38e9e44d6 Fix hover popover font fallbacks (#21945)
Closes #21848

Release Notes:

- Fixed Hover Popover Font Callbacks
2024-12-12 18:30:25 -08:00
Wang Can
e0cbbf8d06 Fix opening repos when .git is a soft link (#21153)
Closes #ISSUE

## background
If a project is big, some times it will be splited into many small git
repos.
[google repo](https://gerrit.googlesource.com/git-repo/) is a tool to
manage a group of git repos.

But, any small git repo manged by this tool, have a difference with
normal git repo.
That is , the path `.git` in the root of the git repo, is not a normal
directory, but a soft link to real git bare dir.

### zed can not recognize the `git-repo` managed git repos
you can use the procedure to genreate this problem
```bash
# tested on linux
mkdir -p bad_git_repo_project
cd bad_git_repo_project
git init
echo "hello" > hi.txt
git add .
git commit -m "init commit"
echo "hello world" >> hi.txt

# modify the repo
mv .git ../.real_git_repo
ln -sf ../.real_git_repo .git
```
with vscode, after opening this project, git works well.
but for Zed, git not work(not git status, no git blame)


## how to fix
libgit2 can recognize git repo from the root of the project(dir that
have `.git`).
so, we can recognize the git project by opening from the project root
dir, but not the `.git` dir

This fix also works with normal git project.

### before fix

![image](https://github.com/user-attachments/assets/1fb53ff4-4ab1-402e-9640-608ca79e12a4)


### after fix

![image](https://github.com/user-attachments/assets/6b16bc54-34f0-4436-b642-3c5fa8b669bd)

Release Notes:
- Fix opening repos when .git is a soft link
2024-12-12 18:29:37 -08:00
Mikayla Maki
4eaa1c2514 Only debounce the cursor position in multibuffer excerpts (#21946)
Follow up to: https://github.com/zed-industries/zed/pull/20211

Release Notes:

- Improved the performance of the cursor position indicator in single
buffers
2024-12-12 18:27:06 -08:00
CharlesChen0823
b3de19a6bd editor: Add duplicate selection command (#21154)
Closes #4890 

Release Notes:

- Add duplicate selection command for editor
2024-12-12 17:57:24 -08:00
CharlesChen0823
241b14eeaf project_panel: Create items when the editor is dismissed via the mouse (#21045)
Closes #5036 

Release Notes:

- Created project panel items when the editor is dismissed via the mouse
2024-12-12 17:24:25 -08:00
Ozan
72d8f2e595 editor: Add "selection" key context (#21927)
This change allows defining keybindings that are active when there is a
text selection.

This is especially useful, as an example, for Emacs-like keybindings
where movement keybindings expand the selection.

Here is a snippet from my keymap.json that implements Emacs movements
when selection is active:

```json
{
    "context": "Editor && selection",
    "bindings": {
      "ctrl-f": "editor::SelectRight",
      "ctrl-b": "editor::SelectLeft",
      "ctrl-n": "editor::SelectDown",
      "ctrl-p": "editor::SelectUp",
      "ctrl-a": "editor::SelectToBeginningOfLine",
      "ctrl-e": "editor::SelectToEndOfLine",
      "alt-f": "editor::SelectToNextWordEnd",
      "alt-b": "editor::SelectToPreviousWordStart",
      "alt-<": "editor::SelectToBeginning",
      "alt->": "editor::SelectToEnd"
    }
  }
  ```

What do you think about inclusion of this feature? Should I add more granular `selection=single` `selection=multi`? 

Release Notes:

- Added "selection" context for keybindings that are active when there is a text selection.
2024-12-12 16:56:42 -08:00
Dan Dascalescu
3f6ac53856 Update GitHub bug issue template to refer to bugs instead of features (#21727)
Release Notes:

- N/A
2024-12-12 16:54:37 -08:00
João Otávio Biondo
74d7ce2d2b elixir: Improve ElixirLS LSP autocomplete to show labelDetails information (#21666)
Closes https://github.com/zed-industries/zed/issues/19688

Release Notes:

- Improved ElixirLS LSP autocomplete to show module, function and struct
field details

![image](https://github.com/user-attachments/assets/2f05183f-8f7f-42c3-ba14-28fc58522488)

![image](https://github.com/user-attachments/assets/bfdea373-79ec-4dec-a9c7-5d15ad9403ee)

![image](https://github.com/user-attachments/assets/c0fd66d5-0e01-4e1e-a2d5-0a78d38e0b72)
2024-12-12 16:16:23 -08:00
tims
6a37307302 Add .prettierignore support (#21297)
Closes #11115

**Context**:

Consider a monorepo setup like this: the root has Prettier installed,
but the individual monorepos do not. In this case, only one Prettier
instance is used, with its installation located at the root. The
monorepos also use this same instance for formatting.

However, monorepo can have its own `.prettierignore` file, which will
take precedence over the `.prettierignore` file at the root level (if
one exists) for files in that monorepo.

<img
src="https://github.com/user-attachments/assets/742f16ac-11ad-4d2f-a5a2-696e47a617b9"
alt="prettier" width="200px" />

**Implementation**:

From the context above, we should keep ignore dir decoupled from the
Prettier instance. This means that even if the project has only one
Prettier installation (and thus a single Prettier instance), there can
still be multiple `.prettierignore` in play.

This approach also allows us to respect `.prettierignore` even when the
project does not have Prettier installed locally and instead relies on
the editor’s Prettier instance.

**Tests**:

1. No Prettier in project, using editor Prettier: Ensures
`.prettierignore` is respected even without a local Prettier
installation.
2. Monorepo with root Prettier and child `.prettierignore`: Confirms
that the child project’s ignore file is correctly used.
3. Monorepo with root and child `.prettierignore` files: Verifies the
child ignore file takes precedence over the root’s.

Release Notes:

- Added `.prettierignore` support to the Prettier integration.
2024-12-12 15:45:44 -08:00
xzbdmw
8dd1c23b92 editor: Add debounce setting for triggering DocumentHighlight (#21702)
Closes https://github.com/zed-industries/zed/issues/6843


I don't see where is the logic to remove old document highlight when new
one applies,
ideally, old highlight should be cleared as soon as possible when cursor
moves if the new position does not
sits in old highlight ranges to avoid linger highlights described in
https://github.com/zed-industries/zed/issues/13682#issuecomment-2498368680.

So current solution is still not ideal, because only when lsp responses
highlight ranges (even is a empty set) can we clear the old one.

Release Notes:

- Added a setting `lsp_highlight_debounce` to configure delay for
querying highlights from language server.

---------

Co-authored-by: mgsloan@gmail.com <michael@zed.dev>
2024-12-12 15:37:58 -08:00
Evren Sen
57874717c1 Add metal icon (#21720)
Release Notes:

- Added file icon for metal

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-12 15:23:20 -08:00
Aaron Feickert
bab6a79ab6 Fix audio tooltip logic (#21941)
Earlier work by @osiewicz in #21931 aims to fix audio control tooltips
in the title bar to close #21929. However, its logic is not quite
correct, and does not match the toggle behavior for the controls.

This PR corrects the logic to match the toggle behavior for the
controls. It also updates capitalization and wording for consistency.

Release Notes:

- N/A
2024-12-12 15:20:21 -08:00
uncenter
9a806f98e6 Improve diff syntax highlighting queries (#21740)
Brings over the improvements made for the same grammar:
https://github.com/nvim-treesitter/nvim-treesitter/pull/6619.

Related to #19986 but not really- the problem brought up there is an
issue of themes not supporting the `diff.plus` and `diff.minus` captures
(already used before this PR).

<details><summary>Theme previews (before/after)</summary>

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
31](https://github.com/user-attachments/assets/698122df-fb63-4d7c-95aa-9559c7dcc684)
| ![CleanShot 2024-12-09 at 07 31
08](https://github.com/user-attachments/assets/ef927c0d-6c77-4fd4-b513-8359fb2442f7)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 34
15](https://github.com/user-attachments/assets/53b825ec-2987-4122-837d-1ebce334f153)
| ![CleanShot 2024-12-09 at 07 31
36](https://github.com/user-attachments/assets/079f19fb-4cc4-4256-b390-868f33e775c5)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 34
27](https://github.com/user-attachments/assets/4e3a80da-edff-4a53-bbf8-abc17cd49c5e)
| ![CleanShot 2024-12-09 at 07 31
53](https://github.com/user-attachments/assets/c6e12d79-5e59-4ebf-9fb9-ef3b0f8c9a81)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
44](https://github.com/user-attachments/assets/a007df22-7012-4de7-a71e-0ce5b523b561)
| ![CleanShot 2024-12-09 at 07 32
13](https://github.com/user-attachments/assets/c8c63292-5a64-4560-ad7c-9235b8b98ca3)
|

| Before | After |
| --- | --- |
| ![CleanShot 2024-12-09 at 07 33
57](https://github.com/user-attachments/assets/1a9c3656-3805-45a6-97af-747ef50e3b6c)
| ![CleanShot 2024-12-09 at 07 32
25](https://github.com/user-attachments/assets/76bac31c-8786-4907-8570-bf3c2888823e)
|

</details>

Release Notes:

- Improved diff syntax highlighting
2024-12-12 15:18:36 -08:00
CharlesChen0823
e778635487 search: Add ToggleRegex for buffer search (#21799)
Closes #21790 

IMO, this is lost

Release Notes:

- Add ToggleRegex for buffer search
2024-12-12 15:16:39 -08:00
5de0bcc990 gpui: Fix for setting window titles on Windows (#21907)
Windows requires `WM_NCCREATE` to be processed by default procedure to
set window title properly.

Release Notes:

- N/A
2024-12-12 14:58:30 -08:00
Marshall Bowers
9143fd2924 language_model_selector: Don't recreate the Picker view each render (#21939)
While working on Assistant2, I noticed that the `LanguageModelSelector`
was recreating its `Picker` view on every single render.

This PR makes it so we create the view once and hold onto it in the
parent view.

Release Notes:

- N/A
2024-12-12 17:08:48 -05:00
Joseph T. Lyons
d7eba54016 Add version control file icon for gitcommit files (#21935)
Closes: https://github.com/zed-industries/zed/issues/21734

<img width="976" alt="SCR-20241212-nlci"
src="https://github.com/user-attachments/assets/d567e2c8-d803-4148-b159-ae781eb59b50"
/>

I added the same file extensions that are used in the `Git Firefly`
extension.


b521b71324/languages/gitcommit/config.toml (L5-L9)

Release Notes:

- Added version control file icon for gitcommit files.
2024-12-12 16:23:17 -05:00
Marshall Bowers
52c0d712a6 assistant2: Add initial support for attaching file context (#21934)
This PR adds the initial support for attaching files as context to a
thread in Assistant2.

Release Notes:

- N/A
2024-12-12 15:30:17 -05:00
Piotr Osiewicz
111e844753 title_bar: Adjust tooltip for mute/deafen buttons (#21931)
Closes #21929 

Release Notes:

- N/A
2024-12-12 20:09:52 +01:00
Kyle Kelley
0eb992219b Set User Agent for Jupyter websocket connections (#21910)
Some VPN configurations require that websockets present a user agent.
This adds it in directly for the repl usage. I wish there was a way to
reuse the user agent from the `cx.http_client`, but I'm not seeing a
simple way to do that for the moment since it's not on the `HttpClient`
trait.

No release notes since this feature hasn't been announced/exposed.

Release Notes:

- N/A
2024-12-12 09:26:16 -08:00
Nate Butler
573e096fc5 More Git panel refinements (#21928)
- Add and wire through git method stubs
- Organize render methods
- Track modifier changes
- Swap commit buttons when `option`/`alt` is held
- More TODOs

Release Notes:

- N/A
2024-12-12 12:21:08 -05:00
Cole Miller
ee6f834028 Fuse LLM completion stream to avoid a panic (#21914)
`LanguageModel::stream_completion_text` can poll the `stream_completion`
stream (ultimately a `futures::Unfold`) after it's returned
`Ready(None)`, which leads to a panic; avoid this by fusing the stream.

Release Notes:

- Fixed a panic when streaming language model completions
2024-12-12 11:39:35 -05:00
Antonio Scandurra
b4c8e04544 Clear completion if model doesn't produce any edit (#21925)
Release Notes:

- N/A
2024-12-12 17:23:22 +01:00
Richard Feldman
bcf8a2f9fc Inline terminal assistant v2 (#21888)
Follow-up to https://github.com/zed-industries/zed/pull/21828 to add it
to the terminal as well.


https://github.com/user-attachments/assets/505d1443-4081-4dd8-9725-17d85532f52d

As with the previous PR, there's plenty of code duplication here; the
plan is to do more code sharing in separate PRs!


Release Notes:

- N/A
2024-12-12 11:06:09 -05:00
Piotr Osiewicz
77d066200a lsp: Fill in a bunch of missing capabilities (#21924)
Also state explicitly that we do support UTF-16 encoding and nothing
else.

See also #19788

Release Notes:

- N/A
2024-12-12 16:39:29 +01:00
Peter Tripp
5d0e75dd73 Improve emacs keybind with better home/end behavior (#21923)
Improve behavior of ctrl-a/ctrl-e home/end in emacs keybind.
Follow up to #21921 to add those to Linux emacs keymap too.

Release Notes:

- emacs: Improved `ctrl-a` / `ctrl-e` / `home` / `end` behavior
- emacs: Added for `ctrl-s` / `ctrl-r` / `ctrl-g` for navigating buffer
search results
2024-12-12 10:37:15 -05:00
Aaron Feickert
181af7804b Fix docstring for CallSettingsContent.share_on_join (#21884) 2024-12-12 10:09:28 -05:00
Antonio Scandurra
ad4c4aff13 Always let two completions race with each other (#21919)
When a user types, chances are the model will anticipate what they are
about to do. Previously, we would continuously cancel the pending
completion until the user stopped typing. With this commit, we allow at
most two completions to race with each other (the first and the last
one):

- If the completion that was requested first completes first, we will
show it (assuming we can interpolate it) but avoid canceling the last
one.
- When the completion that was requested last completes, we will cancel
the first one if it's pending.

In both cases, if a completion is already on-screen we have a special
case for when the completions are just insertions and the new completion
is a superset of the existing one. In this case, we will replace the
existing completion with the new one. Otherwise we will keep showing the
old one to avoid thrashing the UI.

This should make latency a lot better. Note that I also reduced the
debounce timeout to 8ms.

Release Notes:

- N/A
2024-12-12 16:01:05 +01:00
Peter Tripp
91b02a6259 Add emacs keybinds for previous/next/cancel in search (#21921) 2024-12-12 09:50:54 -05:00
xuoe
1f296d8f3b docs: Include restore_on_startup (#21918)
Signed-off-by: xuoe <xuoe@pm.me>
2024-12-12 09:21:27 -05:00
Danilo Leal
c204b0d01a zeta: Add adjustments to the review modal UI (#21920)
Most notably, adding a current iteration of a possible logo to feel it
out! :) Also, I'm hiding the input and instructions container after the
review has been sent. In the future, if we allow changing an already
sent review, we can change this behavior.

<img width="800" alt="Screenshot 2024-12-12 at 10 42 44"
src="https://github.com/user-attachments/assets/37e63d0d-d847-445e-bdf8-bf5c97d0fe4c"
/>

Release Notes:

- N/A
2024-12-12 11:17:08 -03:00
Nate Butler
8e0ae441f3 Initial git panel refinements (#21912)
- Wire up settings
- Update static Panel impl
- Tidy up renders

Release Notes:

- N/A
2024-12-12 09:13:40 -05:00
Raphael Kieling
02fbad18ce toolbar: Add gap between the Kernel and REPL button (#21871)
Before:

![image](https://github.com/user-attachments/assets/dbc382a8-2ba5-4639-964f-35c934875e88)

After:

![image](https://github.com/user-attachments/assets/5faf2144-63c3-41d4-b1b8-fcd6f6fd7b7e)

Also works with dark themes:

![image](https://github.com/user-attachments/assets/1f3e9bfb-94f8-44f2-9727-e46fddccb153)

Release Notes:

- N/A

Co-authored-by: raphael.kieling <raphael.kieling-ext@ab-inbev.com>
2024-12-12 09:49:17 -03:00
Thorsten Ball
227f21f035 zeta: Show timestamps and latency in rating modal (#21863)
Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-12 09:30:20 +01:00
Cole Miller
543a3ef5e4 linux: Don't watch parent directory when target path already exists (#21854)
The Linux watcher was unconditionally watching the parent directory of
every watched path. This is needed in the case of config files that may
not exist when the watch is set up, but not in other cases. Scoping the
parent watch more narrowly cuts down on the amount of error logging from
irrelevant file change notifications being sent to Zed (in my case it
was picking up changes to a random file in `$HOME`).

Release Notes:

- N/A
2024-12-12 01:54:14 -05:00
Cole Miller
cc97e682d5 worktree: Fix privacy check for singleton files (#21861)
Closes #20676

Release Notes:

- Fixed private files not being redacted when not part of a larger
worktree
2024-12-12 01:53:00 -05:00
Nate Butler
59afc27f03 Add placeholder git panel (#21894)
Adds a simple git placeholder panel for us to iterate from. Also
includes a number of assets from the git prototyping branch that we will
use.

Note: This panel is staff flagged for now.

Release Notes:

- N/A
2024-12-11 22:13:52 -05:00
Peter Tripp
611abcadc0 Add schema to .github/ISSUE_TEMPLATE/config.yml (#21836)
Workaround for upstream issue where yaml-language-server
2024-12-11 17:16:21 -05:00
Peter Tripp
fff12ec1e5 Mention Lllama 3.3 in Ollama config panel (#21866)
Trivial, but makes us not look outdated.
2024-12-11 16:38:03 -05:00
Conrad Irwin
13a81e454a Start to split out initialization and registration (#21787)
Still TODO:

* [x] Factor out `start_language_server` so we can call it on register
(instead of on detect language)
* [x] Only call register in singleton editors (or when
editing/go-to-definition etc. in a multibuffer?)
* [x] Refcount on register so we can unregister when no buffer remain
* [ ] (maybe) Stop language servers that are no longer needed after some
time

Release Notes:

- Fixed language servers starting when doing project search
- Fixed high CPU usage when ignoring warnings in the diagnostics view

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Cole <cole@zed.dev>
2024-12-11 14:05:10 -07:00
Jason Lee
de89f8cf83 gpui: Add linear gradient support to fill background (#20812)
Release Notes:

- gpui: Add linear gradient support to fill background

Run example:

```
cargo run -p gpui --example gradient
cargo run -p gpui --example gradient --features macos-blade
```

## Demo

In GPUI (sRGB):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/568c02e8-3065-43c2-b5c2-5618d553dd6e">

In GPUI (Oklab):

<img width="761" alt="image"
src="https://github.com/user-attachments/assets/b008b0de-2705-4f99-831d-998ce48eed42">

In CSS (sRGB): 

https://codepen.io/huacnlee/pen/rNXgxBY

<img width="505" alt="image"
src="https://github.com/user-attachments/assets/239f4b65-24b3-4797-9491-a13eea420158">

In CSS (Oklab):

https://codepen.io/huacnlee/pen/wBwBKOp

<img width="658" alt="image"
src="https://github.com/user-attachments/assets/56fdd55f-d219-45de-922f-7227f535b210">


---

Currently only support 2 color stops with linear-gradient. I think this
is we first introduce the gradient feature in GPUI, and the
linear-gradient is most popular for use. So we can just add this first
and then to add more other supports.
2024-12-11 21:52:52 +02:00
Richard Feldman
c594ccb0af Inline assistant v2 (#21828)
This is behind the Assistant v2 feature flag. As @maxdeviant and I
discussed, the state is currently decoupled from the Assistant Panel's
state, although in the future we plan to introduce a way to refer to
conversations from the panel. Also, we're intentionally duplicating some
code with the v2 panel right now; the plan is to do a future PR to make
them share code more.


https://github.com/user-attachments/assets/bb163bd3-a02d-4a91-8f8f-2a8e60acbc34

It doesn't include the terminal inline assistant, which will be in a
separate PR.

Release Notes:

- N/A
2024-12-11 14:32:30 -05:00
Marshall Bowers
937186da12 gpui: Don't export named Context from prelude (#21869)
This PR updates the `gpui::prelude` to not export the `Context` trait
named.

This prevents some naming clashes in downstream consumers.

Release Notes:

- N/A
2024-12-11 13:21:40 -05:00
Marshall Bowers
b3ffbea376 assistant2: Allow removing individual context (#21868)
This PR adds the ability to remove individual pieces of context from the
message editor:

<img width="1159" alt="Screenshot 2024-12-11 at 12 38 45 PM"
src="https://github.com/user-attachments/assets/77d04272-f667-4ebb-a567-84b382afef3d"
/>

Release Notes:

- N/A
2024-12-11 12:51:05 -05:00
Thorsten Ball
124e63d07c Show inline completions when completion menu is visible (#21858)
This changes the behavior of how we display inline completions and
non-inline completions (i.e. completion menu).

Previously we would never show inline completions if a completion menu
was visible, meaning that we'd never show Copilot/Supermaven/...
suggestions if the language server had a suggestion.

With this change, we now display the inline completions even if there is
a completion menu visible.

In that case `<tab>` then accepts the inline completion and `<enter>`
accepts the selected entry in the completion menu.

Release Notes:

- Changed how inline completions (Copilot, Supermaven, ...) and normal
completions (from language servers) interact. Zed will now also show
inline completions when the completion menu is visible. The user can
accept the inline completion with `<tab>` and the active entry in the
completion menu with `<enter>`. Previously, `<tab>` would also select
the active entry in the completion menu.

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 17:13:22 +01:00
Antonio Scandurra
dd66a20d78 Move prediction diff computation to background thread (#21862)
Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-11 17:12:58 +01:00
Joseph T. Lyons
e8c72d91c3 v0.167.x dev 2024-12-11 11:00:35 -05:00
Danilo Leal
dfe455b054 zeta: Improve UI for feedback instructions (#21857)
If the instructions are added as the input placeholder, when in a
smaller window size (like the one from the screenshot), scrolling is
needed to see them all. So, thought of extracting it out of there. Also
thought it looked more refined this way!

<img width="800" alt="Screenshot 2024-12-11 at 11 48 17"
src="https://github.com/user-attachments/assets/46974b94-6365-4a59-bf71-a6c0863aac68"
/>

Release Notes:

- N/A
2024-12-11 12:07:41 -03:00
Danilo Leal
db7e38464a zeta: Show keybinding on rating buttons (#21853)
<img width="800" alt="Screenshot 2024-12-11 at 10 57 00"
src="https://github.com/user-attachments/assets/6055639c-5b38-444d-b76d-bf7584a82efc"
/>

Release Notes:

- N/A
2024-12-11 11:54:39 -03:00
Kyle Kelley
f8b6d71670 Optimize REPL kernel spec refresh (#21844)
Python kernelspec refresh now only performed on (known) python files. 

Release Notes:

- N/A
2024-12-11 06:20:44 -08:00
Thorsten Ball
ae351298b4 zeta: Fixes to completion-rating modal (#21852)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 15:00:27 +01:00
Thorsten Ball
664468d468 zeta: Invalidate completion in different buffers (#21850)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-11 14:37:53 +01:00
Piotr Osiewicz
714f183ede multi_buffer: optimize runnables layout (#21849)
Related to #21481 ; it fixes a bunch of hotspots I saw while looking at
the provided profiles. MultiBuffer still takes up 100% CPU on the
foreground thread for me - this time around it's on selection updates
(when dragging the selected text towards an edge of a screen).

Release Notes:

- N/A
2024-12-11 13:46:08 +01:00
Mikayla Maki
b36dcf3b92 Improve Zeta rating ergonomics (#21845)
This PR adds keyboard shortcuts to common interactions you might want to
do in the Zeta rating panel.

This PR also adds a way to fake inline completion requests, as well as
the test data used to create this PR, to make it easier to adjust the UI
in the future.

It also changes the status bar from the text "Zeta" to "ζ", because I
thought it looked cool.

Release Notes:

- N/A
2024-12-11 01:57:46 -08:00
Danilo Leal
63e1bf01a4 zeta: Improve reviewing UI (#21838)
Starting to fine-tune it.

| No edits scenario | Rated edits scenario |
|--------|--------|
| <img width="1577" alt="Screenshot 2024-12-11 at 01 57 46"
src="https://github.com/user-attachments/assets/42926e84-7a7f-4692-af44-672b52d3d377">
| <img width="1577" alt="Screenshot 2024-12-11 at 01 58 37"
src="https://github.com/user-attachments/assets/ee8ab0ef-75af-424c-b916-9f1ce8b5264d">

Release Notes:

- N/A
2024-12-11 02:19:57 -03:00
Connor Tsui
62a6a755ec Add musl package for Arch Linux (#21830)
It seems like `musl` is required to build on Arch Linux, but it is not included in the dependencies list.
2024-12-10 21:05:53 -05:00
Ethan Budd
28faba12a2 Recognize .C and .H as supported cpp extensions (#21647)
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-12-10 19:55:21 -05:00
Marshall Bowers
c255e55599 assistant2: Sketch in sending file context to model (#21829)
This PR sketches in support for sending file context attached to a
message to the model.

Right now the context is just mocked.

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 41 PM"
src="https://github.com/user-attachments/assets/3ee4e86a-7893-42dc-98f9-982aa202d310">

<img width="1159" alt="Screenshot 2024-12-10 at 4 18 53 PM"
src="https://github.com/user-attachments/assets/8a3c2dd7-a466-4dbf-83ec-1c7d969c1a4b">

Release Notes:

- N/A
2024-12-10 16:35:53 -05:00
Joseph T. Lyons
f80eb73213 Update event type to conform to standard (#21827)
Release Notes:

- N/A
2024-12-10 16:14:31 -05:00
strowk
faf79e52fe zed_extension_api: Add a short explanation of repo format (#21824)
Improved extension api documentation for latest_github_release function

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-12-10 15:04:47 -05:00
Michael Sloan
ab595b0d55 Resolve documentation for visible completions (#21705)
Release Notes:

- Improved LSP resolution of documentation for completions. It now
queries documentation for visible completions and avoids doing too many
redundant queries.

---

In #21286, documentation resolution was made more efficient by only
resolving the current completion. However, this meant that single line
documentation shown inline in the menu was missing until scrolled
to. This also meant that it would wait for navigation to resolve
completion docs, leading to lag for displaying documentation.

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.
2024-12-10 12:25:30 -07:00
Michael Sloan
ab1e9bf270 On windows, recreate renderer swap chain on restore from minimized (#21756)
Closes #21688

Release Notes:

- Windows: Fix freeze after window minimize and maximize
2024-12-10 11:59:44 -07:00
Minqi Pan
adc66473e7 gpui: Add cursor style methods of nesw nwse resize (#21801)
Release Notes:

- N/A

---

This change adds two new methods to the cursor_style_methods function in
the gpui_macros crate (according to the Tailwind CSS documentation
https://tailwindcss.com/docs/cursor):
1. `cursor_nesw_resize`: Sets the cursor style to nesw-resize when
hovering over an element. This is useful for indicating resizing
diagonally from top-right to bottom-left.
2. `cursor_nwse_resize`: Sets the cursor style to nwse-resize when
hovering over an element. This is used for resizing diagonally from
top-left to bottom-right.
2024-12-10 11:54:26 -07:00
mgsloan@gmail.com
bc9dfc6385 Resolve documentation for visible completions
In #21286, documentation resolution was made more efficient by only
resolving the current completion. However, this meant that single line
documentation shown inline in the menu was missing until scrolled
to. This also meant that it would wait for navigation to resolve
completion docs, leading to lag for displaying documentation.

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.
2024-12-10 11:52:32 -07:00
Marshall Bowers
119b5de384 assistant2: Change chat keybinding to just Enter (#21819)
This PR changes the Assistant2 chat keybinding from `Cmd-Enter` to just
`Enter`.

Release Notes:

- N/A
2024-12-10 12:34:54 -05:00
Marshall Bowers
c80ea60860 assistant2: Update to match latest designs (#21818)
This PR updates the Assistant2 panel to match the latest designs.

<img width="1159" alt="Screenshot 2024-12-10 at 11 49 14 AM"
src="https://github.com/user-attachments/assets/53739709-e7b9-4e35-8a5d-97b6560623ed">

Release Notes:

- N/A
2024-12-10 12:05:30 -05:00
Peter Tripp
bac6896786 Add Dart docs for line length (#21815) 2024-12-10 11:22:17 -05:00
Bennet Bo Fenner
c6932d1f51 zeta: Add action to clear edit history (#21813)
Co-Authored-by: Antonio <antonio@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-10 16:57:24 +01:00
Conrad Irwin
03efd0d1d9 Stop sending data to Clickhouse (#21763)
Release Notes:

- N/A
2024-12-10 08:47:29 -07:00
Thorsten Ball
43ba0c9fa6 zeta: Extend text in popover until EOL (#21811)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:21:45 +01:00
Thorsten Ball
4300ef840b zeta: Use word-wise diff when computing edits (#21810)
Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 16:05:34 +01:00
Bennet Bo Fenner
e0f4c01794 Revert "Improve project_panel diagnostic icon knockout colors (#20760)" (#21807)
This reverts commit 571c7d4f66.

Manually tracking the hovered entities causes issues with hightlighting:


https://github.com/user-attachments/assets/932dc022-a0ad-485c-a9db-ef03d7b86032

cc @danilo-leal @nilskch 

Release Notes:

- Fixed an issue where hovering over project panel would not update the
background correctly
2024-12-10 15:37:33 +01:00
Bennet Bo Fenner
58f9301253 image viewer: Allow dropping images on pane (#21803)
Partially addresses #21484


https://github.com/user-attachments/assets/777da5de-15c3-4af3-a597-1835c0155326

Release Notes:

- Support opening images by dropping them onto a pane
2024-12-10 15:01:14 +01:00
Thorsten Ball
96499b7b25 zeta: Refresh LLM token in case it expired (#21796)
Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-12-10 14:12:49 +01:00
Finn Evers
09006aaee9 Add option to activate left neighbour tab on tab close (#21800)
Closes #21738

Release Notes:

- Added `left_neighbour` option to the `tabs.activate_on_close` setting
to activate the left adjacent tab on tab close.
2024-12-10 15:05:36 +02:00
Kirill Bulatov
2ca3b440a9 Fix a panic when drop-splitting the terminal panel (#21795)
Closes https://github.com/zed-industries/zed/issues/21792

Release Notes:

- (Preview only) Fixed a panic when drop-splitting the terminal panel
2024-12-10 13:50:19 +02:00
Piotr Osiewicz
9219b05c85 chore: Move more local code into LocalLspStore (#21794)
Closes #ISSUE

Release Notes:

- N/A
2024-12-10 12:48:44 +01:00
Nils Koch
bd2087675b Fix git colors in image tabs (#21773)
Note that the git coloring of the icons got removed in
https://github.com/zed-industries/zed/pull/21383

Closes #21772

Release Notes:

- N/A
2024-12-10 01:40:25 -07:00
Jason Lee
44164dbbb8 gpui: Update Bounds, Point, and Axis to be serializable (#21783)
Makes `Bounds`, `Point`, and `Axis` be serializable, for dumping to JSON without conversion.

Release Notes:

- N/A
2024-12-10 00:43:55 -07:00
Conrad Irwin
3c053c7bc4 LspStore: move language_server_ids to local state too (#21786)
Attempt to further clarify what state is present in which mode

Release Notes:

- N/A
2024-12-10 00:15:06 -07:00
Conrad Irwin
48eed7499f Move diagnostics to the LocalLspStore (#21782)
This should be a no-op, but clarifies that some fields of the LspStore
were never actually used in the remote case.

Release Notes:

- N/A
2024-12-09 22:47:13 -07:00
Conrad Irwin
a35ef5b79f Fix diagnostics randomized tests (#21775)
These were silently passing after the delay in updating diagnostics was
added.

Co-Authored-By: Max <max@zed.dev>

cc @someonetoignore

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-12-09 21:56:43 -07:00
Marshall Bowers
8a85d6ef96 collab: Make metrics_id required in LlmTokenClaims (#21771)
This PR makes the `metrics_id` field on the `LlmTokenClaims` required,
as we always have one in practice.

Release Notes:

- N/A
2024-12-09 17:58:14 -05:00
Marshall Bowers
158cdc33ba collab: Attach additional properties to Language Model Used event (#21770)
This PR attaches two new properties to the `Language Model Used` event:

- `has_llm_subscription` - This will tell us if a user is a paid
subscriber.
- `max_monthly_spend_in_cents` - This will indicate what their maximum
monthly spend is set to.

Release Notes:

- N/A
2024-12-09 17:13:41 -05:00
Marshall Bowers
bdeac79d48 collab: Prevent max_monthly_llm_usage_spending_in_cents from being negative (#21768)
This PR fixes an issue where the
`max_monthly_llm_usage_spending_in_cents` preference could be set to a
negative value.

Release Notes:

- N/A
2024-12-09 16:55:26 -05:00
Mikayla Maki
73e0d816c4 Move ContextMenu out of editor.rs and rename ContextMenu to CodeContextMenu (#21766)
This is a no-functionality refactor of where the `ContextMenu` type is
defined. Just the type definition and implementation is up to almost
1,000 lines; so I've moved it to it's own file and renamed the type to
`CodeContextMenu`

Release Notes:

- N/A
2024-12-09 13:31:20 -08:00
Conrad Irwin
6538227f07 Revert "Avoid endless loop of the diagnostic updates (#21209)" (#21764)
This reverts commit 9999c31859.

Release Notes:

- Fixes diagnostics not updating in some circumstances
2024-12-09 14:15:23 -07:00
Marshall Bowers
ef45eca88e extension_host: Fix uploading dev extensions to the remote server (#21761)
This PR fixes an issue where dev extensions were not working when
uploaded to the remote server.

The `extension.toml` for dev extensions may not contain all of the
information (such as the list of languages), as this is something that
we derive from the filesystem at packaging time. This meant that
uploading a dev extension that contained languages could have them
absent from the uploaded `extension.toml`.

For dev extensions we now upload a serialized version of the in-memory
extension manifest, which should have all of the information present.

Release Notes:

- SSH Remoting: Fixed an issue where some dev extensions would not work
after being uploaded to the remote server.

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-09 15:23:28 -05:00
Michael Sloan
803855e7b1 Add async_task::spawn_local variant that includes caller in panics (#21758)
For debugging #21020. Copy-modified [from async_task
here](ca9dbe1db9/src/runnable.rs (L432))

Release Notes:

- N/A
2024-12-09 12:45:37 -07:00
Kirill Bulatov
25a5ad54ae Sync newly added diff hunks (#21759)
Fixed project diff multi buffer not expanding its diff until edited

Release Notes:

- N/A
2024-12-09 21:43:25 +02:00
Michael Sloan
a5355e92e3 Add per-language settings show_completions_on_input and show_completion_documentation (#21722)
Release Notes:

- Added `show_completions_on_input` and `show_completion_documentation`
per-language settings. These settings were available before, but were
not configurable per-language.
2024-12-09 11:53:50 -07:00
Piotr Osiewicz
b7edf31170 lsp: Disable usage of follow-up completion invokes (#21755)
Some of our users ran into a peculiar bug: autoimports with vtsls were
leaving behind an extra curly brace. I think we were slightly incorrect
in always requesting a follow-up completion without regard for last
result of completion request (whether it was incomplete or not).
Specifically, we're falling into this branch in current form:
037c2b615b/packages/service/src/service/completion.ts (L121)
which then leads to incorrect edits being returned from vtsls.

Release Notes:

- Fixed an edge case with appliance of autocompletions in VTSLS that
could result in incorrect edits being applied.
2024-12-09 19:10:34 +01:00
Michael Sloan
7bd69130f8 Make space for documentation aside during followup completion select (#21716)
The goal of #7115 appears to be to limit the disruptiveness of
completion documentation load causing the completion selector to move
around. The approach was to debounce load of documentation via a setting
`completion_documentation_secondary_query_debounce`. This particularly
had a nonideal interaction with #21286, where now this debounce interval
was used between the documentation fetches of every individual
completion item.

I think a better solution is to continue making space for documentation
to be shown as soon as any documentation is shown. #21704 implemented
part of this, but it did not persist across followup completions.

Release Notes:

- Fixed completion list moving around on load of documentation. The
previous approach to mitigating this was to rate-limit the fetch of
docs, configured by a
`completion_documentation_secondary_query_debounce` setting, which is
now deprecated.
2024-12-09 10:47:14 -07:00
Alexandre Hamez
2af9fa7785 docs: Add missing ':' (#21751)
Release Notes:

- N/A
2024-12-09 12:22:19 -05:00
Michael Sloan
16ecbafa7a Skip spawning task for background_executor.timer(Duration::ZERO) (#21729)
Release Notes:

- N/A
2024-12-09 10:18:18 -07:00
Travis Stevens
e5f3a683f0 Fixing Missing comma (#21749)
Fix a missing comma in the docs

Release Notes:

- N/A
2024-12-09 18:49:40 +02:00
Marshall Bowers
8c91eecb67 call: Add test-support feature for livekit_client_macos (#21748)
This PR updates the `call` crate to include the `test-support` feature
for `livekit_client_macos` when `call` is used with `test-support`.

This fixes running `cargo test -p copilot` and `cargo test -p editor`
(and perhaps some other crates).

Release Notes:

- N/A
2024-12-09 11:21:02 -05:00
Thorsten Ball
8fcaf8b870 collab: Fix compilation error by removing dependency on livekit_client (#21744)
This fixes collab not being able to compile anymore for Linux:


https://github.com/zed-industries/zed/actions/runs/12236650046/job/34130962682

Release Notes:

- N/A

Co-authored-by: Antonio <antonio@zed.dev>
2024-12-09 15:14:46 +01:00
Antonio Scandurra
77b8296fbb Introduce staff-only inline completion provider (#21739)
Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Thorsten <thorsten@zed.dev>
2024-12-09 14:26:36 +01:00
Piotr Osiewicz
39e8944dcc language_tools: Split LSP log view selector into two (#21742)
This should make it easier to interact with LSP log view when there are
multiple language servers. I often find the current UI clunky when I
have over 5 servers running (which isn't uncommon with multiple projects
open)


https://github.com/user-attachments/assets/2ecaf17f-4b40-4c8f-aa6f-03b437a3d979


Closes #ISSUE

Release Notes:

- N/A
2024-12-09 14:10:11 +01:00
Danilo Leal
a7d12eea39 Enhance the Vim Mode toggle discoverability (#21589)
Closes https://github.com/zed-industries/zed/issues/21522

This PR adds an info tooltip on the Welcome screen, informing users how
Vim Mode can be toggled on and off. It also adds the Vim Mode toggle in
the Editor Controls menu. This is all so that folks who accidentally
turn it on better know how to turn it off. We're of course already able
to toggle this setting via the command palette, but that may be harder
to reach for beginners. So, maybe that's enough to close the linked
issue? Open to feedback.

(Note: I also added a max-width to the tooltip's label in this PR. I'm
confident that this won't make any tooltip look weird/broken, but if it
does, it may be because of this new property).

| Welcome Page | Editor Controls |
|--------|--------|
| <img width="800" alt="Screenshot 2024-12-05 at 11 20 04"
src="https://github.com/user-attachments/assets/1229f866-6be5-45cd-a6b8-c805f72144a6">
| <img width="800" alt="Screenshot 2024-12-05 at 11 12 15"
src="https://github.com/user-attachments/assets/f082d7f9-7d56-41d1-bc86-c333ad6264c7">
|

Release Notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-12-09 09:28:40 -03:00
Nils Koch
ce9e4629be Add go version to gopls cache key (#20922)
Closes #8071

Release Notes:

- Changed the Go integration to check whether an existing `gopls` was compiled for the current `go` version.

Previously we cached gopls (the go language server) as a file called
`gopls_{GOPLS_VERSION}`. The go version that gopls was built with is
crucial, so we need to cache the go version as well.

It's actually super interesting and very clever; gopls uses go to parse
the AST and do all the analyzation etc. Go exposes its internals in its
standard lib (`go/parser`, `go/types`, ...), which gopls uses to analyze
the user code. So if there is a new go release that contains new
syntax/features/etc. (the libraries `go/parser`, `go/types`, ...
change), we can rebuild the same version of `gopls` with the new version
of go (with the updated `go/xxx` libraries) to support the new language
features.

We had some issues around that (e.g., range over integers introduced in
go1.22, or custom iterators in go1.23) where we never updated gopls,
because we were on the latest gopls version, but built with an old go
version.

After this PR gopls will be cached under the name
`gopls_{GOPLS_VERSION}_go_{GO_VERSION}`.

Most users do not see this issue anymore, because after
https://github.com/zed-industries/zed/pull/8188 we first check if we can
find gopls in the PATH before downloading and caching gopls, but the
issue still exists.
2024-12-09 12:56:01 +01:00
Remco Smits
e58cdca044 Added JavaScript runnable detection for context and suite methods (#21719)
Fixes
https://github.com/zed-industries/zed/pull/21246#issuecomment-2525578141

<img width="545" alt="Screenshot 2024-12-08 at 22 58 33"
src="https://github.com/user-attachments/assets/2f303bfe-9718-4aa9-910e-613feca15ea8">
<img width="409" alt="Screenshot 2024-12-08 at 22 58 44"
src="https://github.com/user-attachments/assets/c4576cf7-fd71-44d2-911e-3ed944c9b794">

Release Notes:

- Added JavaScript runnable detection for `context` and `suite` methods
for mochajs framework
2024-12-09 13:17:51 +02:00
mgsloan@gmail.com
4564273322 Add comment explaining project panel behavior on right-click outside selection 2024-12-08 21:21:16 -07:00
João Marcos
55ee72d84a Simplify TextHighlights map (#21724)
Remove unnecessary `Option` wrapping.
2024-12-08 20:27:54 -07:00
tims
2ce01ead93 Fix right click selection behavior in project panel (#21707)
Closes #21605

Consider you have set of entries selected or even a single entry
selected, and you right click some other entry which is **not** part of
your selected set. This doesn't not clear existing entries selection
(which it should clear, as how file manager right-click logic works, see
more below).

This issue might lead unexpected operation like deletion applied on
those existing selected entries. This PR fixes it.

Release Notes:

- Fix right click selection behavior in project panel
2024-12-08 19:13:12 -07:00
Hendrik
bf1525588d Add .jj to default file exclusion (#21708)
Relates to #21538

Release Notes:

- Added `**/.jj` to the default file exclusion list.
2024-12-08 18:44:46 -07:00
Michael Sloan
d0e99f6496 Bump x11rb version to v0.13.1 (#21723)
From diff looks like no material differences. With a local checkout of
`v0.13.0` I get build errors due to warning checking when I use a `path
= ...` dependency, but it is fixed with `v0.13.1`.

I see mention of this in the [renovate configuration
PR](https://github.com/zed-industries/zed/pull/15132) but doesn't seem
like that initial batch of renovation happened.

Release Notes:

- N/A
2024-12-08 18:42:44 -07:00
Cole Miller
ac07b9197a gpui: Don't panic on failing to set X11 cursor style (#21689)
One more panic (well, two) that should be a `log_err`.

Release Notes:

- N/A
2024-12-08 13:30:23 -05:00
Michael Sloan
4b93a5ca44 Make completions selector continue to show docs aside if ever shown (#21704)
In #21286, documentation fetch was made more efficient by only
fetching the current completion. This has a side effect of causing the
aside to disappear and reappear when navigating the list. This is
particularly jarring when there isn't enough space for the aside,
causing the completions list to jump to the left.

The solution here is to continue to show the aside even if the current
selection does not yet have docs fetched.

Release Notes:

- N/A
2024-12-08 09:44:48 -07:00
223 changed files with 21114 additions and 8221 deletions

View File

@@ -26,8 +26,8 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
description: Drag images / videos into the text input below
validations:
required: false
- type: textarea

View File

@@ -1,3 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Language Request

1353
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -141,6 +141,8 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -226,6 +228,7 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
@@ -325,6 +328,7 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
#
# External crates
@@ -358,7 +362,6 @@ cargo_metadata = "0.19"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"

4
assets/icons/eraser.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
<path d="M22 21H7"/><path d="m5 11 9 9"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

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-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -59,6 +59,11 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"TAG_EDITMSG": "vcs",
"MERGE_MSG": "vcs",
"COMMIT_EDITMSG": "vcs",
"NOTES_EDITMSG": "vcs",
"EDIT_DESCRIPTION": "vcs",
"gleam": "gleam",
"go": "go",
"gql": "graphql",
@@ -108,6 +113,7 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"metal": "metal",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
@@ -317,6 +323,9 @@
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"metal": {
"icon": "icons/file_icons/metal.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 269 B

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-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

After

Width:  |  Height:  |  Size: 348 B

12
assets/icons/info.svg Normal file
View File

@@ -0,0 +1,12 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 479 B

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-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

After

Width:  |  Height:  |  Size: 289 B

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-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

After

Width:  |  Height:  |  Size: 291 B

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-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>

After

Width:  |  Height:  |  Size: 301 B

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-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>

After

Width:  |  Height:  |  Size: 291 B

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-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

After

Width:  |  Height:  |  Size: 309 B

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-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 405 B

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-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -468,13 +468,21 @@
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}

View File

@@ -66,6 +66,7 @@
"cmd-v": "editor::Paste",
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp",
@@ -229,7 +230,7 @@
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant2::Chat"
"enter": "assistant2::Chat"
}
},
{
@@ -540,12 +541,18 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
@@ -788,5 +795,24 @@
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
}
}
]

View File

@@ -15,8 +15,10 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -53,6 +55,14 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -15,8 +15,10 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -53,6 +55,14 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -144,15 +144,15 @@
// 4. Highlight the full line (default):
// "all"
"current_line_highlight": "all",
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
@@ -474,6 +474,14 @@
// Default width of the chat panel.
"default_width": 240
},
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
// For example: typing `:wave:` gets replaced with `👋`.
@@ -564,9 +572,11 @@
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
// "history"
// 2. Activate the right neighbour tab if present
// "neighbour"
// 3. Activate the left neighbour tab if present
// "left_neighbour"
"activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// Diagnostics are only shown when file icons are also active.
@@ -685,6 +695,7 @@
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",

View File

@@ -55,7 +55,7 @@ use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::{LanguageModelPickerDelegate, LanguageModelSelector};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::lsp_store::LocalLspAdapterDelegate;
@@ -143,7 +143,7 @@ pub struct AssistantPanel {
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
@@ -341,11 +341,12 @@ impl AssistantPanel {
) -> Self {
let model_selector_menu_handle = PopoverMenuHandle::default();
let model_summary_editor = cx.new_view(Editor::single_line);
let context_editor_toolbar = cx.new_view(|_| {
let context_editor_toolbar = cx.new_view(|cx| {
ContextEditorToolbarItem::new(
workspace,
model_selector_menu_handle.clone(),
model_summary_editor.clone(),
cx,
)
});
@@ -4455,23 +4456,36 @@ impl FollowableItem for ContextEditor {
}
pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
impl ContextEditorToolbarItem {
pub fn new(
workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_summary_editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
fs: workspace.app_state().fs.clone(),
active_context_editor: None,
model_summary_editor,
model_selector_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = workspace.app_state().fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
language_model_selector_menu_handle: model_selector_menu_handle,
}
}
@@ -4560,17 +4574,8 @@ impl Render for ContextEditorToolbarItem {
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.child(
LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
@@ -4616,7 +4621,7 @@ impl Render for ContextEditorToolbarItem {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.with_handle(self.model_selector_menu_handle.clone()),
.with_handle(self.language_model_selector_menu_handle.clone()),
)
.children(self.render_remaining_tokens(cx));
@@ -5113,9 +5118,11 @@ fn make_lsp_adapter_delegate(
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
};
let http_client = project.client().http_client().clone();
project.lsp_store().update(cx, |lsp_store, cx| {
project.lsp_store().update(cx, |_, cx| {
Ok(Some(LocalLspAdapterDelegate::new(
lsp_store,
project.languages().clone(),
project.environment(),
cx.weak_model(),
&worktree,
http_client,
project.fs().clone(),

View File

@@ -17,7 +17,7 @@ use futures::{
channel::mpsc,
stream::{self, StreamExt},
};
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
@@ -35,7 +35,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{Context as _, IconName, WindowContext};
use ui::{IconName, WindowContext};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},

View File

@@ -33,7 +33,7 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_model_selector::LanguageModelSelector;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
@@ -1358,8 +1358,8 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
fs: Arc<dyn Fs>,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
@@ -1500,43 +1500,27 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(
LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
)
.info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",
),
)
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
@@ -1642,6 +1626,19 @@ impl PromptEditor {
let mut this = Self {
id,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
gutter_dimensions,
prompt_history,
@@ -1650,7 +1647,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_counts: None,
_token_count_subscriptions: token_count_subscriptions,

View File

@@ -11,8 +11,8 @@ use futures::{
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
types::{SerdeBincode, SerdeJson, Str},
@@ -928,10 +928,8 @@ impl PromptLibrary {
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
inline_completion_styles:
editor::make_suggestion_styles(cx),
..EditorStyle::default()
},
)),

View File

@@ -322,14 +322,13 @@ impl CompletionProvider for SlashCommandCompletionProvider {
}
}
fn resolve_completions(
fn resolve_completion(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: Completion,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
) -> Task<Result<Option<Completion>>> {
Task::ready(Ok(None))
}
fn apply_additional_edits_for_completion(

View File

@@ -20,7 +20,7 @@ use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::LanguageModelSelector;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use settings::{update_settings_file, Settings};
use std::{
@@ -476,9 +476,9 @@ enum PromptEditorEvent {
struct PromptEditor {
id: TerminalInlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -614,17 +614,8 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -718,6 +709,19 @@ impl PromptEditor {
id,
height_in_lines: 1,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
@@ -725,7 +729,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_count: None,
_token_count_subscriptions: token_count_subscriptions,

View File

@@ -13,30 +13,53 @@ path = "src/assistant.rs"
doctest = false
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
async-watch.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
paths.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
telemetry_events.workspace = true
terminal_view.workspace = true
text.workspace = true
terminal.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
@@ -45,3 +68,8 @@ unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
rand.workspace = true
indoc.workspace = true

View File

@@ -15,6 +15,7 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakView<Workspace>,
@@ -202,6 +203,8 @@ impl ActiveThread {
return Empty.into_any();
};
let context = self.thread.read(cx).context_for_message(message_id);
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
@@ -229,7 +232,16 @@ impl ActiveThread {
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone())),
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
}),
)
.into_any()
}

View File

@@ -1,14 +1,29 @@
mod active_thread;
mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod ui;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, AppContext};
use prompts::PromptLoadingParams;
use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
@@ -19,15 +34,43 @@ actions!(
NewThread,
ToggleModelSelector,
OpenHistory,
Chat
Chat,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(cx: &mut AppContext) {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
AssistantSettings::register(cx);
assistant_panel::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}

View File

@@ -9,10 +9,8 @@ use gpui::{
WindowContext,
};
use language::LanguageRegistry;
use language_model::LanguageModelRegistry;
use language_model_selector::LanguageModelSelector;
use time::UtcOffset;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
@@ -21,7 +19,7 @@ use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
@@ -90,13 +88,13 @@ impl AssistantPanel {
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace,
workspace.clone(),
language_registry,
tools.clone(),
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread.clone(), cx)),
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
@@ -125,7 +123,8 @@ impl AssistantPanel {
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -147,7 +146,8 @@ impl AssistantPanel {
cx,
)
});
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread, cx));
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -225,7 +225,6 @@ impl AssistantPanel {
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(self.render_language_model_selector(cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
@@ -280,57 +279,6 @@ impl AssistantPanel {
)
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
@@ -358,46 +306,6 @@ impl AssistantPanel {
.mb_4(),
),
)
.child(v_flex())
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Terminal)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("Terminal").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Folder)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.when(!recent_threads.is_empty(), |parent| {
parent
.child(

View File

@@ -0,0 +1,485 @@
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use gpui::Pixels;
use language_model::{CloudModel, LanguageModel};
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
},
}
#[derive(Debug, Default)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: LanguageModelSelection,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContent {
Versioned(VersionedAssistantSettingsContent),
Legacy(LegacyAssistantSettingsContent),
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(gen)
}
fn is_referenceable() -> bool {
VersionedAssistantSettingsContent::is_referenceable()
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(VersionedAssistantSettingsContent::default())
}
}
impl AssistantSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(_) => true,
VersionedAssistantSettingsContent::V2(_) => false,
},
AssistantSettingsContent::Legacy(_) => true,
}
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_width,
default_model: settings
.provider
.clone()
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "ollama".to_string(),
model: model.id().to_string(),
})
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".to_string(),
model: settings
.default_open_ai_model
.clone()
.unwrap_or_default()
.id()
.to_string(),
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
}
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
"zed.dev" => {
log::warn!("attempted to set zed.dev model on outdated settings");
}
"anthropic" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Anthropic {
default_model: AnthropicModel::from_id(&model).ok(),
api_url,
});
}
"ollama" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(&model, None, None)),
api_url,
});
}
"openai" => {
let (api_url, available_models) = match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
api_url,
available_models,
..
}) => (api_url.clone(), available_models.clone()),
_ => (None, None),
};
settings.provider = Some(AssistantProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
api_url,
available_models,
});
}
_ => {}
},
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
AssistantSettingsContent::Legacy(settings) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
#[serde(rename = "2")]
V2(AssistantSettingsContentV2),
}
impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
default_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new chats.
default_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// Enable experimental live diffs in the assistant panel.
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
}
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"google".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: "openai".to_string(),
model: "gpt-4".to_string(),
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
pub dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new chats.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new chats.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();
for value in sources.defaults_and_customizations() {
if value.is_version_outdated() {
settings.using_outdated_settings_version = true;
}
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
&mut settings.default_width,
value.default_width.map(Into::into),
);
merge(
&mut settings.default_height,
value.default_height.map(Into::into),
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
}
Ok(settings)
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
#[cfg(test)]
mod tests {
use fs::Fs;
use gpui::{ReadGlobal, TestAppContext};
use super::*;
#[gpui::test]
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
cx.update(|cx| {
let test_settings = settings::SettingsStore::test(cx);
cx.set_global(test_settings);
AssistantSettings::register(cx);
});
cx.update(|cx| {
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
}
);
});
cx.update(|cx| {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent::Versioned(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
}),
)
},
);
});
cx.run_until_parked();
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
assert!(raw_settings_value.contains(r#""version": "2""#));
#[derive(Debug, Deserialize)]
struct AssistantSettingsTest {
assistant: AssistantSettingsContent,
}
let assistant_settings: AssistantSettingsTest =
serde_json_lenient::from_str(&raw_settings_value).unwrap();
assert!(!assistant_settings.assistant.is_version_outdated());
}
}

View File

@@ -0,0 +1,26 @@
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use util::post_inc;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub id: ContextId,
pub name: SharedString,
pub kind: ContextKind,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
}

View File

@@ -1,15 +1,93 @@
mod file_context_picker;
use std::sync::Arc;
use gpui::{DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
#[derive(IntoElement)]
pub(super) struct ContextPicker<T: PopoverTrigger> {
message_editor: WeakView<MessageEditor>,
trigger: T,
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerMode,
picker: View<Picker<ContextPickerDelegate>>,
}
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
workspace: workspace.clone(),
message_editor: message_editor.clone(),
entries: vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
],
selected_ix: 0,
};
let picker = cx.new_view(|cx| {
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
});
ContextPicker {
mode: ContextPickerMode::Default,
picker,
}
}
pub fn reset_mode(&mut self) {
self.mode = ContextPickerMode::Default;
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().min_w(px(400.)).map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
})
}
}
#[derive(Clone)]
@@ -20,26 +98,18 @@ struct ContextPickerEntry {
}
pub(crate) struct ContextPickerDelegate {
all_entries: Vec<ContextPickerEntry>,
filtered_entries: Vec<ContextPickerEntry>,
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
impl<T: PopoverTrigger> ContextPicker<T> {
pub(crate) fn new(message_editor: WeakView<MessageEditor>, trigger: T) -> Self {
ContextPicker {
message_editor,
trigger,
}
}
}
impl PickerDelegate for ContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.filtered_entries.len()
self.entries.len()
}
fn selected_index(&self) -> usize {
@@ -47,7 +117,7 @@ impl PickerDelegate for ContextPickerDelegate {
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix.min(self.filtered_entries.len().saturating_sub(1));
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
cx.notify();
}
@@ -55,52 +125,41 @@ impl PickerDelegate for ContextPickerDelegate {
"Select a context source…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_commands = self.all_entries.clone();
cx.spawn(|this, mut cx| async move {
let filtered_commands = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
all_commands
} else {
all_commands
.into_iter()
.filter(|model_info| {
model_info
.name
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect()
}
})
.await;
this.update(&mut cx, |this, cx| {
this.delegate.filtered_entries = filtered_commands;
this.delegate.set_selected_index(0, cx);
cx.notify();
})
.ok();
})
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(entry) = self.filtered_entries.get(self.selected_ix) {
self.message_editor
.update(cx, |_message_editor, _cx| {
println!("Insert context from {}", entry.name);
if let Some(entry) = self.entries.get(self.selected_ix) {
self.context_picker
.update(cx, |this, cx| {
match entry.name.to_string().as_str() {
"file" => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
_ => {}
}
cx.focus_self();
})
.ok();
cx.emit(DismissEvent);
.log_err();
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn editor_position(&self) -> PickerEditorPosition {
PickerEditorPosition::End
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_) => {}
})
.log_err();
}
fn render_match(
@@ -109,7 +168,7 @@ impl PickerDelegate for ContextPickerDelegate {
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = self.filtered_entries.get(ix)?;
let entry = &self.entries[ix];
Some(
ListItem::new(ix)
@@ -148,50 +207,3 @@ impl PickerDelegate for ContextPickerDelegate {
)
}
}
impl<T: PopoverTrigger> RenderOnce for ContextPicker<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let entries = vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
];
let delegate = ContextPickerDelegate {
all_entries: entries.clone(),
message_editor: self.message_editor.clone(),
filtered_entries: entries,
selected_ix: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
let handle = self
.message_editor
.update(cx, |this, _| this.context_picker_handle.clone())
.ok();
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(picker.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.when_some(handle, |this, handle| this.with_handle(handle))
}
}

View File

@@ -0,0 +1,289 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FileContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
Self {
context_picker,
workspace,
message_editor,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.map(|entries| entries.0)
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree
.child_entries(Path::new(""))
.filter(|entry| entry.kind.is_file())
.map(move |entry| project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|entry| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Files,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search files…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(|this, mut cx| async move {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let mat = &self.matches[self.selected_index];
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path.clone()), cx)
})
.ok()
else {
return anyhow::Ok(());
};
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
message_editor.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(mat.path.to_string_lossy().to_string()),
)
}
}
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
} else {
write!(text, "untitled").unwrap();
}
if let Some(row_range) = row_range {
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
}
text.push('\n');
text
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,79 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use picker::Picker;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenuHandle,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::thread::{RequestKind, Thread};
use crate::Chat;
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
pub fn new(
workspace: WeakView<Workspace>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything", cx);
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
editor
}),
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
language_model_selector: cx.new_view(|cx| {
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
cx,
)
}),
use_tools: false,
}
}
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
}
@@ -61,9 +100,10 @@ impl MessageEditor {
editor.clear(cx);
text
});
let context = self.context.drain(..).collect::<Vec<_>>();
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, cx);
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
@@ -84,6 +124,55 @@ impl MessageEditor {
None
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
}
impl FocusableView for MessageEditor {
@@ -97,6 +186,7 @@ impl Render for MessageEditor {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx);
let context_picker = self.context_picker.clone();
v_flex()
.key_context("MessageEditor")
@@ -106,12 +196,46 @@ impl Render for MessageEditor {
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex().gap_2().child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)),
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({
let settings = ThemeSettings::get_global(cx);
@@ -152,13 +276,12 @@ impl Render for MessageEditor {
.child(
h_flex()
.gap_2()
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
.child(Label::new("or"))
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),

View File

@@ -0,0 +1,312 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::LineEnding;
use util::ResultExt;
#[derive(Serialize)]
pub struct ContentPromptDiagnosticContext {
pub line_number: usize,
pub error_message: String,
pub code_content: String,
}
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
pub language_name: Option<String>,
pub is_insert: bool,
pub is_truncated: bool,
pub document_content: String,
pub user_prompt: String,
pub rewrite_section: Option<String>,
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
}
#[derive(Serialize)]
pub struct TerminalAssistantPromptContext {
pub os: String,
pub arch: String,
pub shell: Option<String>,
pub working_directory: Option<String>,
pub latest_output: Vec<String>,
pub user_prompt: String,
}
#[derive(Serialize)]
pub struct ProjectSlashCommandPromptContext {
pub context_buffer: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
impl PromptBuilder {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
}
Ok(Self { handlebars })
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
params: PromptLoadingParams,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
.spawn(async move {
let Some(parent_dir) = templates_dir.parent() else {
return;
};
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
}
found_dir_once = true;
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::debug!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for event in changed_paths {
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", event.path.display());
if let Some(content) = params.fs.load(&event.path).await.log_err() {
let file_name = event.path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
})
.detach();
}
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::debug!("Registering built-in prompt template: {}", id);
let prompt = String::from_utf8_lossy(prompt.as_ref());
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
}
}
}
Ok(())
}
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,
language_name: Option<&LanguageName>,
buffer: BufferSnapshot,
range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
None | Some("Markdown" | "Plain Text") => "text",
Some(_) => "code",
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
start..range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
range.end..end
} else {
after_range
};
let mut document_content = String::new();
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
}
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
}
Some(section)
} else {
None
};
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
.map(|entry| {
let start = entry.range.start;
ContentPromptDiagnosticContext {
line_number: (start.row + 1) as usize,
error_message: entry.diagnostic.message.clone(),
code_content: buffer.text_for_range(entry.range.clone()).collect(),
}
})
.collect();
let context = ContentPromptContext {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_insert,
is_truncated,
document_content,
user_prompt,
rewrite_section,
diagnostic_errors,
};
self.handlebars.lock().render("content_prompt", &context)
}
pub fn generate_terminal_assistant_prompt(
&self,
user_prompt: &str,
shell: Option<&str>,
working_directory: Option<&str>,
latest_output: &[String],
) -> Result<String, RenderError> {
let context = TerminalAssistantPromptContext {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
shell: shell.map(|s| s.to_string()),
working_directory: working_directory.map(|s| s.to_string()),
latest_output: latest_output.to_vec(),
user_prompt: user_prompt.to_string(),
};
self.handlebars
.lock()
.render("terminal_assistant_prompt", &context)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,8 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{Context, ContextKind};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
@@ -62,6 +64,7 @@ pub struct Thread {
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context_by_message: HashMap<MessageId, Vec<Context>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
@@ -79,6 +82,7 @@ impl Thread {
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
tools,
@@ -125,12 +129,22 @@ impl Thread {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
self.context_by_message.get(&id)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(&mut self, text: impl Into<String>, cx: &mut ModelContext<Self>) {
self.insert_message(Role::User, text, cx)
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);
self.context_by_message.insert(message_id, context);
}
pub fn insert_message(
@@ -138,7 +152,7 @@ impl Thread {
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
) {
) -> MessageId {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
@@ -147,6 +161,7 @@ impl Thread {
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
id
}
pub fn to_completion_request(
@@ -176,6 +191,29 @@ impl Thread {
}
}
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push_str("\n");
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
context_text.push_str(&file_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
}
if !message.text.is_empty() {
request_message
.content

View File

@@ -159,9 +159,9 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
thread
}));
@@ -169,7 +169,7 @@ impl ThreadStore {
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
```rust
@@ -206,7 +206,7 @@ impl ThreadStore {
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:

View File

@@ -0,0 +1,3 @@
mod context_pill;
pub use context_pill::*;

View File

@@ -0,0 +1,49 @@
use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
}
impl ContextPill {
pub fn new(context: Context) -> Self {
Self {
context,
on_remove: None,
}
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap_1()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new("remove", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
})
}
}

View File

@@ -18,6 +18,7 @@ test-support = [
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"project/test-support",
"util/test-support"
]

View File

@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: true
/// Default: false
pub share_on_join: Option<bool>,
}

View File

@@ -1288,6 +1288,12 @@ impl Room {
})
}
pub fn muted_by_user(&self) -> bool {
self.live_kit
.as_ref()
.map_or(false, |live_kit| live_kit.muted_by_user)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()

View File

@@ -1307,6 +1307,12 @@ impl Room {
})
}
pub fn muted_by_user(&self) -> bool {
self.live_kit
.as_ref()
.map_or(false, |live_kit| live_kit.muted_by_user)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()

View File

@@ -18,7 +18,8 @@ use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -355,6 +356,24 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_inline_completion_rating_event(
self: &Arc<Self>,
rating: InlineCompletionRating,
input_events: Arc<str>,
input_excerpt: Arc<str>,
output_excerpt: Arc<str>,
feedback: String,
) {
let event = Event::InlineCompletionRating(InlineCompletionRatingEvent {
rating,
input_events,
input_excerpt,
output_excerpt,
feedback,
});
self.report_event(event);
}
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
self.report_event(Event::Assistant(event));
}

View File

@@ -19,11 +19,6 @@ LLM_DATABASE_URL = "postgres://postgres@localhost/zed_llm"
LLM_DATABASE_MAX_CONNECTIONS = 5
LLM_API_SECRET = "llm-secret"
# CLICKHOUSE_URL = ""
# CLICKHOUSE_USER = "default"
# CLICKHOUSE_PASSWORD = ""
# CLICKHOUSE_DATABASE = "default"
# SLACK_PANICS_WEBHOOK = ""
# RUST_LOG=info

View File

@@ -29,7 +29,6 @@ axum = { version = "0.6", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.4", features = ["erased-json"] }
base64.workspace = true
chrono.workspace = true
clickhouse.workspace = true
clock.workspace = true
collections.workspace = true
dashmap.workspace = true
@@ -77,12 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true, features = ["test-support"] }
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_tool.workspace = true

View File

@@ -149,6 +149,21 @@ spec:
secretKeyRef:
name: google-ai
key: api_key
- name: PREDICTION_API_URL
valueFrom:
secretKeyRef:
name: prediction
key: api_url
- name: PREDICTION_API_KEY
valueFrom:
secretKeyRef:
name: prediction
key: api_key
- name: PREDICTION_MODEL
valueFrom:
secretKeyRef:
name: prediction
key: model
- name: BLOB_STORE_ACCESS_KEY
valueFrom:
secretKeyRef:
@@ -199,26 +214,6 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: CLICKHOUSE_URL
valueFrom:
secretKeyRef:
name: clickhouse
key: url
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse
key: user
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse
key: password
- name: CLICKHOUSE_DATABASE
valueFrom:
secretKeyRef:
name: clickhouse
key: database
- name: SLACK_PANICS_WEBHOOK
valueFrom:
secretKeyRef:

View File

@@ -102,6 +102,9 @@ async fn update_billing_preferences(
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let max_monthly_llm_usage_spending_in_cents =
body.max_monthly_llm_usage_spending_in_cents.max(0);
let billing_preferences =
if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? {
app.db
@@ -109,7 +112,7 @@ async fn update_billing_preferences(
user.id,
&UpdateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: ActiveValue::set(
body.max_monthly_llm_usage_spending_in_cents,
max_monthly_llm_usage_spending_in_cents,
),
},
)
@@ -119,8 +122,7 @@ async fn update_billing_preferences(
.create_billing_preferences(
user.id,
&crate::db::CreateBillingPreferencesParams {
max_monthly_llm_usage_spending_in_cents: body
.max_monthly_llm_usage_spending_in_cents,
max_monthly_llm_usage_spending_in_cents,
},
)
.await?
@@ -128,7 +130,7 @@ async fn update_billing_preferences(
SnowflakeRow::new(
"Spend Limit Updated",
Some(user.metrics_id),
user.metrics_id,
user.admin,
None,
json!({

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
use serde::Serialize;
/// Writes the given rows to the specified Clickhouse table.
pub async fn write_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if rows.is_empty() {
return Ok(());
}
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
let event_count = rows.len();
log::info!(
"wrote {event_count} {event_specifier} to '{table}'",
event_specifier = if event_count == 1 { "event" } else { "events" }
);
Ok(())
}

View File

@@ -1,7 +1,6 @@
pub mod api;
pub mod auth;
mod cents;
pub mod clickhouse;
pub mod db;
pub mod env;
pub mod executor;
@@ -151,10 +150,6 @@ pub struct Config {
pub seed_path: Option<PathBuf>,
pub database_max_connections: u32,
pub api_token: String,
pub clickhouse_url: Option<String>,
pub clickhouse_user: Option<String>,
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub livekit_server: Option<String>,
pub livekit_key: Option<String>,
@@ -180,6 +175,9 @@ pub struct Config {
pub anthropic_api_key: Option<Arc<str>>,
pub anthropic_staff_api_key: Option<Arc<str>>,
pub llm_closed_beta_model_name: Option<Arc<str>>,
pub prediction_api_url: Option<Arc<str>>,
pub prediction_api_key: Option<Arc<str>>,
pub prediction_model: Option<Arc<str>>,
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
@@ -230,10 +228,9 @@ impl Config {
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,
@@ -283,7 +280,6 @@ pub struct AppState {
pub stripe_billing: Option<Arc<StripeBilling>>,
pub rate_limiter: Arc<RateLimiter>,
pub executor: Executor,
pub clickhouse_client: Option<::clickhouse::Client>,
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
pub config: Config,
}
@@ -337,10 +333,6 @@ impl AppState {
stripe_client,
rate_limiter: Arc::new(RateLimiter::new(db)),
executor,
clickhouse_client: config
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
} else {
@@ -423,31 +415,3 @@ async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
Ok(::clickhouse::Client::default()
.with_url(
config
.clickhouse_url
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_url"))?,
)
.with_user(
config
.clickhouse_user
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_user"))?,
)
.with_password(
config
.clickhouse_password
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_password"))?,
)
.with_database(
config
.clickhouse_database
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_database"))?,
))
}

View File

@@ -1,14 +1,11 @@
mod authorization;
pub mod db;
mod telemetry;
mod token;
use crate::api::events::SnowflakeRow;
use crate::api::CloudflareIpCountryHeader;
use crate::build_kinesis_client;
use crate::{
build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result,
};
use crate::{db::UserId, executor::Executor, Cents, Config, Error, Result};
use anyhow::{anyhow, Context as _};
use authorization::authorize_access_to_language_model;
use axum::routing::get;
@@ -29,7 +26,10 @@ use reqwest_client::ReqwestClient;
use rpc::{
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
};
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
use rpc::{
ListModelsResponse, PredictEditsParams, PredictEditsResponse,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
};
use serde_json::json;
use std::{
pin::Pin,
@@ -37,7 +37,6 @@ use std::{
task::{Context, Poll},
};
use strum::IntoEnumIterator;
use telemetry::{report_llm_rate_limit, report_llm_usage, LlmRateLimitEventRow, LlmUsageEventRow};
use tokio::sync::RwLock;
use util::ResultExt;
@@ -49,7 +48,6 @@ pub struct LlmState {
pub db: Arc<LlmDatabase>,
pub http_client: ReqwestClient,
pub kinesis_client: Option<aws_sdk_kinesis::Client>,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
}
@@ -86,10 +84,6 @@ impl LlmState {
} else {
None
},
clickhouse_client: config
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
active_user_count_by_model: RwLock::new(HashMap::default()),
config,
};
@@ -126,6 +120,7 @@ pub fn routes() -> Router<(), Body> {
Router::new()
.route("/models", get(list_models))
.route("/completion", post(perform_completion))
.route("/predict_edits", post(predict_edits))
.layer(middleware::from_fn(validate_api_token))
}
@@ -439,6 +434,59 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
}
}
async fn predict_edits(
Extension(state): Extension<Arc<LlmState>>,
Extension(claims): Extension<LlmTokenClaims>,
_country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
Json(params): Json<PredictEditsParams>,
) -> Result<impl IntoResponse> {
if !claims.is_staff {
return Err(anyhow!("not found"))?;
}
let api_url = state
.config
.prediction_api_url
.as_ref()
.context("no PREDICTION_API_URL configured on the server")?;
let api_key = state
.config
.prediction_api_key
.as_ref()
.context("no PREDICTION_API_KEY configured on the server")?;
let model = state
.config
.prediction_model
.as_ref()
.context("no PREDICTION_MODEL configured on the server")?;
let prompt = include_str!("./llm/prediction_prompt.md")
.replace("<events>", &params.input_events)
.replace("<excerpt>", &params.input_excerpt);
let mut response = open_ai::complete_text(
&state.http_client,
api_url,
api_key,
open_ai::CompletionRequest {
model: model.to_string(),
prompt: prompt.clone(),
max_tokens: 1024,
temperature: 0.,
prediction: Some(open_ai::Prediction::Content {
content: params.input_excerpt,
}),
rewrite_speculation: Some(true),
},
)
.await?;
let choice = response
.choices
.pop()
.context("no output from completion response")?;
Ok(Json(PredictEditsResponse {
output_excerpt: choice.text,
}))
}
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
@@ -573,34 +621,6 @@ async fn check_usage_limit(
.await
.log_err();
if let Some(client) = state.clickhouse_client.as_ref() {
report_llm_rate_limit(
client,
LlmRateLimitEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model: model.name.clone(),
provider: provider.to_string(),
usage_measure: resource.to_string(),
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
users_in_recent_minutes: users_in_recent_minutes as u64,
users_in_recent_days: users_in_recent_days as u64,
max_requests_per_minute: per_user_max_requests_per_minute as u64,
max_tokens_per_minute: per_user_max_tokens_per_minute as u64,
max_tokens_per_day: per_user_max_tokens_per_day as u64,
},
)
.await
.log_err();
}
return Err(Error::http(
StatusCode::TOO_MANY_REQUESTS,
format!("Rate limit exceeded. Maximum {} reached.", resource),
@@ -687,6 +707,8 @@ impl<S> Drop for TokenCountingStream<S> {
);
let properties = json!({
"has_llm_subscription": claims.has_llm_subscription,
"max_monthly_spend_in_cents": claims.max_monthly_spend_in_cents,
"plan": match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
@@ -706,44 +728,6 @@ impl<S> Drop for TokenCountingStream<S> {
.write(&state.kinesis_client, &state.config.kinesis_stream)
.await
.log_err();
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
report_llm_usage(
clickhouse_client,
LlmUsageEventRow {
time: Utc::now().timestamp_millis(),
user_id: claims.user_id as i32,
is_staff: claims.is_staff,
plan: match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
model,
provider: provider.to_string(),
input_token_count: tokens.input as u64,
cache_creation_input_token_count: tokens.input_cache_creation as u64,
cache_read_input_token_count: tokens.input_cache_read as u64,
output_token_count: tokens.output as u64,
requests_this_minute: usage.requests_this_minute as u64,
tokens_this_minute: usage.tokens_this_minute as u64,
tokens_this_day: usage.tokens_this_day as u64,
input_tokens_this_month: usage.tokens_this_month.input as u64,
cache_creation_input_tokens_this_month: usage
.tokens_this_month
.input_cache_creation
as u64,
cache_read_input_tokens_this_month: usage
.tokens_this_month
.input_cache_read
as u64,
output_tokens_this_month: usage.tokens_this_month.output as u64,
spending_this_month: usage.spending_this_month.0 as u64,
lifetime_spending: usage.lifetime_spending.0 as u64,
},
)
.await
.log_err();
}
}
})
}

View File

@@ -0,0 +1,12 @@
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
### Events:
<events>
### Input:
<excerpt>
### Response:

View File

@@ -1,65 +0,0 @@
use anyhow::{Context, Result};
use serde::Serialize;
use crate::clickhouse::write_to_table;
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct LlmUsageEventRow {
pub time: i64,
pub user_id: i32,
pub is_staff: bool,
pub plan: String,
pub model: String,
pub provider: String,
pub input_token_count: u64,
pub cache_creation_input_token_count: u64,
pub cache_read_input_token_count: u64,
pub output_token_count: u64,
pub requests_this_minute: u64,
pub tokens_this_minute: u64,
pub tokens_this_day: u64,
pub input_tokens_this_month: u64,
pub cache_creation_input_tokens_this_month: u64,
pub cache_read_input_tokens_this_month: u64,
pub output_tokens_this_month: u64,
pub spending_this_month: u64,
pub lifetime_spending: u64,
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct LlmRateLimitEventRow {
pub time: i64,
pub user_id: i32,
pub is_staff: bool,
pub plan: String,
pub model: String,
pub provider: String,
pub usage_measure: String,
pub requests_this_minute: u64,
pub tokens_this_minute: u64,
pub tokens_this_day: u64,
pub users_in_recent_minutes: u64,
pub users_in_recent_days: u64,
pub max_requests_per_minute: u64,
pub max_tokens_per_minute: u64,
pub max_tokens_per_day: u64,
}
pub async fn report_llm_usage(client: &clickhouse::Client, row: LlmUsageEventRow) -> Result<()> {
const LLM_USAGE_EVENTS_TABLE: &str = "llm_usage_events";
write_to_table(LLM_USAGE_EVENTS_TABLE, &[row], client)
.await
.with_context(|| format!("failed to upload to table '{LLM_USAGE_EVENTS_TABLE}'"))?;
Ok(())
}
pub async fn report_llm_rate_limit(
client: &clickhouse::Client,
row: LlmRateLimitEventRow,
) -> Result<()> {
const LLM_RATE_LIMIT_EVENTS_TABLE: &str = "llm_rate_limit_events";
write_to_table(LLM_RATE_LIMIT_EVENTS_TABLE, &[row], client)
.await
.with_context(|| format!("failed to upload to table '{LLM_RATE_LIMIT_EVENTS_TABLE}'"))?;
Ok(())
}

View File

@@ -17,10 +17,8 @@ pub struct LlmTokenClaims {
pub exp: u64,
pub jti: String,
pub user_id: u64,
#[serde(default)]
pub system_id: Option<String>,
#[serde(default)]
pub metrics_id: Option<Uuid>,
pub metrics_id: Uuid,
pub github_user_login: String,
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
@@ -56,7 +54,7 @@ impl LlmTokenClaims {
jti: uuid::Uuid::new_v4().to_string(),
user_id: user.id.to_proto(),
system_id,
metrics_id: Some(user.metrics_id),
metrics_id: user.metrics_id,
github_user_login: user.github_login.clone(),
is_staff,
has_llm_closed_beta_feature_flag,

View File

@@ -310,6 +310,9 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
.add_request_handler(forward_read_only_project_request::<proto::GetStagedText>)
.add_request_handler(
forward_mutating_project_request::<proto::RegisterBufferWithLanguageServers>,
)
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
.add_request_handler(

View File

@@ -994,10 +994,12 @@ async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut Tes
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
let _buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.update(cx_a, |p, cx| {
p.open_local_buffer_with_lsp("/dir/main.rs", cx)
})
.await
.unwrap();
@@ -1587,7 +1589,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
})
.await
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
let editor_a = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -1597,6 +1598,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.downcast::<Editor>()
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
// Set up the language server to return an additional inlay hint on each request.
let edits_made = Arc::new(AtomicUsize::new(0));
let closure_edits_made = Arc::clone(&edits_made);

View File

@@ -3891,13 +3891,7 @@ async fn test_collaborating_with_diagnostics(
// Cause the language server to start.
let _buffer = project_a
.update(cx_a, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new("other.rs").into(),
},
cx,
)
project.open_local_buffer_with_lsp("/a/other.rs", cx)
})
.await
.unwrap();
@@ -4176,7 +4170,9 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
// Join the project as client B and open all three files.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let guest_buffers = futures::future::try_join_all(file_names.iter().map(|file_name| {
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, file_name), cx))
project_b.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, file_name), cx)
})
}))
.await
.unwrap();
@@ -4230,7 +4226,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
cx.subscribe(&project_b, move |_, _, event, cx| {
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
disk_based_diagnostics_finished.store(true, SeqCst);
for buffer in &guest_buffers {
for (buffer, _) in &guest_buffers {
assert_eq!(
buffer
.read(cx)
@@ -4351,7 +4347,6 @@ async fn test_formatting_buffer(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
executor.allow_parking();
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -4379,10 +4374,16 @@ async fn test_formatting_buffer(
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let lsp_store_b = project_b.update(cx_b, |p, _| p.lsp_store());
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
.await
.unwrap();
let _handle = lsp_store_b.update(cx_b, |lsp_store, cx| {
lsp_store.register_buffer_with_language_servers(&buffer_b, cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
Ok(Some(vec![
@@ -4431,6 +4432,8 @@ async fn test_formatting_buffer(
});
});
});
executor.allow_parking();
project_b
.update(cx_b, |project, cx| {
project.format(
@@ -4503,8 +4506,12 @@ async fn test_prettier_formatting_buffer(
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
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();
let (buffer_b, _) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
})
.await
.unwrap();
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
@@ -4620,8 +4627,12 @@ async fn test_definition(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Open the file on client B.
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
})
.await
.unwrap();
// Request the definition of a symbol as the guest.
let fake_language_server = fake_language_servers.next().await.unwrap();
@@ -4765,8 +4776,12 @@ async fn test_references(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Open the file on client B.
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
})
.await
.unwrap();
// Request references to a symbol as the guest.
let fake_language_server = fake_language_servers.next().await.unwrap();
@@ -5012,8 +5027,12 @@ async fn test_document_highlights(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Open the file on client B.
let open_b = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_b).await.unwrap();
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
// Request document highlights as the guest.
let fake_language_server = fake_language_servers.next().await.unwrap();
@@ -5130,8 +5149,12 @@ async fn test_lsp_hover(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Open the file as the guest
let open_buffer = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx));
let buffer_b = cx_b.executor().spawn(open_buffer).await.unwrap();
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let mut servers_with_hover_requests = HashMap::default();
for i in 0..language_server_names.len() {
@@ -5306,9 +5329,12 @@ async fn test_project_symbols(
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Cause the language server to start.
let open_buffer_task =
project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx));
let _buffer = cx_b.executor().spawn(open_buffer_task).await.unwrap();
let _buffer = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "one.rs"), cx)
})
.await
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
@@ -5400,8 +5426,12 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let open_buffer_task = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx));
let buffer_b1 = cx_b.executor().spawn(open_buffer_task).await.unwrap();
let (buffer_b1, _lsp) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "a.rs"), cx)
})
.await
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
@@ -5417,13 +5447,22 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
let buffer_b2;
if rng.gen() {
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
(buffer_b2, _) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
})
.await
.unwrap();
} else {
buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
(buffer_b2, _) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "b.rs"), cx)
})
.await
.unwrap();
definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
}
let buffer_b2 = buffer_b2.await.unwrap();
let definitions = definitions.await.unwrap();
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].target.buffer, buffer_b2);

View File

@@ -426,8 +426,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
executor.run_until_parked();
// Opens the buffer and formats it
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
let (buffer_b, _handle) = project_b
.update(cx_b, |p, cx| {
p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
})
.await
.expect("user B opens buffer for formatting");

View File

@@ -518,7 +518,6 @@ impl TestServer {
stripe_billing: None,
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
executor,
clickhouse_client: None,
kinesis_client: None,
config: Config {
http_port: 0,
@@ -546,10 +545,9 @@ impl TestServer {
anthropic_api_key: None,
anthropic_staff_api_key: None,
llm_closed_beta_model_name: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
prediction_api_url: None,
prediction_api_key: None,
prediction_model: None,
zed_client_checksum_seed: None,
slack_panics_webhook: None,
auto_join_channel_id: None,

View File

@@ -59,18 +59,21 @@ workspace.workspace = true
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
clock.workspace = true
indoc.workspace = true
serde_json.workspace = true
clock = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
node_runtime = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -6,13 +6,12 @@ use anyhow::{anyhow, Result};
use chrono::DateTime;
use fs::Fs;
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Global};
use gpui::{prelude::*, AppContext, AsyncAppContext, Global};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_file;
use strum::EnumIter;
use ui::Context;
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";

View File

@@ -1,14 +1,13 @@
use crate::{Completion, Copilot};
use anyhow::Result;
use client::telemetry::Telemetry;
use gpui::{AppContext, EntityId, Model, ModelContext, Task};
use inline_completion::{CompletionProposal, Direction, InlayProposal, InlineCompletionProvider};
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
use language::{
language_settings::{all_language_settings, AllLanguageSettings},
Buffer, OffsetRangeExt, ToOffset,
};
use settings::Settings;
use std::{path::Path, sync::Arc, time::Duration};
use std::{path::Path, time::Duration};
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -21,7 +20,6 @@ pub struct CopilotCompletionProvider {
pending_refresh: Task<Result<()>>,
pending_cycling_refresh: Task<Result<()>>,
copilot: Model<Copilot>,
telemetry: Option<Arc<Telemetry>>,
}
impl CopilotCompletionProvider {
@@ -35,15 +33,9 @@ impl CopilotCompletionProvider {
pending_refresh: Task::ready(Ok(())),
pending_cycling_refresh: Task::ready(Ok(())),
copilot,
telemetry: None,
}
}
pub fn with_telemetry(mut self, telemetry: Arc<Telemetry>) -> Self {
self.telemetry = Some(telemetry);
self
}
fn active_completion(&self) -> Option<&Completion> {
self.completions.get(self.active_completion_index)
}
@@ -190,23 +182,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
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,
should_report_inline_completion_event: bool,
cx: &mut ModelContext<Self>,
) {
fn discard(&mut self, cx: &mut ModelContext<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
@@ -220,24 +199,14 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
copilot.discard_completions(&self.completions, cx)
})
.detach_and_log_err(cx);
if should_report_inline_completion_event && 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(),
);
}
}
}
fn active_completion_text<'a>(
&'a self,
fn suggest(
&mut self,
buffer: &Model<Buffer>,
cursor_position: language::Anchor,
cx: &'a AppContext,
) -> Option<CompletionProposal> {
cx: &mut ModelContext<Self>,
) -> Option<InlineCompletion> {
let buffer_id = buffer.entity_id();
let buffer = buffer.read(cx);
let completion = self.active_completion()?;
@@ -267,13 +236,9 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
if completion_text.trim().is_empty() {
None
} else {
Some(CompletionProposal {
inlays: vec![InlayProposal::Suggestion(
cursor_position.bias_right(buffer),
completion_text.into(),
)],
text: completion_text.into(),
delete_range: None,
let position = cursor_position.bias_right(buffer);
Some(InlineCompletion {
edits: vec![(position..position, completion_text.into())],
})
}
} else {
@@ -331,7 +296,6 @@ mod tests {
editor.set_inline_completion_provider(Some(copilot_provider), cx)
});
// When inserting, ensure autocompletion is favored over Copilot suggestions.
cx.set_state(indoc! {"
oneˇ
two
@@ -358,8 +322,9 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
// We want to show both: the inline completion and the completion menu
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
// Confirming a completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
@@ -368,45 +333,12 @@ mod tests {
.unwrap()
.detach();
assert!(!editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
// Ensure Copilot suggestions are shown right away if no autocompletion is available.
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec![],
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
// Reset editor and test that accepting completions works
cx.set_state(indoc! {"
oneˇ
two
@@ -434,22 +366,17 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.has_active_inline_completion(cx));
// When hiding the context menu, the Copilot suggestion becomes visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Ensure existing completion is interpolated when inserting again.
// Ensure existing inline completion is interpolated when inserting again.
cx.simulate_keystroke("c");
executor.run_until_parked();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -467,25 +394,25 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should remove the active Copilot suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// After canceling, tabbing shouldn't insert the previously shown suggestion.
editor.tab(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -493,25 +420,25 @@ mod tests {
// If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// AcceptInlineCompletion when there is an active suggestion inserts it.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
// When undoing the previously active suggestion is shown again.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
// Hide suggestion.
editor.cancel(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
});
@@ -520,7 +447,7 @@ mod tests {
// we won't make it visible.
cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
cx.update_editor(|editor, cx| {
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
});
@@ -545,19 +472,19 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
editor.tab(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Using AcceptInlineCompletion again accepts the suggestion.
editor.accept_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
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}");
});
@@ -615,17 +542,17 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
// Accepting the first word of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.copilot\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
// Accepting next word should accept the non-word and copilot suggestion should be gone
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
});
@@ -657,11 +584,11 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
// Accepting the first word (non-word) of the suggestion should only accept the first word and still show the rest.
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. \ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -670,7 +597,7 @@ mod tests {
// Accepting next word should accept the next word and copilot suggestion should still exist
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. copilot\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -679,7 +606,7 @@ mod tests {
// Accepting the whitespace should accept the non-word/whitespaces with newline and copilot suggestion should be gone
editor.accept_partial_inline_completion(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.text(cx), "one.123. copilot\n 456\ntwo\nthree\n");
assert_eq!(
editor.display_text(cx),
@@ -730,29 +657,29 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\nt\nthree\n");
editor.backspace(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
// Deleting across the original suggestion range invalidates it.
editor.backspace(&Default::default(), cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\nthree\n");
assert_eq!(editor.text(cx), "one\nthree\n");
// Undoing the deletion restores the suggestion.
editor.undo(&Default::default(), cx);
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
});
@@ -813,7 +740,7 @@ mod tests {
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
@@ -835,7 +762,7 @@ mod tests {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
@@ -844,7 +771,7 @@ mod tests {
// Type a character, ensuring we don't even try to interpolate the previous suggestion.
editor.handle_input(" ", cx);
assert!(!editor.has_active_inline_completion(cx));
assert!(!editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
@@ -855,7 +782,7 @@ mod tests {
// Ensure the new suggestion is displayed when the debounce timeout expires.
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, cx| {
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(
editor.display_text(cx),
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
@@ -915,8 +842,8 @@ mod tests {
cx.update_editor(|editor, cx| editor.next_inline_completion(&Default::default(), cx));
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible(), "Even there are some completions available, those are not triggered when active copilot suggestion is present");
assert!(editor.has_active_inline_completion(cx));
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
});
@@ -943,7 +870,7 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion(cx));
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo\nthree\n");
});
@@ -969,15 +896,9 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(
editor.context_menu_visible(),
"On completion trigger input, the completions should be fetched and visible"
);
assert!(
!editor.has_active_inline_completion(cx),
"On completion trigger input, copilot suggestion should be dismissed"
);
assert_eq!(editor.display_text(cx), "one\ntwo.\nthree\n");
assert!(editor.context_menu_visible());
assert!(editor.has_active_inline_completion(),);
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}
@@ -998,7 +919,7 @@ mod tests {
"/test",
json!({
".env": "SECRET=something\n",
"README.md": "hello\n"
"README.md": "hello\nworld\nhow\nare\nyou\ntoday"
}),
)
.await;
@@ -1030,7 +951,7 @@ mod tests {
multibuffer.push_excerpts(
public_buffer.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 0),
context: Point::new(0, 0)..Point::new(6, 0),
primary: None,
}],
cx,
@@ -1038,6 +959,7 @@ mod tests {
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {
@@ -1073,7 +995,7 @@ mod tests {
_ = editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, cx);
});

View File

@@ -138,27 +138,16 @@ impl ProjectDiagnosticsEditor {
language_server_id,
path,
} => {
let max_severity = this.max_severity();
let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path)
.into_iter().flatten()
.filter(|(server_id, _)| language_server_id == server_id)
.flat_map(|(_, diagnostics)| diagnostics)
.any(|diagnostic| diagnostic.diagnostic.severity <= max_severity);
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
if has_diagnostics_to_display {
this.paths_to_update
.insert((path.clone(), Some(*language_server_id)));
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.update_stale_excerpts(cx);
}
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display");
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
this.update_stale_excerpts(cx);
}
}
_ => {}
@@ -363,12 +352,16 @@ impl ProjectDiagnosticsEditor {
ExcerptId::min()
};
let max_severity = self.max_severity();
let path_state = &mut self.path_states[path_ix];
let mut new_group_ixs = Vec::new();
let mut blocks_to_add = Vec::new();
let mut blocks_to_remove = HashSet::default();
let mut first_excerpt_id = None;
let max_severity = if self.include_warnings {
DiagnosticSeverity::WARNING
} else {
DiagnosticSeverity::ERROR
};
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
.into_iter()
@@ -657,14 +650,6 @@ impl ProjectDiagnosticsEditor {
prev_path = Some(path);
}
}
fn max_severity(&self) -> DiagnosticSeverity {
if self.include_warnings {
DiagnosticSeverity::WARNING
} else {
DiagnosticSeverity::ERROR
}
}
}
impl FocusableView for ProjectDiagnosticsEditor {

View File

@@ -809,7 +809,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
updated_language_servers.insert(server_id);
project.update(cx, |project, cx| {
lsp_store.update(cx, |lsp_store, cx| {
log::info!("updating diagnostics. language server {server_id} path {path:?}");
randomly_update_diagnostics_for_path(
&fs,
@@ -818,10 +818,12 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
&mut next_group_id,
&mut rng,
);
project
lsp_store
.update_diagnostic_entries(server_id, path, None, diagnostics.clone(), cx)
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -842,10 +844,25 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
cx,
)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
cx.run_until_parked();
let mutated_excerpts = get_diagnostics_excerpts(&mutated_view, cx);
let reference_excerpts = get_diagnostics_excerpts(&reference_view, cx);
for ((path, language_server_id), diagnostics) in current_diagnostics {
for diagnostic in diagnostics {
let found_excerpt = reference_excerpts.iter().any(|info| {
let row_range = info.range.context.start.row..info.range.context.end.row;
info.path == path.strip_prefix("/test").unwrap()
&& info.language_server == language_server_id
&& row_range.contains(&diagnostic.range.start.0.row)
});
assert!(found_excerpt, "diagnostic not found in reference view");
}
}
assert_eq!(mutated_excerpts, reference_excerpts);
}

View File

@@ -251,6 +251,7 @@ gpui::actions!(
DisplayCursorNames,
DuplicateLineDown,
DuplicateLineUp,
DuplicateSelection,
ExpandAllHunkDiffs,
ExpandMacroRecursively,
FindAllReferences,

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
use std::time::Duration;
use futures::{channel::oneshot, FutureExt};
use gpui::{Task, ViewContext};
use crate::Editor;
#[derive(Debug)]
pub struct DebouncedDelay {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DebouncedDelay {
pub fn new() -> DebouncedDelay {
DebouncedDelay {
task: None,
cancel_channel: None,
}
}
pub fn fire_new<F>(&mut self, delay: Duration, cx: &mut ViewContext<Editor>, func: F)
where
F: 'static + Send + FnOnce(&mut Editor, &mut ViewContext<Editor>) -> Task<()>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
}
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
drop(self.task.take());
self.task = Some(cx.spawn(move |model, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse();
futures::select_biased! {
_ = receiver => return,
_ = timer => {}
}
if let Ok(task) = model.update(&mut cx, |project, cx| (func)(project, cx)) {
task.await;
}
}));
}
}

View File

@@ -82,7 +82,7 @@ pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
}
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type TextHighlights = TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
type InlayHighlights = TreeMap<TypeId, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
@@ -434,7 +434,7 @@ impl DisplayMap {
style: HighlightStyle,
) {
self.text_highlights
.insert(Some(type_id), Arc::new((style, ranges)));
.insert(type_id, Arc::new((style, ranges)));
}
pub(crate) fn highlight_inlays(
@@ -457,11 +457,11 @@ impl DisplayMap {
}
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
let highlights = self.text_highlights.get(&Some(type_id))?;
let highlights = self.text_highlights.get(&type_id)?;
Some((highlights.0, &highlights.1))
}
pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
let mut cleared = self.text_highlights.remove(&type_id).is_some();
cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@@ -535,10 +535,16 @@ pub(crate) struct Highlights<'a> {
pub styles: HighlightStyles,
}
#[derive(Clone, Copy, Debug)]
pub struct InlineCompletionStyles {
pub insertion: HighlightStyle,
pub whitespace: HighlightStyle,
}
#[derive(Default, Debug, Clone, Copy)]
pub struct HighlightStyles {
pub inlay_hint: Option<HighlightStyle>,
pub suggestion: Option<HighlightStyle>,
pub inline_completion: Option<InlineCompletionStyles>,
}
#[derive(Clone)]
@@ -859,7 +865,7 @@ impl DisplaySnapshot {
language_aware,
HighlightStyles {
inlay_hint: Some(editor_style.inlay_hints_style),
suggestion: Some(editor_style.suggestions_style),
inline_completion: Some(editor_style.inline_completion_styles),
},
)
.flat_map(|chunk| {
@@ -1125,6 +1131,12 @@ impl DisplaySnapshot {
DisplayRow(self.block_snapshot.longest_row())
}
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
let block_range = BlockRow(range.start.0)..BlockRow(range.end.0);
let longest_row = self.block_snapshot.longest_row_in_range(block_range);
DisplayRow(longest_row.0)
}
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
let max_row = self.buffer_snapshot.max_row();
if buffer_row >= max_row {
@@ -1239,7 +1251,7 @@ impl DisplaySnapshot {
&self,
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
let type_id = TypeId::of::<Tag>();
self.text_highlights.get(&Some(type_id)).cloned()
self.text_highlights.get(&type_id).cloned()
}
#[allow(unused)]

View File

@@ -1339,6 +1339,57 @@ impl BlockSnapshot {
self.transforms.summary().longest_row
}
pub fn longest_row_in_range(&self, range: Range<BlockRow>) -> BlockRow {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&range.start, Bias::Right, &());
let mut longest_row = range.start;
let mut longest_row_chars = 0;
if let Some(transform) = cursor.item() {
if transform.block.is_none() {
let (output_start, input_start) = cursor.start();
let overshoot = range.start.0 - output_start.0;
let wrap_start_row = input_start.0 + overshoot;
let wrap_end_row = cmp::min(
input_start.0 + (range.end.0 - output_start.0),
cursor.end(&()).1 .0,
);
let summary = self
.wrap_snapshot
.text_summary_for_range(wrap_start_row..wrap_end_row);
longest_row = BlockRow(range.start.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
}
cursor.next(&());
}
let cursor_start_row = cursor.start().0;
if range.end > cursor_start_row {
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &());
if summary.longest_row_chars > longest_row_chars {
longest_row = BlockRow(cursor_start_row.0 + summary.longest_row);
longest_row_chars = summary.longest_row_chars;
}
if let Some(transform) = cursor.item() {
if transform.block.is_none() {
let (output_start, input_start) = cursor.start();
let overshoot = range.end.0 - output_start.0;
let wrap_start_row = input_start.0;
let wrap_end_row = input_start.0 + overshoot;
let summary = self
.wrap_snapshot
.text_summary_for_range(wrap_start_row..wrap_end_row);
if summary.longest_row_chars > longest_row_chars {
longest_row = BlockRow(output_start.0 + summary.longest_row);
}
}
}
}
longest_row
}
pub(super) fn line_len(&self, row: BlockRow) -> u32 {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(row.0), Bias::Right, &());
@@ -2705,6 +2756,40 @@ mod tests {
longest_line_len,
);
for _ in 0..10 {
let end_row = rng.gen_range(1..=expected_lines.len());
let start_row = rng.gen_range(0..end_row);
let mut expected_longest_rows_in_range = vec![];
let mut longest_line_len_in_range = 0;
let mut row = start_row as u32;
for line in &expected_lines[start_row..end_row] {
let line_char_count = line.chars().count() as isize;
match line_char_count.cmp(&longest_line_len_in_range) {
Ordering::Less => {}
Ordering::Equal => expected_longest_rows_in_range.push(row),
Ordering::Greater => {
longest_line_len_in_range = line_char_count;
expected_longest_rows_in_range.clear();
expected_longest_rows_in_range.push(row);
}
}
row += 1;
}
let longest_row_in_range = blocks_snapshot
.longest_row_in_range(BlockRow(start_row as u32)..BlockRow(end_row as u32));
assert!(
expected_longest_rows_in_range.contains(&longest_row_in_range.0),
"incorrect longest row {} in range {:?}. expected {:?} with length {}",
longest_row,
start_row..end_row,
expected_longest_rows_in_range,
longest_line_len_in_range,
);
}
// Ensure that conversion between block points and wrap points is stable.
for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
let wrap_point = WrapPoint::new(row, 0);

View File

@@ -62,9 +62,9 @@ impl Inlay {
}
}
pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
pub fn inline_completion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::Suggestion(id),
id: InlayId::InlineCompletion(id),
position,
text: text.into(),
}
@@ -211,7 +211,7 @@ pub struct InlayBufferRows<'a> {
struct HighlightEndpoint {
offset: InlayOffset,
is_start: bool,
tag: Option<TypeId>,
tag: TypeId,
style: HighlightStyle,
}
@@ -239,7 +239,7 @@ pub struct InlayChunks<'a> {
max_output_offset: InlayOffset,
highlight_styles: HighlightStyles,
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
active_highlights: BTreeMap<TypeId, HighlightStyle>,
highlights: Highlights<'a>,
snapshot: &'a InlaySnapshot,
}
@@ -346,7 +346,15 @@ impl<'a> Iterator for InlayChunks<'a> {
}
let mut highlight_style = match inlay.id {
InlayId::Suggestion(_) => self.highlight_styles.suggestion,
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
if inlay.text.chars().all(|c| c.is_whitespace()) {
s.whitespace
} else {
s.insertion
}
})
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
};
let next_inlay_highlight_endpoint;
@@ -693,7 +701,7 @@ impl InlayMap {
let inlay_id = if i % 2 == 0 {
InlayId::Hint(post_inc(next_inlay_id))
} else {
InlayId::Suggestion(post_inc(next_inlay_id))
InlayId::InlineCompletion(post_inc(next_inlay_id))
};
log::info!(
"creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
@@ -1096,7 +1104,7 @@ impl InlaySnapshot {
&self,
cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
range: &Range<InlayOffset>,
text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
text_highlights: &TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
highlight_endpoints: &mut Vec<HighlightEndpoint>,
) {
while cursor.start().0 < range.end {
@@ -1112,7 +1120,7 @@ impl InlaySnapshot {
)))
};
for (tag, text_highlights) in text_highlights.iter() {
for (&tag, text_highlights) in text_highlights.iter() {
let style = text_highlights.0;
let ranges = &text_highlights.1;
@@ -1134,13 +1142,13 @@ impl InlaySnapshot {
highlight_endpoints.push(HighlightEndpoint {
offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
is_start: true,
tag: *tag,
tag,
style,
});
highlight_endpoints.push(HighlightEndpoint {
offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
is_start: false,
tag: *tag,
tag,
style,
});
}
@@ -1389,7 +1397,7 @@ mod tests {
text: "|123|".into(),
},
Inlay {
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
position: buffer.read(cx).snapshot(cx).anchor_after(3),
text: "|456|".into(),
},
@@ -1605,7 +1613,7 @@ mod tests {
text: "|456|".into(),
},
Inlay {
id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
position: buffer.read(cx).snapshot(cx).anchor_before(7),
text: "\n|567|\n".into(),
},
@@ -1708,7 +1716,7 @@ mod tests {
text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
log::info!("highlighting text ranges {text_highlight_ranges:?}");
text_highlights.insert(
Some(TypeId::of::<()>()),
TypeId::of::<()>(),
Arc::new((
HighlightStyle::default(),
text_highlight_ranges

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,8 @@ pub struct EditorSettings {
pub cursor_blink: bool,
pub cursor_shape: Option<CursorShape>,
pub current_line_highlight: CurrentLineHighlight,
pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub completion_documentation_secondary_query_debounce: u64,
pub toolbar: Toolbar,
pub scrollbar: Scrollbar,
pub gutter: Gutter,
@@ -188,27 +186,17 @@ pub struct EditorSettingsContent {
///
/// Default: all
pub current_line_highlight: Option<CurrentLineHighlight>,
/// The debounce delay before querying highlights from the language
/// server based on the current cursor location.
///
/// Default: 75
pub lsp_highlight_debounce: Option<u64>,
/// Whether to show the informational hover box when moving the mouse
/// over symbols in the editor.
///
/// Default: true
pub hover_popover_enabled: Option<bool>,
/// Whether to pop the completions menu while typing in an editor without
/// explicitly requesting it.
///
/// Default: true
pub show_completions_on_input: Option<bool>,
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
///
/// Default: true
pub show_completion_documentation: Option<bool>,
/// The debounce delay before re-querying the language server for completion
/// documentation when not included in original completion list.
///
/// Default: 300 ms
pub completion_documentation_secondary_query_debounce: Option<u64>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings

View File

@@ -9,8 +9,8 @@ use crate::{
};
use futures::StreamExt;
use gpui::{
div, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds,
WindowOptions,
div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
WindowBounds, WindowOptions,
};
use indoc::indoc;
use language::{
@@ -25,16 +25,16 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::{self, AtomicBool};
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use test::editor_lsp_test_context::rust_lang;
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use unindent::Unindent;
use util::{
assert_set_eq,
@@ -3892,6 +3892,28 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
]
);
});
let view = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx)
});
_ = view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
])
});
view.duplicate_selection(&DuplicateSelection, cx);
assert_eq!(view.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
assert_eq!(
view.selections.display_ranges(cx),
vec![
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
]
);
});
}
#[gpui::test]
@@ -6836,14 +6858,15 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let save = editor
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
@@ -7117,6 +7140,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!buffer.is_dirty());
assert_eq!(buffer.text(), sample_text_3,)
});
cx.executor().run_until_parked();
cx.executor().start_waiting();
let save = multi_buffer_editor
@@ -7188,14 +7212,15 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
assert!(cx.read(|cx| editor.is_dirty(cx)));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let save = editor
.update(cx, |editor, cx| editor.save(true, project.clone(), cx))
.unwrap();
@@ -7339,13 +7364,14 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
.await
.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) =
cx.add_window_view(|cx| build_editor_with_project(project.clone(), buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(
@@ -8376,12 +8402,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.show_completions_on_input = Some(false);
});
})
update_test_language_settings(&mut cx, |settings| {
settings.defaults.show_completions_on_input = Some(false);
});
cx.set_state("editorˇ");
cx.simulate_keystroke(".");
@@ -8447,7 +8469,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["first", "last"]
@@ -8459,7 +8481,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_down(&MovePageDown::default(), cx);
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 1,
"expected PageDown to select the last item from the context menu"
@@ -8471,7 +8493,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| {
editor.move_page_up(&MovePageUp::default(), cx);
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert!(
menu.selected_item == 0,
"expected PageUp to select the first item from the context menu"
@@ -8539,7 +8561,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["r", "ret", "Range", "return"]
@@ -9927,7 +9949,8 @@ async fn go_to_prev_overlapping_diagnostic(
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
let lsp_store =
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
@@ -9935,8 +9958,8 @@ async fn go_to_prev_overlapping_diagnostic(
"});
cx.update(|cx| {
project.update(cx, |project, cx| {
project
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
@@ -10025,11 +10048,12 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
fn func(abˇc def: i32) -> u32 {
}
"});
let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
let lsp_store =
cx.update_editor(|editor, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
cx.update(|cx| {
project.update(cx, |project, cx| {
project.update_diagnostics(
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/root/file").unwrap(),
@@ -10334,9 +10358,6 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
})
.await
.unwrap();
cx.executor().run_until_parked();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
let editor_handle = workspace
.update(cx, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -10347,6 +10368,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
.downcast::<Editor>()
.unwrap();
cx.executor().start_waiting();
let fake_server = fake_servers.next().await.unwrap();
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
@@ -10436,7 +10460,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let _buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/a/main.rs", cx)
project.open_local_buffer_with_lsp("/a/main.rs", cx)
})
.await
.unwrap();
@@ -10670,12 +10694,12 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
}
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
@@ -10701,7 +10725,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
.as_ref()
.expect("Should have the context menu deployed");
match context_menu {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!(
@@ -10710,7 +10734,7 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
"Should update the completion label after resolving"
);
}
ContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
}
});
}
@@ -10719,6 +10743,62 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -10735,138 +10815,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
items,
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10883,51 +10840,76 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
cx.update_editor(|editor, _| {
let menu = editor.context_menu.read();
match menu.as_ref().expect("should have the completions menu") {
ContextMenu::Completions(completions_menu) => {
CodeContextMenu::Completions(completions_menu) => {
assert_eq!(
completions_menu
.matches
.iter()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
.map(|c| c.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
);
}
ContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_first(&ContextMenuFirst, cx);
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
}
#[gpui::test]
@@ -10992,7 +10974,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
@@ -11005,7 +10987,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
@@ -11021,7 +11003,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.executor().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]

View File

@@ -1,5 +1,6 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
code_context_menus::CodeActionsMenu,
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
},
@@ -16,13 +17,13 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet};
@@ -31,7 +32,7 @@ use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
@@ -47,7 +48,10 @@ use language::{
ChunkRendererContext,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, ToOffset,
};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
ProjectPath,
@@ -214,6 +218,7 @@ impl EditorElement {
register_action(view, cx, Editor::cut_to_end_of_line);
register_action(view, cx, Editor::duplicate_line_up);
register_action(view, cx, Editor::duplicate_line_down);
register_action(view, cx, Editor::duplicate_selection);
register_action(view, cx, Editor::move_line_up);
register_action(view, cx, Editor::move_line_down);
register_action(view, cx, Editor::transpose);
@@ -1677,11 +1682,11 @@ impl EditorElement {
) -> Vec<AnyElement> {
self.editor.update(cx, |editor, cx| {
let active_task_indicator_row =
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
actions,
..
})) = editor.context_menu.read().as_ref()
})) = editor.context_menu.borrow().as_ref()
{
actions
.tasks
@@ -1692,16 +1697,23 @@ impl EditorElement {
None
};
let offset_range_start = snapshot
.display_point_to_anchor(DisplayPoint::new(range.start, 0), Bias::Left)
.to_offset(&snapshot.buffer_snapshot);
let offset_range_end = snapshot
.display_point_to_anchor(DisplayPoint::new(range.end, 0), Bias::Right)
.to_offset(&snapshot.buffer_snapshot);
editor
.tasks
.iter()
.filter_map(|(_, tasks)| {
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let display_row = multibuffer_point.to_display_point(snapshot).row();
if range.start > display_row || range.end < display_row {
if tasks.offset.0 < offset_range_start || tasks.offset.0 >= offset_range_end {
return None;
}
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row);
if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold.
if multibuffer_row
@@ -1714,6 +1726,7 @@ impl EditorElement {
return None;
}
}
let display_row = multibuffer_point.to_display_point(snapshot).row();
let button = editor.render_run_indicator(
&self.style,
Some(display_row) == active_task_indicator_row,
@@ -1752,10 +1765,10 @@ impl EditorElement {
let mut button = None;
let row = newest_selection_head.row();
self.editor.update(cx, |editor, cx| {
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator,
..
})) = editor.context_menu.read().as_ref()
})) = editor.context_menu.borrow().as_ref()
{
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
};
@@ -2720,6 +2733,163 @@ impl EditorElement {
true
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_completion_popover(
&self,
text_bounds: &Bounds<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
editor_width: Pixels,
style: &EditorStyle,
cx: &mut WindowContext,
) -> Option<AnyElement> {
const PADDING_X: Pixels = Pixels(25.);
const PADDING_Y: Pixels = Pixels(2.);
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
match &active_inline_completion.completion {
InlineCompletion::Move(target_position) => {
let tab_kbd = h_flex()
.px_0p5()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.text_color(cx.theme().colors().text.opacity(0.8))
.child("tab");
let icon_container = div().mt(px(2.5)); // For optical alignment
let container_element = h_flex()
.items_center()
.py_0p5()
.px_1()
.gap_1()
.bg(cx.theme().colors().editor_subheader_background)
.border_1()
.border_color(cx.theme().colors().text_accent.opacity(0.2))
.rounded_md()
.shadow_sm();
let target_display_point = target_position.to_display_point(editor_snapshot);
if target_display_point.row().as_f32() < scroll_top {
let mut element = container_element
.child(tab_kbd)
.child(Label::new("Jump to Edit").size(LabelSize::Small))
.child(
icon_container
.child(Icon::new(IconName::ArrowUp).size(IconSize::Small)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
element.prepaint_at(text_bounds.origin + offset, cx);
Some(element)
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = container_element
.child(tab_kbd)
.child(Label::new("Jump to Edit").size(LabelSize::Small))
.child(
icon_container
.child(Icon::new(IconName::ArrowDown).size(IconSize::Small)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height - size.height - PADDING_Y,
);
element.prepaint_at(text_bounds.origin + offset, cx);
Some(element)
} else {
let mut element = container_element
.child(tab_kbd)
.child(Label::new("Jump to Edit").size(LabelSize::Small))
.into_any();
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let origin = self.editor.update(cx, |editor, cx| {
editor.display_to_pixel_point(target_line_end, editor_snapshot, cx)
})?;
element.prepaint_as_root(
text_bounds.origin + origin + point(PADDING_X, px(0.)),
AvailableSpace::min_size(),
cx,
);
Some(element)
}
}
InlineCompletion::Edit(edits) => {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
if all_edits_insertions_or_deletions(edits, &editor_snapshot.buffer_snapshot) {
return None;
}
let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
cx,
)
.width
};
let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
let mut element = div()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.px_1()
.child(text)
.into_any();
let origin = text_bounds.origin
+ point(
longest_line_width + PADDING_X - scroll_pixel_position.x,
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
);
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
Some(element)
}
}
}
fn layout_mouse_context_menu(
&self,
editor_snapshot: &EditorSnapshot,
@@ -3942,6 +4112,16 @@ impl EditorElement {
}
}
fn paint_inline_completion_popover(
&mut self,
layout: &mut EditorLayout,
cx: &mut WindowContext,
) {
if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
inline_completion_popover.paint(cx);
}
}
fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
mouse_context_menu.paint(cx);
@@ -4134,6 +4314,89 @@ impl EditorElement {
}
}
fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>,
cx: &WindowContext,
) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let mut text = String::new();
let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
let mut highlights = Vec::new();
for (old_range, new_text) in edits {
let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..old_offset_range.start, false)
.map(|chunk| chunk.text),
);
offset = old_offset_range.end;
let start = text.len();
text.push_str(new_text);
let end = text.len();
highlights.push((
start..end,
HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..Default::default()
},
));
}
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
.to_offset(editor_snapshot, Bias::Right);
text.extend(
editor_snapshot
.buffer_snapshot
.chunks(offset..end_of_line, false)
.map(|chunk| chunk.text),
);
(text, highlights)
}
fn all_edits_insertions_or_deletions(
edits: &Vec<(Range<Anchor>, String)>,
snapshot: &MultiBufferSnapshot,
) -> bool {
let mut all_insertions = true;
let mut all_deletions = true;
for (range, new_text) in edits.iter() {
let range_is_empty = range.to_offset(&snapshot).is_empty();
let text_is_empty = new_text.is_empty();
if range_is_empty != text_is_empty {
if range_is_empty {
all_deletions = false;
} else {
all_insertions = false;
}
} else {
return false;
}
if !all_insertions && !all_deletions {
return false;
}
}
all_insertions || all_deletions
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_button(
button: IconButton,
@@ -5566,6 +5829,20 @@ impl Element for EditorElement {
);
}
let inline_completion_popover = self.layout_inline_completion_popover(
&text_hitbox.bounds,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
editor_width,
&style,
cx,
);
let mouse_context_menu = self.layout_mouse_context_menu(
&snapshot,
start_row..end_row,
@@ -5652,6 +5929,7 @@ impl Element for EditorElement {
cursors,
visible_cursors,
selections,
inline_completion_popover,
mouse_context_menu,
test_indicators,
code_actions_indicator,
@@ -5741,6 +6019,7 @@ impl Element for EditorElement {
}
self.paint_scrollbar(layout, cx);
self.paint_inline_completion_popover(layout, cx);
self.paint_mouse_context_menu(layout, cx);
});
})
@@ -5796,6 +6075,7 @@ pub struct EditorLayout {
test_indicators: Vec<AnyElement>,
crease_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
inline_completion_popover: Option<AnyElement>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
@@ -6389,7 +6669,6 @@ mod tests {
use language::language_settings;
use log::info;
use std::num::NonZeroU32;
use ui::Context;
use util::test::sample_text;
#[gpui::test]
@@ -6837,6 +7116,161 @@ mod tests {
}
}
#[gpui::test]
fn test_inline_completion_popover_text(cx: &mut TestAppContext) {
init_test(cx, |_| {});
// Test case 1: Simple insertion
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
let edits = vec![(edit_range, " beautiful".to_string())];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Hello, beautiful world!");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 6..16);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 2: Replacement
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("This is a test.", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
"That".to_string(),
)];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "That is a test.");
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].0, 0..4);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 3: Multiple edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
"Greetings".into(),
),
(
snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
" and universe".into(),
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Greetings, world and universe!");
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].0, 0..9);
assert_eq!(highlights[1].0, 16..29);
assert_eq!(
highlights[0].1.background_color,
Some(cx.theme().status().created_background)
);
assert_eq!(
highlights[1].1.background_color,
Some(cx.theme().status().created_background)
);
})
.unwrap();
}
// Test case 4: Multiple lines with edits
{
let window = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
"First line\nSecond line\nThird line\nFourth line",
cx,
);
Editor::new(EditorMode::Full, buffer, None, true, cx)
});
let cx = &mut VisualTestContext::from_window(*window, cx);
window
.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let edits = vec![
(
snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
"modified".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
"New third line".to_string(),
),
(
snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
" updated".to_string(),
),
];
let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
assert_eq!(highlights.len(), 3);
assert_eq!(highlights[0].0, 7..15); // "modified"
assert_eq!(highlights[1].0, 16..30); // "New third line"
assert_eq!(highlights[2].0, 37..45); // " updated"
for highlight in &highlights {
assert_eq!(
highlight.1.background_color,
Some(cx.theme().status().created_background)
);
}
})
.unwrap();
}
}
fn collect_invisibles_from_new_editor(
cx: &mut TestAppContext,
editor_mode: EditorMode,

View File

@@ -22,10 +22,7 @@ use multi_buffer::{ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer}
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
use text::{OffsetRangeExt, ToPoint};
use theme::ActiveTheme;
use ui::{
div, h_flex, Color, Context, FluentBuilder, Icon, IconName, IntoElement, Label, LabelCommon,
ParentElement, SharedString, Styled, ViewContext, VisualContext, WindowContext,
};
use ui::prelude::*;
use util::{paths::compare_paths, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
@@ -243,7 +240,7 @@ impl ProjectDiffEditor {
.map_err(|_| anyhow!("Unexpected non-buffer"))
})
.with_context(|| {
format!("loading {} for git diff", entry_path.path.display())
format!("loading {:?} for git diff", entry_path.path)
})
.log_err()
else {
@@ -313,11 +310,11 @@ impl ProjectDiffEditor {
project_diff_editor
.update(&mut cx, |project_diff_editor, cx| {
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
for change_set in change_sets {
project_diff_editor.editor.update(cx, |editor, cx| {
project_diff_editor.editor.update(cx, |editor, cx| {
for change_set in change_sets {
editor.diff_map.add_change_set(change_set, cx)
});
}
}
});
})
.ok();
}),

View File

@@ -694,6 +694,65 @@ pub(crate) fn find_url(
None
}
pub(crate) fn find_url_from_range(
buffer: &Model<language::Buffer>,
range: Range<text::Anchor>,
mut cx: AsyncWindowContext,
) -> Option<String> {
const LIMIT: usize = 2048;
let Ok(snapshot) = buffer.update(&mut cx, |buffer, _| buffer.snapshot()) else {
return None;
};
let start_offset = range.start.to_offset(&snapshot);
let end_offset = range.end.to_offset(&snapshot);
let mut token_start = start_offset.min(end_offset);
let mut token_end = start_offset.max(end_offset);
let range_len = token_end - token_start;
if range_len >= LIMIT {
return None;
}
// Skip leading whitespace
for ch in snapshot.chars_at(token_start).take(range_len) {
if !ch.is_whitespace() {
break;
}
token_start += ch.len_utf8();
}
// Skip trailing whitespace
for ch in snapshot.reversed_chars_at(token_end).take(range_len) {
if !ch.is_whitespace() {
break;
}
token_end -= ch.len_utf8();
}
if token_start >= token_end {
return None;
}
let text = snapshot
.text_for_range(token_start..token_end)
.collect::<String>();
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
if let Some(link) = finder.links(&text).next() {
if link.start() == 0 && link.end() == text.len() {
return Some(link.as_str().to_string());
}
}
None
}
pub(crate) async fn find_file(
buffer: &Model<language::Buffer>,
project: Option<Model<Project>>,

View File

@@ -359,6 +359,7 @@ fn show_hover(
let mut base_text_style = cx.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: Some(settings.ui_font_size.into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
@@ -547,11 +548,14 @@ async fn parse_blocks(
.new_view(|cx| {
let settings = ThemeSettings::get_global(cx);
let ui_font_family = settings.ui_font.family.clone();
let ui_font_fallbacks = settings.ui_font.fallbacks.clone();
let buffer_font_family = settings.buffer_font.family.clone();
let buffer_font_fallbacks = settings.buffer_font.fallbacks.clone();
let mut base_text_style = cx.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(ui_font_family.clone()),
font_fallbacks: ui_font_fallbacks,
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
});
@@ -562,6 +566,7 @@ async fn parse_blocks(
inline_code: TextStyleRefinement {
background_color: Some(cx.theme().colors().background),
font_family: Some(buffer_font_family),
font_fallbacks: buffer_font_fallbacks,
..Default::default()
},
rule_color: cx.theme().colors().border,

View File

@@ -89,7 +89,6 @@ impl DiffMap {
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
self.diff_bases.insert(
buffer_id,
DiffBaseState {
@@ -105,6 +104,7 @@ impl DiffMap {
change_set,
},
);
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
}
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,359 @@
use gpui::{prelude::*, Model};
use indoc::indoc;
use inline_completion::InlineCompletionProvider;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use text::{Point, ToOffset};
use crate::{
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
};
#[gpui::test]
async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let absolute_zero_celsius = ˇ;");
propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "-273.15");
});
accept_completion(&mut cx);
cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
}
#[gpui::test]
async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let pi = ˇ\"foo\";");
propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_edit_completion(&mut cx, |_, edits| {
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].1.as_str(), "3.14159");
});
accept_completion(&mut cx);
cx.assert_editor_state("let pi = 3.14159ˇ;")
}
#[gpui::test]
async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
// Cursor is 2+ lines above the proposed edit
cx.set_state(indoc! {"
line 0
line ˇ1
line 2
line 3
line
"});
propose_edits(
&provider,
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
});
// When accepting, cursor is moved to the proposed location
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
line 0
line 1
line 2
line 3
linˇe
"});
// Cursor is 2+ lines below the proposed edit
cx.set_state(indoc! {"
line 0
line
line 2
line 3
line ˇ4
"});
propose_edits(
&provider,
vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
});
// When accepting, cursor is moved to the proposed location
accept_completion(&mut cx);
cx.assert_editor_state(indoc! {"
line 0
linˇe
line 2
line 3
line 4
"});
}
#[gpui::test]
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
// Cursor is 3+ lines above the proposed edit
cx.set_state(indoc! {"
line 0
line ˇ1
line 2
line 3
line 4
line
"});
let edit_location = Point::new(5, 3);
propose_edits(
&provider,
vec![(edit_location..edit_location, " 5")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *towards* the completion, it stays active
cx.set_selections_state(indoc! {"
line 0
line 1
line ˇ2
line 3
line 4
line
"});
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *away* from the completion, it is discarded
cx.set_selections_state(indoc! {"
line ˇ0
line 1
line 2
line 3
line 4
line
"});
cx.editor(|editor, _| {
assert!(editor.active_inline_completion.is_none());
});
// Cursor is 3+ lines below the proposed edit
cx.set_state(indoc! {"
line
line 1
line 2
line 3
line ˇ4
line 5
"});
let edit_location = Point::new(0, 3);
propose_edits(
&provider,
vec![(edit_location..edit_location, " 0")],
&mut cx,
);
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *towards* the completion, it stays active
cx.set_selections_state(indoc! {"
line
line 1
line 2
line ˇ3
line 4
line 5
"});
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
assert_eq!(move_target.to_point(&snapshot), edit_location);
});
// If we move *away* from the completion, it is discarded
cx.set_selections_state(indoc! {"
line
line 1
line 2
line 3
line 4
line ˇ5
"});
cx.editor(|editor, _| {
assert!(editor.active_inline_completion.is_none());
});
}
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
) {
cx.editor(|editor, cx| {
let completion_state = editor
.active_inline_completion
.as_ref()
.expect("editor has no active completion");
if let InlineCompletion::Edit(edits) = &completion_state.completion {
assert(editor.buffer().read(cx).snapshot(cx), edits);
} else {
panic!("expected edit completion");
}
})
}
fn assert_editor_active_move_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, Anchor),
) {
cx.editor(|editor, cx| {
let completion_state = editor
.active_inline_completion
.as_ref()
.expect("editor has no active completion");
if let InlineCompletion::Move(anchor) = &completion_state.completion {
assert(editor.buffer().read(cx).snapshot(cx), *anchor);
} else {
panic!("expected move completion");
}
})
}
fn accept_completion(cx: &mut EditorTestContext) {
cx.update_editor(|editor, cx| {
editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx)
})
}
fn propose_edits<T: ToOffset>(
provider: &Model<FakeInlineCompletionProvider>,
edits: Vec<(Range<T>, &str)>,
cx: &mut EditorTestContext,
) {
let snapshot = cx.buffer_snapshot();
let edits = edits.into_iter().map(|(range, text)| {
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
(range, text.into())
});
cx.update(|cx| {
provider.update(cx, |provider, _| {
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
edits: edits.collect(),
}))
})
});
}
fn assign_editor_completion_provider(
provider: Model<FakeInlineCompletionProvider>,
cx: &mut EditorTestContext,
) {
cx.update_editor(|editor, cx| {
editor.set_inline_completion_provider(Some(provider), cx);
})
}
#[derive(Default, Clone)]
struct FakeInlineCompletionProvider {
completion: Option<inline_completion::InlineCompletion>,
}
impl FakeInlineCompletionProvider {
pub fn set_inline_completion(
&mut self,
completion: Option<inline_completion::InlineCompletion>,
) {
self.completion = completion;
}
}
impl InlineCompletionProvider for FakeInlineCompletionProvider {
fn name() -> &'static str {
"fake-completion-provider"
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &gpui::AppContext,
) -> bool {
true
}
fn refresh(
&mut self,
_buffer: gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_debounce: bool,
_cx: &mut gpui::ModelContext<Self>,
) {
}
fn cycle(
&mut self,
_buffer: gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_direction: inline_completion::Direction,
_cx: &mut gpui::ModelContext<Self>,
) {
}
fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
fn suggest<'a>(
&mut self,
_buffer: &gpui::Model<language::Buffer>,
_cursor_position: language::Anchor,
_cx: &mut gpui::ModelContext<Self>,
) -> Option<inline_completion::InlineCompletion> {
self.completion.clone()
}
}

View File

@@ -37,7 +37,7 @@ where
.find_map(|(trigger_anchor, language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some((

View File

@@ -841,12 +841,12 @@ mod tests {
.flat_map(|offset| {
[
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
id: InlayId::InlineCompletion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: "test".into(),
},
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
id: InlayId::InlineCompletion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: "test".into(),
},

View File

@@ -6,8 +6,8 @@ use collections::BTreeMap;
use futures::Future;
use git::diff::DiffHunkStatus;
use gpui::{
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
VisualTestContext, WindowHandle,
prelude::*, AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View,
ViewContext, VisualTestContext, WindowHandle,
};
use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry};
@@ -23,8 +23,6 @@ use std::{
Arc,
},
};
use ui::Context;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},

View File

@@ -24,6 +24,8 @@ interface github {
}
/// Returns the latest release for the given GitHub repository.
///
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
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.

View File

@@ -32,7 +32,7 @@ use gpui::{
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, Rope,
QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
@@ -1387,6 +1387,7 @@ impl ExtensionStore {
fn prepare_remote_extension(
&mut self,
extension_id: Arc<str>,
is_dev: bool,
tmp_dir: PathBuf,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
@@ -1397,26 +1398,45 @@ impl ExtensionStore {
};
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
for well_known_path in ["extension.toml", "extension.json", "extension.wasm"] {
if fs.is_file(&src_dir.join(well_known_path)).await {
fs.copy_file(
&src_dir.join(well_known_path),
&tmp_dir.join(well_known_path),
fs::CopyOptions::default(),
)
.await?
}
const EXTENSION_TOML: &str = "extension.toml";
const EXTENSION_WASM: &str = "extension.wasm";
const CONFIG_TOML: &str = "config.toml";
if is_dev {
let manifest_toml = toml::to_string(&loaded_extension.manifest)?;
fs.save(
&tmp_dir.join(EXTENSION_TOML),
&Rope::from(manifest_toml),
language::LineEnding::Unix,
)
.await?;
} else {
fs.copy_file(
&src_dir.join(EXTENSION_TOML),
&tmp_dir.join(EXTENSION_TOML),
fs::CopyOptions::default(),
)
.await?
}
if fs.is_file(&src_dir.join(EXTENSION_WASM)).await {
fs.copy_file(
&src_dir.join(EXTENSION_WASM),
&tmp_dir.join(EXTENSION_WASM),
fs::CopyOptions::default(),
)
.await?
}
for language_path in loaded_extension.manifest.languages.iter() {
if fs
.is_file(&src_dir.join(language_path).join("config.toml"))
.is_file(&src_dir.join(language_path).join(CONFIG_TOML))
.await
{
fs.create_dir(&tmp_dir.join(language_path)).await?;
fs.copy_file(
&src_dir.join(language_path).join("config.toml"),
&tmp_dir.join(language_path).join("config.toml"),
&src_dir.join(language_path).join(CONFIG_TOML),
&tmp_dir.join(language_path).join(CONFIG_TOML),
fs::CopyOptions::default(),
)
.await?
@@ -1462,6 +1482,7 @@ impl ExtensionStore {
this.update(cx, |this, cx| {
this.prepare_remote_extension(
missing_extension.id.clone().into(),
missing_extension.dev,
tmp_dir.path().to_owned(),
cx,
)
@@ -1476,6 +1497,11 @@ impl ExtensionStore {
})?
.await?;
log::info!(
"Finished uploading extension {}",
missing_extension.clone().id
);
client
.update(cx, |client, _cx| {
client.proto_client().request(proto::InstallExtension {

View File

@@ -623,9 +623,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
None,
);
let buffer = project
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer(project_dir.join("test.gleam"), cx)
project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
})
.await
.unwrap();

View File

@@ -59,6 +59,16 @@ impl FeatureFlag for ToolUseFeatureFlag {
}
}
pub struct ZetaFeatureFlag;
impl FeatureFlag for ZetaFeatureFlag {
const NAME: &'static str = "zeta";
}
pub struct GitUiFeatureFlag;
impl FeatureFlag for GitUiFeatureFlag {
const NAME: &'static str = "git-ui";
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";

View File

@@ -21,6 +21,7 @@ git.workspace = true
git2.workspace = true
gpui.workspace = true
libc.workspace = true
log.workspace = true
parking_lot.workspace = true
paths.workspace = true
rope.workspace = true

View File

@@ -695,10 +695,13 @@ impl Fs for RealFs {
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let watcher = Arc::new(linux_watcher::LinuxWatcher::new(tx, pending_paths.clone()));
watcher.add(&path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
if let Some(parent) = path.parent() {
// watch the parent dir so we can tell when settings.json is created
watcher.add(parent).log_err();
if watcher.add(path).is_err() {
// If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
if let Some(parent) = path.parent() {
if let Err(e) = watcher.add(parent) {
log::warn!("Failed to watch: {e}");
}
}
}
// Check if path is a symlink and follow the target parent
@@ -777,7 +780,10 @@ impl Fs for RealFs {
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
let repo = git2::Repository::open(dotgit_path).log_err()?;
// with libgit2, we can open git repo from an existing work dir
// https://libgit2.org/docs/reference/main/repository/git_repository_open.html
let workdir_root = dotgit_path.parent()?;
let repo = git2::Repository::open(workdir_root).log_err()?;
Some(Arc::new(RealGitRepository::new(
repo,
self.git_binary_path.clone(),

View File

@@ -15,3 +15,4 @@ doctest = false
[dependencies]
gpui.workspace = true
util.workspace = true
log.workspace = true

View File

@@ -61,9 +61,23 @@ impl StringMatch {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
if let Some(start) = positions.next().copied() {
if start >= self.string.len() {
log::error!(
"Invariant violation: Index {start} out of range in string {:?}",
self.string
);
return None;
}
let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() {
if end == **next_start {
if end >= self.string.len() {
log::error!(
"Invariant violation: Index {end} out of range in string {:?}",
self.string
);
return None;
}
end += self.char_len_at_index(end);
positions.next();
} else {

35
crates/git_ui/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
[package]
name = "git_ui"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
name = "git_ui"
path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
db.workspace = true
gpui.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
git.workspace = true
collections.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[features]
default = []

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