Compare commits

...

45 Commits

Author SHA1 Message Date
Mikayla
f67815df05 better documentation for the pixel format 2024-12-17 10:56:31 -08:00
Mikayla
f2ca21ae44 Add rendering of self screen capture to test app
Fix bugs in screen share stream configuration
2024-12-15 22:47:39 -08: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
77 changed files with 3978 additions and 754 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

28
Cargo.lock generated
View File

@@ -469,6 +469,7 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
"handlebars 4.5.0",
"indoc",
@@ -499,6 +500,7 @@ dependencies = [
"similar",
"smol",
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
@@ -5174,9 +5176,19 @@ dependencies = [
name = "git_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"db",
"git",
"gpui",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"ui",
"util",
"windows 0.58.0",
"workspace",
]
@@ -10354,6 +10366,7 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"async-dispatcher",
"async-tungstenite 0.28.1",
"base64 0.22.1",
"client",
"collections",
@@ -16128,7 +16141,7 @@ dependencies = [
name = "zed_elixir"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.1.0",
"zed_extension_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -16172,6 +16185,17 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fd16b8b30a9dc920fc1678ff852f696b5bdf5b5843bc745a128be0aac29859e"
dependencies = [
"serde",
"serde_json",
"wit-bindgen",
]
[[package]]
name = "zed_glsl"
version = "0.1.0"
@@ -16400,6 +16424,7 @@ name = "zeta"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"call",
"client",
"clock",
@@ -16426,7 +16451,6 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"util",
"uuid",
"workspace",
"worktree",

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

@@ -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,6 +144,9 @@
// 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,
@@ -471,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 `👋`.

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

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

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

@@ -28,6 +28,7 @@ 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
@@ -58,6 +59,7 @@ 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

View File

@@ -7,6 +7,7 @@ mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
@@ -63,6 +64,12 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}

View File

@@ -88,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(),
@@ -123,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);
}
@@ -145,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);
}

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
}

View File

@@ -2,6 +2,7 @@ use crate::{
assistant_settings::AssistantSettings,
prompts::PromptBuilder,
streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff},
terminal_inline_assistant::TerminalInlineAssistant,
CycleNextInlineAssist, CyclePreviousInlineAssist, ToggleInlineAssist,
};
use anyhow::{Context as _, Result};
@@ -30,7 +31,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;
@@ -207,16 +208,16 @@ impl InlineAssistant {
.map_or(false, |provider| provider.is_authenticated(cx))
};
let handle_assist = |cx: &mut ViewContext<Workspace>| {
match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
})
}
InlineAssistTarget::Terminal(_active_terminal) => {
// TODO show the terminal inline assistant
}
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
})
}
InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx)
})
}
};
@@ -1453,8 +1454,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>,
@@ -1588,43 +1589,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;
@@ -1713,6 +1698,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,
@@ -1721,7 +1719,6 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
show_rate_limit_notice: false,
};
this.subscribe_to_editor(cx);

View File

@@ -1,19 +1,19 @@
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 language_model_selector::LanguageModelSelector;
use picker::Picker;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenuHandle, Tooltip,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::{ContextPicker, ContextPickerDelegate};
use crate::context_picker::ContextPicker;
use crate::thread::{RequestKind, Thread};
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
@@ -23,13 +23,20 @@ pub struct MessageEditor {
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
pub(crate) context_picker_handle: PopoverMenuHandle<Picker<ContextPickerDelegate>>,
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 {
let mut this = 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);
@@ -39,18 +46,32 @@ impl MessageEditor {
}),
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,
};
}
}
this.context.push(Context {
id: this.next_context_id.post_inc(),
name: "shape.rs".into(),
kind: ContextKind::File,
text: "```rs\npub enum Shape {\n Circle,\n Square,\n Triangle,\n}".into(),
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(),
});
this
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
@@ -108,10 +129,8 @@ impl MessageEditor {
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());
},
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
@@ -167,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")
@@ -179,12 +199,22 @@ impl Render for MessageEditor {
h_flex()
.flex_wrap()
.gap_2()
.child(ContextPicker::new(
cx.view().downgrade(),
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
))
.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();

View File

@@ -288,4 +288,25 @@ impl PromptBuilder {
};
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

View File

@@ -42,10 +42,10 @@ serde_derive.workspace = true
settings.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
[target.'cfg(any())'.dependencies]
livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
[target.'cfg(all())'.dependencies]
livekit_client = { workspace = true }
[dev-dependencies]

View File

@@ -1,13 +1,13 @@
pub mod call_settings;
#[cfg(target_os = "macos")]
#[cfg(any())]
mod macos;
#[cfg(target_os = "macos")]
#[cfg(any())]
pub use macos::*;
#[cfg(not(target_os = "macos"))]
#[cfg(all())]
mod cross_platform;
#[cfg(not(target_os = "macos"))]
#[cfg(all())]
pub use cross_platform::*;

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()
@@ -1435,7 +1441,7 @@ impl Room {
let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let (track, stream) = capture_local_video_track(&**source).await?;
let (track, stream) = capture_local_video_track(&**source, None).await?;
let publication = participant
.publish_track(

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

@@ -2078,17 +2078,7 @@ async fn test_mute_deafen(
audio_tracks_playing: participant
.audio_tracks
.values()
.map({
#[cfg(target_os = "macos")]
{
|track| track.is_playing()
}
#[cfg(not(target_os = "macos"))]
{
|(track, _)| track.rtc_track().enabled()
}
})
.map(|(track, _)| track.rtc_track().enabled())
.collect(),
})
.collect::<Vec<_>>()

View File

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

View File

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

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(),
}
@@ -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 {:?}",
@@ -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(),
},

View File

@@ -176,7 +176,7 @@ use workspace::{
};
use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url;
use crate::hover_links::{find_url, find_url_from_range};
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
pub const FILE_HEADER_HEIGHT: u32 = 2;
@@ -190,8 +190,6 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
#[doc(hidden)]
pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
@@ -261,14 +259,14 @@ pub fn render_parsed_markdown(
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum InlayId {
Suggestion(usize),
InlineCompletion(usize),
Hint(usize),
}
impl InlayId {
fn id(&self) -> usize {
match self {
Self::Suggestion(id) => *id,
Self::InlineCompletion(id) => *id,
Self::Hint(id) => *id,
}
}
@@ -407,7 +405,7 @@ pub struct EditorStyle {
pub syntax: Arc<SyntaxTheme>,
pub status: StatusColors,
pub inlay_hints_style: HighlightStyle,
pub suggestions_style: HighlightStyle,
pub inline_completion_styles: InlineCompletionStyles,
pub unnecessary_code_fade: f32,
}
@@ -424,7 +422,10 @@ impl Default for EditorStyle {
// style and retrieve them directly from the theme.
status: StatusColors::dark(),
inlay_hints_style: HighlightStyle::default(),
suggestions_style: HighlightStyle::default(),
inline_completion_styles: InlineCompletionStyles {
insertion: HighlightStyle::default(),
whitespace: HighlightStyle::default(),
},
unnecessary_code_fade: Default::default(),
}
}
@@ -442,6 +443,19 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
}
}
pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
InlineCompletionStyles {
insertion: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
whitespace: HighlightStyle {
background_color: Some(cx.theme().status().created_background),
..HighlightStyle::default()
},
}
}
type CompletionId = usize;
enum InlineCompletion {
@@ -1404,6 +1418,15 @@ impl Editor {
key_context.add("inline_completion");
}
if !self
.selections
.disjoint
.iter()
.all(|selection| selection.start == selection.end)
{
key_context.add("selection");
}
key_context
}
@@ -4311,10 +4334,10 @@ impl Editor {
if cursor_buffer != tail_buffer {
return None;
}
let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce;
self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
.timer(Duration::from_millis(debounce))
.await;
let highlights = if let Some(highlights) = cx
@@ -4728,7 +4751,7 @@ impl Editor {
{
let mut inlays = Vec::new();
for (range, new_text) in &edits {
let inlay = Inlay::suggestion(
let inlay = Inlay::inline_completion(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
@@ -6112,6 +6135,28 @@ impl Editor {
});
}
pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let mut edits = Vec::new();
for selection in selections.iter() {
let start = selection.start;
let end = selection.end;
let text = buffer.text_for_range(start..end).collect::<String>();
edits.push((selection.end..selection.end, text));
}
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn duplicate_line(&mut self, upwards: bool, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
@@ -9264,23 +9309,42 @@ impl Editor {
}
pub fn open_url(&mut self, _: &OpenUrl, cx: &mut ViewContext<Self>) {
let position = self.selections.newest_anchor().head();
let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(position, cx)
let selection = self.selections.newest_anchor();
let head = selection.head();
let tail = selection.tail();
let Some((buffer, start_position)) =
self.buffer.read(cx).text_anchor_for_position(head, cx)
else {
return;
};
cx.spawn(|editor, mut cx| async move {
if let Some((_, url)) = find_url(&buffer, buffer_position, cx.clone()) {
let end_position = if head != tail {
let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else {
return;
};
Some(pos)
} else {
None
};
let url_finder = cx.spawn(|editor, mut cx| async move {
let url = if let Some(end_pos) = end_position {
find_url_from_range(&buffer, start_position..end_pos, cx.clone())
} else {
find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url)
};
if let Some(url) = url {
editor.update(&mut cx, |_, cx| {
cx.open_url(&url);
})
} else {
Ok(())
}
})
.detach();
});
url_finder.detach();
}
pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
@@ -9853,10 +9917,9 @@ impl Editor {
font_weight: Some(FontWeight::BOLD),
..make_inlay_hints_style(cx)
},
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
inline_completion_styles: make_suggestion_styles(
cx,
),
..EditorStyle::default()
},
))
@@ -13857,10 +13920,7 @@ impl Render for Editor {
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style: make_inlay_hints_style(cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
inline_completion_styles: make_suggestion_styles(cx),
unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade,
},
)

View File

@@ -9,6 +9,7 @@ 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 toolbar: Toolbar,
pub scrollbar: Scrollbar,
@@ -185,6 +186,11 @@ 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.
///

View File

@@ -3895,6 +3895,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]

View File

@@ -218,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);
@@ -2754,22 +2755,34 @@ impl EditorElement {
match &active_inline_completion.completion {
InlineCompletion::Move(target_position) => {
let container_element = div()
.bg(cx.theme().colors().editor_background)
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().border)
.border_color(cx.theme().colors().text_accent.opacity(0.2))
.rounded_md()
.px_1();
.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(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowUp)),
icon_container
.child(Icon::new(IconName::ArrowUp).size(IconSize::Small)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
@@ -2778,12 +2791,11 @@ impl EditorElement {
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(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit"))
.child(Icon::new(IconName::ArrowDown)),
icon_container
.child(Icon::new(IconName::ArrowDown).size(IconSize::Small)),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
@@ -2795,12 +2807,8 @@ impl EditorElement {
Some(element)
} else {
let mut element = container_element
.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Tab))
.child(Label::new("Jump to Edit")),
)
.child(tab_kbd)
.child(Label::new("Jump to Edit").size(LabelSize::Small))
.into_any();
let target_line_end = DisplayPoint::new(

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

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

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

@@ -13,10 +13,20 @@ 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
workspace.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

45
crates/git_ui/TODO.md Normal file
View File

@@ -0,0 +1,45 @@
### General
- [x] Disable staging and committing actions for read-only projects
### List
- [x] Add uniform list
- [x] Git status item
- [ ] Directory item
- [x] Scrollbar
- [ ] Add indent size setting
- [ ] Add tree settings
### List Items
- [x] Checkbox for staging
- [x] Git status icon
- [ ] Context menu
- [ ] Discard Changes
- ---
- [ ] Ignore
- [ ] Ignore directory
- ---
- [ ] Copy path
- [ ] Copy relative path
- ---
- [ ] Reveal in Finder
### Commit Editor
- [ ] Add commit editor
- [ ] Add commit message placeholder & add commit message to store
- [ ] Add a way to get the current collaborators & automatically add them to the commit message as co-authors
- [ ] Add action to clear commit message
- [x] Swap commit button between "Commit" and "Commit All" based on modifier key
### Component Updates
- [ ] ChangedLineCount (new)
- takes `lines_added: usize, lines_removed: usize`, returns a added/removed badge
- [x] GitStatusIcon (new)
- [ ] Checkbox
- update checkbox design
- [ ] ScrollIndicator
- shows a gradient overlay when more content is available to be scrolled

View File

@@ -1,8 +1,36 @@
use collections::HashMap;
use std::{
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use git::repository::GitFileStatus;
use util::{ResultExt, TryFutureExt};
use db::kvp::KEY_VALUE_STORE;
use gpui::*;
use ui::{prelude::*, Checkbox, Divider, DividerColor, ElevationIndex};
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
actions!(git_panel, [ToggleFocus]);
const GIT_PANEL_KEY: &str = "GitPanel";
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -14,12 +42,52 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
actions!(git_panel, [Deploy, ToggleFocus]);
#[derive(Debug)]
pub enum Event {
Focus,
}
pub struct GitStatusEntry {}
#[derive(Debug, PartialEq, Eq, Clone)]
struct EntryDetails {
filename: String,
display_name: String,
path: Arc<Path>,
kind: EntryKind,
depth: usize,
is_expanded: bool,
status: Option<GitFileStatus>,
}
impl EntryDetails {
pub fn is_dir(&self) -> bool {
self.kind.is_dir()
}
}
#[derive(Serialize, Deserialize)]
struct SerializedGitPanel {
width: Option<Pixels>,
}
#[derive(Clone)]
pub struct GitPanel {
_workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
pending_serialization: Task<Option<()>>,
project: Model<Project>,
scroll_handle: UniformListScrollHandle,
scrollbar_state: ScrollbarState,
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
width: Option<Pixels>,
}
@@ -29,23 +97,365 @@ impl GitPanel {
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
cx.new_view(|cx| Self::new(workspace_handle, cx))
})
// Clippy incorrectly classifies this as a redundant closure
#[allow(clippy::redundant_closure)]
workspace.update(&mut cx, |workspace, cx| Self::new(workspace, cx))
})
}
pub fn new(workspace: WeakView<Workspace>, cx: &mut ViewContext<Self>) -> Self {
Self {
_workspace: workspace,
focus_handle: cx.focus_handle(),
width: Some(px(360.)),
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let fs = workspace.app_state().fs.clone();
let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| {
this.hide_scrollbar(cx);
})
.detach();
cx.subscribe(&project, |this, _project, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
cx.notify();
}
_ => {}
})
.detach();
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
_workspace: weak_workspace,
focus_handle: cx.focus_handle(),
fs,
pending_serialization: Task::ready(None),
project,
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
scroll_handle,
selected_item: None,
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
};
this.update_visible_entries(None, cx);
this
});
git_panel
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
GIT_PANEL_KEY.into(),
serde_json::to_string(&SerializedGitPanel { width })?,
)
.await?;
anyhow::Ok(())
}
.log_err(),
);
}
fn dispatch_context(&self) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("GitPanel");
dispatch_context.add("menu");
dispatch_context
}
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.contains_focused(cx) {
cx.emit(Event::Focus);
}
}
fn should_show_scrollbar(_cx: &AppContext) -> bool {
// todo!(): plug into settings
true
}
fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
// todo!(): plug into settings
true
}
fn hide_scrollbar(&mut self, cx: &mut ViewContext<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
if !Self::should_autohide_scrollbar(cx) {
return;
}
self.hide_scrollbar_task = Some(cx.spawn(|panel, mut cx| async move {
cx.background_executor()
.timer(SCROLLBAR_SHOW_INTERVAL)
.await;
panel
.update(&mut cx, |panel, cx| {
panel.show_scrollbar = false;
cx.notify();
})
.log_err();
}))
}
fn handle_modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) {
self.current_modifiers = event.modifiers;
cx.notify();
}
fn calculate_depth_and_difference(
entry: &Entry,
visible_worktree_entries: &HashSet<Arc<Path>>,
) -> (usize, usize) {
let (depth, difference) = entry
.path
.ancestors()
.skip(1) // Skip the entry itself
.find_map(|ancestor| {
if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
let entry_path_components_count = entry.path.components().count();
let parent_path_components_count = parent_entry.components().count();
let difference = entry_path_components_count - parent_path_components_count;
let depth = parent_entry
.ancestors()
.skip(1)
.filter(|ancestor| visible_worktree_entries.contains(*ancestor))
.count();
Some((depth + 1, difference))
} else {
None
}
})
.unwrap_or((0, 0));
(depth, difference)
}
}
impl GitPanel {
fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement stage all
println!("Stage all triggered");
}
fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement unstage all
println!("Unstage all triggered");
}
fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement discard all
println!("Discard all triggered");
}
/// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
// todo!(): Implement commit all staged
println!("Commit staged changes triggered");
}
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
// todo!(): Implement commit all changes
println!("Commit all changes triggered");
}
fn all_staged(&self) -> bool {
// todo!(): Implement all_staged
true
}
fn no_entries(&self) -> bool {
self.visible_entries.is_empty()
}
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
.map(|(_, entries, _)| {
entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
})
.sum()
}
fn for_each_visible_entry(
&self,
range: Range<usize>,
cx: &mut ViewContext<Self>,
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
if ix >= range.end {
return;
}
if ix + visible_worktree_entries.len() <= range.start {
ix += visible_worktree_entries.len();
continue;
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| {
visible_worktree_entries
.iter()
.map(|e| (e.path.clone()))
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
let filename = match difference {
diff if diff > 1 => entry
.path
.iter()
.skip(entry.path.components().count() - diff)
.collect::<PathBuf>()
.to_str()
.unwrap_or_default()
.to_string(),
_ => entry
.path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
};
let display_name = entry.path.to_string_lossy().into_owned();
let details = EntryDetails {
filename,
display_name,
kind: entry.kind,
is_expanded,
path: entry.path.clone(),
status,
depth,
};
callback(entry.id, details, cx);
}
}
ix = end_ix;
}
}
// todo!(): Update expanded directory state
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
self.visible_entries.clear();
for worktree in project.visible_worktrees(cx) {
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
while let Some(entry) = entry_iter.entry() {
// Only include entries with a git status
if entry.git_status.is_some() {
visible_worktree_entries.push(entry.clone());
}
entry_iter.advance();
}
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
if !visible_worktree_entries.is_empty() {
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
}
}
if let Some((worktree_id, entry_id)) = new_selected_entry {
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|(worktree_index, (id, entries, _))| {
if *id == worktree_id {
entries
.iter()
.position(|entry| entry.id == entry_id)
.map(|entry_index| worktree_index * entries.len() + entry_index)
} else {
None
}
},
);
}
cx.notify();
}
}
impl GitPanel {
pub fn panel_button(
&self,
id: impl Into<SharedString>,
label: impl Into<SharedString>,
) -> Button {
let id = id.into().clone();
let label = label.into().clone();
Button::new(id, label)
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
}
pub fn render_divider(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border))
}
pub fn render_panel_header(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let changes_string = format!("{} changes", self.entry_count());
h_flex()
.h(px(32.))
.items_center()
@@ -53,31 +463,75 @@ impl GitPanel {
.bg(ElevationIndex::Surface.bg(cx))
.child(
h_flex()
.gap_1()
.gap_2()
.child(Checkbox::new("all-changes", true.into()).disabled(true))
.child(div().text_buffer(cx).text_ui_sm(cx).child("0 changes")),
.child(div().text_buffer(cx).text_ui_sm(cx).child(changes_string)),
)
.child(div().flex_grow())
.child(
h_flex()
.gap_1()
.gap_2()
.child(
IconButton::new("discard-changes", IconName::Undo)
.tooltip(move |cx| {
let focus_handle = focus_handle.clone();
Tooltip::for_action_in(
"Discard all changes",
&DiscardAll,
&focus_handle,
cx,
)
})
.icon_size(IconSize::Small)
.disabled(true),
)
.child(
Button::new("stage-all", "Stage All")
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
.disabled(true),
),
.child(if self.all_staged() {
self.panel_button("unstage-all", "Unstage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(DiscardAll))),
)
} else {
self.panel_button("stage-all", "Stage All").on_click(
cx.listener(move |_, _, cx| cx.dispatch_action(Box::new(StageAll))),
)
}),
)
}
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
let commit_staged_button = self
.panel_button("commit-staged-changes", "Commit")
.tooltip(move |cx| {
let focus_handle = focus_handle_1.clone();
Tooltip::for_action_in(
"Commit all staged changes",
&CommitStagedChanges,
&focus_handle,
cx,
)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.commit_staged_changes(&CommitStagedChanges, cx)
}));
let commit_all_button = self
.panel_button("commit-all-changes", "Commit All")
.tooltip(move |cx| {
let focus_handle = focus_handle_2.clone();
Tooltip::for_action_in(
"Commit all changes, including unstaged changes",
&CommitAllChanges,
&focus_handle,
cx,
)
})
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.commit_all_changes(&CommitAllChanges, cx)
}));
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex()
.h_full()
@@ -90,47 +544,188 @@ impl GitPanel {
.child("Add a message")
.gap_1()
.child(div().flex_grow())
.child(
h_flex().child(div().gap_1().flex_grow()).child(
Button::new("commit", "Commit")
.label_size(LabelSize::Small)
.layer(ElevationIndex::ElevatedSurface)
.size(ButtonSize::Compact)
.style(ButtonStyle::Filled)
.disabled(true),
),
)
.child(h_flex().child(div().gap_1().flex_grow()).child(
if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
},
))
.cursor(CursorStyle::OperationNotAllowed)
.opacity(0.5),
)
}
fn render_empty_state(&self, cx: &ViewContext<Self>) -> impl IntoElement {
h_flex()
.h_full()
.flex_1()
.justify_center()
.items_center()
.child(
v_flex()
.gap_3()
.child("No changes to commit")
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),
)
}
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.scrollbar_state.is_dragging())
{
return None;
}
Some(
div()
.occlude()
.id("project-panel-vertical-scroll")
.on_mouse_move(cx.listener(|_, _, cx| {
cx.notify();
cx.stop_propagation()
}))
.on_hover(|_, cx| {
cx.stop_propagation();
})
.on_any_mouse_down(|_, cx| {
cx.stop_propagation();
})
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, cx| {
if !this.scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(cx)
{
this.hide_scrollbar(cx);
cx.notify();
}
cx.stop_propagation();
}),
)
.on_scroll_wheel(cx.listener(|_, _, cx| {
cx.notify();
}))
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.cursor_default()
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.scrollbar_state.clone(),
)),
)
}
fn render_entries(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let item_count = self
.visible_entries
.iter()
.map(|(_, worktree_entries, _)| worktree_entries.len())
.sum();
h_flex()
.size_full()
.overflow_hidden()
.child(
uniform_list(cx.view().clone(), "entries", item_count, {
|this, range, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
this.for_each_visible_entry(range, cx, |id, details, cx| {
items.push(this.render_entry(id, details, cx));
});
items
}
})
.size_full()
.with_sizing_behavior(ListSizingBehavior::Infer)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
// .with_width_from_item(self.max_width_item_index)
.track_scroll(self.scroll_handle.clone()),
)
.children(self.render_scrollbar(cx))
}
fn render_entry(
&self,
id: ProjectEntryId,
details: EntryDetails,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let is_staged = Selection::Selected;
h_flex()
.id(id)
.h(px(28.))
.w_full()
.pl(px(12. + 12. * details.depth as f32))
.pr(px(4.))
.items_center()
.gap_2()
.font_buffer(cx)
.text_ui_sm(cx)
.when(!details.is_dir(), |this| {
this.child(Checkbox::new(checkbox_id, is_staged))
})
.when_some(details.status, |this, status| {
this.child(git_status_icon(status))
})
.child(h_flex().gap_1p5().child(details.display_name.clone()))
}
}
impl Render for GitPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let project = self.project.read(cx);
v_flex()
.key_context("GitPanel")
.font_buffer(cx)
.py_1()
.id("git_panel")
.key_context(self.dispatch_context())
.track_focus(&self.focus_handle)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.when(!project.is_read_only(cx), |this| {
this.on_action(cx.listener(|this, &StageAll, cx| this.stage_all(&StageAll, cx)))
.on_action(
cx.listener(|this, &UnstageAll, cx| this.unstage_all(&UnstageAll, cx)),
)
.on_action(
cx.listener(|this, &DiscardAll, cx| this.discard_all(&DiscardAll, cx)),
)
.on_action(cx.listener(|this, &CommitStagedChanges, cx| {
this.commit_staged_changes(&CommitStagedChanges, cx)
}))
.on_action(cx.listener(|this, &CommitAllChanges, cx| {
this.commit_all_changes(&CommitAllChanges, cx)
}))
})
.on_hover(cx.listener(|this, hovered, cx| {
if *hovered {
this.show_scrollbar = true;
this.hide_scrollbar_task.take();
cx.notify();
} else if !this.focus_handle.contains_focused(cx) {
this.hide_scrollbar(cx);
}
}))
.size_full()
.overflow_hidden()
.font_buffer(cx)
.py_1()
.bg(ElevationIndex::Surface.bg(cx))
.child(self.render_panel_header(cx))
.child(
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
)
.child(div().flex_1())
.child(
h_flex()
.items_center()
.h(px(8.))
.child(Divider::horizontal_dashed().color(DividerColor::Border)),
)
.child(self.render_divider(cx))
.child(if !self.no_entries() {
self.render_entries(cx).into_any_element()
} else {
self.render_empty_state(cx).into_any_element()
})
.child(self.render_divider(cx))
.child(self.render_commit_editor(cx))
}
}
@@ -141,6 +736,8 @@ impl FocusableView for GitPanel {
}
}
impl EventEmitter<Event> for GitPanel {}
impl EventEmitter<PanelEvent> for GitPanel {}
impl Panel for GitPanel {
@@ -148,27 +745,35 @@ impl Panel for GitPanel {
"GitPanel"
}
fn position(&self, _cx: &gpui::WindowContext) -> DockPosition {
DockPosition::Left
fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
GitPanelSettings::get_global(cx).dock
}
fn position_is_valid(&self, position: DockPosition) -> bool {
matches!(position, DockPosition::Left | DockPosition::Right)
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
settings::update_settings_file::<GitPanelSettings>(
self.fs.clone(),
cx,
move |settings, _| settings.dock = Some(position),
);
}
fn size(&self, _cx: &gpui::WindowContext) -> Pixels {
self.width.unwrap_or(px(360.))
fn size(&self, cx: &gpui::WindowContext) -> Pixels {
self.width
.unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
self.width = size;
self.serialize(cx);
cx.notify();
}
fn icon(&self, _cx: &gpui::WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::GitBranch)
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::GitBranch).filter(|_| GitPanelSettings::get_global(cx).button)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View File

@@ -1 +1,53 @@
use ::settings::Settings;
use git::repository::GitFileStatus;
use gpui::{actions, AppContext, Hsla};
use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement};
pub mod git_panel;
mod settings;
actions!(
git_ui,
[
StageAll,
UnstageAll,
DiscardAll,
CommitStagedChanges,
CommitAllChanges
]
);
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
}
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,
l: 0.45,
a: 1.0,
};
const MODIFIED_COLOR: Hsla = Hsla {
h: 48. / 360.,
s: 0.76,
l: 0.47,
a: 1.0,
};
const REMOVED_COLOR: Hsla = Hsla {
h: 355. / 360.,
s: 0.65,
l: 0.65,
a: 1.0,
};
// todo!(): Add updated status colors to theme
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
match status {
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),
GitFileStatus::Modified => {
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
}
}

View File

@@ -0,0 +1,41 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
pub struct GitPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the panel.
///
/// Default: left
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 360
pub default_width: Option<f32>,
}
impl Settings for GitPanelSettings {
const KEY: Option<&'static str> = Some("git_panel");
type FileContent = PanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
sources.json_merge()
}
}

View File

@@ -48,8 +48,17 @@ impl CursorPosition {
) {
let editor = editor.downgrade();
self.update_position = cx.spawn(|cursor_position, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
let is_singleton = editor
.update(&mut cx, |editor, cx| {
editor.buffer().read(cx).is_singleton()
})
.ok()
.unwrap_or(true);
if !is_singleton {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
}
editor

View File

@@ -239,7 +239,7 @@ pub trait PlatformDisplay: Send + Sync + Debug {
/// A source of on-screen video content that can be captured.
pub trait ScreenCaptureSource {
/// Returns the video resolution of this source.
fn resolution(&self) -> Result<Size<Pixels>>;
fn resolution(&self) -> Size<DevicePixels>;
/// Start capture video from this source, invoking the given callback
/// with each frame.
@@ -253,6 +253,7 @@ pub trait ScreenCaptureSource {
pub trait ScreenCaptureStream {}
/// A frame of video captured from a screen.
#[derive(Clone)]
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
/// An opaque identifier for a hardware display

View File

@@ -1,6 +1,6 @@
use crate::{
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
px, size, Pixels, Size,
size, DevicePixels, Size,
};
use anyhow::{anyhow, Result};
use block::ConcreteBlock;
@@ -9,6 +9,10 @@ use cocoa::{
foundation::NSArray,
};
use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth, CGDisplayModeRelease,
};
use ctor::ctor;
use futures::channel::oneshot;
use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
@@ -25,6 +29,7 @@ use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
#[derive(Clone)]
pub struct MacScreenCaptureSource {
sc_display: id,
size: Size<DevicePixels>,
}
pub struct MacScreenCaptureStream {
@@ -43,12 +48,8 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource {
fn resolution(&self) -> Result<Size<Pixels>> {
unsafe {
let width: i64 = msg_send![self.sc_display, width];
let height: i64 = msg_send![self.sc_display, height];
Ok(size(px(width as f32), px(height as f32)))
}
fn resolution(&self) -> Size<DevicePixels> {
self.size
}
fn stream(
@@ -61,13 +62,21 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
let configuration: id = msg_send![class!(SCStreamConfiguration), alloc];
let delegate: id = msg_send![DELEGATE_CLASS, alloc];
let output: id = msg_send![OUTPUT_CLASS, alloc];
let excluded_windows = NSArray::array(nil);
let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows];
let configuration: id = msg_send![configuration, init];
let delegate: id = msg_send![delegate, init];
let output: id = msg_send![output, init];
// ASCII for '420f': https://developer.apple.com/documentation/screencapturekit/scstreamconfiguration/pixelformat?language=objc
let format = u32::from_be_bytes([52u8, 50u8, 48u8, 102u8]);
let _: () = msg_send![configuration, setShowsCursor:YES];
let _: () = msg_send![configuration, setWidth:self.size.width];
let _: () = msg_send![configuration, setHeight:self.size.height];
let _: () = msg_send![configuration, setPixelFormat:format];
let _: () = msg_send![configuration, setQueueDepth:5i32];
output.as_mut().unwrap().set_ivar(
FRAME_CALLBACK_IVAR,
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
@@ -94,6 +103,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
sc_stream: stream,
sc_stream_output: output,
};
Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
} else {
let message: id = msg_send![error, localizedDescription];
@@ -159,8 +169,16 @@ pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptur
let mut result = Vec::new();
for i in 0..displays.count() {
let display = displays.objectAtIndex(i);
let display: id = msg_send![display, retain];
let display_id: CGDirectDisplayID = msg_send![display, displayID];
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
CGDisplayModeRelease(display_mode_ref);
let source = MacScreenCaptureSource {
sc_display: msg_send![display, retain],
sc_display: display,
size: size(DevicePixels(width as i32), DevicePixels(height as i32)),
};
result.push(Box::new(source) as Box<dyn ScreenCaptureSource>);
}

View File

@@ -1,7 +1,8 @@
use crate::{
px, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource,
ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams,
size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
ForegroundExecutor, Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame,
ScreenCaptureSource, ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance,
WindowParams,
};
use anyhow::Result;
use collections::VecDeque;
@@ -46,8 +47,8 @@ pub struct TestScreenCaptureSource {}
pub struct TestScreenCaptureStream {}
impl ScreenCaptureSource for TestScreenCaptureSource {
fn resolution(&self) -> Result<crate::Size<crate::Pixels>> {
Ok(size(px(1.), px(1.)))
fn resolution(&self) -> crate::Size<crate::DevicePixels> {
size(DevicePixels(1), DevicePixels(1))
}
fn stream(

View File

@@ -1068,7 +1068,7 @@ unsafe extern "system" fn wnd_proc(
let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap()));
unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
ctx.inner = Some(creation_result);
return LRESULT(1);
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
}
let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowStatePtr>;
if ptr.is_null() {

View File

@@ -4,8 +4,8 @@ use editor::{scroll::Autoscroll, Editor};
use feature_flags::{FeatureFlagAppExt, ZetaFeatureFlag};
use fs::Fs;
use gpui::{
div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
Render, Subscription, View, ViewContext, WeakView, WindowContext,
actions, div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement,
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
};
use language::{
language_settings::{
@@ -16,7 +16,6 @@ use language::{
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use supermaven::{AccountStatus, Supermaven};
use ui::{Button, LabelSize};
use workspace::{
create_and_open_local_file,
item::ItemHandle,
@@ -29,6 +28,8 @@ use workspace::{
use zed_actions::OpenBrowser;
use zeta::RateCompletionModal;
actions!(zeta, [RateCompletions]);
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
struct CopilotErrorToast;
@@ -204,16 +205,22 @@ impl Render for InlineCompletionButton {
}
div().child(
Button::new("zeta", "ζ")
.label_size(LabelSize::Small)
IconButton::new("zeta", IconName::ZedPredict)
.tooltip(|cx| {
Tooltip::with_meta(
"Zed Predict",
Some(&RateCompletions),
"Click to rate completions",
cx,
)
})
.on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
RateCompletionModal::toggle(workspace, cx)
});
}
}))
.tooltip(|cx| Tooltip::text("Rate Completions", cx)),
})),
)
}
}

View File

@@ -147,7 +147,7 @@ pub trait LanguageModel: Send + Sync {
let events = self.stream_completion(request, cx);
async move {
let mut events = events.await?;
let mut events = events.await?.fuse();
let mut message_id = None;
let mut first_item_text = None;

View File

@@ -1,7 +1,10 @@
use std::sync::Arc;
use feature_flags::ZedPro;
use gpui::{Action, AnyElement, AppContext, DismissEvent, SharedString, Task};
use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
View, WeakView,
};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use picker::{Picker, PickerDelegate};
use proto::Plan;
@@ -12,19 +15,101 @@ const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
#[derive(IntoElement)]
pub struct LanguageModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
on_model_changed: OnModelChanged,
trigger: T,
info_text: Option<SharedString>,
pub struct LanguageModelSelector {
picker: View<Picker<LanguageModelPickerDelegate>>,
}
pub struct LanguageModelPickerDelegate {
on_model_changed: OnModelChanged,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
impl LanguageModelSelector {
pub fn new(
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
cx: &mut ViewContext<Self>,
) -> Self {
let on_model_changed = Arc::new(on_model_changed);
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let icon = provider.icon();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.view().downgrade(),
on_model_changed: on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
LanguageModelSelector { picker }
}
}
impl EventEmitter<DismissEvent> for LanguageModelSelector {}
impl FocusableView for LanguageModelSelector {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for LanguageModelSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(IntoElement)]
pub struct LanguageModelSelectorPopoverMenu<T>
where
T: PopoverTrigger,
{
language_model_selector: View<LanguageModelSelector>,
trigger: T,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
}
impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
pub fn new(language_model_selector: View<LanguageModelSelector>, trigger: T) -> Self {
Self {
language_model_selector,
trigger,
handle: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
self.handle = Some(handle);
self
}
}
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let language_model_selector = self.language_model_selector.clone();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(language_model_selector.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
}
}
#[derive(Clone)]
@@ -32,34 +117,14 @@ struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
impl<T: PopoverTrigger> LanguageModelSelector<T> {
pub fn new(
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
trigger: T,
) -> Self {
LanguageModelSelector {
handle: None,
on_model_changed: Arc::new(on_model_changed),
trigger,
info_text: None,
}
}
pub fn with_handle(
mut self,
handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
) -> Self {
self.handle = Some(handle);
self
}
pub fn info_text(mut self, text: impl Into<SharedString>) -> Self {
self.info_text = Some(text.into());
self
}
pub struct LanguageModelPickerDelegate {
language_model_selector: WeakView<LanguageModelSelector>,
on_model_changed: OnModelChanged,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
}
impl PickerDelegate for LanguageModelPickerDelegate {
@@ -142,23 +207,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let model = model_info.model.clone();
(self.on_model_changed)(model.clone(), cx);
// Update the selection status
let selected_model_id = model_info.model.id();
let selected_provider_id = model_info.model.provider_id();
for model in &mut self.all_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
for model in &mut self.filtered_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.language_model_selector
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let configured_models_count = LanguageModelRegistry::global(cx)
@@ -195,6 +252,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let model_info = self.filtered_models.get(ix)?;
let provider_name: String = model_info.model.provider_name().0.clone().into();
let active_provider_id = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let active_model_id = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
Some(
ListItem::new(ix)
.inset(true)
@@ -235,7 +303,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
}),
),
)
.end_slot(div().when(model_info.is_selected, |this| {
.end_slot(div().when(is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
@@ -296,58 +364,3 @@ impl PickerDelegate for LanguageModelPickerDelegate {
)
}
}
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let selected_model = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
on_model_changed: self.on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
}

View File

@@ -1,3 +1,5 @@
(comment) @comment
[
(addition)
(new_file)
@@ -12,4 +14,35 @@
(location) @attribute
(command) @function
(command
"diff" @function
(argument) @variable.parameter)
(filename) @string.special.path
(mode) @number
([
".."
"+"
"++"
"+++"
"++++"
"-"
"--"
"---"
"----"
] @punctuation.special)
[
(binary_change)
(similarity)
(file_change)
] @label
(index
"index" @keyword)
(similarity
(score) @number
"%" @number)

View File

@@ -1,18 +1,23 @@
; Identifier naming conventions; these "soft conventions" should stay at the top of the file as they're often overridden
; CamelCase for classes
((identifier) @type.class
(#match? @type.class "^_*[A-Z][A-Za-z0-9_]*$"))
; ALL_CAPS for constants:
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z0-9_]*$"))
(attribute attribute: (identifier) @property)
(type (identifier) @type)
(generic_type (identifier) @type)
(comment) @comment
(string) @string
(escape_sequence) @string.escape
; Type alias
(type_alias_statement "type" @keyword)
; Identifier naming conventions
((identifier) @type.class
(#match? @type.class "^[A-Z]"))
((identifier) @constant
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
; TypeVar with constraints in type parameters
(type
(tuple (identifier) @type)
@@ -26,15 +31,20 @@
; Function calls
(decorator
"@" @punctuation.special
(identifier) @function.decorator)
(call
function: (attribute attribute: (identifier) @function.method.call))
(call
function: (identifier) @function.call)
(decorator
"@" @punctuation.special
[
(identifier) @function.decorator
(attribute attribute: (identifier) @function.decorator)
(call function: (identifier) @function.decorator.call)
(call (attribute attribute: (identifier) @function.decorator.call))
])
; Function and class definitions
(function_definition
@@ -47,9 +57,9 @@
(call
function: (identifier) @type.class.call
(#match? @type.class.call "^[A-Z][A-Z0-9_]*[a-z]"))
(#match? @type.class.call "^_*[A-Z][A-Za-z0-9_]*$"))
; Builtin functions
; Builtins
((call
function: (identifier) @function.builtin)
@@ -57,6 +67,9 @@
@function.builtin
"^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
((identifier) @type.builtin
(#any-of? @type.builtin "int" "float" "complex" "bool" "list" "tuple" "range" "str" "bytes" "bytearray" "memoryview" "set" "frozenset" "dict"))
; Literals
[
@@ -79,10 +92,6 @@
(#match? @variable.special "^self|cls$")
]
(comment) @comment
(string) @string
(escape_sequence) @string.escape
[
"("
")"
@@ -114,7 +123,10 @@
. (expression_statement (string) @string.doc))
(module
(expression_statement (assignment))
[
(expression_statement (assignment))
(type_alias_statement)
]
. (expression_statement (string) @string.doc))
(class_definition
@@ -163,6 +175,9 @@
">>"
"|"
"~"
] @operator
[
"and"
"in"
"is"
@@ -170,7 +185,7 @@
"or"
"is not"
"not in"
] @operator
] @keyword.operator
[
"as"
@@ -185,6 +200,7 @@
"elif"
"else"
"except"
"except*"
"exec"
"finally"
"for"

View File

@@ -49,6 +49,7 @@ livekit.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
coreaudio-rs = "0.12.1"
media.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }

View File

@@ -3,11 +3,12 @@
// it causes compile errors.
#![cfg_attr(target_os = "macos", allow(unused_imports))]
use futures::StreamExt;
use gpui::{
actions, bounds, div, point,
prelude::{FluentBuilder as _, IntoElement},
px, rgb, size, AsyncAppContext, Bounds, InteractiveElement, KeyBinding, Menu, MenuItem,
ParentElement, Pixels, Render, ScreenCaptureStream, SharedString,
ParentElement, Pixels, Render, ScreenCaptureFrame, ScreenCaptureStream, SharedString,
StatefulInteractiveElement as _, Styled, Task, View, ViewContext, VisualContext, WindowBounds,
WindowHandle, WindowOptions,
};
@@ -22,6 +23,7 @@ use livekit_client::{
track::{LocalTrack, RemoteTrack, RemoteVideoTrack, TrackSource},
AudioStream, RemoteVideoTrackView, Room, RoomEvent, RoomOptions,
};
use media::core_video::CVImageBuffer;
#[cfg(not(target_os = "windows"))]
use postage::stream::Stream;
@@ -108,6 +110,7 @@ struct LivekitWindow {
screen_share_track: Option<LocalTrackPublication>,
microphone_stream: Option<AudioStream>,
screen_share_stream: Option<Box<dyn ScreenCaptureStream>>,
latest_self_frame: Option<ScreenCaptureFrame>,
#[cfg(not(target_os = "windows"))]
remote_participants: Vec<(ParticipantIdentity, ParticipantState)>,
_events_task: Task<()>,
@@ -156,6 +159,7 @@ impl LivekitWindow {
microphone_stream: None,
screen_share_track: None,
screen_share_stream: None,
latest_self_frame: None,
remote_participants: Vec::new(),
_events_task,
}
@@ -312,7 +316,25 @@ impl LivekitWindow {
cx.spawn(|this, mut cx| async move {
let sources = sources.await.unwrap()?;
let source = sources.into_iter().next().unwrap();
let (track, stream) = capture_local_video_track(&*source).await?;
let (self_stream_tx, mut self_stream_rx) = futures::channel::mpsc::unbounded();
let (track, stream) =
capture_local_video_track(&*source, Some(self_stream_tx)).await?;
cx.spawn({
let this = this.clone();
|mut cx| async move {
while let Some(frame) = self_stream_rx.next().await {
this.update(&mut cx, |this, cx| {
this.latest_self_frame = Some(frame);
cx.notify();
})
.ok();
}
}
})
.detach();
let publication = participant
.publish_track(
LocalTrack::Video(track),
@@ -394,6 +416,11 @@ impl Render for LivekitWindow {
.on_click(cx.listener(|this, _, cx| this.toggle_screen_share(cx))),
]),
)
.children(
self.latest_self_frame
.as_ref()
.map(|frame| gpui::surface(frame.0.clone()).size_full()),
)
.child(
div()
.id("remote-participants")
@@ -403,7 +430,7 @@ impl Render for LivekitWindow {
.flex_grow()
.children(self.remote_participants.iter().map(|(identity, state)| {
div()
.h(px(300.0))
.size_full()
.flex()
.flex_col()
.m_2()

View File

@@ -142,8 +142,9 @@ pub fn init(
#[cfg(not(target_os = "windows"))]
pub async fn capture_local_video_track(
capture_source: &dyn ScreenCaptureSource,
show_capture: Option<futures::channel::mpsc::UnboundedSender<ScreenCaptureFrame>>,
) -> Result<(track::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
let resolution = capture_source.resolution()?;
let resolution = capture_source.resolution();
let track_source = NativeVideoSource::new(VideoResolution {
width: resolution.width.0 as u32,
height: resolution.height.0 as u32,
@@ -153,6 +154,10 @@ pub async fn capture_local_video_track(
.stream({
let track_source = track_source.clone();
Box::new(move |frame| {
if let Some(show_capture) = show_capture.as_ref() {
show_capture.unbounded_send(frame.clone()).unwrap();
}
if let Some(buffer) = video_frame_buffer_to_webrtc(frame) {
track_source.capture_frame(&VideoFrame {
rotation: VideoRotation::VideoRotation0,

View File

@@ -608,6 +608,10 @@ impl LanguageServer {
root_uri: Some(root_uri.clone()),
initialization_options: None,
capabilities: ClientCapabilities {
general: Some(GeneralClientCapabilities {
position_encodings: Some(vec![PositionEncodingKind::UTF16]),
..Default::default()
}),
workspace: Some(WorkspaceClientCapabilities {
configuration: Some(true),
did_change_watched_files: Some(DidChangeWatchedFilesClientCapabilities {
@@ -644,6 +648,7 @@ impl LanguageServer {
will_rename: Some(true),
..Default::default()
}),
apply_edit: Some(true),
..Default::default()
}),
text_document: Some(TextDocumentClientCapabilities {
@@ -760,9 +765,11 @@ impl LanguageServer {
})),
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),
show_message: Some(ShowMessageRequestClientCapabilities {
message_action_item: None,
}),
..Default::default()
}),
general: None,
},
trace: None,
workspace_folders: Some(vec![WorkspaceFolder {
@@ -776,6 +783,7 @@ impl LanguageServer {
}
}),
locale: None,
..Default::default()
}
}

View File

@@ -58,6 +58,7 @@ impl Prettier {
"prettier.config.js",
"prettier.config.cjs",
".editorconfig",
".prettierignore",
];
pub async fn locate_prettier_installation(
@@ -134,6 +135,101 @@ impl Prettier {
}
}
pub async fn locate_prettier_ignore(
fs: &dyn Fs,
prettier_ignores: &HashSet<PathBuf>,
locate_from: &Path,
) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
let mut path_to_check = locate_from
.components()
.take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
.collect::<PathBuf>();
if path_to_check != locate_from {
log::debug!(
"Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
);
return Ok(ControlFlow::Break(()));
}
let path_to_check_metadata = fs
.metadata(&path_to_check)
.await
.with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
.with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
if !path_to_check_metadata.is_dir {
path_to_check.pop();
}
let mut closest_package_json_path = None;
loop {
if prettier_ignores.contains(&path_to_check) {
log::debug!("Found prettier ignore at {path_to_check:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else if let Some(package_json_contents) =
read_package_json(fs, &path_to_check).await?
{
let ignore_path = path_to_check.join(".prettierignore");
if let Some(metadata) = fs
.metadata(&ignore_path)
.await
.with_context(|| format!("fetching metadata for {ignore_path:?}"))?
{
if !metadata.is_dir && !metadata.is_symlink {
log::info!("Found prettier ignore at {ignore_path:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
}
}
match &closest_package_json_path {
None => closest_package_json_path = Some(path_to_check.clone()),
Some(closest_package_json_path) => {
if let Some(serde_json::Value::Array(workspaces)) =
package_json_contents.get("workspaces")
{
let subproject_path = closest_package_json_path
.strip_prefix(&path_to_check)
.expect("traversing path parents, should be able to strip prefix");
if workspaces
.iter()
.filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
log::warn!(
"Skipping non-string 'workspaces' value: {value:?}"
);
None
}
})
.any(|workspace_definition| {
workspace_definition == subproject_path.to_string_lossy()
|| PathMatcher::new(&[workspace_definition])
.ok()
.map_or(false, |path_matcher| {
path_matcher.is_match(subproject_path)
})
})
{
let workspace_ignore = path_to_check.join(".prettierignore");
if let Some(metadata) = fs.metadata(&workspace_ignore).await? {
if !metadata.is_dir {
log::info!("Found prettier ignore at workspace root {workspace_ignore:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
}
}
}
}
}
}
}
if !path_to_check.pop() {
log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn start(
_: LanguageServerId,
@@ -201,6 +297,7 @@ impl Prettier {
&self,
buffer: &Model<Buffer>,
buffer_path: Option<PathBuf>,
ignore_dir: Option<PathBuf>,
cx: &mut AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
@@ -315,11 +412,17 @@ impl Prettier {
}
let ignore_path = ignore_dir.and_then(|dir| {
let ignore_file = dir.join(".prettierignore");
ignore_file.is_file().then_some(ignore_file)
});
log::debug!(
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
"Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
buffer.file().map(|f| f.full_path(cx)),
plugins,
prettier_options,
ignore_path,
);
anyhow::Ok(FormatParams {
@@ -329,6 +432,7 @@ impl Prettier {
plugins,
path: buffer_path,
prettier_options,
ignore_path,
},
})
})?
@@ -449,6 +553,7 @@ struct FormatOptions {
#[serde(rename = "filepath")]
path: Option<PathBuf>,
prettier_options: Option<HashMap<String, serde_json::Value>>,
ignore_path: Option<PathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -840,4 +945,150 @@ mod tests {
},
};
}
#[gpui::test]
async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"project": {
"src": {
"index.js": "// index.js file contents",
"ignored.js": "// this file should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "test-project"
}"#
}
}),
)
.await;
assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/project/src/index.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
"Should find prettierignore in project root"
);
}
#[gpui::test]
async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
cx: &mut gpui::TestAppContext,
) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"monorepo": {
"node_modules": {
"prettier": {
"index.js": "// Dummy prettier package file",
}
},
"packages": {
"web": {
"src": {
"index.js": "// index.js contents",
"ignored.js": "// this should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "web-package"
}"#
}
},
"package.json": r#"{
"workspaces": ["packages/*"],
"devDependencies": {
"prettier": "^2.0.0"
}
}"#
}
}),
)
.await;
assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/index.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find prettierignore in child package"
);
}
#[gpui::test]
async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
cx: &mut gpui::TestAppContext,
) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"monorepo": {
"node_modules": {
"prettier": {
"index.js": "// Dummy prettier package file",
}
},
".prettierignore": "main.js",
"packages": {
"web": {
"src": {
"main.js": "// this should not be ignored",
"ignored.js": "// this should be ignored",
},
".prettierignore": "ignored.js",
"package.json": r#"{
"name": "web-package"
}"#
}
},
"package.json": r#"{
"workspaces": ["packages/*"],
"devDependencies": {
"prettier": "^2.0.0"
}
}"#
}
}),
)
.await;
assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/main.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find child package prettierignore first"
);
assert_eq!(
Prettier::locate_prettier_ignore(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/monorepo/packages/web/src/ignored.js"),
)
.await
.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
"Should find child package prettierignore first"
);
}
}

View File

@@ -44,7 +44,9 @@ class Prettier {
process.exit(1);
}
process.stderr.write(
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`,
`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(
config,
)}\n`,
);
process.stdin.resume();
handleBuffer(new Prettier(prettierPath, prettier, config));
@@ -68,7 +70,9 @@ async function handleBuffer(prettier) {
sendResponse({
id: message.id,
...makeError(
`error during message '${JSON.stringify(errorMessage)}' handling: ${e}`,
`error during message '${JSON.stringify(
errorMessage,
)}' handling: ${e}`,
),
});
});
@@ -189,6 +193,22 @@ async function handleMessage(message, prettier) {
if (params.options.filepath) {
resolvedConfig =
(await prettier.prettier.resolveConfig(params.options.filepath)) || {};
if (params.options.ignorePath) {
const fileInfo = await prettier.prettier.getFileInfo(
params.options.filepath,
{
ignorePath: params.options.ignorePath,
},
);
if (fileInfo.ignored) {
process.stderr.write(
`Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`,
);
sendResponse({ id, result: { text: params.text } });
return;
}
}
}
// Marking the params.options.filepath as undefined makes

View File

@@ -36,6 +36,7 @@ pub struct PrettierStore {
worktree_store: Model<WorktreeStore>,
default_prettier: DefaultPrettier,
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
prettier_ignores_per_worktree: HashMap<WorktreeId, HashSet<PathBuf>>,
prettier_instances: HashMap<PathBuf, PrettierInstance>,
}
@@ -65,11 +66,13 @@ impl PrettierStore {
worktree_store,
default_prettier: DefaultPrettier::default(),
prettiers_per_worktree: HashMap::default(),
prettier_ignores_per_worktree: HashMap::default(),
prettier_instances: HashMap::default(),
}
}
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
self.prettier_ignores_per_worktree.remove(&id_to_remove);
let mut prettier_instances_to_clean = FuturesUnordered::new();
if let Some(prettier_paths) = self.prettiers_per_worktree.remove(&id_to_remove) {
for path in prettier_paths.iter().flatten() {
@@ -211,6 +214,65 @@ impl PrettierStore {
}
}
fn prettier_ignore_for_buffer(
&mut self,
buffer: &Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Option<PathBuf>> {
let buffer = buffer.read(cx);
let buffer_file = buffer.file();
if buffer.language().is_none() {
return Task::ready(None);
}
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx))) {
Some((worktree_id, buffer_path)) => {
let fs = Arc::clone(&self.fs);
let prettier_ignores = self
.prettier_ignores_per_worktree
.get(&worktree_id)
.cloned()
.unwrap_or_default();
cx.spawn(|lsp_store, mut cx| async move {
match cx
.background_executor()
.spawn(async move {
Prettier::locate_prettier_ignore(
fs.as_ref(),
&prettier_ignores,
&buffer_path,
)
.await
})
.await
{
Ok(ControlFlow::Break(())) => None,
Ok(ControlFlow::Continue(None)) => None,
Ok(ControlFlow::Continue(Some(ignore_dir))) => {
log::debug!("Found prettier ignore in {ignore_dir:?}");
lsp_store
.update(&mut cx, |store, _| {
store
.prettier_ignores_per_worktree
.entry(worktree_id)
.or_default()
.insert(ignore_dir.clone());
})
.ok();
Some(ignore_dir)
}
Err(e) => {
log::error!(
"Failed to determine prettier ignore path for buffer: {e:#}"
);
None
}
}
})
}
None => Task::ready(None),
}
}
fn start_prettier(
node: NodeRuntime,
prettier_dir: PathBuf,
@@ -654,6 +716,13 @@ pub(super) async fn format_with_prettier(
.ok()?
.await;
let ignore_dir = prettier_store
.update(cx, |prettier_store, cx| {
prettier_store.prettier_ignore_for_buffer(buffer, cx)
})
.ok()?
.await;
let (prettier_path, prettier_task) = prettier_instance?;
let prettier_description = match prettier_path.as_ref() {
@@ -671,7 +740,7 @@ pub(super) async fn format_with_prettier(
.flatten();
let format_result = prettier
.format(buffer, buffer_path, cx)
.format(buffer, buffer_path, ignore_dir, cx)
.await
.map(crate::lsp_store::FormatOperation::Prettier)
.with_context(|| format!("{} failed to format buffer", prettier_description));

View File

@@ -281,6 +281,7 @@ impl ProjectPanel {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| {
this.focus_out(cx);
this.hide_scrollbar(cx);
})
.detach();
@@ -595,6 +596,12 @@ impl ProjectPanel {
}
}
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
if !self.focus_handle.is_focused(cx) {
self.confirm(&Confirm, cx);
}
}
fn deploy_context_menu(
&mut self,
position: Point<Pixels>,
@@ -3140,6 +3147,8 @@ impl ProjectPanel {
details: EntryDetails,
cx: &mut ViewContext<Self>,
) -> Stateful<Div> {
const GROUP_NAME: &str = "project_entry";
let kind = details.kind;
let settings = ProjectPanelSettings::get_global(cx);
let show_editor = details.is_editing && !details.is_processing;
@@ -3185,8 +3194,37 @@ impl ProjectPanel {
marked_selections: selections,
};
let default_color = if is_marked || is_active {
item_colors.marked_active
} else {
item_colors.default
};
let bg_hover_color = if self.mouse_down {
item_colors.marked_active
} else {
item_colors.hover
};
let border_color =
if !self.mouse_down && is_active && self.focus_handle.contains_focused(cx) {
item_colors.focused
} else if self.mouse_down && is_marked || is_active {
item_colors.marked_active
} else {
item_colors.default
};
div()
.id(entry_id.to_proto() as usize)
.group(GROUP_NAME)
.cursor_pointer()
.rounded_none()
.bg(default_color)
.border_1()
.border_r_2()
.border_color(border_color)
.hover(|style| style.bg(bg_hover_color))
.when(is_local, |div| {
div.on_drag_move::<ExternalPaths>(cx.listener(
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
@@ -3322,12 +3360,11 @@ impl ProjectPanel {
this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
}
}))
.cursor_pointer()
.child(
ListItem::new(entry_id.to_proto() as usize)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.selected(is_marked || is_active)
.selectable(false)
.when_some(canonical_path, |this, path| {
this.end_slot::<AnyElement>(
div()
@@ -3367,13 +3404,11 @@ impl ProjectPanel {
} else {
IconDecorationKind::Dot
},
if is_marked || is_active {
item_colors.marked_active
} else {
item_colors.default
},
default_color,
cx,
)
.group_name(Some(GROUP_NAME.into()))
.knockout_hover_color(bg_hover_color)
.color(decoration_color.color(cx))
.position(Point {
x: px(-2.),
@@ -3489,26 +3524,6 @@ impl ProjectPanel {
))
.overflow_x(),
)
.border_1()
.border_r_2()
.rounded_none()
.hover(|style| {
if is_active {
style
} else {
style.bg(item_colors.hover).border_color(item_colors.hover)
}
})
.when(is_marked || is_active, |this| {
this.when(is_marked, |this| {
this.bg(item_colors.marked_active)
.border_color(item_colors.marked_active)
})
})
.when(
!self.mouse_down && is_active && self.focus_handle.contains_focused(cx),
|this| this.border_color(item_colors.focused),
)
}
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {

View File

@@ -16,6 +16,7 @@ doctest = false
alacritty_terminal.workspace = true
anyhow.workspace = true
async-dispatcher.workspace = true
async-tungstenite = { workspace = true, features = ["async-std", "async-tls"] }
base64.workspace = true
client.workspace = true
collections.workspace = true

View File

@@ -3,6 +3,11 @@ use gpui::{Task, View, WindowContext};
use http_client::{AsyncBody, HttpClient, Request};
use jupyter_protocol::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply};
use async_tungstenite::{
async_std::connect_async,
tungstenite::{client::IntoClientRequest, http::HeaderValue},
};
use futures::StreamExt;
use smol::io::AsyncReadExt as _;
@@ -11,8 +16,8 @@ use crate::Session;
use super::RunningKernel;
use anyhow::Result;
use jupyter_websocket_client::{
JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, KernelSpecsResponse,
RemoteServer,
JupyterWebSocket, JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest,
KernelSpecsResponse, RemoteServer,
};
use std::{fmt::Debug, sync::Arc};
@@ -151,7 +156,31 @@ impl RemoteRunningKernel {
)
.await?;
let (kernel_socket, _response) = remote_server.connect_to_kernel(&kernel_id).await?;
let ws_url = format!(
"{}/api/kernels/{}/channels?token={}",
remote_server.base_url.replace("http", "ws"),
kernel_id,
remote_server.token
);
let mut req: Request<()> = ws_url.into_client_request()?;
let headers = req.headers_mut();
headers.insert(
"User-Agent",
HeaderValue::from_str(&format!(
"Zed/{} ({}; {})",
"repl",
std::env::consts::OS,
std::env::consts::ARCH
))?,
);
let response = connect_async(req).await;
let (ws_stream, _response) = response?;
let kernel_socket = JupyterWebSocket { inner: ws_stream };
let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) =
kernel_socket.split();

View File

@@ -209,6 +209,7 @@ impl Render for BufferSearchBar {
let input_base_styles = || {
h_flex()
.min_w_32()
.w(input_width)
.h_8()
.px_2()
@@ -529,6 +530,11 @@ impl BufferSearchBar {
this.toggle_whole_word(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, cx| {
if this.supported_options().regex {
this.toggle_regex(action, cx);
}
}));
registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, cx| {
if this.supported_options().selection {
this.toggle_selection(action, cx);

View File

@@ -1595,6 +1595,7 @@ impl Render for ProjectSearchBar {
let input_base_styles = || {
h_flex()
.min_w_32()
.w(input_width)
.h_8()
.px_2()

View File

@@ -292,6 +292,7 @@ impl TitleBar {
let is_local = project.is_local() || project.is_via_ssh();
let is_shared = is_local && project.is_shared();
let is_muted = room.is_muted();
let muted_by_user = room.muted_by_user();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_screen_sharing = room.is_screen_sharing();
let can_use_microphone = room.can_use_microphone(cx);
@@ -362,14 +363,20 @@ impl TitleBar {
},
)
.tooltip(move |cx| {
Tooltip::text(
if is_muted {
"Unmute microphone"
if is_muted {
if is_deafened {
Tooltip::with_meta(
"Unmute Microphone",
None,
"Audio will be unmuted",
cx,
)
} else {
"Mute microphone"
},
cx,
)
Tooltip::text("Unmute Microphone", cx)
}
} else {
Tooltip::text("Mute Microphone", cx)
}
})
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
@@ -395,7 +402,23 @@ impl TitleBar {
.icon_size(IconSize::Small)
.selected(is_deafened)
.tooltip(move |cx| {
Tooltip::with_meta("Deafen Audio", None, "Mic will be muted", cx)
if is_deafened {
let label = "Unmute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be unmuted", cx)
} else {
Tooltip::text(label, cx)
}
} else {
let label = "Mute Audio";
if !muted_by_user {
Tooltip::with_meta(label, None, "Microphone will be muted", cx)
} else {
Tooltip::text(label, cx)
}
}
})
.on_click(move |_, cx| toggle_deafen(&Default::default(), cx))
.into_any_element(),

View File

@@ -303,6 +303,7 @@ pub enum IconName {
XCircle,
ZedAssistant,
ZedAssistantFilled,
ZedPredict,
ZedXCopilot,
}
@@ -426,7 +427,9 @@ pub struct IconDecoration {
kind: IconDecorationKind,
color: Hsla,
knockout_color: Hsla,
knockout_hover_color: Hsla,
position: Point<Pixels>,
group_name: Option<SharedString>,
}
impl IconDecoration {
@@ -439,7 +442,9 @@ impl IconDecoration {
kind,
color,
knockout_color,
knockout_hover_color: knockout_color,
position,
group_name: None,
}
}
@@ -464,11 +469,23 @@ impl IconDecoration {
self
}
/// Sets the color of the decoration that is used on hover
pub fn knockout_hover_color(mut self, color: Hsla) -> Self {
self.knockout_hover_color = color;
self
}
/// Sets the position of the decoration
pub fn position(mut self, position: Point<Pixels>) -> Self {
self.position = position;
self
}
/// Sets the name of the group the decoration belongs to
pub fn group_name(mut self, name: Option<SharedString>) -> Self {
self.group_name = name;
self
}
}
impl RenderOnce for IconDecoration {
@@ -497,7 +514,15 @@ impl RenderOnce for IconDecoration {
.right_0()
.size(px(ICON_DECORATION_SIZE))
.path(self.kind.bg().path())
.text_color(self.knockout_color),
.text_color(self.knockout_color)
.when(self.group_name.is_none(), |this| {
this.hover(|style| style.text_color(self.knockout_hover_color))
})
.when_some(self.group_name.clone(), |this, group_name| {
this.group_hover(group_name, |style| {
style.text_color(self.knockout_hover_color)
})
}),
)
}
}

View File

@@ -1,11 +1,11 @@
#[cfg(target_os = "macos")]
#[cfg(any())]
mod macos;
#[cfg(target_os = "macos")]
#[cfg(any())]
pub use macos::*;
#[cfg(not(target_os = "macos"))]
#[cfg(all())]
mod cross_platform;
#[cfg(not(target_os = "macos"))]
#[cfg(all())]
pub use cross_platform::*;

View File

@@ -463,6 +463,7 @@ fn main() {
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
git_ui::init(cx);
vcs_menu::init(cx);
feedback::init(cx);
markdown_preview::init(cx);

View File

@@ -3477,6 +3477,7 @@ mod tests {
language::init(cx);
editor::init(cx);
collab_ui::init(&app_state, cx);
git_ui::init(cx);
project_panel::init((), cx);
outline_panel::init((), cx);
terminal_view::init(cx);

View File

@@ -353,6 +353,7 @@ impl QuickActionBar {
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
Some(
h_flex()
.gap(DynamicSpacing::Base06.rems(cx))
.child(self.render_kernel_selector(cx))
.child(
IconButton::new("toggle_repl_icon", IconName::ReplNeutral)

View File

@@ -18,6 +18,7 @@ test-support = []
[dependencies]
anyhow.workspace = true
arrayvec.workspace = true
client.workspace = true
collections.workspace = true
editor.workspace = true
@@ -37,7 +38,6 @@ similar.workspace = true
telemetry_events.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
@@ -58,7 +58,6 @@ settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-go.workspace = true
tree-sitter-rust.workspace = true
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
call = { workspace = true, features = ["test-support"] }

View File

@@ -8,7 +8,7 @@ use language::{language_settings, OffsetRangeExt};
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, TintColor, Tooltip};
use ui::{prelude::*, KeyBinding, List, ListItem, ListItemSpacing, Tooltip};
use workspace::{ModalView, Workspace};
actions!(
@@ -344,6 +344,7 @@ impl RateCompletionModal {
};
let rated = self.zeta.read(cx).is_completion_rated(completion_id);
let was_shown = self.zeta.read(cx).was_completion_shown(completion_id);
let feedback_empty = active_completion
.feedback_editor
.read(cx)
@@ -369,34 +370,46 @@ impl RateCompletionModal {
.overflow_scroll()
.child(StyledText::new(diff).with_highlights(&text_style, diff_highlights)),
)
.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
.child(
div()
.h_40()
.pt_1()
.bg(bg_color)
.child(active_completion.feedback_editor.clone()),
)
.when_some((!rated).then(|| ()), |this, _| {
this.child(
h_flex()
.p_2()
.gap_2()
.border_y_1()
.border_color(border_color)
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Muted)
)
.child(
div()
.w_full()
.pr_2()
.flex_wrap()
.child(
Label::new("Ensure you explain why this completion is negative or positive. In case it's negative, report what you expected instead.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
)
})
.when_some((!rated).then(|| ()), |this, _| {
this.child(
div()
.h_40()
.pt_1()
.bg(bg_color)
.child(active_completion.feedback_editor.clone())
)
})
.child(
h_flex()
.p_1()
.h_8()
.max_h_8()
.border_t_1()
.border_color(border_color)
.max_w_full()
@@ -409,7 +422,7 @@ impl RateCompletionModal {
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Rated completion").color(Color::Muted)),
.child(Label::new("Rated completion.").color(Color::Muted)),
)
} else if active_completion.completion.edits.is_empty() {
Some(
@@ -419,7 +432,17 @@ impl RateCompletionModal {
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("No edits produced").color(Color::Muted)),
.child(Label::new("No edits produced.").color(Color::Muted)),
)
} else if !was_shown {
Some(
label_container()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Small)
.color(Color::Warning),
)
.child(Label::new("Completion wasn't shown because another valid one was already on screen.")),
)
} else {
Some(label_container())
@@ -434,15 +457,14 @@ impl RateCompletionModal {
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Negative))
.style(ButtonStyle::Filled)
.icon(IconName::ThumbsDown)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Error)
.disabled(rated || feedback_empty)
.when(feedback_empty, |this| {
this.tooltip(|cx| {
Tooltip::text("Explain why this completion is bad before reporting it", cx)
Tooltip::text("Explain what's bad about it before reporting it", cx)
})
})
.on_click(cx.listener(move |this, _, cx| {
@@ -459,11 +481,10 @@ impl RateCompletionModal {
&self.focus_handle(cx),
cx,
))
.style(ButtonStyle::Tinted(TintColor::Positive))
.style(ButtonStyle::Filled)
.icon(IconName::ThumbsUp)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.icon_color(Color::Success)
.disabled(rated)
.on_click(cx.listener(move |this, _, cx| {
this.thumbs_up_active(&ThumbsUpActiveCompletion, cx);
@@ -503,63 +524,83 @@ impl Render for RateCompletionModal {
.rounded_lg()
.shadow_lg()
.child(
div()
.id("completion_list")
v_flex()
.border_r_1()
.border_color(border_color)
.w_96()
.h_full()
.p_0p5()
.overflow_y_scroll()
.flex_shrink_0()
.overflow_hidden()
.child(
List::new()
.empty_message(
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some and rate them!")
.color(Color::Muted),
)
.into_any_element(),
h_flex()
.px_2()
.py_1()
.justify_between()
.border_b_1()
.border_color(border_color)
.child(
Icon::new(IconName::ZedPredict)
.size(IconSize::Small)
)
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id
});
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
.child(
Label::new("From most recent to oldest")
.color(Color::Muted)
.size(LabelSize::Small),
)
)
.child(
div()
.id("completion_list")
.p_0p5()
.h_full()
.overflow_y_scroll()
.child(
List::new()
.empty_message(
div()
.p_2()
.child(
Label::new("No completions yet. Use the editor to generate some and rate them!")
.color(Color::Muted),
)
.into_any_element(),
)
.children(self.zeta.read(cx).recent_completions().cloned().enumerate().map(
|(index, completion)| {
let selected =
self.active_completion.as_ref().map_or(false, |selected| {
selected.completion.id == completion.id
});
let rated =
self.zeta.read(cx).is_completion_rated(completion.id);
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected)
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success)
} else if completion.edits.is_empty() {
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
} else {
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
})
.child(
v_flex()
.child(Label::new(completion.path.to_string_lossy().to_string()).size(LabelSize::Small))
.child(div()
.overflow_hidden()
.text_ellipsis()
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
.color(Color::Muted)
.size(LabelSize::XSmall)
)
ListItem::new(completion.id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected)
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success).size(IconSize::Small)
} else if completion.edits.is_empty() {
Icon::new(IconName::File).color(Color::Muted).size(IconSize::Small)
} else {
Icon::new(IconName::FileDiff).color(Color::Accent).size(IconSize::Small)
})
.child(
v_flex()
.pl_1p5()
.child(Label::new(completion.path.to_string_lossy().to_string()).size(LabelSize::Small))
.child(Label::new(format!("{} ago, {:.2?}", format_time_ago(completion.response_received_at.elapsed()), completion.latency()))
.color(Color::Muted)
.size(LabelSize::XSmall)
)
)
)
.on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), true, cx);
}))
},
)),
.on_click(cx.listener(move |this, _, cx| {
this.select_completion(Some(completion.clone()), true, cx);
}))
},
)),
)
),
)
.children(self.render_active_completion(cx))

View File

@@ -3,6 +3,7 @@ mod rate_completion_modal;
pub use rate_completion_modal::*;
use anyhow::{anyhow, Context as _, Result};
use arrayvec::ArrayVec;
use client::Client;
use collections::{HashMap, HashSet, VecDeque};
use futures::AsyncReadExt;
@@ -29,7 +30,6 @@ use std::{
time::{Duration, Instant},
};
use telemetry_events::InlineCompletionRating;
use util::ResultExt;
use uuid::Uuid;
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
@@ -86,7 +86,7 @@ impl InlineCompletion {
.duration_since(self.request_sent_at)
}
fn interpolate(&self, new_snapshot: BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
let mut edits = Vec::new();
let mut user_edits = new_snapshot
@@ -131,7 +131,11 @@ impl InlineCompletion {
}
}
Some(edits)
if edits.is_empty() {
None
} else {
Some(edits)
}
}
}
@@ -151,6 +155,7 @@ pub struct Zeta {
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
recent_completions: VecDeque<InlineCompletion>,
rated_completions: HashSet<InlineCompletionId>,
shown_completions: HashSet<InlineCompletionId>,
llm_token: LlmApiToken,
_llm_token_subscription: Subscription,
}
@@ -180,6 +185,7 @@ impl Zeta {
events: VecDeque::new(),
recent_completions: VecDeque::new(),
rated_completions: HashSet::default(),
shown_completions: HashSet::default(),
registered_buffers: HashMap::default(),
llm_token: LlmApiToken::default(),
_llm_token_subscription: cx.subscribe(
@@ -329,7 +335,9 @@ impl Zeta {
this.recent_completions
.push_front(inline_completion.clone());
if this.recent_completions.len() > 50 {
this.recent_completions.pop_back();
let completion = this.recent_completions.pop_back().unwrap();
this.shown_completions.remove(&completion.id);
this.rated_completions.remove(&completion.id);
}
cx.notify();
})?;
@@ -665,6 +673,14 @@ and then another
self.rated_completions.contains(&completion_id)
}
pub fn was_completion_shown(&self, completion_id: InlineCompletionId) -> bool {
self.shown_completions.contains(&completion_id)
}
pub fn completion_shown(&mut self, completion_id: InlineCompletionId) {
self.shown_completions.insert(completion_id);
}
pub fn rate_completion(
&mut self,
completion: &InlineCompletion,
@@ -783,7 +799,7 @@ fn prompt_for_excerpt(
}
fn excerpt_range_for_position(point: Point, snapshot: &BufferSnapshot) -> Range<usize> {
const CONTEXT_LINES: u32 = 16;
const CONTEXT_LINES: u32 = 32;
let mut context_lines_before = CONTEXT_LINES;
let mut context_lines_after = CONTEXT_LINES;
@@ -855,25 +871,56 @@ impl Event {
}
}
#[derive(Debug, Clone)]
struct CurrentInlineCompletion {
buffer_id: EntityId,
completion: InlineCompletion,
}
impl CurrentInlineCompletion {
fn should_replace_completion(&self, old_completion: &Self, snapshot: &BufferSnapshot) -> bool {
if self.buffer_id != old_completion.buffer_id {
return true;
}
let Some(old_edits) = old_completion.completion.interpolate(&snapshot) else {
return true;
};
let Some(new_edits) = self.completion.interpolate(&snapshot) else {
return false;
};
if old_edits.len() == 1 && new_edits.len() == 1 {
let (old_range, old_text) = &old_edits[0];
let (new_range, new_text) = &new_edits[0];
new_range == old_range && new_text.starts_with(old_text)
} else {
true
}
}
}
struct PendingCompletion {
id: usize,
_task: Task<Result<()>>,
}
pub struct ZetaInlineCompletionProvider {
zeta: Model<Zeta>,
pending_completions: ArrayVec<PendingCompletion, 2>,
next_pending_completion_id: usize,
current_completion: Option<CurrentInlineCompletion>,
pending_refresh: Task<()>,
}
impl ZetaInlineCompletionProvider {
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(8);
pub fn new(zeta: Model<Zeta>) -> Self {
Self {
zeta,
pending_completions: ArrayVec::new(),
next_pending_completion_id: 0,
current_completion: None,
pending_refresh: Task::ready(()),
}
}
}
@@ -903,34 +950,72 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
debounce: bool,
cx: &mut ModelContext<Self>,
) {
self.pending_refresh =
cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
let pending_completion_id = self.next_pending_completion_id;
self.next_pending_completion_id += 1;
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
})
});
let task = cx.spawn(|this, mut cx| async move {
if debounce {
cx.background_executor().timer(Self::DEBOUNCE_TIMEOUT).await;
}
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = completion_request.await.log_err().map(|completion| {
CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion,
}
});
}
this.update(&mut cx, |this, cx| {
this.current_completion = completion;
cx.notify();
let completion_request = this.update(&mut cx, |this, cx| {
this.zeta.update(cx, |zeta, cx| {
zeta.request_completion(&buffer, position, cx)
})
.ok();
});
let mut completion = None;
if let Ok(completion_request) = completion_request {
completion = Some(CurrentInlineCompletion {
buffer_id: buffer.entity_id(),
completion: completion_request.await?,
});
}
this.update(&mut cx, |this, cx| {
if this.pending_completions[0].id == pending_completion_id {
this.pending_completions.remove(0);
} else {
this.pending_completions.clear();
}
if let Some(new_completion) = completion {
if let Some(old_completion) = this.current_completion.as_ref() {
let snapshot = buffer.read(cx).snapshot();
if new_completion.should_replace_completion(&old_completion, &snapshot) {
this.zeta.update(cx, |zeta, _cx| {
zeta.completion_shown(new_completion.completion.id)
});
this.current_completion = Some(new_completion);
}
} else {
this.zeta.update(cx, |zeta, _cx| {
zeta.completion_shown(new_completion.completion.id)
});
this.current_completion = Some(new_completion);
}
} else {
this.current_completion = None;
}
cx.notify();
})
});
// We always maintain at most two pending completions. When we already
// have two, we replace the newest one.
if self.pending_completions.len() <= 1 {
self.pending_completions.push(PendingCompletion {
id: pending_completion_id,
_task: task,
});
} else if self.pending_completions.len() == 2 {
self.pending_completions.pop();
self.pending_completions.push(PendingCompletion {
id: pending_completion_id,
_task: task,
});
}
}
fn cycle(
@@ -943,9 +1028,12 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
// Right now we don't support cycling.
}
fn accept(&mut self, _cx: &mut ModelContext<Self>) {}
fn accept(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_completions.clear();
}
fn discard(&mut self, _cx: &mut ModelContext<Self>) {
self.pending_completions.clear();
self.current_completion.take();
}
@@ -958,6 +1046,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
let CurrentInlineCompletion {
buffer_id,
completion,
..
} = self.current_completion.as_mut()?;
// Invalidate previous completion if it was generated for a different buffer.
@@ -967,7 +1056,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
}
let buffer = buffer.read(cx);
let Some(edits) = completion.interpolate(buffer.snapshot()) else {
let Some(edits) = completion.interpolate(&buffer.snapshot()) else {
self.current_completion.take();
return None;
};
@@ -1044,7 +1133,7 @@ mod tests {
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1054,7 +1143,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1064,7 +1153,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.undo(cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1074,7 +1163,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1084,7 +1173,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1094,7 +1183,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1104,7 +1193,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1114,7 +1203,7 @@ mod tests {
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
assert_eq!(
from_completion_edits(
&completion.interpolate(buffer.read(cx).snapshot()).unwrap(),
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
&buffer,
cx
),
@@ -1122,7 +1211,7 @@ mod tests {
);
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
assert_eq!(completion.interpolate(buffer.read(cx).snapshot()), None);
assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None);
}
#[gpui::test]

View File

@@ -133,6 +133,38 @@ Define extensions which should be installed (`true`) or never installed (`false`
}
```
## Restore on Startup
- Description: Controls session restoration on startup.
- Setting: `restore_on_startup`
- Default: `last_session`
**Options**
1. Restore all workspaces that were open when quitting Zed:
```json
{
"restore_on_startup": "last_session"
}
```
2. Restore the workspace that was closed last:
```json
{
"restore_on_startup": "last_workspace"
}
```
3. Always start with an empty editor:
```json
{
"restore_on_startup": "none"
}
```
## Autoscroll on Clicks
- Description: Whether to scroll when clicking near the edge of the visible text area.
@@ -435,6 +467,12 @@ List of `string` values
"current_line_highlight": "all"
```
## LSP Highlight Debounce
- Description: The debounce delay before querying highlights from the language server based on the current cursor location.
- Setting: `lsp_highlight_debounce`
- Default: `75`
## Cursor Blink
- Description: Whether or not the cursor blinks.

View File

@@ -13,4 +13,4 @@ path = "src/elixir.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"
zed_extension_api = "0.2.0"

View File

@@ -107,36 +107,85 @@ impl ElixirLs {
}
pub fn label_for_completion(&self, completion: Completion) -> Option<CodeLabel> {
let name = &completion.label;
let detail = completion
.detail
.filter(|detail| detail != "alias")
.map(|detail| format!(": {detail}"))
.unwrap_or("".to_string());
let detail_span = CodeLabelSpan::literal(detail, Some("comment.unused".to_string()));
match completion.kind? {
CompletionKind::Module
| CompletionKind::Class
| CompletionKind::Interface
| CompletionKind::Struct => {
let name = completion.label;
CompletionKind::Module | CompletionKind::Class | CompletionKind::Struct => {
let defmodule = "defmodule ";
let code = format!("{defmodule}{name}");
let alias = completion
.label_details
.and_then(|details| details.description)
.filter(|description| description.starts_with("alias"))
.map(|description| format!(" ({description})"))
.unwrap_or("".to_string());
let code = format!("{defmodule}{name}{alias}");
let name_start = defmodule.len();
let name_end = name_start + name.len();
Some(CodeLabel {
code,
spans: vec![CodeLabelSpan::code_range(
defmodule.len()..defmodule.len() + name.len(),
)],
spans: vec![
CodeLabelSpan::code_range(name_start..name_end),
detail_span,
CodeLabelSpan::code_range(name_end..(name_end + alias.len())),
],
filter_range: (0..name.len()).into(),
})
}
CompletionKind::Interface => Some(CodeLabel {
code: name.to_string(),
spans: vec![CodeLabelSpan::code_range(0..name.len()), detail_span],
filter_range: (0..name.len()).into(),
}),
CompletionKind::Field => Some(CodeLabel {
code: name.to_string(),
spans: vec![
CodeLabelSpan::literal(name, Some("function".to_string())),
detail_span,
],
filter_range: (0..name.len()).into(),
}),
CompletionKind::Function | CompletionKind::Constant => {
let name = completion.label;
let detail = completion
.label_details
.clone()
.and_then(|details| details.detail)
.unwrap_or("".to_string());
let description = completion
.label_details
.clone()
.and_then(|details| details.description)
.map(|description| format!(" ({description})"))
.unwrap_or("".to_string());
let def = "def ";
let code = format!("{def}{name}");
let code = format!("{def}{name}{detail}{description}");
let name_start = def.len();
let name_end = name_start + name.len();
let detail_end = name_end + detail.len();
let description_end = detail_end + description.len();
Some(CodeLabel {
code,
spans: vec![CodeLabelSpan::code_range(def.len()..def.len() + name.len())],
spans: vec![
CodeLabelSpan::code_range(name_start..name_end),
CodeLabelSpan::code_range(name_end..detail_end),
CodeLabelSpan::code_range(detail_end..description_end),
],
filter_range: (0..name.len()).into(),
})
}
CompletionKind::Operator => {
let name = completion.label;
let def_a = "def a ";
let code = format!("{def_a}{name} b");