Compare commits

...

80 Commits

Author SHA1 Message Date
Ben Kunkle
0e6f44d87c wip - vim syntax node objects 2025-09-24 11:01:44 -04:00
Ben Brandt
707a4c7f20 Remove unused editor_model configuration option (#31492)
It seems that this configuration option is no longer used and can be
removed.


Release Notes:

- Removed unused `agent.editor_model` setting
2025-06-03 13:50:33 +00:00
Oleksiy Syvokon
854076f96d agent: Lower "no thread found" logging level to debug (#31972)
This code path is not really an error, as it can happen due to normal,
albeit uncommon, actions. Like, for example, this scenario:

1. Create a thread X in Zed instance A
2. Open Zed instance B
3. Delete the thread X in instance A
4. Close instance B. This will write non-existing thread id X to
`agent-navigation-history.json`
5. Open Zed instance C. It won't be able to load the thread X.

Another way to get into this state is by running Zed with LMDB and
SQLite thread storages side-by-side.

In any case, this is not severe enough for an error.

Closes #ISSUE

Release Notes:

- N/A
2025-06-03 13:27:58 +00:00
90aca
cf931247d0 Add thinking budget for Gemini custom models (#31251)
Closes #31243

As described in my issue, the [thinking
budget](https://ai.google.dev/gemini-api/docs/thinking) gets
automatically chosen by Gemini unless it is specifically set to
something. In order to have fast responses (inline assistant) I prefer
to set it to 0.

Release Notes:

- ai: Added `thinking` mode for custom Google models with configurable
token budget

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-06-03 13:40:20 +02:00
Ben Brandt
b74477d12e Option to auto-close deleted files with no unsaved edits (#31920)
Closes #27982

Release Notes:

- Added `close_on_file_delete` setting (off by default) to allow closing
open files after they have been deleted on disk

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-06-03 13:18:29 +02:00
Fernando Freire
3077abf9cf google_ai: Parse thought parts in Gemini responses (#31925)
Fixes thinking Gemini models.

Closes #31902

Release Notes:

- Updated Google Gemini client to match the latest API
2025-06-03 10:37:06 +00:00
Kiran_Peraka
07dab4e94a multi_buffer: Merge adjacent matches into a single excerpt when separated by only one line (#31708)
Closes #31252

Release Notes:

- Improved displaying of project search matches or diagnostics when the
excerpts are adjacent.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-06-03 10:07:59 +00:00
Umesh Yadav
59686f1f44 language_models: Add images support for Ollama vision models (#31883)
Ollama supports vision to process input images. This PR adds support for
same. I have tested this with gemma3:4b and have attached the screenshot
of it working.

<img width="435" alt="image"
src="https://github.com/user-attachments/assets/5f17d742-0a37-4e6c-b4d8-05b750a0a158"
/>


Release Notes:

- Add image support for [Ollama vision models](https://ollama.com/search?c=vision)
2025-06-03 11:12:59 +02:00
Piotr Osiewicz
a60bea8a3d collab: Reconnect to channel notes (#31950)
Closes #31758

Release Notes:

- Fixed channel notes not getting re-connected when a connection to Zed
servers is restored.

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-06-03 11:12:45 +02:00
THELOSTSOUL
b820aa1fcd Add tool support for DeepSeek (#30223)
[deepseek function call
api](https://api-docs.deepseek.com/guides/function_calling)
has been released and it is same as openai.

Release Notes:

- Added tool calling support for Deepseek Models

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-06-03 10:59:36 +02:00
Piotr Osiewicz
55d91bce53 debugger: Add tooltips to the new process modal (#31953)
Closes #ISSUE

Release Notes:

- N/A
2025-06-03 08:49:56 +00:00
clauses3
b798392050 Expand tilde paths in edit prediction settings (#31235)
Release Notes:

- edit_prediction: Handle `~` in paths in `disabled_globs` setting
2025-06-03 10:32:23 +02:00
Finn Evers
657c8b1084 project_panel: Improve behavior for cut-pasting entries (#31931)
Previously, we would move entries each time they were pasted. Thus, if
you were to cut some files and pasted them in folder `a` and then `b`,
they would only occur in folder `b` and not in folder `a`. This is
unintuitive - e.g. the same does not apply to text and does not happen
in other editors.

This PR improves this behavior - after the first paste of a cut
clipboard, we change the clipboard to a copy clipboard, ensuring that
for all folloing pastes, the entries are not moved again. In the above
example, the files would then also be found in folder `a`. This is also
reflected in the added test.

Release Notes:

- Ensured that cut project panel entries are cut-pasted only on the
first use, and copy-pasted on all subsequent pastes.
2025-06-03 03:51:42 -04:00
Piotr Osiewicz
2bb8aa2f73 go_to_line: Show position relative to current excerpt in a multi-buffer (#31947)
Closes #31515

This PR explicitly leaves the behavior of go to line unspecified with
multi-buffer.

Release Notes:

- Fixed wrong line number being shown in the status bar when in
multi-buffer.
2025-06-03 09:41:45 +02:00
Michael Sloan
beeb42da29 snippets: Show completions on first range in tabstop instead of last (#31939)
Release Notes:

- N/A
2025-06-02 23:56:45 -06:00
thebasilisk
6d66ff1d95 Add Helix implementation for Motion::FindForward and Motion::FindBackward (#31547)
Closes #30462 

Release Notes:

- Added text selection for "vim::PushFindForward" and
"vim::PushFindBackward" keybinds in helix mode
2025-06-02 22:15:21 -06:00
Arseny Kapoulkine
e0b818af62 Fix duplicate prefixes when repeating completions in Vim mode (#31818)
When text is completed, new_text contains the entire new completion
which replaces the old_text. In Vim mode, pressing . repeats the
completion; if InputHandled records the full text and no range to
replace, the entire completion gets appended; this happens after the
completion prefix typing repeats, and we get a duplicate prefix.

Using range to replace has some downsides when the completion is
repeated as a standalone action; in a common case, it should be
sufficient to record the new suffix. This is actually what used to
happen before #28586, which removed this code in a larger attempt to fix
completions at multiple cursors:

```rust
let text = &new_text[common_prefix_len..];
let utf16_range_to_replace = ...

cx.emit(EditorEvent::InputHandled {
    utf16_range_to_replace,
    text: text.into(),
});
```

Fixes #30758
Fixes #31759
Fixes #31779

Release Notes:

- Vim: Fix duplicate prefixes when repeating completions via `.`
2025-06-02 21:34:46 -06:00
Fernando Carletti
58a400b1ee keymap: Fix subword navigation and selection on Sublime Text keymap (#31840)
On Linux, the correct modifier key for this action is `alt`, not `ctrl`.
I mistakenly set it to `ctrl` on #30268.

From Sublime's keymap: 
```json
{ "keys": ["ctrl+left"], "command": "move", "args": {"by": "words", "forward": false} },
{ "keys": ["ctrl+right"], "command": "move", "args": {"by": "word_ends", "forward": true} },
{ "keys": ["ctrl+shift+left"], "command": "move", "args": {"by": "words", "forward": false, "extend": true} },
{ "keys": ["ctrl+shift+right"], "command": "move", "args": {"by": "word_ends", "forward": true, "extend": true} },

{ "keys": ["alt+left"], "command": "move", "args": {"by": "subwords", "forward": false} },
{ "keys": ["alt+right"], "command": "move", "args": {"by": "subword_ends", "forward": true} },
{ "keys": ["alt+shift+left"], "command": "move", "args": {"by": "subwords", "forward": false, "extend": true} },
{ "keys": ["alt+shift+right"], "command": "move", "args": {"by": "subword_ends", "forward": true, "extend": true} },
```

Release Notes:

- N/A
2025-06-02 21:22:27 -06:00
tidely
8ab7d44d51 terminal: Match trait bounds with terminal input (#31441)
The core change here is the following:

```rust
fn write_to_pty(&self, input: impl Into<Vec<u8>>);

// into
fn write_to_pty(&self, input: impl Into<Cow<'static, [u8]>>);
```

This matches the trait bounds that's used by the Alacritty crate. We are
now allowed to effectively pass `&'static str` instead of always needing
a `String`.

The main benefit comes from making the `to_esc_str` function return a
`Cow<'static, str>` instead of `String`. We save an allocation in the
following instances:

- When the user presses any special key that isn't alphanumerical (in
the terminal)
- When the uses presses any key while a modifier is active (in the
terminal)
- When focusing/un-focusing the terminal
- When completing or undoing a terminal transaction
- When starting a terminal assist

This basically saves us an allocation on **every key** press in the
terminal.

NOTE: This same optimization can be done for **nearly all** keypresses
in the entirety of Zed by changing the signature of the `Keystroke`
struct in gpui. If the Zed team is interested in a PR for it, let me
know.

Release Notes:

- N/A
2025-06-02 21:12:28 -06:00
Michael Sloan
56d4c0af9f snippets: Preserve leading whitespace (#31933)
Closes #18481

Release Notes:

- Snippet insertions now preserve leading whitespace instead of using
language-specific auto-indentation.
2025-06-03 02:37:06 +00:00
Michael Sloan
feeda7fa37 Add newlines between messages in LSP RPC logs for more navigability (#31863)
Release Notes:

- N/A
2025-06-03 02:12:58 +00:00
Cole Miller
4a5c55a8f2 debugger: Use new icons for quick debug/spawn button (#31932)
This PR wires up the new icons that were added in #31784.

Release Notes:

- N/A
2025-06-03 01:26:41 +00:00
Cole Miller
7c1ae9bcc3 debugger: Go back to loading task contexts asynchronously for new session modal (#31908)
Release Notes:

- N/A
2025-06-02 21:14:30 -04:00
Cole Miller
6f97da3435 debugger: Align zoom behavior with other panels (#31901)
Release Notes:

- Debugger Beta: `shift-escape` (`workspace::ToggleZoom`) now zooms the
entire debug panel; `alt-shift-escape` (`debugger::ToggleExpandItem`)
triggers the old behavior of zooming a specific item.
2025-06-03 00:59:36 +00:00
Danilo Leal
63c1033448 agent: Generate a notification when reaching tool use limit (#31894)
When reaching the consecutive tool call limit, the agent gets blocked
and without a notification, you wouldn't know that. This PR adds the
ability to be notified when that happens, and you can use either sound
_and_ toast, or just one of them.

Release Notes:

- agent: Added support for getting notified (via toast and/or sound)
when reaching the consecutive tool call limit.
2025-06-02 21:57:42 -03:00
Cole Miller
b16911e756 debugger: Extend f5 binding to contextually rerun the last session (#31753)
Release Notes:

- Debugger Beta: if there is no stopped or running session, `f5` now
reruns the last session, or opens the new session modal if there is no
previously-run session.
2025-06-03 00:35:52 +00:00
Cole Miller
b14401f817 Remove agent_diff key context when agent review ends for an editor (#31930)
Release Notes:

- Fixed an issue that prevented `git::Restore` keybindings from working
in editors for buffers that had previously been modified by the agent.

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-06-02 19:46:04 -04:00
Michael Sloan
17cf865d1e Avoid re-querying language server completions when possible (#31872)
Also adds reuse of the markdown documentation cache even when
completions are re-queried, so that markdown documentation doesn't
flicker when `is_incomplete: true` (completions provided by rust
analyzer always set this)

Release Notes:

- Added support for filtering language server completions instead of
re-querying.
2025-06-02 22:19:09 +00:00
Mikayla Maki
b7ec437b13 Simplify debug launcher UI (#31928)
This PR updates the name of the `NewSessionModal` to `NewProcessModal`
(to reflect it's new purpose), changes the tabs in the modal to read
`Run | Debug | Attach | Launch` and changes the associated types in code
to match the tabs. In addition, this PR adds a few labels to the text
fields in the `Launch` tab, and adds a link to open the associated
settings file. In both debug.json files, added links to the zed.dev
debugger docs.

Release Notes:

- Debugger Beta: Improve the new process modal
2025-06-02 21:24:08 +00:00
Piotr Osiewicz
f1aab1120d terminal: Persist pinned tabs in terminal (#31921)
Closes #31098

Release Notes:

- Fixed terminal pinned tab state not persisting across restarts.
2025-06-02 22:36:57 +02:00
Joe Polny
3f90bc81bd gpui: Filter out NoAction bindings from pending input (#30260)
This prevents the 1 second delay happening on input when all of the
pending bindings are NoAction

Closes #30259

Release Notes:

- Fixed unnecessary delay when typing a multi-stroke binding that
doesn't match any non-null bindings

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-02 13:22:36 -06:00
AidanV
9d5fb3c3f3 Add :delm[arks] {marks} command to delete vim marks (#31140)
Release Notes:

- Implements `:delm[arks] {marks}` specified
[here](https://vimhelp.org/motion.txt.html#%3Adelmarks)
- Adds `ArgumentRequired` action for vim commands that require arguments

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-02 13:18:28 -06:00
Oleksiy Syvokon
864767ad35 agent: Support vim-mode in the agent panel's editor (#31915)
Closes #30081

Release Notes:

- Added vim-mode support in the agent panel's editor

---------

Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-06-02 22:10:31 +03:00
Bennet Bo Fenner
ec69b68e72 indent guides: Fix issue with entirely-whitespace lines (#31916)
Closes #26957

Release Notes:

- Fix an edge case where indent guides would be rendered incorrectly if
lines consisted of entirely whitespace

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-06-02 17:35:00 +00:00
Piotr Osiewicz
9dd18e5ee1 python: Re-land usage of source file path in toolchain picker (#31893)
This reverts commit 1e55e88c18.

Closes #ISSUE

Release Notes:

- Python toolchain selector now uses path to the closest pyproject.toml
as a basis for picking a toolchain. All files under the same
pyproject.toml (in filesystem hierarchy) will share a single virtual
environment. It is possible to have multiple Python virtual environments
selected for disjoint parts of the same project.
2025-06-02 16:29:06 +00:00
Smit Barmase
2ebe16a52f workspace: Fix empty pane becomes unresponsive to keybindings after commit via terminal (#31905)
Closes #27579

This PR fixes issue where keybinding wouldn’t work in a pane after
focusing it from dock using the `ActivatePaneInDirection` action in
certain cases.


https://github.com/user-attachments/assets/9ceca580-a63f-4807-acff-29b61819f424

Release Notes:

- Fixed the issue where keybinding wouldn’t work in a pane after
focusing it from dock using the `ActivatePaneInDirection` action in
certain cases.
2025-06-02 21:52:35 +05:30
Joseph T. Lyons
1ed4647203 Add test for pane: toggle pin tab (#31906)
Also adds the optimization to not move a tab being pinned when its
destination index is the same as its index.

Release Notes:

- N/A
2025-06-02 15:58:10 +00:00
Dino
ebed567adb vim: Handle paste in visual line mode when cursor is at newline (#30791)
This Pull Request fixes the current paste behavior in vim mode, when in
visual mode, and the cursor is at a newline character. Currently this
joins the pasted contents with the line right below it, but in vim this
does not happen, so these changes make it so that Zed's vim mode behaves
the same as vim for this specific case.

Closes #29270 

Release Notes:

- Fixed pasting in vim's visual line mode when cursor is on a newline
character
2025-06-02 09:50:13 -06:00
5brian
a6544c70c5 vim: Fix add empty line (#30987)
Fixes: 

`] space` does not consume counts, and it gets applied to the next
action.

`] space` on an empty line causes cursor to move to the next line.

Release Notes:

- N/A
2025-06-02 09:49:31 -06:00
AidanV
b363e1a482 vim: Add support for :e[dit] {file} command to open files (#31227)
Closes #17786

Release Notes:

- Adds `:e[dit] {file}` command to open files
2025-06-02 09:47:40 -06:00
Umesh Yadav
65e3e84cbc language_models: Add thinking support for ollama (#31665)
This PR updates how we handle Ollama responses, leveraging the new
[v0.9.0](https://github.com/ollama/ollama/releases/tag/v0.9.0) release.
Previously, thinking text was embedded within the model's main content,
leading to it appearing directly in the agent's response. Now, thinking
content is provided as a separate parameter, allowing us to display it
correctly within the agent panel, similar to other providers. I have
tested this with qwen3:8b and works nicely. ~~We can release this once
the ollama is release is stable.~~ It's released now as stable.

<img width="433" alt="image"
src="https://github.com/user-attachments/assets/2983ef06-6679-4033-82c2-231ea9cd6434"
/>


Release Notes:

- Add thinking support for ollama

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-06-02 15:12:41 +00:00
morgankrey
1e1d4430c2 Fixing 404 in AI Configuration Docs (#31899)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-02 09:09:00 -05:00
Oleksiy Syvokon
c874f1fa9d agent: Migrate thread storage to SQLite with zstd compression (#31741)
Previously, LMDB was used for storing threads, but it consumed excessive
disk space and was capped at 1GB.

This change migrates thread storage to an SQLite database. Thread JSON
objects are now compressed using zstd.

I considered training a custom zstd dictionary and storing it in a
separate table. However, the additional complexity outweighed the modest
space savings (up to 20%). I ended up using the default dictionary
stored with data.

Threads can be exported relatively easily from outside the application:

```
$ sqlite3 threads.db "SELECT hex(data) FROM threads LIMIT 5;" |
    xxd -r -p |
    zstd -d |
    fx
```

Benchmarks:
- Original heed database: 200MB
- Sqlite uncompressed: 51MB
- sqlite compressed (this PR): 4.0MB
- sqlite compressed with a trained dictionary: 3.8MB


Release Notes:

- Migrated thread storage to SQLite with compression
2025-06-02 17:01:34 +03:00
Alisina Bahadori
9a9e96ed5a Increase terminal inline assistant block height (#31807)
Closes #31806
Closes #28969

Not sure if a static value is best. Maybe it is better to somehow use
`count_lines` function here too.

### Before
<img width="871" alt="449463234-ab1a33a0-2331-4605-aaee-cae60ddd0f9d"
src="https://github.com/user-attachments/assets/1e3bec86-4cad-426c-9f59-5ad3d14fc9d7"
/>


### After
<img width="861" alt="Screenshot 2025-05-31 at 1 12 33 AM"
src="https://github.com/user-attachments/assets/0c8219a9-0812-45af-8125-1f4294fe2142"
/>

Release Notes:

- Fixed terminal inline assistant clipping when cursor is at bottom of
terminal.
2025-06-02 10:55:40 -03:00
Danilo Leal
8c46e290df docs: Add more details to the agent checkpoint section (#31898)
Figured this was worth highlighting as part of the "Restore Checkpoint"
feature behavior.

Release Notes:

- N/A
2025-06-02 10:55:03 -03:00
Thiago Pacheco
aacbb9c2f4 python: Respect picked toolchain (when it's not at the root) when running tests (#31150)
# Fix Python venv Detection for Test Runner
## Problem
Zed’s Python test runner was not reliably detecting and activating the
project’s Python virtual environment (.venv or venv), causing it to
default to the system Python. This led to issues such as missing
dependencies (e.g., pytest) when running tests.
## Solution
Project Root Awareness: The Python context provider now receives the
project root path, ensuring venv detection always starts from the
project root rather than the test file’s directory.
Robust venv Activation: The test runner now correctly detects and
activates the Python interpreter from .venv or venv in the project root,
setting VIRTUAL_ENV and updating PATH as needed.
Minimal Impact: The change is limited in scope, affecting only the
necessary code paths for Python test runner venv detection. No broad
architectural changes were made.
## Additional Improvements
Updated trait and function signatures to thread the project root path
where needed.
Cleaned up linter warnings and unused code.
## Result
Python tests now reliably run using the project’s virtual environment,
matching the behavior of other IDEs and ensuring all dependencies are
available.

Release Notes:

- Fixed Python tasks always running with a toolchain selected for the
root of a workspace.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-06-02 15:29:34 +02:00
Ben Kunkle
f90333f92e zlog: Check crate name against filters if scope empty (#31892)
Fixes
https://github.com/zed-industries/zed/discussions/29541#discussioncomment-13243073

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-02 13:22:32 +00:00
Ben Kunkle
b24f614ca3 docs: Improve documentation around Vulkan/GPU issues on Linux (#31895)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-06-02 13:16:49 +00:00
Danilo Leal
cefa0cbed8 Improve the file finder picker footer design (#31777)
The current filter icon button in the file finder picker footer confused
me because it is a really a toggleable button that adds a specific
filter. From the icon used, I was expecting more configuration options
rather than just one. Also, I was really wanting a way to trigger it
with the keyboard, even if I need my mouse to initially learn about the
keybinding.

So, this PR transforms that icon button into an actual popover trigger,
in which (for now) there's only one filter option. However, being a menu
is cool because it allows to accomodate more items like, for example,
"Include Git Submodule Files" and others, in the future. Also, there's
now a keybinding that you can hit to open that popover, as well as an
indicator that pops in to communicate that a certain item inside it has
been toggled.

Lastly, also added a keybinding to the "Split" menu in the spirit of
making everything more keyboard accessible!

| Before | After |
|--------|--------|
| ![CleanShot 2025-05-30 at 4  29
57@2x](https://github.com/user-attachments/assets/88a30588-289d-4d76-bb50-0a4e7f72ef84)
| ![CleanShot 2025-05-30 at 4  24
31@2x](https://github.com/user-attachments/assets/30b8f3eb-4d5c-43e1-abad-59d32ed7c89f)
|

Release Notes:

- Improved the keyboard navigability of the file finder filtering
options.
2025-06-02 09:54:15 -03:00
Smit Barmase
3fb1023667 editor: Fix columnar selection incorrectly uses cursor to start selection instead of mouse position (#31888)
Closes #13905

This PR fixes columnar selection to originate from mouse position
instead of current cursor position. Now columnar selection behaves as
same as Sublime Text.

1. Columnar selection from click-and-drag on text (New):


https://github.com/user-attachments/assets/f2e721f4-109f-4d81-a25b-8534065bfb37

2. Columnar selection from click-and-drag on empty space (New): 


https://github.com/user-attachments/assets/c2bb02e9-c006-4193-8d76-097233a47a3c

3. Multi cursors at end of line when no interecting text found (New): 


https://github.com/user-attachments/assets/e47d5ab3-0b5f-4e55-81b3-dfe450f149b5

4. Converting normal selection to columnar selection (Existing):


https://github.com/user-attachments/assets/e5715679-ebae-4f5a-ad17-d29864e14e1e


Release Notes:

- Fixed the issue where the columnar selection (`opt+shift`) incorrectly
used the cursor to start the selection instead of the mouse position.
2025-06-02 16:37:36 +05:30
Umesh Yadav
9c715b470e agent: Show actual file name and icon in context pill (#31813)
Previously in the agent context pill if we added images it showed
generic Image tag on the image context pill. This PR make sure if we
have a path available for a image context show the filename which is in
line with other context pills.


Before | After
--- | ---
![Screenshot 2025-05-31 at 3 14 07
PM](https://github.com/user-attachments/assets/b342f046-2c1c-4c18-bb26-2926933d5d34)
| ![Screenshot 2025-05-31 at 3 14 07
PM](https://github.com/user-attachments/assets/90ad4062-cdc6-4274-b9cd-834b76e8e11b)





Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-06-02 10:35:22 +00:00
Oleksiy Syvokon
ae219e9e99 agent: Fix bug with double-counting tokens in Gemini (#31885)
We report the total number of input tokens by summing the numbers of
1. Prompt tokens
2. Cached tokens

But Google API returns prompt tokens (1) that already include cached
tokens (2), so we were double counting tokens in some cases.

Release Notes:

- Fixed bug with double-counting tokens in Gemini
2025-06-02 10:18:44 +00:00
Bennet Bo Fenner
6d99c12796 assistant_context_editor: Fix copy paste regression (#31882)
Closes #31166

Release Notes:

- Fixed an issue where copying and pasting an assistant response in text
threads would result in duplicate text
2025-06-02 11:52:47 +02:00
Kirill Bulatov
8fb7fa941a Suppress log blade_graphics -related logs by default (#31881)
Release Notes:

- N/A
2025-06-02 08:59:57 +00:00
Joseph T. Lyons
22d75b798e Notify when pinning a tab even if the tab isn't moved (#31880)
The optimization to not move a tab being pinned (when the destination
index is the same as its index) in
https://github.com/zed-industries/zed/pull/31871 caused a regression, as
we were no longer calling `cx.notify()` indirectly through `move_item`.
Thanks for catching this, @smitbarmase.

Release Notes:

- N/A
2025-06-02 06:58:57 +00:00
Smit Barmase
06a199da4d editor: Fix completion accept for optional chaining in Typescript (#31878)
Closes #31662

Currently, we assume `insert_range` will always end at the cursor and
`replace_range` will also always end after the cursor for calculating
range to replace. This is a particular case for the rust-analyzer, but
not widely true for other language servers.

This PR fixes this assumption, and now `insert_range` and
`replace_range` both can end before cursor.

In this particular case:
```ts
let x: string | undefined;

x.tostˇ // here insert as well as replace range is just "." while new_text is "?.toString()"
```

This change makes it such that if final range to replace ends before
cursor, we extend it till the cursor.

Bonus:
- Improves suffix and subsequence matching to use `label` over
`new_text` as `new_text` can contain end characters like `()` or `$`
which is not visible while accepting the completion.
- Make suffix and subsequence check case insensitive.
- Fixes broken subsequence matching which was not considering the order
of characters while matching subsequence.

Release Notes:

- Fixed an issue where autocompleting optional chaining methods in
TypeScript, such as `x.tostr`, would result in `x?.toString()tostr`
instead of `x?.toString()`.
2025-06-02 10:45:40 +05:30
Joseph T. Lyons
ab6125ddde Fix bugs around pinned tabs (#31871)
Closes https://github.com/zed-industries/zed/issues/31870

Release Notes:

- Allowed opening 1 more item if `n` tabs are pinned, where `n` equals
`max_tabs` count.
- Fixed a bug where pinned tabs would eventually be closed out when
exceeding the `max_tabs` count.
- Fixed a bug where a tab could be lost when pinning a tab while at the
`max_tabs` count.
- Fixed a bug where pinning a tab when already at the `max_tabs` limit
could cause other tabs to be incorrectly closed.
2025-06-01 19:50:06 -04:00
Joseph T. Lyons
d3bc561f26 Disable close clean menu item when all are dirty (#31859)
This PR disables the "Close Clean" tab context menu action if all items
are dirty.

<img width="595" alt="SCR-20250601-kaev"
src="https://github.com/user-attachments/assets/add30762-b483-4701-9053-141d2dfe9b05"
/>

<img width="573" alt="SCR-20250601-kahl"
src="https://github.com/user-attachments/assets/24f260e4-01d6-48d6-a6f4-a13ae59c246e"
/>

Also did a bit more general refactoring.

Release Notes:

- N/A
2025-06-01 15:15:33 +00:00
Joseph T. Lyons
f13f2dfb70 Ensure item-closing actions do not panic when no items are present (#31845)
This PR adds a comprehensive test that ensures that no item-closing
action will panic when no items are present. A test already existed
(`test_remove_active_empty `) that ensured `CloseActiveItem` didn't
panic, but the new test covers:

- `CloseActiveItem`
- `CloseInactiveItems`
- `CloseAllItems`
- `CloseCleanItems`
- `CloseItemsToTheRight`
- `CloseItemsToTheLeft`

I plan to do a bit more clean up in `pane.rs` and this feels like a good
thing to add before that.

Release Notes:

- N/A
2025-06-01 03:31:38 +00:00
Joseph T. Lyons
24e4446cd3 Refactor item-closing actions (#31838)
While working on 

- https://github.com/zed-industries/zed/pull/31783
- https://github.com/zed-industries/zed/pull/31786

... I noticed some areas that could be improved through refactoring. The
bug in https://github.com/zed-industries/zed/pull/31783 came from having
duplicate code. The fix had been applied to one version, but not the
duplicated code.

This PR attempts to do some initial clean up, through some refactoring.

Release Notes:

- N/A
2025-05-31 19:38:32 -04:00
Aleksei Gusev
cc536655a1 Fix slowness in Terminal when vi-mode is enabled (#31824)
It seems alacritty handles vi-mode motions in a special way and it is up
to the client to decide when redraw is necessary. With this change,
`TerminalView` notifies the context if a keystroke is processed and vi
mode is enabled.

Fixes #31447

Before:


https://github.com/user-attachments/assets/a78d4ba0-23a3-4660-a834-2f92948f586c

After:


https://github.com/user-attachments/assets/cabbb0f4-a1f9-4f1c-87d8-a56a10e35cc8

Release Notes:

- Fixed sluggish cursor motions in Terminal when Vi Mode is enabled
[#31447]
2025-05-31 20:02:56 +03:00
Kartik Vashistha
2a9e73c65d docs: Update Ansible docs (#31817)
Release Notes:

- N/A
2025-05-31 10:30:29 -04:00
Kirill Bulatov
4f1728e5ee Do not unwrap when updating inline diagnostics (#31814)
Also do not hold the strong editor reference while debounced.

Release Notes:

- N/A
2025-05-31 10:10:15 +00:00
Christian Bergschneider
40c91d5df0 gpui: Implement window_handle and display_handle for wayland platform (#28152)
This PR implements the previously unimplemented window_handle and
display_handle methods in the wayland platform. It also exposes the
display_handle method through the Window struct.

Release Notes:

- N/A
2025-05-30 15:45:03 -07:00
Kirill Bulatov
fe1b36671d Do not error on debugger server connection close (#31795)
Start to show islands of logs in a successful debugger runs for Rust:

<img width="1728" alt="1"
src="https://github.com/user-attachments/assets/7400201b-b900-4b20-8adf-21850ae59671"
/>

<img width="1728" alt="2"
src="https://github.com/user-attachments/assets/e75cc5f1-1f74-41d6-b7aa-697a4b2f055b"
/>

Release Notes:

- N/A
2025-05-30 22:37:40 +00:00
Agus Zubiaga
bb9e2b0403 Fix existing CompletionMode deserialization (#31790)
https://github.com/zed-industries/zed/pull/31668 renamed
`CompletionMode::Max` to `CompletionMode::Burn` which is a good change,
but this broke the deserialization for threads whose completion mode was
stored in LMDB. This adds a deserialization alias so that both values
work.

We could make a full new `SerializedThread` version which migrates this
value, but that seems overkill for this single change, we can batch that
with more changes later. Also, people in nightly already have some v1
threads with `burn` stored, so it wouldn't quite work for everybody.

Release Notes:

- N/A
2025-05-30 19:06:08 -03:00
Yaroslav Pietukhov
4f8d7f0a6b Disallow running Zed with root privileges (#31331)
This will fix a lot of weird problems that are based on file access
issues.

As discussed in
https://github.com/zed-industries/zed/pull/31219#issuecomment-2905371710,
for now it's better to just prevent running Zed with root privileges.

Release Notes:

- Explicitly disallow running Zed with root privileges

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-30 21:22:52 +00:00
Danilo Leal
caf3d30bf6 Harmonize quick action icons (#31784)
Just ensuring they're more harmonized in size, stroke width, and overall
dimensions. Adding here two new "play" icons, too, that will be used in
the context of the debugger.

<img
src="https://github.com/user-attachments/assets/79bcf0b0-e995-4c8e-9c78-0aba32f42f2d"
width="400" />

Release Notes:

- N/A
2025-05-30 18:18:23 -03:00
Peter Tripp
df0cf22347 Add powershell language docs (#31787)
Release Notes:

- N/A
2025-05-30 17:09:55 -04:00
Piotr Osiewicz
a305eda8d1 debugger: Relax implementation of validate_config to not run validation (#31785)
When we moved to schema-based debug configs, we've added validate_config
- a trait method
that is supposed to both validate the configuration and determine
whether it is a launch configuration
or an attach configuration.

The validation bit is a bit problematic though - we received reports on
Discords about
scenarios not starting up properly; it turned out that Javascript's
implementation was overly strict.
Thus, I got rid of any code that tries to validate the config - let's
let the debug adapter itself
decide whether it can digest the configuration or not. validate_config
is now left unimplemented for most
DebugAdapter implementations (except for PHP), because all adapters use
`request`: 'launch'/'attach' for that.
Let's leave the trait method in place though, as nothing guarantees this
to be true for all adapters.

cc @Anthony-Eid

Release Notes:

- debugger: Improved error messages when the debug scenario is not
valid.
- debugger: Fixed cases where valid configs were rejected.
2025-05-30 23:08:41 +02:00
Joseph T. Lyons
ba7b1db054 Mark items as pinned via ! in tests (#31786)
Release Notes:

- N/A
2025-05-30 21:03:31 +00:00
Joseph T. Lyons
019c8ded77 Fix bug where pinned tabs were closed when closing others (#31783)
Closes https://github.com/zed-industries/zed/issues/28166

Release Notes:

- Fixed a bug where pinned tabs were closed when running `Close Others`
from the tab context menu
2025-05-30 20:48:33 +00:00
Fernando Carletti
1704dbea7e keymap: Add subword navigation and selection to Sublime Text keymap (#30268)
For reference, this is what is set in Sublime Text's default-keymap
files for both MacOS and Linux:

```json
{ "keys": ["ctrl+left"], "command": "move", "args": {"by": "words", "forward": false} },
{ "keys": ["ctrl+right"], "command": "move", "args": {"by": "word_ends", "forward": true} },
{ "keys": ["ctrl+shift+left"], "command": "move", "args": {"by": "words", "forward": false, "extend": true} },
{ "keys": ["ctrl+shift+right"], "command": "move", "args": {"by": "word_ends", "forward": true, "extend": true} },
```

Release Notes:

- Add subword navigation and selection to Sublime keymap

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-30 20:38:21 +00:00
Hendrik Sollich
eefa6c4882 Icon theme selector: Don't select last list item when fuzzy searching (#29560)
Adds manual icon-theme selection persistence

Store manually selected icon-themes to maintain selection when query
changes. This allows the theme selector to remember the user's choice
rather than resetting selection when filtering results.

mentioned in #28081 and #28278

Release Notes:

- Improved persistence when selecting themes and icon themes.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-30 20:37:38 +00:00
Alex
1f17df7fb0 debugger: Add support for go tests (#31772)
In the https://github.com/zed-industries/zed/pull/31559 I did not
introduce ability to debug test invocations.
Adding it here. E.g:
![Kapture 2025-05-30 at 19 59
13](https://github.com/user-attachments/assets/1111d4a5-8b0a-42e6-aa98-2d797f61ffe3)

Release Notes:
- Added support for debugging single tests written in go
2025-05-30 22:18:32 +02:00
tidely
6d687a2c2c ollama: Change default context size to 4096 (#31682)
Ollama increased their default context size from 2048 to 4096 tokens in
version v0.6.7, which released over a month ago.

https://github.com/ollama/ollama/releases/tag/v0.6.7

Release Notes:

- ollama: Update default model context to 4096 (matching upstream)
2025-05-30 16:12:39 -04:00
Umesh Yadav
32214abb64 Improve TypeScript shebang detection (#31437)
Closes #13981

Release Notes:

- Improved TypeScript shebang detection
2025-05-30 16:11:13 -04:00
Peter Tripp
a78563b80b ci: Prevent "Tests Pass" job from running on forks (#31778)
Example fork [actions run](https://github.com/alphaArgon/zed/actions/runs/15349715488/job/43194591563)
which would be suppressed in the future.

(Sorry @alphaArgon)

Release Notes:

- N/A
2025-05-30 16:09:22 -04:00
Kirill Bulatov
f881cacd8a Use both language and LSP icons for LSP tasks (#31773)
Make more explicit which language LSP tasks are used.

Before:

![image](https://github.com/user-attachments/assets/27f93c5f-942e-47a0-9b74-2c6d4d6248de)

After:

![image
(1)](https://github.com/user-attachments/assets/5a29fb0a-2e16-4c35-9dda-ae7925eaa034)


![image](https://github.com/user-attachments/assets/d1bf518e-63d1-4ebf-af3d-3c9d464c6532)


Release Notes:

- N/A
2025-05-30 19:28:56 +00:00
Umesh Yadav
a539a38f13 Revert "copilot: Fix vision request detection for follow-up messages" (#31776)
Reverts zed-industries/zed#31760

see this comment for context:
https://github.com/zed-industries/zed/pull/31760#issuecomment-2923158611.

Release Notes:

- N/A
2025-05-30 21:28:31 +02:00
152 changed files with 6719 additions and 3054 deletions

View File

@@ -482,7 +482,9 @@ jobs:
- macos_tests
- windows_clippy
- windows_tests
if: always()
if: |
github.repository_owner == 'zed-industries' &&
always()
steps:
- name: Check all tests passed
run: |

5
Cargo.lock generated
View File

@@ -114,6 +114,7 @@ dependencies = [
"serde_json_lenient",
"settings",
"smol",
"sqlez",
"streaming_diff",
"telemetry",
"telemetry_events",
@@ -133,6 +134,7 @@ dependencies = [
"workspace-hack",
"zed_actions",
"zed_llm_client",
"zstd",
]
[[package]]
@@ -525,6 +527,7 @@ dependencies = [
"fuzzy",
"gpui",
"indexed_docs",
"indoc",
"language",
"language_model",
"languages",
@@ -7070,6 +7073,7 @@ dependencies = [
"image",
"inventory",
"itertools 0.14.0",
"libc",
"log",
"lyon",
"media",
@@ -8758,6 +8762,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"shellexpand 2.1.2",
"smallvec",
"smol",
"streaming-iterator",

View File

@@ -1,5 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3L13 8L4 13V3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 12C2.35977 11.85 1 10.575 1 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.00875 15.2C1.00875 13.625 0.683456 12.275 4.00001 12.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 9C7 10.575 5.62857 11.85 4 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12.2C6.98117 12.2 7 13.625 7 15.2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2.5" y="9" width="3" height="6" rx="1.5" fill="black"/>
<path d="M9 10L13 8L4 3V7.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 813 B

View File

@@ -1,3 +1,8 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 657 B

View File

@@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 998 B

View File

@@ -31,8 +31,6 @@
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::Restart",
"f6": "debugger::Pause",
@@ -583,11 +581,24 @@
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }],
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
// or by tag:
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
"f5": "debugger::RerunLastSession"
}
},
{
"context": "Workspace && debugger_running",
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"bindings": {
"f5": "debugger::Continue"
}
},
{
@@ -873,7 +884,8 @@
"context": "DebugPanel",
"bindings": {
"ctrl-t": "debugger::ToggleThreadPicker",
"ctrl-i": "debugger::ToggleSessionPicker"
"ctrl-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
}
},
{
@@ -928,6 +940,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"bindings": {
"ctrl-shift-a": "file_finder::ToggleSplitMenu",
"ctrl-shift-i": "file_finder::ToggleFilterMenu"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"bindings": {

View File

@@ -4,8 +4,6 @@
"use_key_equivalents": true,
"bindings": {
"f4": "debugger::Start",
"alt-f4": "debugger::RerunLastSession",
"f5": "debugger::Continue",
"shift-f5": "debugger::Stop",
"shift-cmd-f5": "debugger::Restart",
"f6": "debugger::Pause",
@@ -635,7 +633,8 @@
"cmd-k shift-right": "workspace::SwapPaneRight",
"cmd-k shift-up": "workspace::SwapPaneUp",
"cmd-k shift-down": "workspace::SwapPaneDown",
"cmd-shift-x": "zed::Extensions"
"cmd-shift-x": "zed::Extensions",
"f5": "debugger::RerunLastSession"
}
},
{
@@ -652,6 +651,20 @@
// "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
}
},
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction"
}
},
{
"context": "Workspace && debugger_stopped",
"use_key_equivalents": true,
"bindings": {
"f5": "debugger::Continue"
}
},
// Bindings from Sublime Text
{
"context": "Editor",
@@ -936,7 +949,8 @@
"context": "DebugPanel",
"bindings": {
"cmd-t": "debugger::ToggleThreadPicker",
"cmd-i": "debugger::ToggleSessionPicker"
"cmd-i": "debugger::ToggleSessionPicker",
"shift-alt-escape": "debugger::ToggleExpandItem"
}
},
{
@@ -987,6 +1001,14 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "file_finder::ToggleSplitMenu",
"cmd-shift-i": "file_finder::ToggleFilterMenu"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
"use_key_equivalents": true,

View File

@@ -51,7 +51,11 @@
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"alt-right": "editor::MoveToNextSubwordEnd",
"alt-left": "editor::MoveToPreviousSubwordStart",
"alt-shift-right": "editor::SelectToNextSubwordEnd",
"alt-shift-left": "editor::SelectToPreviousSubwordStart"
}
},
{

View File

@@ -53,7 +53,11 @@
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-right": "editor::MoveToNextSubwordEnd",
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart"
}
},
{

View File

@@ -838,6 +838,19 @@
"tab": "editor::AcceptEditPrediction"
}
},
{
"context": "MessageEditor > Editor && VimControl",
"bindings": {
"enter": "agent::Chat",
// TODO: Implement search
"/": null,
"?": null,
"#": null,
"*": null,
"n": null,
"shift-n": null
}
},
{
"context": "os != macos && Editor && edit_prediction_conflict",
"bindings": {

View File

@@ -128,6 +128,8 @@
//
// Default: true
"restore_on_file_reopen": true,
// Whether to automatically close files that have been deleted on disk.
"close_on_file_delete": false,
// Size of the drop target in the editor.
"drop_target_size": 0.2,
// Whether the window should be closed when using 'close active item' on a window with no tabs.
@@ -731,13 +733,6 @@
// The model to use.
"model": "claude-sonnet-4"
},
// The model to use when applying edits from the agent.
"editor_model": {
// The provider to use.
"provider": "zed.dev",
// The model to use.
"model": "claude-sonnet-4"
},
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
// and model are optional, so that you can specify parameters for either one.

View File

@@ -1,3 +1,7 @@
// Some example tasks for common languages.
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[
{
"label": "Debug active PHP file",

View File

@@ -0,0 +1,5 @@
// Project-local debug tasks
//
// For more documentation on how to configure debug tasks,
// see: https://zed.dev/docs/debugger
[]

View File

@@ -46,6 +46,7 @@ git.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
indoc.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
inventory.workspace = true
@@ -78,6 +79,7 @@ serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
smol.workspace = true
sqlez.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
@@ -97,6 +99,7 @@ workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
zstd.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }

View File

@@ -1017,6 +1017,15 @@ impl ActiveThread {
self.play_notification_sound(cx);
self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx);
}
ThreadEvent::ToolUseLimitReached => {
self.play_notification_sound(cx);
self.show_notification(
"Consecutive tool use limit reached.",
IconName::Warning,
window,
cx,
);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
rendered_message.append_text(text, cx);

View File

@@ -1372,6 +1372,7 @@ impl AgentDiff {
| ThreadEvent::ToolFinished { .. }
| ThreadEvent::CheckpointChanged
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing => {}
}
}
@@ -1464,7 +1465,10 @@ impl AgentDiff {
if !AgentSettings::get_global(cx).single_file_review {
for (editor, _) in self.reviewing_editors.drain() {
editor
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();
}
return;
@@ -1560,7 +1564,10 @@ impl AgentDiff {
if in_workspace {
editor
.update(cx, |editor, cx| editor.end_temporary_diff_override(cx))
.update(cx, |editor, cx| {
editor.end_temporary_diff_override(cx);
editor.unregister_addon::<EditorAgentDiffAddon>();
})
.ok();
self.reviewing_editors.remove(&editor);
}

View File

@@ -734,6 +734,7 @@ impl Display for RulesContext {
#[derive(Debug, Clone)]
pub struct ImageContext {
pub project_path: Option<ProjectPath>,
pub full_path: Option<Arc<Path>>,
pub original_image: Arc<gpui::Image>,
// TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
// needed due to a false positive of `clippy::mutable_key_type`.

View File

@@ -14,7 +14,7 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -746,7 +746,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_trigger: CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
@@ -756,13 +756,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
MentionCompletion::try_parse(line, offset_to_line)
});
let Some(state) = state else {
return Task::ready(Ok(None));
return Task::ready(Ok(Vec::new()));
};
let Some((workspace, context_store)) =
self.workspace.upgrade().zip(self.context_store.upgrade())
else {
return Task::ready(Ok(None));
return Task::ready(Ok(Vec::new()));
};
let snapshot = buffer.read(cx).snapshot();
@@ -815,10 +815,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let Some(editor) = editor.upgrade() else {
return Ok(None);
return Ok(Vec::new());
};
Ok(Some(cx.update(|cx| {
let completions = cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
@@ -901,7 +901,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
),
})
.collect()
})?))
})?;
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}

View File

@@ -7,7 +7,7 @@ use assistant_context_editor::AssistantContext;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::Buffer;
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
use project::image_store::is_image_file;
use project::{Project, ProjectItem, ProjectPath, Symbol};
@@ -304,11 +304,13 @@ impl ContextStore {
project.open_image(project_path.clone(), cx)
})?;
let image_item = open_image_task.await?;
let image = image_item.read_with(cx, |image_item, _| image_item.image.clone())?;
this.update(cx, |this, cx| {
let item = image_item.read(cx);
this.insert_image(
Some(image_item.read(cx).project_path(cx)),
image,
Some(item.project_path(cx)),
Some(item.file.full_path(cx).into()),
item.image.clone(),
remove_if_exists,
cx,
)
@@ -317,12 +319,13 @@ impl ContextStore {
}
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
self.insert_image(None, image, false, cx);
self.insert_image(None, None, image, false, cx);
}
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<Arc<Path>>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
@@ -330,6 +333,7 @@ impl ContextStore {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext {
project_path,
full_path,
original_image: image,
image_task,
context_id: self.next_context_id.post_inc(),

View File

@@ -152,7 +152,7 @@ impl HistoryStore {
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.filter_map(|result| result.log_with_level(log::Level::Debug))
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {

View File

@@ -112,6 +112,7 @@ pub(crate) fn create_editor(
editor.set_placeholder_text("Message the agent @ to include context", cx);
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,

View File

@@ -179,18 +179,17 @@ impl TerminalTransaction {
// Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
let input = Self::sanitize_input(hunk);
self.terminal
.update(cx, |terminal, _| terminal.input(input));
.update(cx, |terminal, _| terminal.input(input.into_bytes()));
}
pub fn undo(&self, cx: &mut App) {
self.terminal
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
}
pub fn complete(&self, cx: &mut App) {
self.terminal.update(cx, |terminal, _| {
terminal.input(CARRIAGE_RETURN.to_string())
});
self.terminal
.update(cx, |terminal, _| terminal.input(CARRIAGE_RETURN.as_bytes()));
}
fn sanitize_input(mut input: String) -> String {

View File

@@ -106,7 +106,7 @@ impl TerminalInlineAssistant {
});
let prompt_editor_render = prompt_editor.clone();
let block = terminal_view::BlockProperties {
height: 2,
height: 4,
render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
};
terminal_view.update(cx, |terminal_view, cx| {
@@ -202,7 +202,7 @@ impl TerminalInlineAssistant {
.update(cx, |terminal, cx| {
terminal
.terminal()
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
.update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
})
.log_err();

View File

@@ -1673,6 +1673,7 @@ impl Thread {
}
CompletionRequestStatus::ToolUseLimitReached => {
thread.tool_use_limit_reached = true;
cx.emit(ThreadEvent::ToolUseLimitReached);
}
}
}
@@ -2843,6 +2844,7 @@ pub enum ThreadEvent {
},
CheckpointChanged,
ToolConfirmationNeeded,
ToolUseLimitReached,
CancelEditing,
CompletionCanceled,
}

View File

@@ -1,8 +1,7 @@
use std::borrow::Cow;
use std::cell::{Ref, RefCell};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Context as _, Result, anyhow};
@@ -17,8 +16,7 @@ use gpui::{
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
Subscription, Task, prelude::*,
};
use heed::Database;
use heed::types::SerdeBincode;
use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use project::{Project, ProjectItem, ProjectPath, Worktree};
@@ -35,6 +33,42 @@ use crate::context_server_tool::ContextServerTool;
use crate::thread::{
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
};
use indoc::indoc;
use sqlez::{
bindable::{Bind, Column},
connection::Connection,
statement::Statement,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
Json,
#[serde(rename = "zstd")]
Zstd,
}
impl Bind for DataType {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let value = match self {
DataType::Json => "json",
DataType::Zstd => "zstd",
};
value.bind(statement, start_index)
}
}
impl Column for DataType {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (value, next_index) = String::column(statement, start_index)?;
let data_type = match value.as_str() {
"json" => DataType::Json,
"zstd" => DataType::Zstd,
_ => anyhow::bail!("Unknown data type: {}", value),
};
Ok((data_type, next_index))
}
}
const RULES_FILE_NAMES: [&'static str; 6] = [
".rules",
@@ -866,25 +900,27 @@ impl Global for GlobalThreadsDatabase {}
pub(crate) struct ThreadsDatabase {
executor: BackgroundExecutor,
env: heed::Env,
threads: Database<SerdeBincode<ThreadId>, SerializedThread>,
connection: Arc<Mutex<Connection>>,
}
impl heed::BytesEncode<'_> for SerializedThread {
type EItem = SerializedThread;
impl ThreadsDatabase {
fn connection(&self) -> Arc<Mutex<Connection>> {
self.connection.clone()
}
fn bytes_encode(item: &Self::EItem) -> Result<Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(item).map(Cow::Owned).map_err(Into::into)
const COMPRESSION_LEVEL: i32 = 3;
}
impl Bind for ThreadId {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.to_string().bind(statement, start_index)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThread {
type DItem = SerializedThread;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
// We implement this type manually because we want to call `SerializedThread::from_json`,
// instead of the Deserialize trait implementation for `SerializedThread`.
SerializedThread::from_json(bytes).map_err(Into::into)
impl Column for ThreadId {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (id_str, next_index) = String::column(statement, start_index)?;
Ok((ThreadId::from(id_str.as_str()), next_index))
}
}
@@ -900,8 +936,8 @@ impl ThreadsDatabase {
let database_future = executor
.spawn({
let executor = executor.clone();
let database_path = paths::data_dir().join("threads/threads-db.1.mdb");
async move { ThreadsDatabase::new(database_path, executor) }
let threads_dir = paths::data_dir().join("threads");
async move { ThreadsDatabase::new(threads_dir, executor) }
})
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
.boxed()
@@ -910,41 +946,144 @@ impl ThreadsDatabase {
cx.set_global(GlobalThreadsDatabase(database_future));
}
pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&path)?;
pub fn new(threads_dir: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
std::fs::create_dir_all(&threads_dir)?;
let sqlite_path = threads_dir.join("threads.db");
let mdb_path = threads_dir.join("threads-db.1.mdb");
let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
summary TEXT NOT NULL,
updated_at TEXT NOT NULL,
data_type TEXT NOT NULL,
data BLOB NOT NULL
)
"})?()
.map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
let db = Self {
executor: executor.clone(),
connection: Arc::new(Mutex::new(connection)),
};
if needs_migration_from_heed {
let db_connection = db.connection();
let executor_clone = executor.clone();
executor
.spawn(async move {
log::info!("Starting threads.db migration");
Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?;
std::fs::remove_dir_all(mdb_path)?;
log::info!("threads.db migrated to sqlite");
Ok::<(), anyhow::Error>(())
})
.detach();
}
Ok(db)
}
// Remove this migration after 2025-09-01
fn migrate_from_heed(
mdb_path: &Path,
connection: Arc<Mutex<Connection>>,
_executor: BackgroundExecutor,
) -> Result<()> {
use heed::types::SerdeBincode;
struct SerializedThreadHeed(SerializedThread);
impl heed::BytesEncode<'_> for SerializedThreadHeed {
type EItem = SerializedThreadHeed;
fn bytes_encode(
item: &Self::EItem,
) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {
serde_json::to_vec(&item.0)
.map(std::borrow::Cow::Owned)
.map_err(Into::into)
}
}
impl<'a> heed::BytesDecode<'a> for SerializedThreadHeed {
type DItem = SerializedThreadHeed;
fn bytes_decode(bytes: &'a [u8]) -> Result<Self::DItem, heed::BoxedError> {
SerializedThread::from_json(bytes)
.map(SerializedThreadHeed)
.map_err(Into::into)
}
}
const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
let env = unsafe {
heed::EnvOpenOptions::new()
.map_size(ONE_GB_IN_BYTES)
.max_dbs(1)
.open(path)?
.open(mdb_path)?
};
let mut txn = env.write_txn()?;
let threads = env.create_database(&mut txn, Some("threads"))?;
txn.commit()?;
let txn = env.write_txn()?;
let threads: heed::Database<SerdeBincode<ThreadId>, SerializedThreadHeed> = env
.open_database(&txn, Some("threads"))?
.ok_or_else(|| anyhow!("threads database not found"))?;
Ok(Self {
executor,
env,
threads,
})
for result in threads.iter(&txn)? {
let (thread_id, thread_heed) = result?;
Self::save_thread_sync(&connection, thread_id, thread_heed.0)?;
}
Ok(())
}
fn save_thread_sync(
connection: &Arc<Mutex<Connection>>,
id: ThreadId,
thread: SerializedThread,
) -> Result<()> {
let json_data = serde_json::to_string(&thread)?;
let summary = thread.summary.to_string();
let updated_at = thread.updated_at.to_rfc3339();
let connection = connection.lock().unwrap();
let compressed = zstd::encode_all(json_data.as_bytes(), Self::COMPRESSION_LEVEL)?;
let data_type = DataType::Zstd;
let data = compressed;
let mut insert = connection.exec_bound::<(ThreadId, String, String, DataType, Vec<u8>)>(indoc! {"
INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
"})?;
insert((id, summary, updated_at, data_type, data))?;
Ok(())
}
pub fn list_threads(&self) -> Task<Result<Vec<SerializedThreadMetadata>>> {
let env = self.env.clone();
let threads = self.threads;
let connection = self.connection.clone();
self.executor.spawn(async move {
let txn = env.read_txn()?;
let mut iter = threads.iter(&txn)?;
let connection = connection.lock().unwrap();
let mut select =
connection.select_bound::<(), (ThreadId, String, String)>(indoc! {"
SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC
"})?;
let rows = select(())?;
let mut threads = Vec::new();
while let Some((key, value)) = iter.next().transpose()? {
for (id, summary, updated_at) in rows {
threads.push(SerializedThreadMetadata {
id: key,
summary: value.summary,
updated_at: value.updated_at,
id,
summary: summary.into(),
updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
});
}
@@ -953,36 +1092,51 @@ impl ThreadsDatabase {
}
pub fn try_find_thread(&self, id: ThreadId) -> Task<Result<Option<SerializedThread>>> {
let env = self.env.clone();
let threads = self.threads;
let connection = self.connection.clone();
self.executor.spawn(async move {
let txn = env.read_txn()?;
let thread = threads.get(&txn, &id)?;
Ok(thread)
let connection = connection.lock().unwrap();
let mut select = connection.select_bound::<ThreadId, (DataType, Vec<u8>)>(indoc! {"
SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
"})?;
let rows = select(id)?;
if let Some((data_type, data)) = rows.into_iter().next() {
let json_data = match data_type {
DataType::Zstd => {
let decompressed = zstd::decode_all(&data[..])?;
String::from_utf8(decompressed)?
}
DataType::Json => String::from_utf8(data)?,
};
let thread = SerializedThread::from_json(json_data.as_bytes())?;
Ok(Some(thread))
} else {
Ok(None)
}
})
}
pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
let connection = self.connection.clone();
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.put(&mut txn, &id, &thread)?;
txn.commit()?;
Ok(())
})
self.executor
.spawn(async move { Self::save_thread_sync(&connection, id, thread) })
}
pub fn delete_thread(&self, id: ThreadId) -> Task<Result<()>> {
let env = self.env.clone();
let threads = self.threads;
let connection = self.connection.clone();
self.executor.spawn(async move {
let mut txn = env.write_txn()?;
threads.delete(&mut txn, &id)?;
txn.commit()?;
let connection = connection.lock().unwrap();
let mut delete = connection.exec_bound::<ThreadId>(indoc! {"
DELETE FROM threads WHERE id = ?
"})?;
delete(id)?;
Ok(())
})
}

View File

@@ -304,7 +304,7 @@ impl AddedContext {
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => Some(Self::image(handle)),
AgentContextHandle::Image(handle) => Some(Self::image(handle, cx)),
}
}
@@ -318,7 +318,7 @@ impl AddedContext {
AgentContext::Thread(context) => Self::attached_thread(context),
AgentContext::TextThread(context) => Self::attached_text_thread(context),
AgentContext::Rules(context) => Self::attached_rules(context),
AgentContext::Image(context) => Self::image(context.clone()),
AgentContext::Image(context) => Self::image(context.clone(), cx),
}
}
@@ -333,14 +333,8 @@ impl AddedContext {
fn file(handle: FileContextHandle, full_path: &Path, cx: &App) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
AddedContext {
kind: ContextKind::File,
name,
@@ -370,14 +364,8 @@ impl AddedContext {
fn directory(handle: DirectoryContextHandle, full_path: &Path) -> AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let name = full_path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| full_path_string.clone());
let parent = full_path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
AddedContext {
kind: ContextKind::Directory,
name,
@@ -605,13 +593,23 @@ impl AddedContext {
}
}
fn image(context: ImageContext) -> AddedContext {
fn image(context: ImageContext, cx: &App) -> AddedContext {
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
let icon_path = FileIcons::get_icon(&full_path, cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
};
AddedContext {
kind: ContextKind::Image,
name: "Image".into(),
parent: None,
name,
parent,
tooltip: None,
icon_path: None,
icon_path,
status: match context.status() {
ImageStatus::Loading => ContextStatus::Loading {
message: "Loading…".into(),
@@ -639,6 +637,22 @@ impl AddedContext {
}
}
fn extract_file_name_and_directory_from_full_path(
path: &Path,
name_fallback: &SharedString,
) -> (SharedString, Option<SharedString>) {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned().into())
.unwrap_or_else(|| name_fallback.clone());
let parent = path
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
(name, parent)
}
#[derive(Debug, Clone)]
struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
@@ -765,37 +779,49 @@ impl Component for AddedContext {
let mut next_context_id = ContextId::zero();
let image_ready = (
"Ready",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
}),
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
cx,
),
);
let image_loading = (
"Loading",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
}),
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
},
cx,
),
);
let image_error = (
"Error",
AddedContext::image(ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
}),
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
},
cx,
),
);
Some(

View File

@@ -372,6 +372,8 @@ impl AgentSettingsContent {
None,
None,
Some(language_model.supports_tools()),
Some(language_model.supports_images()),
None,
)),
api_url,
});
@@ -689,6 +691,7 @@ pub struct AgentSettingsContentV2 {
pub enum CompletionMode {
#[default]
Normal,
#[serde(alias = "max")]
Burn,
}

View File

@@ -60,6 +60,7 @@ zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
indoc.workspace = true
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true

View File

@@ -1646,34 +1646,35 @@ impl ContextEditor {
let context = self.context.read(cx);
let mut text = String::new();
for message in context.messages(cx) {
if message.offset_range.start >= selection.range().end {
break;
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if range.is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(range.start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot.point_to_offset(cmp::min(
Point::new(point.row + 1, 0),
snapshot.max_point(),
));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
if message.offset_range.end < selection.range().end {
text.push('\n');
// If selection is empty, we want to copy the entire line
if selection.range().is_empty() {
let snapshot = context.buffer().read(cx).snapshot();
let point = snapshot.offset_to_point(selection.range().start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
for chunk in context.buffer().read(cx).text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for message in context.messages(cx) {
if message.offset_range.start >= selection.range().end {
break;
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
if !range.is_empty() {
for chunk in context.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
if message.offset_range.end < selection.range().end {
text.push('\n');
}
}
}
}
}
(text, CopyMetadata { creases }, vec![selection])
}
@@ -3264,74 +3265,92 @@ mod tests {
use super::*;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
use language::{Buffer, LanguageRegistry};
use pretty_assertions::assert_eq;
use prompt_store::PromptBuilder;
use text::OffsetRangeExt;
use unindent::Unindent;
use util::path;
#[gpui::test]
async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
let (context, context_editor, mut cx) = setup_context_editor_text(vec![
(Role::User, "What is the Zed editor?"),
(
Role::Assistant,
"Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
),
(Role::User, ""),
],cx).await;
// Select & Copy whole user message
assert_copy_paste_context_editor(
&context_editor,
message_range(&context, 0, &mut cx),
indoc! {"
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
What is the Zed editor?
"},
&mut cx,
);
// Select & Copy whole assistant message
assert_copy_paste_context_editor(
&context_editor,
message_range(&context, 1, &mut cx),
indoc! {"
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
What is the Zed editor?
Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
"},
&mut cx,
);
}
#[gpui::test]
async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
cx.update(init_test);
let (context, context_editor, mut cx) = setup_context_editor_text(
vec![
(Role::User, "user1"),
(Role::Assistant, "assistant1"),
(Role::Assistant, "assistant2"),
(Role::User, ""),
],
cx,
)
.await;
let fs = FakeFs::new(cx.executor());
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let cx = &mut VisualTestContext::from_window(*window, cx);
// Copy and paste first assistant message
let message_2_range = message_range(&context, 1, &mut cx);
assert_copy_paste_context_editor(
&context_editor,
message_2_range.start..message_2_range.start,
indoc! {"
user1
assistant1
assistant2
assistant1
"},
&mut cx,
);
let context_editor = window
.update(cx, |_, window, cx| {
cx.new(|cx| {
ContextEditor::for_context(
context,
fs,
workspace.downgrade(),
project,
None,
window,
cx,
)
})
})
.unwrap();
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.set_text("abc\ndef\nghi", window, cx);
editor.move_to_beginning(&Default::default(), window, cx);
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.copy(&Default::default(), window, cx);
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.cut(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\ndef\nghi");
editor.paste(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "abc\nabc\ndef\nghi");
})
});
// Copy and cut second assistant message
let message_3_range = message_range(&context, 2, &mut cx);
assert_copy_paste_context_editor(
&context_editor,
message_3_range.start..message_3_range.start,
indoc! {"
user1
assistant1
assistant2
assistant1
assistant2
"},
&mut cx,
);
}
#[gpui::test]
@@ -3408,6 +3427,129 @@ mod tests {
}
}
async fn setup_context_editor_text(
messages: Vec<(Role, &str)>,
cx: &mut TestAppContext,
) -> (
Entity<AssistantContext>,
Entity<ContextEditor>,
VisualTestContext,
) {
cx.update(init_test);
let fs = FakeFs::new(cx.executor());
let context = create_context_with_messages(messages, cx);
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let mut cx = VisualTestContext::from_window(*window, cx);
let context_editor = window
.update(&mut cx, |_, window, cx| {
cx.new(|cx| {
let editor = ContextEditor::for_context(
context.clone(),
fs,
workspace.downgrade(),
project,
None,
window,
cx,
);
editor
})
})
.unwrap();
(context, context_editor, cx)
}
fn message_range(
context: &Entity<AssistantContext>,
message_ix: usize,
cx: &mut TestAppContext,
) -> Range<usize> {
context.update(cx, |context, cx| {
context
.messages(cx)
.nth(message_ix)
.unwrap()
.anchor_range
.to_offset(&context.buffer().read(cx).snapshot())
})
}
fn assert_copy_paste_context_editor<T: editor::ToOffset>(
context_editor: &Entity<ContextEditor>,
range: Range<T>,
expected_text: &str,
cx: &mut VisualTestContext,
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
});
context_editor.copy(&Default::default(), window, cx);
context_editor.editor.update(cx, |editor, cx| {
editor.move_to_end(&Default::default(), window, cx);
});
context_editor.paste(&Default::default(), window, cx);
context_editor.editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), expected_text);
});
});
}
fn create_context_with_messages(
mut messages: Vec<(Role, &str)>,
cx: &mut TestAppContext,
) -> Entity<AssistantContext> {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
cx.new(|cx| {
let mut context = AssistantContext::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
);
let mut message_1 = context.messages(cx).next().unwrap();
let (role, text) = messages.remove(0);
loop {
if role == message_1.role {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(message_1.offset_range, text)], None, cx);
});
break;
}
let mut ids = HashSet::default();
ids.insert(message_1.id);
context.cycle_message_roles(ids, cx);
message_1 = context.messages(cx).next().unwrap();
}
let mut last_message_id = message_1.id;
for (role, text) in messages {
context.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
let message = context.messages(cx).last().unwrap();
last_message_id = message.id;
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(message.offset_range, text)], None, cx);
})
}
context
})
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
prompt_store::init(cx);

View File

@@ -48,7 +48,7 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Option<Vec<project::Completion>>>> {
) -> Task<Result<Vec<project::CompletionResponse>>> {
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
.command_names(cx)
@@ -71,28 +71,27 @@ impl SlashCommandCompletionProvider {
.await;
cx.update(|_, cx| {
Some(
matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
let completions = matches
.into_iter()
.filter_map(|mat| {
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
if requires_argument || accepts_arguments {
new_text.push(' ');
}
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
@@ -118,22 +117,27 @@ impl SlashCommandCompletionProvider {
}
},
) as Arc<_>
});
Some(project::Completion {
replace_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
icon_path: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
})
});
Some(project::Completion {
replace_range: name_range.clone(),
documentation: Some(CompletionDocumentation::SingleLine(
command.description().into(),
)),
new_text,
label: command.label(cx),
icon_path: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
})
.collect(),
)
})
.collect();
vec![project::CompletionResponse {
completions,
is_incomplete: false,
}]
})
})
}
@@ -147,7 +151,7 @@ impl SlashCommandCompletionProvider {
last_argument_range: Range<Anchor>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<Option<Vec<project::Completion>>>> {
) -> Task<Result<Vec<project::CompletionResponse>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
@@ -165,28 +169,27 @@ impl SlashCommandCompletionProvider {
let workspace = self.workspace.clone();
let arguments = arguments.to_vec();
cx.background_spawn(async move {
Ok(Some(
completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let completions = completions
.await?
.into_iter()
.map(|new_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let mut completed_arguments = arguments.clone();
if new_argument.replace_previous_arguments {
completed_arguments.clear();
} else {
completed_arguments.pop();
}
completed_arguments.push(new_argument.new_text.clone());
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
let command_range = command_range.clone();
let command_name = command_name.clone();
move |intent: CompletionIntent,
window: &mut Window,
cx: &mut App| {
if new_argument.after_completion.run()
@@ -210,34 +213,41 @@ impl SlashCommandCompletionProvider {
!new_argument.after_completion.run()
}
}
}) as Arc<_>
});
}) as Arc<_>
});
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
let mut new_text = new_argument.new_text.clone();
if new_argument.after_completion == AfterCompletion::Continue {
new_text.push(' ');
}
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,
}
})
.collect(),
))
project::Completion {
replace_range: if new_argument.replace_previous_arguments {
argument_range.clone()
} else {
last_argument_range.clone()
},
label: new_argument.label,
icon_path: None,
new_text,
documentation: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,
}
})
.collect();
Ok(vec![project::CompletionResponse {
completions,
is_incomplete: false,
}])
})
} else {
Task::ready(Ok(Some(Vec::new())))
Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
is_incomplete: false,
}]))
}
}
}
@@ -251,7 +261,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
_: editor::CompletionContext,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<project::Completion>>>> {
) -> Task<Result<Vec<project::CompletionResponse>>> {
let Some((name, arguments, command_range, last_argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
@@ -295,7 +305,10 @@ impl CompletionProvider for SlashCommandCompletionProvider {
Some((name, arguments, command_range, last_argument_range))
})
else {
return Task::ready(Ok(Some(Vec::new())));
return Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
is_incomplete: false,
}]));
};
if let Some((arguments, argument_range)) = arguments {

View File

@@ -35,6 +35,7 @@ pub struct ChannelBuffer {
pub enum ChannelBufferEvent {
CollaboratorsChanged,
Disconnected,
Connected,
BufferEdited,
ChannelChanged,
}
@@ -103,6 +104,17 @@ impl ChannelBuffer {
}
}
pub fn connected(&mut self, cx: &mut Context<Self>) {
self.connected = true;
if self.subscription.is_none() {
let Ok(subscription) = self.client.subscribe_to_entity(self.channel_id.0) else {
return;
};
self.subscription = Some(subscription.set_entity(&cx.entity(), &mut cx.to_async()));
cx.emit(ChannelBufferEvent::Connected);
}
}
pub fn remote_id(&self, cx: &App) -> BufferId {
self.buffer.read(cx).remote_id()
}

View File

@@ -972,6 +972,7 @@ impl ChannelStore {
.log_err();
if let Some(operations) = operations {
channel_buffer.connected(cx);
let client = this.client.clone();
cx.background_spawn(async move {
let operations = operations.await;
@@ -1012,8 +1013,8 @@ impl ChannelStore {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
for (_, buffer) in this.opened_buffers.drain() {
if let OpenEntityHandle::Open(buffer) = buffer {
for (_, buffer) in &this.opened_buffers {
if let OpenEntityHandle::Open(buffer) = &buffer {
if let Some(buffer) = buffer.upgrade() {
buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
}

View File

@@ -1010,7 +1010,6 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.close_inactive_items(&Default::default(), window, cx)
.unwrap()
.detach();
});
});

View File

@@ -354,6 +354,10 @@ impl ChannelView {
editor.set_read_only(true);
cx.notify();
}),
ChannelBufferEvent::Connected => self.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
cx.notify();
}),
ChannelBufferEvent::ChannelChanged => {
self.editor.update(cx, |_, cx| {
cx.emit(editor::EditorEvent::TitleChanged);

View File

@@ -12,7 +12,7 @@ use language::{
Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
language_settings::SoftWrap,
};
use project::{Completion, CompletionSource, search::SearchQuery};
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
use settings::Settings;
use std::{
cell::RefCell,
@@ -64,9 +64,9 @@ impl CompletionProvider for MessageEditorCompletionProvider {
_: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(handle) = self.0.upgrade() else {
return Task::ready(Ok(None));
return Task::ready(Ok(Vec::new()));
};
handle.update(cx, |message_editor, cx| {
message_editor.completions(buffer, buffer_position, cx)
@@ -248,22 +248,21 @@ impl MessageEditor {
buffer: &Entity<Buffer>,
end_anchor: Anchor,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
if let Some((start_anchor, query, candidates)) =
self.collect_mention_candidates(buffer, end_anchor, cx)
{
if !candidates.is_empty() {
return cx.spawn(async move |_, cx| {
Ok(Some(
Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
&candidates,
start_anchor..end_anchor,
Self::completion_for_mention,
)
.await,
))
let completion_response = Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
&candidates,
start_anchor..end_anchor,
Self::completion_for_mention,
)
.await;
Ok(vec![completion_response])
});
}
}
@@ -273,21 +272,23 @@ impl MessageEditor {
{
if !candidates.is_empty() {
return cx.spawn(async move |_, cx| {
Ok(Some(
Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
candidates,
start_anchor..end_anchor,
Self::completion_for_emoji,
)
.await,
))
let completion_response = Self::resolve_completions_for_candidates(
&cx,
query.as_str(),
candidates,
start_anchor..end_anchor,
Self::completion_for_emoji,
)
.await;
Ok(vec![completion_response])
});
}
}
Task::ready(Ok(Some(Vec::new())))
Task::ready(Ok(vec![CompletionResponse {
completions: Vec::new(),
is_incomplete: false,
}]))
}
async fn resolve_completions_for_candidates(
@@ -296,18 +297,19 @@ impl MessageEditor {
candidates: &[StringMatchCandidate],
range: Range<Anchor>,
completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel),
) -> Vec<Completion> {
) -> CompletionResponse {
const LIMIT: usize = 10;
let matches = fuzzy::match_strings(
candidates,
query,
true,
10,
LIMIT,
&Default::default(),
cx.background_executor().clone(),
)
.await;
matches
let completions = matches
.into_iter()
.map(|mat| {
let (new_text, label) = completion_fn(&mat);
@@ -322,7 +324,12 @@ impl MessageEditor {
source: CompletionSource::Custom,
}
})
.collect()
.collect::<Vec<_>>();
CompletionResponse {
is_incomplete: completions.len() >= LIMIT,
completions,
}
}
fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) {

View File

@@ -581,7 +581,7 @@ async fn stream_completion(
api_key: String,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.iter().any(|message| match message {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
@@ -736,116 +736,4 @@ mod tests {
assert_eq!(schema.data[0].id, "gpt-4");
assert_eq!(schema.data[1].id, "claude-3.7-sonnet");
}
#[test]
fn test_vision_request_detection() {
fn message_contains_image(message: &ChatMessage) -> bool {
match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if
parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
}
}
// Helper function to detect if a request is a vision request
fn is_vision_request(request: &Request) -> bool {
request.messages.iter().any(message_contains_image)
}
let request_with_image_in_last = Request {
intent: true,
n: 1,
stream: true,
temperature: 0.1,
model: "claude-3.7-sonnet".to_string(),
messages: vec![
ChatMessage::User {
content: ChatMessageContent::Plain("Hello".to_string()),
},
ChatMessage::Assistant {
content: ChatMessageContent::Plain("How can I help?".to_string()),
tool_calls: vec![],
},
ChatMessage::User {
content: ChatMessageContent::Multipart(vec![
ChatMessagePart::Text {
text: "What's in this image?".to_string(),
},
ChatMessagePart::Image {
image_url: ImageUrl {
url: "data:image/png;base64,abc123".to_string(),
},
},
]),
},
],
tools: vec![],
tool_choice: None,
};
let request_with_image_in_earlier = Request {
intent: true,
n: 1,
stream: true,
temperature: 0.1,
model: "claude-3.7-sonnet".to_string(),
messages: vec![
ChatMessage::User {
content: ChatMessageContent::Plain("Hello".to_string()),
},
ChatMessage::User {
content: ChatMessageContent::Multipart(vec![
ChatMessagePart::Text {
text: "What's in this image?".to_string(),
},
ChatMessagePart::Image {
image_url: ImageUrl {
url: "data:image/png;base64,abc123".to_string(),
},
},
]),
},
ChatMessage::Assistant {
content: ChatMessageContent::Plain("I see a cat in the image.".to_string()),
tool_calls: vec![],
},
ChatMessage::User {
content: ChatMessageContent::Plain("What color is it?".to_string()),
},
],
tools: vec![],
tool_choice: None,
};
let request_with_no_images = Request {
intent: true,
n: 1,
stream: true,
temperature: 0.1,
model: "claude-3.7-sonnet".to_string(),
messages: vec![
ChatMessage::User {
content: ChatMessageContent::Plain("Hello".to_string()),
},
ChatMessage::Assistant {
content: ChatMessageContent::Plain("How can I help?".to_string()),
tool_calls: vec![],
},
ChatMessage::User {
content: ChatMessageContent::Plain("Tell me about Rust.".to_string()),
},
],
tools: vec![],
tool_choice: None,
};
assert!(is_vision_request(&request_with_image_in_last));
assert!(is_vision_request(&request_with_image_in_earlier));
assert!(!is_vision_request(&request_with_no_images));
}
}

View File

@@ -370,21 +370,19 @@ pub trait DebugAdapter: 'static + Send + Sync {
None
}
fn validate_config(
/// Extracts the kind (attach/launch) of debug configuration from the given JSON config.
/// This method should only return error when the kind cannot be determined for a given configuration;
/// in particular, it *should not* validate whether the request as a whole is valid, because that's best left to the debug adapter itself to decide.
fn request_kind(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
match config.get("request") {
Some(val) if val == "launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
Some(val) if val == "attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!(
"missing or invalid `request` field in config. Expected 'launch' or 'attach'"
)),
}
}
@@ -414,7 +412,7 @@ impl DebugAdapter for FakeAdapter {
serde_json::Value::Null
}
fn validate_config(
fn request_kind(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
@@ -459,7 +457,7 @@ impl DebugAdapter for FakeAdapter {
envs: HashMap::default(),
cwd: None,
request_args: StartDebuggingRequestArguments {
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
configuration: task_definition.config.clone(),
},
})

View File

@@ -52,7 +52,7 @@ pub fn send_telemetry(scenario: &DebugScenario, location: TelemetrySpawnLocation
return;
};
let kind = adapter
.validate_config(&scenario.config)
.request_kind(&scenario.config)
.ok()
.map(serde_json::to_value)
.and_then(Result::ok);

View File

@@ -4,7 +4,7 @@ use dap_types::{
messages::{Message, Response},
};
use futures::{AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _, channel::oneshot, select};
use gpui::AsyncApp;
use gpui::{AppContext as _, AsyncApp, Task};
use settings::Settings as _;
use smallvec::SmallVec;
use smol::{
@@ -22,7 +22,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::{ResultExt as _, TryFutureExt};
use util::{ConnectionResult, ResultExt as _};
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
@@ -126,7 +126,7 @@ pub(crate) struct TransportDelegate {
pending_requests: Requests,
transport: Transport,
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
_tasks: Vec<gpui::Task<Option<()>>>,
_tasks: Vec<Task<()>>,
}
impl TransportDelegate {
@@ -141,7 +141,7 @@ impl TransportDelegate {
log_handlers: Default::default(),
current_requests: Default::default(),
pending_requests: Default::default(),
_tasks: Default::default(),
_tasks: Vec::new(),
};
let messages = this.start_handlers(transport_pipes, cx).await?;
Ok((messages, this))
@@ -166,45 +166,76 @@ impl TransportDelegate {
None
};
let adapter_log_handler = log_handler.clone();
cx.update(|cx| {
if let Some(stdout) = params.stdout.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()).log_err()),
);
self._tasks.push(cx.background_spawn(async move {
match Self::handle_adapter_log(stdout, adapter_log_handler).await {
ConnectionResult::Timeout => {
log::error!("Timed out when handling debugger log");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger logs connection closed");
}
ConnectionResult::Result(Ok(())) => {}
ConnectionResult::Result(Err(e)) => {
log::error!("Error handling debugger log: {e}");
}
}
}));
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_output(
params.output,
client_tx,
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
let pending_requests = self.pending_requests.clone();
let output_log_handler = log_handler.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_output(
params.output,
client_tx,
pending_requests,
output_log_handler,
)
.await
{
Ok(()) => {}
Err(e) => log::error!("Error handling debugger output: {e}"),
}
}));
if let Some(stderr) = params.stderr.take() {
self._tasks.push(
cx.background_executor()
.spawn(Self::handle_error(stderr, self.log_handlers.clone()).log_err()),
);
let log_handlers = self.log_handlers.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_error(stderr, log_handlers).await {
ConnectionResult::Timeout => {
log::error!("Timed out reading debugger error stream")
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed its error stream")
}
ConnectionResult::Result(Ok(())) => {}
ConnectionResult::Result(Err(e)) => {
log::error!("Error handling debugger error: {e}")
}
}
}));
}
self._tasks.push(
cx.background_executor().spawn(
Self::handle_input(
params.input,
client_rx,
self.current_requests.clone(),
self.pending_requests.clone(),
log_handler.clone(),
)
.log_err(),
),
);
let current_requests = self.current_requests.clone();
let pending_requests = self.pending_requests.clone();
let log_handler = log_handler.clone();
self._tasks.push(cx.background_spawn(async move {
match Self::handle_input(
params.input,
client_rx,
current_requests,
pending_requests,
log_handler,
)
.await
{
Ok(()) => {}
Err(e) => log::error!("Error handling debugger input: {e}"),
}
}));
})?;
{
@@ -235,7 +266,7 @@ impl TransportDelegate {
async fn handle_adapter_log<Stdout>(
stdout: Stdout,
log_handlers: Option<LogHandlers>,
) -> Result<()>
) -> ConnectionResult<()>
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
@@ -245,13 +276,14 @@ impl TransportDelegate {
let result = loop {
line.truncate(0);
let bytes_read = match reader.read_line(&mut line).await {
Ok(bytes_read) => bytes_read,
Err(e) => break Err(e.into()),
};
if bytes_read == 0 {
anyhow::bail!("Debugger log stream closed");
match reader
.read_line(&mut line)
.await
.context("reading adapter log line")
{
Ok(0) => break ConnectionResult::ConnectionReset,
Ok(_) => {}
Err(e) => break ConnectionResult::Result(Err(e)),
}
if let Some(log_handlers) = log_handlers.as_ref() {
@@ -337,35 +369,35 @@ impl TransportDelegate {
let mut reader = BufReader::new(server_stdout);
let result = loop {
let message =
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await;
match message {
Ok(Message::Response(res)) => {
match Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
.await
{
ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"),
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
return Ok(());
}
ConnectionResult::Result(Ok(Message::Response(res))) => {
if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
if let Err(e) = tx.send(Self::process_response(res)) {
log::trace!("Did not send response `{:?}` for a cancelled", e);
}
} else {
client_tx.send(Message::Response(res)).await?;
};
}
}
Ok(message) => {
client_tx.send(message).await?;
}
Err(e) => break Err(e),
ConnectionResult::Result(Ok(message)) => client_tx.send(message).await?,
ConnectionResult::Result(Err(e)) => break Err(e),
}
};
drop(client_tx);
log::debug!("Handle adapter output dropped");
result
}
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()>
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> ConnectionResult<()>
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
@@ -375,8 +407,12 @@ impl TransportDelegate {
let mut reader = BufReader::new(stderr);
let result = loop {
match reader.read_line(&mut buffer).await {
Ok(0) => anyhow::bail!("debugger error stream closed"),
match reader
.read_line(&mut buffer)
.await
.context("reading error log line")
{
Ok(0) => break ConnectionResult::ConnectionReset,
Ok(_) => {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Adapter) {
@@ -386,7 +422,7 @@ impl TransportDelegate {
buffer.truncate(0);
}
Err(error) => break Err(error.into()),
Err(error) => break ConnectionResult::Result(Err(error)),
}
};
@@ -420,7 +456,7 @@ impl TransportDelegate {
reader: &mut BufReader<Stdout>,
buffer: &mut String,
log_handlers: Option<&LogHandlers>,
) -> Result<Message>
) -> ConnectionResult<Message>
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
@@ -428,48 +464,58 @@ impl TransportDelegate {
loop {
buffer.truncate(0);
if reader
match reader
.read_line(buffer)
.await
.with_context(|| "reading a message from server")?
== 0
.with_context(|| "reading a message from server")
{
anyhow::bail!("debugger reader stream closed, last string output: '{buffer}'");
Ok(0) => return ConnectionResult::ConnectionReset,
Ok(_) => {}
Err(e) => return ConnectionResult::Result(Err(e)),
};
if buffer == "\r\n" {
break;
}
let parts = buffer.trim().split_once(": ");
match parts {
Some(("Content-Length", value)) => {
content_length = Some(value.parse().context("invalid content length")?);
if let Some(("Content-Length", value)) = buffer.trim().split_once(": ") {
match value.parse().context("invalid content length") {
Ok(length) => content_length = Some(length),
Err(e) => return ConnectionResult::Result(Err(e)),
}
_ => {}
}
}
let content_length = content_length.context("missing content length")?;
let content_length = match content_length.context("missing content length") {
Ok(length) => length,
Err(e) => return ConnectionResult::Result(Err(e)),
};
let mut content = vec![0; content_length];
reader
if let Err(e) = reader
.read_exact(&mut content)
.await
.with_context(|| "reading after a loop")?;
.with_context(|| "reading after a loop")
{
return ConnectionResult::Result(Err(e));
}
let message = std::str::from_utf8(&content).context("invalid utf8 from server")?;
let message_str = match std::str::from_utf8(&content).context("invalid utf8 from server") {
Ok(str) => str,
Err(e) => return ConnectionResult::Result(Err(e)),
};
if let Some(log_handlers) = log_handlers {
for (kind, log_handler) in log_handlers.lock().iter_mut() {
if matches!(kind, LogKind::Rpc) {
log_handler(IoKind::StdOut, &message);
log_handler(IoKind::StdOut, message_str);
}
}
}
Ok(serde_json::from_str::<Message>(message)?)
ConnectionResult::Result(
serde_json::from_str::<Message>(message_str).context("deserializing server message"),
)
}
pub async fn shutdown(&self) -> Result<()> {
@@ -777,71 +823,31 @@ impl FakeTransport {
let response_handlers = this.response_handlers.clone();
let stdout_writer = Arc::new(Mutex::new(stdout_writer));
cx.background_executor()
.spawn(async move {
let mut reader = BufReader::new(stdin_reader);
let mut buffer = String::new();
cx.background_spawn(async move {
let mut reader = BufReader::new(stdin_reader);
let mut buffer = String::new();
loop {
let message =
TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
.await;
match message {
Err(error) => {
break anyhow::anyhow!(error);
}
Ok(message) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Request(request))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
} else {
let response = if let Some(handle) = request_handlers
.lock()
.get_mut(request.command.as_str())
{
handle(
request.seq,
request.arguments.unwrap_or(json!({})),
)
} else {
panic!("No request handler for {}", request.command);
};
let message =
serde_json::to_string(&Message::Response(response))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
}
Message::Event(event) => {
loop {
match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
.await
{
ConnectionResult::Timeout => {
anyhow::bail!("Timed out when connecting to debugger");
}
ConnectionResult::ConnectionReset => {
log::info!("Debugger closed the connection");
break Ok(());
}
ConnectionResult::Result(Err(e)) => break Err(e),
ConnectionResult::Result(Ok(message)) => {
match message {
Message::Request(request) => {
// redirect reverse requests to stdout writer/reader
if request.command == RunInTerminal::COMMAND
|| request.command == StartDebugging::COMMAND
{
let message =
serde_json::to_string(&Message::Event(event)).unwrap();
serde_json::to_string(&Message::Request(request)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
@@ -852,22 +858,58 @@ impl FakeTransport {
.await
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
} else {
let response = if let Some(handle) =
request_handlers.lock().get_mut(request.command.as_str())
{
handle(response);
handle(request.seq, request.arguments.unwrap_or(json!({})))
} else {
log::error!("No response handler for {}", response.command);
}
panic!("No request handler for {}", request.command);
};
let message =
serde_json::to_string(&Message::Response(response))
.unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
}
Message::Event(event) => {
let message =
serde_json::to_string(&Message::Event(event)).unwrap();
let mut writer = stdout_writer.lock().await;
writer
.write_all(
TransportDelegate::build_rpc_message(message).as_bytes(),
)
.await
.unwrap();
writer.flush().await.unwrap();
}
Message::Response(response) => {
if let Some(handle) =
response_handlers.lock().get(response.command.as_str())
{
handle(response);
} else {
log::error!("No response handler for {}", response.command);
}
}
}
}
}
})
.detach();
}
})
.detach();
Ok((
TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None),

View File

@@ -1,11 +1,8 @@
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use anyhow::{Context as _, Result, anyhow};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use dap::{
StartDebuggingRequestArgumentsRequest,
adapters::{DebugTaskDefinition, latest_github_release},
};
use dap::adapters::{DebugTaskDefinition, latest_github_release};
use futures::StreamExt;
use gpui::AsyncApp;
use serde_json::Value;
@@ -37,7 +34,7 @@ impl CodeLldbDebugAdapter {
Value::String(String::from(task_definition.label.as_ref())),
);
let request = self.validate_config(&configuration)?;
let request = self.request_kind(&configuration)?;
Ok(dap::StartDebuggingRequestArguments {
request,
@@ -89,48 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config
.as_object()
.ok_or_else(|| anyhow!("Config isn't an object"))?;
let request_variant = map
.get("request")
.and_then(|r| r.as_str())
.ok_or_else(|| anyhow!("request field is required and must be a string"))?;
match request_variant {
"launch" => {
// For launch, verify that one of the required configs exists
if !(map.contains_key("program")
|| map.contains_key("targetCreateCommands")
|| map.contains_key("cargo"))
{
return Err(anyhow!(
"launch request requires either 'program', 'targetCreateCommands', or 'cargo' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
"attach" => {
// For attach, verify that either pid or program exists
if !(map.contains_key("pid") || map.contains_key("program")) {
return Err(anyhow!(
"attach request requires either 'pid' or 'program' field"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!(
"request must be either 'launch' or 'attach', got '{}'",
request_variant
)),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut configuration = json!({
"request": match zed_scenario.request {

View File

@@ -178,7 +178,7 @@ impl DebugAdapter for GdbDebugAdapter {
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
let request_args = StartDebuggingRequestArguments {
request: self.validate_config(&config.config)?,
request: self.request_kind(&config.config)?,
configuration: config.config.clone(),
};

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, anyhow, bail};
use anyhow::{Context as _, bail};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
StartDebuggingRequestArguments,
adapters::{
DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
latest_github_release,
@@ -350,24 +350,6 @@ impl DebugAdapter for GoDebugAdapter {
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request argument is not found or invalid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = match &zed_scenario.request {
dap::DebugRequest::Attach(attach_config) => {
@@ -488,7 +470,7 @@ impl DebugAdapter for GoDebugAdapter {
connection: None,
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
},
})
}

View File

@@ -1,9 +1,6 @@
use adapters::latest_github_release;
use anyhow::{Context as _, anyhow};
use dap::{
StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use anyhow::Context as _;
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::AsyncApp;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
@@ -95,7 +92,7 @@ impl JsDebugAdapter {
}),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: self.validate_config(&task_definition.config)?,
request: self.request_kind(&task_definition.config)?,
},
})
}
@@ -107,29 +104,6 @@ impl DebugAdapter for JsDebugAdapter {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<dap::StartDebuggingRequestArgumentsRequest> {
match config.get("request") {
Some(val) if val == "launch" => {
if config.get("program").is_none() && config.get("url").is_none() {
return Err(anyhow!(
"either program or url is required for launch request"
));
}
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
Some(val) if val == "attach" => {
if !config.get("processId").is_some_and(|val| val.is_u64()) {
return Err(anyhow!("processId must be a number"));
}
Ok(StartDebuggingRequestArgumentsRequest::Attach)
}
_ => Err(anyhow!("missing or invalid request field in config")),
}
}
fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
let mut args = json!({
"type": "pwa-node",

View File

@@ -94,7 +94,7 @@ impl PhpDebugAdapter {
envs: HashMap::default(),
request_args: StartDebuggingRequestArguments {
configuration: task_definition.config.clone(),
request: <Self as DebugAdapter>::validate_config(self, &task_definition.config)?,
request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
},
})
}
@@ -282,10 +282,7 @@ impl DebugAdapter for PhpDebugAdapter {
Some(SharedString::new_static("PHP").into())
}
fn validate_config(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}

View File

@@ -1,9 +1,6 @@
use crate::*;
use anyhow::{Context as _, anyhow};
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::DebugTaskDefinition,
};
use anyhow::Context as _;
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{AsyncApp, SharedString};
use json_dotpath::DotPaths;
use language::{LanguageName, Toolchain};
@@ -86,7 +83,7 @@ impl PythonDebugAdapter {
&self,
task_definition: &DebugTaskDefinition,
) -> Result<StartDebuggingRequestArguments> {
let request = self.validate_config(&task_definition.config)?;
let request = self.request_kind(&task_definition.config)?;
let mut configuration = task_definition.config.clone();
if let Ok(console) = configuration.dot_get_mut("console") {
@@ -254,24 +251,6 @@ impl DebugAdapter for PythonDebugAdapter {
})
}
fn validate_config(
&self,
config: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
let map = config.as_object().context("Config isn't an object")?;
let request_variant = map
.get("request")
.and_then(|val| val.as_str())
.context("request is not valid")?;
match request_variant {
"launch" => Ok(StartDebuggingRequestArgumentsRequest::Launch),
"attach" => Ok(StartDebuggingRequestArgumentsRequest::Attach),
_ => Err(anyhow!("request must be either 'launch' or 'attach'")),
}
}
async fn dap_schema(&self) -> serde_json::Value {
json!({
"properties": {

View File

@@ -265,7 +265,7 @@ impl DebugAdapter for RubyDebugAdapter {
cwd: None,
envs: std::collections::HashMap::default(),
request_args: StartDebuggingRequestArguments {
request: self.validate_config(&definition.config)?,
request: self.request_kind(&definition.config)?,
configuration: definition.config.clone(),
},
})

View File

@@ -50,6 +50,7 @@ project.workspace = true
rpc.workspace = true
serde.workspace = true
serde_json.workspace = true
# serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
sysinfo.workspace = true

View File

@@ -3,11 +3,12 @@ use crate::session::DebugSession;
use crate::session::running::RunningState;
use crate::{
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
persistence, spawn_task_or_modal,
};
use anyhow::{Context as _, Result, anyhow};
use anyhow::Result;
use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
@@ -24,7 +25,7 @@ use gpui::{
use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent};
use project::{Fs, ProjectPath, WorktreeId};
use project::{Fs, WorktreeId};
use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self};
use settings::Settings;
@@ -69,6 +70,7 @@ pub struct DebugPanel {
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>,
is_zoomed: bool,
_subscriptions: [Subscription; 1],
}
@@ -103,6 +105,7 @@ impl DebugPanel {
fs: workspace.app_state().fs.clone(),
thread_picker_menu_handle,
session_picker_menu_handle,
is_zoomed: false,
_subscriptions: [focus_subscription],
debug_scenario_scheduled_last: true,
}
@@ -334,10 +337,17 @@ impl DebugPanel {
let Some(task_inventory) = task_store.read(cx).task_inventory() else {
return;
};
let workspace = self.workspace.clone();
let Some(scenario) = task_inventory.read(cx).last_scheduled_scenario().cloned() else {
window.defer(cx, move |window, cx| {
workspace
.update(cx, |workspace, cx| {
NewProcessModal::show(workspace, window, NewProcessMode::Launch, None, cx);
})
.ok();
});
return;
};
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = workspace
@@ -942,68 +952,69 @@ impl DebugPanel {
cx.notify();
}
pub(crate) fn save_scenario(
&self,
scenario: &DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<ProjectPath>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
};
// TODO: restore once we have proper comment preserving file edits
// pub(crate) fn save_scenario(
// &self,
// scenario: &DebugScenario,
// worktree_id: WorktreeId,
// window: &mut Window,
// cx: &mut App,
// ) -> Task<Result<ProjectPath>> {
// self.workspace
// .update(cx, |workspace, cx| {
// let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
// return Task::ready(Err(anyhow!("Couldn't get worktree path")));
// };
let serialized_scenario = serde_json::to_value(scenario);
// let serialized_scenario = serde_json::to_value(scenario);
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let fs =
workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
// cx.spawn_in(window, async move |workspace, cx| {
// let serialized_scenario = serialized_scenario?;
// let fs =
// workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
path.push(paths::local_settings_folder_relative_path());
if !fs.is_dir(path.as_path()).await {
fs.create_dir(path.as_path()).await?;
}
path.pop();
// path.push(paths::local_settings_folder_relative_path());
// if !fs.is_dir(path.as_path()).await {
// fs.create_dir(path.as_path()).await?;
// }
// path.pop();
path.push(paths::local_debug_file_relative_path());
let path = path.as_path();
// path.push(paths::local_debug_file_relative_path());
// let path = path.as_path();
if !fs.is_file(path).await {
let content =
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
serialized_scenario,
]))?;
// if !fs.is_file(path).await {
// fs.create_file(path, Default::default()).await?;
// fs.write(
// path,
// initial_local_debug_tasks_content().to_string().as_bytes(),
// )
// .await?;
// }
fs.create_file(path, Default::default()).await?;
fs.save(path, &content.into(), Default::default()).await?;
} else {
let content = fs.load(path).await?;
let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
values.push(serialized_scenario);
fs.save(
path,
&serde_json::to_string_pretty(&values).map(Into::into)?,
Default::default(),
)
.await?;
}
// let content = fs.load(path).await?;
// let mut values =
// serde_json_lenient::from_str::<Vec<serde_json::Value>>(&content)?;
// values.push(serialized_scenario);
// fs.save(
// path,
// &serde_json_lenient::to_string_pretty(&values).map(Into::into)?,
// Default::default(),
// )
// .await?;
workspace.update(cx, |workspace, cx| {
workspace
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
})?
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
// workspace.update(cx, |workspace, cx| {
// workspace
// .project()
// .read(cx)
// .project_path_for_absolute_path(&path, cx)
// .context(
// "Couldn't get project path for .zed/debug.json in active worktree",
// )
// })?
// })
// })
// .unwrap_or_else(|err| Task::ready(Err(err)))
// }
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_picker_menu_handle.toggle(window, cx);
@@ -1012,6 +1023,22 @@ impl DebugPanel {
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.session_picker_menu_handle.toggle(window, cx);
}
fn toggle_zoom(
&mut self,
_: &workspace::ToggleZoom,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.is_zoomed {
cx.emit(PanelEvent::ZoomOut);
} else {
if !self.focus_handle(cx).contains_focused(window, cx) {
cx.focus_self(window);
}
cx.emit(PanelEvent::ZoomIn);
}
}
}
async fn register_session_inner(
@@ -1167,6 +1194,15 @@ impl Panel for DebugPanel {
}
fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
self.is_zoomed
}
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
self.is_zoomed = zoomed;
cx.notify();
}
}
impl Render for DebugPanel {
@@ -1307,6 +1343,23 @@ impl Render for DebugPanel {
.ok();
}
})
.on_action(cx.listener(Self::toggle_zoom))
.on_action(cx.listener(|panel, _: &ToggleExpandItem, _, cx| {
let Some(session) = panel.active_session() else {
return;
};
let active_pane = session
.read(cx)
.running_state()
.read(cx)
.active_pane()
.clone();
active_pane.update(cx, |pane, cx| {
let is_zoomed = pane.is_zoomed();
pane.set_zoomed(!is_zoomed, cx);
});
cx.notify();
}))
.when(self.active_session.is_some(), |this| {
this.on_mouse_down(
MouseButton::Right,
@@ -1410,4 +1463,10 @@ impl workspace::DebuggerProvider for DebuggerProvider {
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool {
self.0.read(cx).debug_scenario_scheduled_last
}
fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus> {
let session = self.0.read(cx).active_session()?;
let thread = session.read(cx).running_state().read(cx).thread_id()?;
session.read(cx).session(cx).read(cx).thread_state(thread)
}
}

View File

@@ -3,7 +3,7 @@ use debugger_panel::{DebugPanel, ToggleFocus};
use editor::Editor;
use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
use gpui::{App, EntityInputHandler, actions};
use new_session_modal::{NewSessionModal, NewSessionMode};
use new_process_modal::{NewProcessModal, NewProcessMode};
use project::debugger::{self, breakpoint_store::SourceBreakpoint};
use session::DebugSession;
use settings::Settings;
@@ -15,7 +15,7 @@ use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
mod dropdown_menus;
mod new_session_modal;
mod new_process_modal;
mod persistence;
pub(crate) mod session;
mod stack_trace_view;
@@ -49,6 +49,7 @@ actions!(
ToggleThreadPicker,
ToggleSessionPicker,
RerunLastSession,
ToggleExpandItem,
]
);
@@ -210,7 +211,7 @@ pub fn init(cx: &mut App) {
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
NewSessionModal::show(workspace, window, NewSessionMode::Launch, None, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
})
.register_action(
|workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
@@ -352,7 +353,7 @@ fn spawn_task_or_modal(
.detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => {
NewSessionModal::show(workspace, window, NewSessionMode::Task, *reveal_target, cx);
NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
}
}
}

View File

@@ -8,7 +8,8 @@ pub mod variable_list;
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
use crate::{
new_session_modal::resolve_path,
ToggleExpandItem,
new_process_modal::resolve_path,
persistence::{self, DebuggerPaneItem, SerializedLayout},
};
@@ -347,6 +348,7 @@ pub(crate) fn new_debugger_pane(
false
}
})));
pane.set_can_toggle_zoom(false, cx);
pane.display_nav_history_buttons(None);
pane.set_custom_drop_handle(cx, custom_drop_handle);
pane.set_should_display_tab_bar(|_, _| true);
@@ -472,17 +474,19 @@ pub(crate) fn new_debugger_pane(
},
)
.icon_size(IconSize::XSmall)
.on_click(cx.listener(move |pane, _, window, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
.on_click(cx.listener(move |pane, _, _, cx| {
let is_zoomed = pane.is_zoomed();
pane.set_zoomed(!is_zoomed, cx);
cx.notify();
}))
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
let zoomed_text =
if zoomed { "Zoom Out" } else { "Zoom In" };
if zoomed { "Minimize" } else { "Expand" };
Tooltip::for_action_in(
zoomed_text,
&workspace::ToggleZoom,
&ToggleExpandItem,
&focus_handle,
window,
cx,
@@ -566,7 +570,7 @@ impl RunningState {
}
}
pub(crate) fn relativlize_paths(
pub(crate) fn relativize_paths(
key: Option<&str>,
config: &mut serde_json::Value,
context: &TaskContext,
@@ -574,12 +578,12 @@ impl RunningState {
match config {
serde_json::Value::Object(obj) => {
obj.iter_mut()
.for_each(|(key, value)| Self::relativlize_paths(Some(key), value, context));
.for_each(|(key, value)| Self::relativize_paths(Some(key), value, context));
}
serde_json::Value::Array(array) => {
array
.iter_mut()
.for_each(|value| Self::relativlize_paths(None, value, context));
.for_each(|value| Self::relativize_paths(None, value, context));
}
serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => {
// Some built-in zed tasks wrap their arguments in quotes as they might contain spaces.
@@ -806,13 +810,13 @@ impl RunningState {
mut config,
tcp_connection,
} = scenario;
Self::relativlize_paths(None, &mut config, &task_context);
Self::relativize_paths(None, &mut config, &task_context);
Self::substitute_variables_in_config(&mut config, &task_context);
let request_type = dap_registry
.adapter(&adapter)
.ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))
.and_then(|adapter| adapter.validate_config(&config));
.and_then(|adapter| adapter.request_kind(&config));
let config_is_valid = request_type.is_ok();
@@ -954,7 +958,10 @@ impl RunningState {
config = scenario.config;
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
anyhow::bail!("No request or build provided");
let Err(e) = request_type else {
unreachable!();
};
anyhow::bail!("Zed cannot determine how to run this debug scenario. `build` field was not provided and Debug Adapter won't accept provided configuration because: {e}");
};
Ok(DebugTaskDefinition {
@@ -1257,18 +1264,6 @@ impl RunningState {
Event::Focus => {
this.active_pane = source_pane.clone();
}
Event::ZoomIn => {
source_pane.update(cx, |pane, cx| {
pane.set_zoomed(true, cx);
});
cx.notify();
}
Event::ZoomOut => {
source_pane.update(cx, |pane, cx| {
pane.set_zoomed(false, cx);
});
cx.notify();
}
_ => {}
}
}

View File

@@ -13,7 +13,7 @@ use gpui::{
use language::{Buffer, CodeLabel, ToOffset};
use menu::Confirm;
use project::{
Completion,
Completion, CompletionResponse,
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
};
use settings::Settings;
@@ -262,9 +262,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
_trigger: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(console) = self.0.upgrade() else {
return Task::ready(Ok(None));
return Task::ready(Ok(Vec::new()));
};
let support_completions = console
@@ -322,7 +322,7 @@ impl ConsoleQueryBarCompletionProvider {
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let (variables, string_matches) = console.update(cx, |console, cx| {
let mut variables = HashMap::default();
let mut string_matches = Vec::default();
@@ -354,39 +354,43 @@ impl ConsoleQueryBarCompletionProvider {
let query = buffer.read(cx).text();
cx.spawn(async move |_, cx| {
const LIMIT: usize = 10;
let matches = fuzzy::match_strings(
&string_matches,
&query,
true,
10,
LIMIT,
&Default::default(),
cx.background_executor().clone(),
)
.await;
Ok(Some(
matches
.iter()
.filter_map(|string_match| {
let variable_value = variables.get(&string_match.string)?;
let completions = matches
.iter()
.filter_map(|string_match| {
let variable_value = variables.get(&string_match.string)?;
Some(project::Completion {
replace_range: buffer_position..buffer_position,
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: format!("{} {}", string_match.string, variable_value),
runs: Vec::new(),
},
icon_path: None,
documentation: None,
confirm: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
})
Some(project::Completion {
replace_range: buffer_position..buffer_position,
new_text: string_match.string.clone(),
label: CodeLabel {
filter_range: 0..string_match.string.len(),
text: format!("{} {}", string_match.string, variable_value),
runs: Vec::new(),
},
icon_path: None,
documentation: None,
confirm: None,
source: project::CompletionSource::Custom,
insert_text_mode: None,
})
.collect(),
))
})
.collect::<Vec<_>>();
Ok(vec![project::CompletionResponse {
is_incomplete: completions.len() >= LIMIT,
completions,
}])
})
}
@@ -396,7 +400,7 @@ impl ConsoleQueryBarCompletionProvider {
buffer: &Entity<Buffer>,
buffer_position: language::Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let completion_task = console.update(cx, |console, cx| {
console.session.update(cx, |state, cx| {
let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
@@ -411,53 +415,56 @@ impl ConsoleQueryBarCompletionProvider {
cx.background_executor().spawn(async move {
let completions = completion_task.await?;
Ok(Some(
completions
.into_iter()
.map(|completion| {
let new_text = completion
.text
.as_ref()
.unwrap_or(&completion.label)
.to_owned();
let buffer_text = snapshot.text();
let buffer_bytes = buffer_text.as_bytes();
let new_bytes = new_text.as_bytes();
let completions = completions
.into_iter()
.map(|completion| {
let new_text = completion
.text
.as_ref()
.unwrap_or(&completion.label)
.to_owned();
let buffer_text = snapshot.text();
let buffer_bytes = buffer_text.as_bytes();
let new_bytes = new_text.as_bytes();
let mut prefix_len = 0;
for i in (0..new_bytes.len()).rev() {
if buffer_bytes.ends_with(&new_bytes[0..i]) {
prefix_len = i;
break;
}
let mut prefix_len = 0;
for i in (0..new_bytes.len()).rev() {
if buffer_bytes.ends_with(&new_bytes[0..i]) {
prefix_len = i;
break;
}
}
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - prefix_len;
let start = snapshot.clip_offset(start, Bias::Left);
let start = snapshot.anchor_before(start);
let replace_range = start..buffer_position;
let buffer_offset = buffer_position.to_offset(&snapshot);
let start = buffer_offset - prefix_len;
let start = snapshot.clip_offset(start, Bias::Left);
let start = snapshot.anchor_before(start);
let replace_range = start..buffer_position;
project::Completion {
replace_range,
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label,
runs: Vec::new(),
},
icon_path: None,
documentation: None,
confirm: None,
source: project::CompletionSource::BufferWord {
word_range: buffer_position..language::Anchor::MAX,
resolved: false,
},
insert_text_mode: None,
}
})
.collect(),
))
project::Completion {
replace_range,
new_text,
label: CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label,
runs: Vec::new(),
},
icon_path: None,
documentation: None,
confirm: None,
source: project::CompletionSource::BufferWord {
word_range: buffer_position..language::Anchor::MAX,
resolved: false,
},
insert_text_mode: None,
}
})
.collect();
Ok(vec![project::CompletionResponse {
completions,
is_incomplete: false,
}])
})
}
}

View File

@@ -25,7 +25,7 @@ mod inline_values;
#[cfg(test)]
mod module_list;
#[cfg(test)]
mod new_session_modal;
mod new_process_modal;
#[cfg(test)]
mod persistence;
#[cfg(test)]

View File

@@ -1,13 +1,13 @@
use dap::DapRegistry;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Fs, Project};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig};
use util::path;
use crate::new_session_modal::NewSessionMode;
// use crate::new_process_modal::NewProcessMode;
use crate::tests::{init_test, init_test_workspace};
#[gpui::test]
@@ -152,111 +152,111 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
}
}
#[gpui::test]
async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
// #[gpui::test]
// async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "fn main() {}"
}),
)
.await;
// let fs = FakeFs::new(executor.clone());
// fs.insert_tree(
// path!("/project"),
// json!({
// "main.rs": "fn main() {}"
// }),
// )
// .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
crate::new_session_modal::NewSessionModal::show(
workspace,
window,
NewSessionMode::Launch,
None,
cx,
);
})
.unwrap();
// workspace
// .update(cx, |workspace, window, cx| {
// crate::new_process_modal::NewProcessModal::show(
// workspace,
// window,
// NewProcessMode::Debug,
// None,
// cx,
// );
// })
// .unwrap();
cx.run_until_parked();
// cx.run_until_parked();
let modal = workspace
.update(cx, |workspace, _, cx| {
workspace.active_modal::<crate::new_session_modal::NewSessionModal>(cx)
})
.unwrap()
.expect("Modal should be active");
// let modal = workspace
// .update(cx, |workspace, _, cx| {
// workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
// })
// .unwrap()
// .expect("Modal should be active");
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/main", "/project", false, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/main", "/project", false, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist");
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
modal.update_in(cx, |modal, window, cx| {
modal.set_configure("/project/other", "/project", true, window, cx);
modal.save_scenario(window, cx);
});
// modal.update_in(cx, |modal, window, cx| {
// modal.set_configure("/project/other", "/project", true, window, cx);
// modal.save_scenario(window, cx);
// });
cx.executor().run_until_parked();
// cx.executor().run_until_parked();
let debug_json_content = fs
.load(path!("/project/.zed/debug.json").as_ref())
.await
.expect("debug.json should exist after second save");
// let debug_json_content = fs
// .load(path!("/project/.zed/debug.json").as_ref())
// .await
// .expect("debug.json should exist after second save");
let expected_content = vec![
"[",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "main (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/main","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" },",
" {",
r#" "adapter": "fake-adapter","#,
r#" "label": "other (fake-adapter)","#,
r#" "request": "launch","#,
r#" "program": "/project/other","#,
r#" "cwd": "/project","#,
r#" "args": [],"#,
r#" "env": {}"#,
" }",
"]",
];
// let expected_content = vec![
// "[",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "main (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/main","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " },",
// " {",
// r#" "adapter": "fake-adapter","#,
// r#" "label": "other (fake-adapter)","#,
// r#" "request": "launch","#,
// r#" "program": "/project/other","#,
// r#" "cwd": "/project","#,
// r#" "args": [],"#,
// r#" "env": {}"#,
// " }",
// "]",
// ];
let actual_lines: Vec<&str> = debug_json_content.lines().collect();
pretty_assertions::assert_eq!(expected_content, actual_lines);
}
// let actual_lines: Vec<&str> = debug_json_content.lines().collect();
// pretty_assertions::assert_eq!(expected_content, actual_lines);
// }
#[gpui::test]
async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
@@ -322,7 +322,7 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
);
let request_type = adapter
.validate_config(&debug_scenario.config)
.request_kind(&debug_scenario.config)
.unwrap_or_else(|_| {
panic!(
"Adapter {} should validate the config successfully",

View File

@@ -1,9 +1,8 @@
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, div, px, uniform_list,
Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
};
use gpui::{AsyncWindowContext, WeakEntity};
use itertools::Itertools;
use language::CodeLabel;
use language::{Buffer, LanguageName, LanguageRegistry};
@@ -18,6 +17,7 @@ use task::TaskContext;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
cell::RefCell,
cmp::{Reverse, min},
@@ -47,15 +47,10 @@ pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
// documentation not yet being parsed.
//
// The size of the cache is set to the number of items fetched around the current selection plus one
// for the current selection and another to avoid cases where and adjacent selection exits the
// cache. The only current benefit of a larger cache would be doing less markdown parsing when the
// selection revisits items.
//
// One future benefit of a larger cache would be reducing flicker on backspace. This would require
// not recreating the menu on every change, by not re-querying the language server when
// `is_incomplete = false`.
const MARKDOWN_CACHE_MAX_SIZE: usize = MARKDOWN_CACHE_BEFORE_ITEMS + MARKDOWN_CACHE_AFTER_ITEMS + 2;
// The size of the cache is set to 16, which is roughly 3 times more than the number of items
// fetched around the current selection. This way documentation is more often ready for render when
// revisiting previous entries, such as when pressing backspace.
const MARKDOWN_CACHE_MAX_SIZE: usize = 16;
const MARKDOWN_CACHE_BEFORE_ITEMS: usize = 2;
const MARKDOWN_CACHE_AFTER_ITEMS: usize = 2;
@@ -197,27 +192,48 @@ pub enum ContextMenuOrigin {
QuickActionBar,
}
#[derive(Clone)]
pub struct CompletionsMenu {
pub id: CompletionId,
sort_completions: bool,
pub initial_position: Anchor,
pub initial_query: Option<Arc<String>>,
pub is_incomplete: bool,
pub buffer: Entity<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>,
pub entries: Rc<RefCell<Vec<StringMatch>>>,
match_candidates: Arc<[StringMatchCandidate]>,
pub entries: Rc<RefCell<Box<[StringMatch]>>>,
pub selected_item: usize,
filter_task: Task<()>,
cancel_filter: Arc<AtomicBool>,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
show_completion_documentation: bool,
pub(super) ignore_completion_provider: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
markdown_cache: Rc<RefCell<VecDeque<(usize, Entity<Markdown>)>>>,
markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
language_registry: Option<Arc<LanguageRegistry>>,
language: Option<LanguageName>,
snippet_sort_order: SnippetSortOrder,
}
#[derive(Clone, Debug, PartialEq)]
enum MarkdownCacheKey {
ForCandidate {
candidate_id: usize,
},
ForCompletionMatch {
new_text: String,
markdown_source: SharedString,
},
}
// TODO: There should really be a wrapper around fuzzy match tasks that does this.
impl Drop for CompletionsMenu {
fn drop(&mut self) {
self.cancel_filter.store(true, Ordering::Relaxed);
}
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
@@ -225,6 +241,8 @@ impl CompletionsMenu {
show_completion_documentation: bool,
ignore_completion_provider: bool,
initial_position: Anchor,
initial_query: Option<Arc<String>>,
is_incomplete: bool,
buffer: Entity<Buffer>,
completions: Box<[Completion]>,
snippet_sort_order: SnippetSortOrder,
@@ -242,17 +260,21 @@ impl CompletionsMenu {
id,
sort_completions,
initial_position,
initial_query,
is_incomplete,
buffer,
show_completion_documentation,
ignore_completion_provider,
completions: RefCell::new(completions).into(),
match_candidates,
entries: RefCell::new(Vec::new()).into(),
entries: Rc::new(RefCell::new(Box::new([]))),
selected_item: 0,
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
markdown_cache: RefCell::new(VecDeque::with_capacity(MARKDOWN_CACHE_MAX_SIZE)).into(),
markdown_cache: RefCell::new(VecDeque::new()).into(),
language_registry,
language,
snippet_sort_order,
@@ -303,16 +325,20 @@ impl CompletionsMenu {
positions: vec![],
string: completion.clone(),
})
.collect::<Vec<_>>();
.collect();
Self {
id,
sort_completions,
initial_position: selection.start,
initial_query: None,
is_incomplete: false,
buffer,
completions: RefCell::new(completions).into(),
match_candidates,
entries: RefCell::new(entries).into(),
selected_item: 0,
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
show_completion_documentation: false,
@@ -390,14 +416,7 @@ impl CompletionsMenu {
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
self.start_markdown_parse_for_nearby_entries(cx);
if let Some(provider) = provider {
self.handle_selection_changed(provider, window, cx);
}
cx.notify();
self.handle_selection_changed(provider, window, cx);
}
}
@@ -418,18 +437,25 @@ impl CompletionsMenu {
}
fn handle_selection_changed(
&self,
provider: &dyn CompletionProvider,
&mut self,
provider: Option<&dyn CompletionProvider>,
window: &mut Window,
cx: &mut App,
cx: &mut Context<Editor>,
) {
let entries = self.entries.borrow();
let entry = if self.selected_item < entries.len() {
Some(&entries[self.selected_item])
} else {
None
};
provider.selection_changed(entry, window, cx);
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
if let Some(provider) = provider {
let entries = self.entries.borrow();
let entry = if self.selected_item < entries.len() {
Some(&entries[self.selected_item])
} else {
None
};
provider.selection_changed(entry, window, cx);
}
self.resolve_visible_completions(provider, cx);
self.start_markdown_parse_for_nearby_entries(cx);
cx.notify();
}
pub fn resolve_visible_completions(
@@ -444,6 +470,19 @@ impl CompletionsMenu {
return;
};
let entries = self.entries.borrow();
if entries.is_empty() {
return;
}
if self.selected_item >= entries.len() {
log::error!(
"bug: completion selected_item >= entries.len(): {} >= {}",
self.selected_item,
entries.len()
);
self.selected_item = entries.len() - 1;
}
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
@@ -455,7 +494,6 @@ impl CompletionsMenu {
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entries = self.entries.borrow();
let entry_range = if self.selected_item == 0 {
0..min(visible_count, entries.len())
} else if self.selected_item == entries.len() - 1 {
@@ -508,11 +546,11 @@ impl CompletionsMenu {
.update(cx, |editor, cx| {
// `resolve_completions` modified state affecting display.
cx.notify();
editor.with_completions_menu_matching_id(
completion_id,
|| (),
|this| this.start_markdown_parse_for_nearby_entries(cx),
);
editor.with_completions_menu_matching_id(completion_id, |menu| {
if let Some(menu) = menu {
menu.start_markdown_parse_for_nearby_entries(cx)
}
});
})
.ok();
}
@@ -548,11 +586,11 @@ impl CompletionsMenu {
return None;
}
let candidate_id = entries[index].candidate_id;
match &self.completions.borrow()[candidate_id].documentation {
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => Some(
self.get_or_create_markdown(candidate_id, source.clone(), false, cx)
.1,
),
let completions = self.completions.borrow();
match &completions[candidate_id].documentation {
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => self
.get_or_create_markdown(candidate_id, Some(source), false, &completions, cx)
.map(|(_, markdown)| markdown),
Some(_) => None,
_ => None,
}
@@ -561,38 +599,75 @@ impl CompletionsMenu {
fn get_or_create_markdown(
&self,
candidate_id: usize,
source: SharedString,
source: Option<&SharedString>,
is_render: bool,
completions: &[Completion],
cx: &mut Context<Editor>,
) -> (bool, Entity<Markdown>) {
) -> Option<(bool, Entity<Markdown>)> {
let mut markdown_cache = self.markdown_cache.borrow_mut();
if let Some((cache_index, (_, markdown))) = markdown_cache
.iter()
.find_position(|(id, _)| *id == candidate_id)
{
let markdown = if is_render && cache_index != 0 {
let mut has_completion_match_cache_entry = false;
let mut matching_entry = markdown_cache.iter().find_position(|(key, _)| match key {
MarkdownCacheKey::ForCandidate { candidate_id: id } => *id == candidate_id,
MarkdownCacheKey::ForCompletionMatch { .. } => {
has_completion_match_cache_entry = true;
false
}
});
if has_completion_match_cache_entry && matching_entry.is_none() {
if let Some(source) = source {
matching_entry = markdown_cache.iter().find_position(|(key, _)| {
matches!(key, MarkdownCacheKey::ForCompletionMatch { markdown_source, .. }
if markdown_source == source)
});
} else {
// Heuristic guess that documentation can be reused when new_text matches. This is
// to mitigate documentation flicker while typing. If this is wrong, then resolution
// should cause the correct documentation to be displayed soon.
let completion = &completions[candidate_id];
matching_entry = markdown_cache.iter().find_position(|(key, _)| {
matches!(key, MarkdownCacheKey::ForCompletionMatch { new_text, .. }
if new_text == &completion.new_text)
});
}
}
if let Some((cache_index, (key, markdown))) = matching_entry {
let markdown = markdown.clone();
// Since the markdown source matches, the key can now be ForCandidate.
if source.is_some() && matches!(key, MarkdownCacheKey::ForCompletionMatch { .. }) {
markdown_cache[cache_index].0 = MarkdownCacheKey::ForCandidate { candidate_id };
}
if is_render && cache_index != 0 {
// Move the current selection's cache entry to the front.
markdown_cache.rotate_right(1);
let cache_len = markdown_cache.len();
markdown_cache.swap(0, (cache_index + 1) % cache_len);
&markdown_cache[0].1
} else {
markdown
};
}
let is_parsing = markdown.update(cx, |markdown, cx| {
// `reset` is called as it's possible for documentation to change due to resolve
// requests. It does nothing if `source` is unchanged.
markdown.reset(source, cx);
if let Some(source) = source {
// `reset` is called as it's possible for documentation to change due to resolve
// requests. It does nothing if `source` is unchanged.
markdown.reset(source.clone(), cx);
}
markdown.is_parsing()
});
return (is_parsing, markdown.clone());
return Some((is_parsing, markdown));
}
let Some(source) = source else {
// Can't create markdown as there is no source.
return None;
};
if markdown_cache.len() < MARKDOWN_CACHE_MAX_SIZE {
let markdown = cx.new(|cx| {
Markdown::new(
source,
source.clone(),
self.language_registry.clone(),
self.language.clone(),
cx,
@@ -601,17 +676,20 @@ impl CompletionsMenu {
// Handles redraw when the markdown is done parsing. The current render is for a
// deferred draw, and so without this did not redraw when `markdown` notified.
cx.observe(&markdown, |_, _, cx| cx.notify()).detach();
markdown_cache.push_front((candidate_id, markdown.clone()));
(true, markdown)
markdown_cache.push_front((
MarkdownCacheKey::ForCandidate { candidate_id },
markdown.clone(),
));
Some((true, markdown))
} else {
debug_assert_eq!(markdown_cache.capacity(), MARKDOWN_CACHE_MAX_SIZE);
// Moves the last cache entry to the start. The ring buffer is full, so this does no
// copying and just shifts indexes.
markdown_cache.rotate_right(1);
markdown_cache[0].0 = candidate_id;
markdown_cache[0].0 = MarkdownCacheKey::ForCandidate { candidate_id };
let markdown = &markdown_cache[0].1;
markdown.update(cx, |markdown, cx| markdown.reset(source, cx));
(true, markdown.clone())
markdown.update(cx, |markdown, cx| markdown.reset(source.clone(), cx));
Some((true, markdown.clone()))
}
}
@@ -774,37 +852,46 @@ impl CompletionsMenu {
}
let mat = &self.entries.borrow()[self.selected_item];
let multiline_docs = match self.completions.borrow_mut()[mat.candidate_id]
.documentation
.as_ref()?
{
CompletionDocumentation::MultiLinePlainText(text) => div().child(text.clone()),
CompletionDocumentation::SingleLineAndMultiLinePlainText {
let completions = self.completions.borrow_mut();
let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: Some(text),
..
} => div().child(text.clone()),
CompletionDocumentation::MultiLineMarkdown(source) if !source.is_empty() => {
let (is_parsing, markdown) =
self.get_or_create_markdown(mat.candidate_id, source.clone(), true, cx);
if is_parsing {
}) => div().child(text.clone()),
Some(CompletionDocumentation::MultiLineMarkdown(source)) if !source.is_empty() => {
let Some((false, markdown)) = self.get_or_create_markdown(
mat.candidate_id,
Some(source),
true,
&completions,
cx,
) else {
return None;
}
div().child(
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: false,
border: false,
})
.on_url_click(open_markdown_url),
)
};
Self::render_markdown(markdown, window, cx)
}
CompletionDocumentation::MultiLineMarkdown(_) => return None,
CompletionDocumentation::SingleLine(_) => return None,
CompletionDocumentation::Undocumented => return None,
CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: None, ..
} => {
None => {
// Handle the case where documentation hasn't yet been resolved but there's a
// `new_text` match in the cache.
//
// TODO: It's inconsistent that documentation caching based on matching `new_text`
// only works for markdown. Consider generally caching the results of resolving
// completions.
let Some((false, markdown)) =
self.get_or_create_markdown(mat.candidate_id, None, true, &completions, cx)
else {
return None;
};
Self::render_markdown(markdown, window, cx)
}
Some(CompletionDocumentation::MultiLineMarkdown(_)) => return None,
Some(CompletionDocumentation::SingleLine(_)) => return None,
Some(CompletionDocumentation::Undocumented) => return None,
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
plain_text: None,
..
}) => {
return None;
}
};
@@ -824,6 +911,177 @@ impl CompletionsMenu {
)
}
fn render_markdown(
markdown: Entity<Markdown>,
window: &mut Window,
cx: &mut Context<Editor>,
) -> Div {
div().child(
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
.code_block_renderer(markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: false,
border: false,
})
.on_url_click(open_markdown_url),
)
}
pub fn filter(
&mut self,
query: Option<Arc<String>>,
provider: Option<Rc<dyn CompletionProvider>>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.cancel_filter.store(true, Ordering::Relaxed);
if let Some(query) = query {
self.cancel_filter = Arc::new(AtomicBool::new(false));
let matches = self.do_async_filtering(query, cx);
let id = self.id;
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
let matches = matches.await;
editor
.update_in(cx, |editor, window, cx| {
editor.with_completions_menu_matching_id(id, |this| {
if let Some(this) = this {
this.set_filter_results(matches, provider, window, cx);
}
});
})
.ok();
});
} else {
self.filter_task = Task::ready(());
let matches = self.unfiltered_matches();
self.set_filter_results(matches, provider, window, cx);
}
}
pub fn do_async_filtering(
&self,
query: Arc<String>,
cx: &Context<Editor>,
) -> Task<Vec<StringMatch>> {
let matches_task = cx.background_spawn({
let query = query.clone();
let match_candidates = self.match_candidates.clone();
let cancel_filter = self.cancel_filter.clone();
let background_executor = cx.background_executor().clone();
async move {
fuzzy::match_strings(
&match_candidates,
&query,
query.chars().any(|c| c.is_uppercase()),
100,
&cancel_filter,
background_executor,
)
.await
}
});
let completions = self.completions.clone();
let sort_completions = self.sort_completions;
let snippet_sort_order = self.snippet_sort_order;
cx.foreground_executor().spawn(async move {
let mut matches = matches_task.await;
if sort_completions {
matches = Self::sort_string_matches(
matches,
Some(&query),
snippet_sort_order,
completions.borrow().as_ref(),
);
}
matches
})
}
/// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
let mut matches = self
.match_candidates
.iter()
.enumerate()
.map(|(candidate_id, candidate)| StringMatch {
candidate_id,
score: Default::default(),
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
if self.sort_completions {
matches = Self::sort_string_matches(
matches,
None,
self.snippet_sort_order,
self.completions.borrow().as_ref(),
);
}
matches
}
pub fn set_filter_results(
&mut self,
matches: Vec<StringMatch>,
provider: Option<Rc<dyn CompletionProvider>>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
*self.entries.borrow_mut() = matches.into_boxed_slice();
self.selected_item = 0;
self.handle_selection_changed(provider.as_deref(), window, cx);
}
fn sort_string_matches(
matches: Vec<StringMatch>,
query: Option<&str>,
snippet_sort_order: SnippetSortOrder,
completions: &[Completion],
) -> Vec<StringMatch> {
let mut sortable_items: Vec<SortableMatch<'_>> = matches
.into_iter()
.map(|string_match| {
let completion = &completions[string_match.candidate_id];
let is_snippet = matches!(
&completion.source,
CompletionSource::Lsp { lsp_completion, .. }
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
let sort_text =
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
lsp_completion.sort_text.as_deref()
} else {
None
};
let (sort_kind, sort_label) = completion.sort_key();
SortableMatch {
string_match,
is_snippet,
sort_text,
sort_kind,
sort_label,
}
})
.collect();
Self::sort_matches(&mut sortable_items, query, snippet_sort_order);
sortable_items
.into_iter()
.map(|sortable| sortable.string_match)
.collect()
}
pub fn sort_matches(
matches: &mut Vec<SortableMatch<'_>>,
query: Option<&str>,
@@ -857,6 +1115,7 @@ impl CompletionsMenu {
let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
let query_start_lower = query
.as_ref()
.and_then(|q| q.chars().next())
.and_then(|c| c.to_lowercase().next());
@@ -890,6 +1149,7 @@ impl CompletionsMenu {
};
let sort_mixed_case_prefix_length = Reverse(
query
.as_ref()
.map(|q| {
q.chars()
.zip(mat.string_match.string.chars())
@@ -920,97 +1180,32 @@ impl CompletionsMenu {
});
}
pub async fn filter(
&mut self,
query: Option<&str>,
provider: Option<Rc<dyn CompletionProvider>>,
editor: WeakEntity<Editor>,
cx: &mut AsyncWindowContext,
) {
let mut matches = if let Some(query) = query {
fuzzy::match_strings(
&self.match_candidates,
query,
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
cx.background_executor().clone(),
)
.await
} else {
self.match_candidates
.iter()
.enumerate()
.map(|(candidate_id, candidate)| StringMatch {
candidate_id,
score: Default::default(),
positions: Default::default(),
string: candidate.string.clone(),
})
.collect()
};
pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
self.markdown_cache = prev_menu.markdown_cache.clone();
if self.sort_completions {
let completions = self.completions.borrow();
let mut sortable_items: Vec<SortableMatch<'_>> = matches
.into_iter()
.map(|string_match| {
let completion = &completions[string_match.candidate_id];
let is_snippet = matches!(
&completion.source,
CompletionSource::Lsp { lsp_completion, .. }
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
let sort_text =
if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
lsp_completion.sort_text.as_deref()
} else {
None
};
let (sort_kind, sort_label) = completion.sort_key();
SortableMatch {
string_match,
is_snippet,
sort_text,
sort_kind,
sort_label,
// Convert ForCandidate cache keys to ForCompletionMatch keys.
let prev_completions = prev_menu.completions.borrow();
self.markdown_cache
.borrow_mut()
.retain_mut(|(key, _markdown)| match key {
MarkdownCacheKey::ForCompletionMatch { .. } => true,
MarkdownCacheKey::ForCandidate { candidate_id } => {
if let Some(completion) = prev_completions.get(*candidate_id) {
match &completion.documentation {
Some(CompletionDocumentation::MultiLineMarkdown(source)) => {
*key = MarkdownCacheKey::ForCompletionMatch {
new_text: completion.new_text.clone(),
markdown_source: source.clone(),
};
true
}
_ => false,
}
} else {
false
}
})
.collect();
Self::sort_matches(&mut sortable_items, query, self.snippet_sort_order);
matches = sortable_items
.into_iter()
.map(|sortable| sortable.string_match)
.collect();
}
*self.entries.borrow_mut() = matches;
self.selected_item = 0;
// This keeps the display consistent when y_flipped.
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
if let Some(provider) = provider {
cx.update(|window, cx| {
// Since this is async, it's possible the menu has been closed and possibly even
// another opened. `provider.selection_changed` should not be called in this case.
let this_menu_still_active = editor
.read_with(cx, |editor, _cx| {
editor.with_completions_menu_matching_id(self.id, || false, |_| true)
})
.unwrap_or(false);
if this_menu_still_active {
self.handle_selection_changed(&*provider, window, cx);
}
})
.ok();
}
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
use super::*;
use crate::{
JoinLines,
code_context_menus::CodeContextMenu,
inline_completion_tests::FakeInlineCompletionProvider,
linked_editing_ranges::LinkedEditingRanges,
scroll::scroll_amount::ScrollAmount,
@@ -8512,108 +8513,123 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
async fn test_snippets(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
a.ˇ b
a.ˇ b
a.ˇ b
"},
false,
);
let mut cx = EditorTestContext::new(cx).await;
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
cx.set_state(indoc! {"
a.ˇ b
a.ˇ b
a.ˇ b
"});
editor.update_in(cx, |editor, window, cx| {
cx.update_editor(|editor, window, cx| {
let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
let insertion_ranges = editor
.selections
.all(cx)
.iter()
.map(|s| s.range().clone())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
assert_eq!(editor.text(cx), expected_text);
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
}
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
// Can't move earlier than the first tab stop
assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, «two», three) b
a.f(one, «two», three) b
a.f(one, «two», three) b
"},
);
editor.move_to_prev_snippet_tabstop(window, cx);
assert(
editor,
cx,
indoc! {"
a.f(«one», two, «three») b
a.f(«one», two, «three») b
a.f(«one», two, «three») b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, «two», three) b
a.f(one, «two», three) b
a.f(one, «two», three) b
"},
);
assert!(editor.move_to_next_snippet_tabstop(window, cx));
assert(
editor,
cx,
indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"},
);
// As soon as the last tab stop is reached, snippet state is gone
editor.move_to_prev_snippet_tabstop(window, cx);
assert(
editor,
cx,
indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"},
);
});
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
// Can't move earlier than the first tab stop
cx.update_editor(|editor, window, cx| {
assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
});
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
a.f(«oneˇ», two, «threeˇ») b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
a.f(one, «twoˇ», three) b
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"});
// As soon as the last tab stop is reached, snippet state is gone
cx.update_editor(|editor, window, cx| {
assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
});
cx.assert_editor_state(indoc! {"
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
a.f(one, two, three)ˇ b
"});
}
#[gpui::test]
async fn test_snippet_indentation(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_editor(|editor, window, cx| {
let snippet = Snippet::parse(indoc! {"
/*
* Multiline comment with leading indentation
*
* $1
*/
$0"})
.unwrap();
let insertion_ranges = editor
.selections
.all(cx)
.iter()
.map(|s| s.range().clone())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
});
cx.assert_editor_state(indoc! {"
/*
* Multiline comment with leading indentation
*
* ˇ
*/
"});
cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
cx.assert_editor_state(indoc! {"
/*
* Multiline comment with leading indentation
*
*•
*/
ˇ"});
}
#[gpui::test]
@@ -10479,6 +10495,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: &'static str,
initial_state: String,
buffer_marked_text: String,
completion_label: &'static str,
completion_text: &'static str,
expected_with_insert_mode: String,
expected_with_replace_mode: String,
@@ -10491,6 +10508,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Start of word matches completion text",
initial_state: "before ediˇ after".into(),
buffer_marked_text: "before <edi|> after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
@@ -10501,6 +10519,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Accept same text at the middle of the word",
initial_state: "before ediˇtor after".into(),
buffer_marked_text: "before <edi|tor> after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
@@ -10511,6 +10530,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "End of word matches completion text -- cursor at end",
initial_state: "before torˇ after".into(),
buffer_marked_text: "before <tor|> after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇ after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
@@ -10521,6 +10541,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "End of word matches completion text -- cursor at start",
initial_state: "before ˇtor after".into(),
buffer_marked_text: "before <|tor> after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
@@ -10531,6 +10552,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Prepend text containing whitespace",
initial_state: "pˇfield: bool".into(),
buffer_marked_text: "<p|field>: bool".into(),
completion_label: "pub ",
completion_text: "pub ",
expected_with_insert_mode: "pub ˇfield: bool".into(),
expected_with_replace_mode: "pub ˇ: bool".into(),
@@ -10541,6 +10563,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Add element to start of list",
initial_state: "[element_ˇelement_2]".into(),
buffer_marked_text: "[<element_|element_2>]".into(),
completion_label: "element_1",
completion_text: "element_1",
expected_with_insert_mode: "[element_1ˇelement_2]".into(),
expected_with_replace_mode: "[element_1ˇ]".into(),
@@ -10551,6 +10574,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Add element to start of list -- first and second elements are equal",
initial_state: "[elˇelement]".into(),
buffer_marked_text: "[<el|element>]".into(),
completion_label: "element",
completion_text: "element",
expected_with_insert_mode: "[elementˇelement]".into(),
expected_with_replace_mode: "[elementˇ]".into(),
@@ -10561,6 +10585,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Ends with matching suffix",
initial_state: "SubˇError".into(),
buffer_marked_text: "<Sub|Error>".into(),
completion_label: "SubscriptionError",
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇError".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
@@ -10571,6 +10596,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Suffix is a subsequence -- contiguous",
initial_state: "SubˇErr".into(),
buffer_marked_text: "<Sub|Err>".into(),
completion_label: "SubscriptionError",
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
@@ -10581,6 +10607,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
initial_state: "Suˇscrirr".into(),
buffer_marked_text: "<Su|scrirr>".into(),
completion_label: "SubscriptionError",
completion_text: "SubscriptionError",
expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
@@ -10591,12 +10618,46 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
initial_state: "foo(indˇix)".into(),
buffer_marked_text: "foo(<ind|ix>)".into(),
completion_label: "node_index",
completion_text: "node_index",
expected_with_insert_mode: "foo(node_indexˇix)".into(),
expected_with_replace_mode: "foo(node_indexˇ)".into(),
expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
},
Run {
run_description: "Replace range ends before cursor - should extend to cursor",
initial_state: "before editˇo after".into(),
buffer_marked_text: "before <{ed}>it|o after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇo after".into(),
expected_with_replace_mode: "before editorˇo after".into(),
expected_with_replace_subsequence_mode: "before editorˇo after".into(),
expected_with_replace_suffix_mode: "before editorˇo after".into(),
},
Run {
run_description: "Uses label for suffix matching",
initial_state: "before ediˇtor after".into(),
buffer_marked_text: "before <edi|tor> after".into(),
completion_label: "editor",
completion_text: "editor()",
expected_with_insert_mode: "before editor()ˇtor after".into(),
expected_with_replace_mode: "before editor()ˇ after".into(),
expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
},
Run {
run_description: "Case insensitive subsequence and suffix matching",
initial_state: "before EDiˇtoR after".into(),
buffer_marked_text: "before <EDi|toR> after".into(),
completion_label: "editor",
completion_text: "editor",
expected_with_insert_mode: "before editorˇtoR after".into(),
expected_with_replace_mode: "before editorˇ after".into(),
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
expected_with_replace_suffix_mode: "before editorˇ after".into(),
},
];
for run in runs {
@@ -10637,7 +10698,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
handle_completion_request_with_insert_and_replace(
&mut cx,
&run.buffer_marked_text,
vec![run.completion_text],
vec![(run.completion_label, run.completion_text)],
counter.clone(),
)
.await;
@@ -10697,7 +10758,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
vec![(completion_text, completion_text)],
counter.clone(),
)
.await;
@@ -10731,7 +10792,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
handle_completion_request_with_insert_and_replace(
&mut cx,
&buffer_marked_text,
vec![completion_text],
vec![(completion_text, completion_text)],
counter.clone(),
)
.await;
@@ -10818,7 +10879,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
handle_completion_request_with_insert_and_replace(
&mut cx,
completion_marked_buffer,
vec![completion_text],
vec![(completion_text, completion_text)],
Arc::new(AtomicUsize::new(0)),
)
.await;
@@ -10872,7 +10933,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
handle_completion_request_with_insert_and_replace(
&mut cx,
completion_marked_buffer,
vec![completion_text],
vec![(completion_text, completion_text)],
Arc::new(AtomicUsize::new(0)),
)
.await;
@@ -10921,7 +10982,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
handle_completion_request_with_insert_and_replace(
&mut cx,
completion_marked_buffer,
vec![completion_text],
vec![(completion_text, completion_text)],
Arc::new(AtomicUsize::new(0)),
)
.await;
@@ -11139,14 +11200,15 @@ async fn test_completion(cx: &mut TestAppContext) {
"});
cx.simulate_keystroke(".");
handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec!["first_completion", "second_completion"],
true,
counter.clone(),
&mut cx,
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -11246,7 +11308,6 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit
"});
handle_completion_request(
&mut cx,
indoc! {"
one.second_completion
two s
@@ -11254,7 +11315,9 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit
"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
true,
counter.clone(),
&mut cx,
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -11264,7 +11327,6 @@ async fn test_completion(cx: &mut TestAppContext) {
cx.simulate_keystroke("i");
handle_completion_request(
&mut cx,
indoc! {"
one.second_completion
two si
@@ -11272,7 +11334,9 @@ async fn test_completion(cx: &mut TestAppContext) {
additional edit
"},
vec!["fourth_completion", "fifth_completion", "sixth_completion"],
true,
counter.clone(),
&mut cx,
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -11306,10 +11370,11 @@ async fn test_completion(cx: &mut TestAppContext) {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
handle_completion_request(
&mut cx,
"editor.<clo|>",
vec!["close", "clobber"],
true,
counter.clone(),
&mut cx,
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
@@ -11326,6 +11391,128 @@ async fn test_completion(cx: &mut TestAppContext) {
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_completion_reuse(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let counter = Arc::new(AtomicUsize::new(0));
cx.set_state("objˇ");
cx.simulate_keystroke(".");
// Initial completion request returns complete results
let is_incomplete = false;
handle_completion_request(
"obj.|<>",
vec!["a", "ab", "abc"],
is_incomplete,
counter.clone(),
&mut cx,
)
.await;
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.ˇ");
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
// Type "a" - filters existing completions
cx.simulate_keystroke("a");
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.aˇ");
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
// Type "b" - filters existing completions
cx.simulate_keystroke("b");
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.abˇ");
check_displayed_completions(vec!["ab", "abc"], &mut cx);
// Type "c" - filters existing completions
cx.simulate_keystroke("c");
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.abcˇ");
check_displayed_completions(vec!["abc"], &mut cx);
// Backspace to delete "c" - filters existing completions
cx.update_editor(|editor, window, cx| {
editor.backspace(&Backspace, window, cx);
});
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.abˇ");
check_displayed_completions(vec!["ab", "abc"], &mut cx);
// Moving cursor to the left dismisses menu.
cx.update_editor(|editor, window, cx| {
editor.move_left(&MoveLeft, window, cx);
});
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
cx.assert_editor_state("obj.aˇb");
cx.update_editor(|editor, _, _| {
assert_eq!(editor.context_menu_visible(), false);
});
// Type "b" - new request
cx.simulate_keystroke("b");
let is_incomplete = false;
handle_completion_request(
"obj.<ab|>a",
vec!["ab", "abc"],
is_incomplete,
counter.clone(),
&mut cx,
)
.await;
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
cx.assert_editor_state("obj.abˇb");
check_displayed_completions(vec!["ab", "abc"], &mut cx);
// Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
cx.update_editor(|editor, window, cx| {
editor.backspace(&Backspace, window, cx);
});
let is_incomplete = false;
handle_completion_request(
"obj.<a|>b",
vec!["a", "ab", "abc"],
is_incomplete,
counter.clone(),
&mut cx,
)
.await;
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
cx.assert_editor_state("obj.aˇb");
check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
// Backspace to delete "a" - dismisses menu.
cx.update_editor(|editor, window, cx| {
editor.backspace(&Backspace, window, cx);
});
cx.run_until_parked();
assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
cx.assert_editor_state("obj.ˇb");
cx.update_editor(|editor, _, _| {
assert_eq!(editor.context_menu_visible(), false);
});
}
#[gpui::test]
async fn test_word_completion(cx: &mut TestAppContext) {
let lsp_fetch_timeout_ms = 10;
@@ -12006,9 +12193,11 @@ async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
let task_completion_item = closure_completion_item.clone();
counter_clone.fetch_add(1, atomic::Ordering::Release);
async move {
Ok(Some(lsp::CompletionResponse::Array(vec![
task_completion_item,
])))
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: true,
item_defaults: None,
items: vec![task_completion_item],
})))
}
});
@@ -17082,6 +17271,64 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
function component() {
\treturn (
\t\t\t
\t\t<div>
\t\t\t<abc></abc>
\t\t</div>
\t)
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..8,
vec![
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 4, 4, 2),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
function component() {
\treturn (
\t
\t\t<div>
\t\t\t<abc></abc>
\t\t</div>
\t)
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..8,
vec![
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 2, 5, 1),
indent_guide(buffer_id, 4, 4, 2),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
@@ -20016,7 +20263,6 @@ println!("5");
pane_1
.update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
.unwrap()
})
.await
.unwrap();
@@ -20053,7 +20299,6 @@ println!("5");
pane_2
.update_in(cx, |pane, window, cx| {
pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
.unwrap()
})
.await
.unwrap();
@@ -20229,7 +20474,6 @@ println!("5");
});
pane.update_in(cx, |pane, window, cx| {
pane.close_all_items(&CloseAllItems::default(), window, cx)
.unwrap()
})
.await
.unwrap();
@@ -20583,7 +20827,6 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
pane.update_in(cx, |pane, window, cx| {
pane.close_active_item(&CloseActiveItem::default(), window, cx)
})
.unwrap()
.await
.unwrap();
pane.update_in(cx, |pane, window, cx| {
@@ -21010,6 +21253,22 @@ pub fn handle_signature_help_request(
}
}
#[track_caller]
pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
cx.update_editor(|editor, _, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
let entries = menu.entries.borrow();
let entries = entries
.iter()
.map(|entry| entry.string.as_str())
.collect::<Vec<_>>();
assert_eq!(entries, expected);
} else {
panic!("Expected completions menu");
}
});
}
/// Handle completion request passing a marked string specifying where the completion
/// should be triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range.
@@ -21017,10 +21276,11 @@ pub fn handle_signature_help_request(
/// Also see `handle_completion_request_with_insert_and_replace`.
#[track_caller]
pub fn handle_completion_request(
cx: &mut EditorLspTestContext,
marked_string: &str,
completions: Vec<&'static str>,
is_incomplete: bool,
counter: Arc<AtomicUsize>,
cx: &mut EditorLspTestContext,
) -> impl Future<Output = ()> {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
@@ -21044,8 +21304,10 @@ pub fn handle_completion_request(
params.text_document_position.position,
complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array(
completions
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: is_incomplete,
item_defaults: None,
items: completions
.iter()
.map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(),
@@ -21056,7 +21318,7 @@ pub fn handle_completion_request(
..Default::default()
})
.collect(),
)))
})))
}
});
@@ -21068,19 +21330,27 @@ pub fn handle_completion_request(
/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
/// given instead, which also contains an `insert` range.
///
/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
/// that is, `replace_range.start..cursor_pos`.
/// This function uses markers to define ranges:
/// - `|` marks the cursor position
/// - `<>` marks the replace range
/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
pub fn handle_completion_request_with_insert_and_replace(
cx: &mut EditorLspTestContext,
marked_string: &str,
completions: Vec<&'static str>,
completions: Vec<(&'static str, &'static str)>, // (label, new_text)
counter: Arc<AtomicUsize>,
) -> impl Future<Output = ()> {
let complete_from_marker: TextRangeMarker = '|'.into();
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let insert_range_marker: TextRangeMarker = ('{', '}').into();
let (_, mut marked_ranges) = marked_text_ranges_by(
marked_string,
vec![complete_from_marker.clone(), replace_range_marker.clone()],
vec![
complete_from_marker.clone(),
replace_range_marker.clone(),
insert_range_marker.clone(),
],
);
let complete_from_position =
@@ -21088,6 +21358,14 @@ pub fn handle_completion_request_with_insert_and_replace(
let replace_range =
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
let insert_range = match marked_ranges.remove(&insert_range_marker) {
Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
_ => lsp::Range {
start: replace_range.start,
end: complete_from_position,
},
};
let mut request =
cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
@@ -21101,16 +21379,13 @@ pub fn handle_completion_request_with_insert_and_replace(
Ok(Some(lsp::CompletionResponse::Array(
completions
.iter()
.map(|completion_text| lsp::CompletionItem {
label: completion_text.to_string(),
.map(|(label, new_text)| lsp::CompletionItem {
label: label.to_string(),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
insert: lsp::Range {
start: replace_range.start,
end: complete_from_position,
},
insert: insert_range,
replace: replace_range,
new_text: completion_text.to_string(),
new_text: new_text.to_string(),
},
)),
..Default::default()

View File

@@ -682,7 +682,7 @@ impl EditorElement {
editor.select(
SelectPhase::BeginColumnar {
position,
reset: false,
reset: true,
goal_column: point_for_position.exact_unclipped.column(),
},
window,

View File

@@ -1095,14 +1095,15 @@ mod tests {
//prompt autocompletion menu
cx.simulate_keystroke(".");
handle_completion_request(
&mut cx,
indoc! {"
one.|<>
two
three
"},
vec!["first_completion", "second_completion"],
true,
counter.clone(),
&mut cx,
)
.await;
cx.condition(|editor, _| editor.context_menu_visible()) // wait until completion menu is visible

View File

@@ -600,7 +600,7 @@ pub(crate) fn handle_from(
})
.collect::<Vec<_>>();
this.update_in(cx, |this, window, cx| {
this.change_selections_without_showing_completions(None, window, cx, |s| {
this.change_selections_without_updating_completions(None, window, cx, |s| {
s.select(base_selections);
});
})

View File

@@ -22,6 +22,7 @@ use smol::stream::StreamExt;
use task::ResolvedTask;
use task::TaskContext;
use text::BufferId;
use ui::SharedString;
use util::ResultExt as _;
pub(crate) fn find_specific_language_server_in_selection<F>(
@@ -133,13 +134,22 @@ pub fn lsp_tasks(
cx.spawn(async move |cx| {
cx.spawn(async move |cx| {
let mut lsp_tasks = Vec::new();
let mut lsp_tasks = HashMap::default();
while let Some(server_to_query) = lsp_task_sources.next().await {
if let Some((server_id, buffers)) = server_to_query {
let source_kind = TaskSourceKind::Lsp(server_id);
let id_base = source_kind.to_id_base();
let mut new_lsp_tasks = Vec::new();
for buffer in buffers {
let source_kind = match buffer.update(cx, |buffer, _| {
buffer.language().map(|language| language.name())
}) {
Ok(Some(language_name)) => TaskSourceKind::Lsp {
server: server_id,
language_name: SharedString::from(language_name),
},
Ok(None) => continue,
Err(_) => return Vec::new(),
};
let id_base = source_kind.to_id_base();
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
.await
.unwrap_or_default();
@@ -168,11 +178,14 @@ pub fn lsp_tasks(
);
}
}
lsp_tasks
.entry(source_kind)
.or_insert_with(Vec::new)
.append(&mut new_lsp_tasks);
}
lsp_tasks.push((source_kind, new_lsp_tasks));
}
}
lsp_tasks
lsp_tasks.into_iter().collect()
})
.race({
// `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast

View File

@@ -532,7 +532,9 @@ impl EditorTestContext {
#[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
generate_marked_text(&self.buffer_text(), &expected_selections, true)
.replace(" \n", "\n");
self.assert_selections(expected_selections, expected_marked_text)
}
@@ -561,7 +563,8 @@ impl EditorTestContext {
) {
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
generate_marked_text(&self.buffer_text(), &actual_selections, true)
.replace(" \n", "\n");
if expected_selections != actual_selections {
pretty_assertions::assert_eq!(
actual_marked_text,

View File

@@ -246,6 +246,7 @@ impl ExampleContext {
| ThreadEvent::StreamedAssistantThinking(_, _)
| ThreadEvent::UsePendingTools { .. }
| ThreadEvent::CompletionCanceled => {}
ThreadEvent::ToolUseLimitReached => {}
ThreadEvent::ToolFinished {
tool_use_id,
pending_tool_use,

View File

@@ -759,8 +759,8 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
})
.await
.unwrap()
.unwrap()
.into_iter()
.flat_map(|response| response.completions)
.map(|c| c.label.text)
.collect::<Vec<_>>();
assert_eq!(

View File

@@ -38,8 +38,8 @@ use std::{
};
use text::Point;
use ui::{
ContextMenu, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, PopoverMenu,
PopoverMenuHandle, Tooltip, prelude::*,
ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing,
PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, maybe, paths::PathWithPosition, post_inc};
use workspace::{
@@ -47,7 +47,10 @@ use workspace::{
notifications::NotifyResultExt, pane,
};
actions!(file_finder, [SelectPrevious, ToggleMenu]);
actions!(
file_finder,
[SelectPrevious, ToggleFilterMenu, ToggleSplitMenu]
);
impl ModalView for FileFinder {
fn on_before_dismiss(
@@ -56,7 +59,14 @@ impl ModalView for FileFinder {
cx: &mut Context<Self>,
) -> workspace::DismissDecision {
let submenu_focused = self.picker.update(cx, |picker, cx| {
picker.delegate.popover_menu_handle.is_focused(window, cx)
picker
.delegate
.filter_popover_menu_handle
.is_focused(window, cx)
|| picker
.delegate
.split_popover_menu_handle
.is_focused(window, cx)
});
workspace::DismissDecision::Dismiss(!submenu_focused)
}
@@ -212,9 +222,30 @@ impl FileFinder {
window.dispatch_action(Box::new(menu::SelectPrevious), cx);
}
fn handle_toggle_menu(&mut self, _: &ToggleMenu, window: &mut Window, cx: &mut Context<Self>) {
fn handle_filter_toggle_menu(
&mut self,
_: &ToggleFilterMenu,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.picker.update(cx, |picker, cx| {
let menu_handle = &picker.delegate.popover_menu_handle;
let menu_handle = &picker.delegate.filter_popover_menu_handle;
if menu_handle.is_deployed() {
menu_handle.hide(cx);
} else {
menu_handle.show(window, cx);
}
});
}
fn handle_split_toggle_menu(
&mut self,
_: &ToggleSplitMenu,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.picker.update(cx, |picker, cx| {
let menu_handle = &picker.delegate.split_popover_menu_handle;
if menu_handle.is_deployed() {
menu_handle.hide(cx);
} else {
@@ -345,7 +376,8 @@ impl Render for FileFinder {
.w(modal_max_width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::handle_select_prev))
.on_action(cx.listener(Self::handle_toggle_menu))
.on_action(cx.listener(Self::handle_filter_toggle_menu))
.on_action(cx.listener(Self::handle_split_toggle_menu))
.on_action(cx.listener(Self::handle_toggle_ignored))
.on_action(cx.listener(Self::go_to_file_split_left))
.on_action(cx.listener(Self::go_to_file_split_right))
@@ -371,7 +403,8 @@ pub struct FileFinderDelegate {
history_items: Vec<FoundPath>,
separate_history: bool,
first_update: bool,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
filter_popover_menu_handle: PopoverMenuHandle<ContextMenu>,
split_popover_menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
include_ignored: Option<bool>,
include_ignored_refresh: Task<()>,
@@ -758,7 +791,8 @@ impl FileFinderDelegate {
history_items,
separate_history,
first_update: true,
popover_menu_handle: PopoverMenuHandle::default(),
filter_popover_menu_handle: PopoverMenuHandle::default(),
split_popover_menu_handle: PopoverMenuHandle::default(),
focus_handle: cx.focus_handle(),
include_ignored: FileFinderSettings::get_global(cx).include_ignored,
include_ignored_refresh: Task::ready(()),
@@ -1137,8 +1171,13 @@ impl FileFinderDelegate {
fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("FileFinder");
if self.popover_menu_handle.is_focused(window, cx) {
key_context.add("menu_open");
if self.filter_popover_menu_handle.is_focused(window, cx) {
key_context.add("filter_menu_open");
}
if self.split_popover_menu_handle.is_focused(window, cx) {
key_context.add("split_menu_open");
}
key_context
}
@@ -1492,62 +1531,112 @@ impl PickerDelegate for FileFinderDelegate {
)
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
let context = self.focus_handle.clone();
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
Some(
h_flex()
.w_full()
.p_2()
.p_1p5()
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
IconButton::new("toggle-ignored", IconName::Sliders)
.on_click({
let focus_handle = self.focus_handle.clone();
move |_, window, cx| {
focus_handle.dispatch_action(&ToggleIncludeIgnored, window, cx);
}
PopoverMenu::new("filter-menu-popover")
.with_handle(self.filter_popover_menu_handle.clone())
.attach(gpui::Corner::BottomRight)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(1.0),
y: px(1.0),
})
.style(ButtonStyle::Subtle)
.shape(IconButtonShape::Square)
.toggle_state(self.include_ignored.unwrap_or(false))
.tooltip({
let focus_handle = self.focus_handle.clone();
.trigger_with_tooltip(
IconButton::new("filter-trigger", IconName::Sliders)
.icon_size(IconSize::Small)
.icon_size(IconSize::Small)
.toggle_state(self.include_ignored.unwrap_or(false))
.when(self.include_ignored.is_some(), |this| {
this.indicator(Indicator::dot().color(Color::Info))
}),
{
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Filter Options",
&ToggleFilterMenu,
&focus_handle,
window,
cx,
)
}
},
)
.menu({
let focus_handle = focus_handle.clone();
let include_ignored = self.include_ignored;
move |window, cx| {
Tooltip::for_action_in(
"Use ignored files",
&ToggleIncludeIgnored,
&focus_handle,
window,
cx,
)
Some(ContextMenu::build(window, cx, {
let focus_handle = focus_handle.clone();
move |menu, _, _| {
menu.context(focus_handle.clone())
.header("Filter Options")
.toggleable_entry(
"Include Ignored Files",
include_ignored.unwrap_or(false),
ui::IconPosition::End,
Some(ToggleIncludeIgnored.boxed_clone()),
move |window, cx| {
window.focus(&focus_handle);
window.dispatch_action(
ToggleIncludeIgnored.boxed_clone(),
cx,
);
},
)
}
}))
}
}),
)
.child(
h_flex()
.gap_2()
.gap_0p5()
.child(
Button::new("open-selection", "Open").on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.child(
PopoverMenu::new("menu-popover")
.with_handle(self.popover_menu_handle.clone())
.attach(gpui::Corner::TopRight)
.anchor(gpui::Corner::BottomRight)
PopoverMenu::new("split-menu-popover")
.with_handle(self.split_popover_menu_handle.clone())
.attach(gpui::Corner::BottomRight)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(1.0),
y: px(1.0),
})
.trigger(
Button::new("actions-trigger", "Split…")
.selected_label_color(Color::Accent),
ButtonLike::new("split-trigger")
.child(Label::new("Split…"))
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.children(
KeyBinding::for_action_in(
&ToggleSplitMenu,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
),
)
.menu({
let focus_handle = focus_handle.clone();
move |window, cx| {
Some(ContextMenu::build(window, cx, {
let context = context.clone();
let focus_handle = focus_handle.clone();
move |menu, _, _| {
menu.context(context)
menu.context(focus_handle.clone())
.action(
"Split Left",
pane::SplitLeft.boxed_clone(),
@@ -1565,6 +1654,21 @@ impl PickerDelegate for FileFinderDelegate {
}))
}
}),
)
.child(
Button::new("open-selection", "Open")
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
),
)
.into_any(),

View File

@@ -739,7 +739,6 @@ async fn test_ignored_root(cx: &mut TestAppContext) {
.update_in(cx, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.close_active_item(&CloseActiveItem::default(), window, cx)
.unwrap()
})
})
.await

View File

@@ -39,15 +39,32 @@ pub struct UserCaretPosition {
}
impl UserCaretPosition {
pub fn at_selection_end(selection: &Selection<Point>, snapshot: &MultiBufferSnapshot) -> Self {
pub(crate) fn at_selection_end(
selection: &Selection<Point>,
snapshot: &MultiBufferSnapshot,
) -> Self {
let selection_end = selection.head();
let line_start = Point::new(selection_end.row, 0);
let chars_to_last_position = snapshot
.text_summary_for_range::<text::TextSummary, _>(line_start..selection_end)
.chars as u32;
let (line, character) = if let Some((buffer_snapshot, point, _)) =
snapshot.point_to_buffer_point(selection_end)
{
let line_start = Point::new(point.row, 0);
let chars_to_last_position = buffer_snapshot
.text_summary_for_range::<text::TextSummary, _>(line_start..point)
.chars as u32;
(line_start.row, chars_to_last_position)
} else {
let line_start = Point::new(selection_end.row, 0);
let chars_to_last_position = snapshot
.text_summary_for_range::<text::TextSummary, _>(line_start..selection_end)
.chars as u32;
(selection_end.row, chars_to_last_position)
};
Self {
line: NonZeroU32::new(selection_end.row + 1).expect("added 1"),
character: NonZeroU32::new(chars_to_last_position + 1).expect("added 1"),
line: NonZeroU32::new(line + 1).expect("added 1"),
character: NonZeroU32::new(character + 1).expect("added 1"),
}
}
}

View File

@@ -202,6 +202,7 @@ pub enum Part {
InlineDataPart(InlineDataPart),
FunctionCallPart(FunctionCallPart),
FunctionResponsePart(FunctionResponsePart),
ThoughtPart(ThoughtPart),
}
#[derive(Debug, Serialize, Deserialize)]
@@ -235,6 +236,13 @@ pub struct FunctionResponsePart {
pub function_response: FunctionResponse,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThoughtPart {
pub thought: bool,
pub thought_signature: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CitationSource {
@@ -281,6 +289,22 @@ pub struct UsageMetadata {
pub total_token_count: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThinkingConfig {
pub thinking_budget: u32,
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum GoogleModelMode {
#[default]
Default,
Thinking {
budget_tokens: Option<u32>,
},
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerationConfig {
@@ -296,6 +320,8 @@ pub struct GenerationConfig {
pub top_p: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thinking_config: Option<ThinkingConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -488,6 +514,8 @@ pub enum Model {
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
max_tokens: usize,
#[serde(default)]
mode: GoogleModelMode,
},
}
@@ -544,6 +572,21 @@ impl Model {
Model::Custom { max_tokens, .. } => *max_tokens,
}
}
pub fn mode(&self) -> GoogleModelMode {
match self {
Self::Gemini15Pro
| Self::Gemini15Flash
| Self::Gemini20Pro
| Self::Gemini20Flash
| Self::Gemini20FlashThinking
| Self::Gemini20FlashLite
| Self::Gemini25ProExp0325
| Self::Gemini25ProPreview0325
| Self::Gemini25FlashPreview0417 => GoogleModelMode::Default,
Self::Custom { mode, .. } => *mode,
}
}
}
impl std::fmt::Display for Model {

View File

@@ -126,6 +126,7 @@ uuid.workspace = true
waker-fn = "1.2.0"
lyon = "1.0"
workspace-hack.workspace = true
libc.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"

View File

@@ -147,14 +147,49 @@ impl Keymap {
});
let mut bindings: SmallVec<[(KeyBinding, usize); 1]> = SmallVec::new();
let mut is_pending = None;
// (pending, is_no_action, depth, keystrokes)
let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None;
'outer: for (binding, pending) in possibilities {
for depth in (0..=context_stack.len()).rev() {
if self.binding_enabled(binding, &context_stack[0..depth]) {
if is_pending.is_none() {
is_pending = Some(pending);
let is_no_action = is_no_action(&*binding.action);
// We only want to consider a binding pending if it has an action
// This, however, means that if we have both a NoAction binding and a binding
// with an action at the same depth, we should still set is_pending to true.
if let Some(pending_info) = pending_info_opt.as_mut() {
let (
already_pending,
pending_is_no_action,
pending_depth,
pending_keystrokes,
) = *pending_info;
// We only want to change the pending status if it's not already pending AND if
// the existing pending status was set by a NoAction binding. This avoids a NoAction
// binding erroneously setting the pending status to true when a binding with an action
// already set it to false
//
// We also want to change the pending status if the keystrokes don't match,
// meaning it's different keystrokes than the NoAction that set pending to false
if pending
&& !already_pending
&& pending_is_no_action
&& (pending_depth == depth
|| pending_keystrokes != binding.keystrokes())
{
pending_info.0 = !is_no_action;
}
} else {
pending_info_opt = Some((
pending && !is_no_action,
is_no_action,
depth,
binding.keystrokes(),
));
}
if !pending {
bindings.push((binding.clone(), depth));
continue 'outer;
@@ -174,7 +209,7 @@ impl Keymap {
})
.collect();
(bindings, is_pending.unwrap_or_default())
(bindings, pending_info_opt.unwrap_or_default().0)
}
/// Check if the given binding is enabled, given a certain key context.
@@ -310,6 +345,102 @@ mod tests {
);
}
#[test]
/// Tests for https://github.com/zed-industries/zed/issues/30259
fn test_multiple_keystroke_binding_disabled() {
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space = || Keystroke::parse("space").unwrap();
let w = || Keystroke::parse("w").unwrap();
let space_w = [space(), w()];
let space_w_w = [space(), w(), w()];
let workspace_context = || [KeyContext::parse("workspace").unwrap()];
let editor_workspace_context = || {
[
KeyContext::parse("workspace").unwrap(),
KeyContext::parse("editor").unwrap(),
]
};
// Ensure `space` results in pending input on the workspace, but not editor
let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
assert!(space_workspace.0.is_empty());
assert_eq!(space_workspace.1, true);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert_eq!(space_editor.1, false);
// Ensure `space w` results in pending input on the workspace, but not editor
let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
assert!(space_w_workspace.0.is_empty());
assert_eq!(space_w_workspace.1, true);
let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
assert!(space_w_editor.0.is_empty());
assert_eq!(space_w_editor.1, false);
// Ensure `space w w` results in the binding in the workspace, but not in the editor
let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
assert!(!space_w_w_workspace.0.is_empty());
assert_eq!(space_w_w_workspace.1, false);
let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
assert!(space_w_w_editor.0.is_empty());
assert_eq!(space_w_w_editor.1, false);
// Now test what happens if we have another binding defined AFTER the NoAction
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert_eq!(space_editor.1, true);
// Now test what happens if we have another binding defined BEFORE the NoAction
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert_eq!(space_editor.1, true);
// Now test what happens if we have another binding defined at a higher context
// that should result in pending
let bindings = [
KeyBinding::new("space w w", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w x", ActionAlpha {}, Some("workspace")),
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
keymap.add_bindings(bindings.clone());
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
assert_eq!(space_editor.1, true);
}
#[test]
fn test_bindings_for_action() {
let bindings = [

View File

@@ -751,12 +751,28 @@ where
impl rwh::HasWindowHandle for WaylandWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
unimplemented!()
let surface = self.0.surface().id().as_ptr() as *mut libc::c_void;
let c_ptr = NonNull::new(surface).ok_or(rwh::HandleError::Unavailable)?;
let handle = rwh::WaylandWindowHandle::new(c_ptr);
let raw_handle = rwh::RawWindowHandle::Wayland(handle);
Ok(unsafe { rwh::WindowHandle::borrow_raw(raw_handle) })
}
}
impl rwh::HasDisplayHandle for WaylandWindow {
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
unimplemented!()
let display = self
.0
.surface()
.backend()
.upgrade()
.ok_or(rwh::HandleError::Unavailable)?
.display_ptr() as *mut libc::c_void;
let c_ptr = NonNull::new(display).ok_or(rwh::HandleError::Unavailable)?;
let handle = rwh::WaylandDisplayHandle::new(c_ptr);
let raw_handle = rwh::RawDisplayHandle::Wayland(handle);
Ok(unsafe { rwh::DisplayHandle::borrow_raw(raw_handle) })
}
}

View File

@@ -25,7 +25,7 @@ use derive_more::{Deref, DerefMut};
use futures::FutureExt;
use futures::channel::oneshot;
use parking_lot::RwLock;
use raw_window_handle::{HandleError, HasWindowHandle};
use raw_window_handle::{HandleError, HasDisplayHandle, HasWindowHandle};
use refineable::Refineable;
use slotmap::SlotMap;
use smallvec::SmallVec;
@@ -4428,6 +4428,14 @@ impl HasWindowHandle for Window {
}
}
impl HasDisplayHandle for Window {
fn display_handle(
&self,
) -> std::result::Result<raw_window_handle::DisplayHandle<'_>, HandleError> {
self.platform_window.display_handle()
}
}
/// An identifier for an [`Element`](crate::Element).
///
/// Can be constructed with a string, a number, or both, as well

View File

@@ -179,6 +179,8 @@ pub enum IconName {
PhoneIncoming,
Pin,
Play,
PlayAlt,
PlayBug,
Plus,
PocketKnife,
Power,

View File

@@ -11,7 +11,7 @@ use gpui::{
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, Task, WeakEntity,
Window, canvas, div, fill, img, opaque_grey, point, size,
};
use language::File as _;
use language::{DiskState, File as _};
use persistence::IMAGE_VIEWER;
use project::{ImageItem, Project, ProjectPath, image_store::ImageItemEvent};
use settings::Settings;
@@ -191,6 +191,10 @@ impl Item for ImageView {
focus_handle: cx.focus_handle(),
}))
}
fn has_deleted_file(&self, cx: &App) -> bool {
self.image_item.read(cx).file.disk_state() == DiskState::Deleted
}
}
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {

View File

@@ -11,7 +11,7 @@ use language::{
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
};
use project::lsp_store::CompletionDocumentation;
use project::{Completion, CompletionSource, Project, ProjectPath};
use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath};
use std::cell::RefCell;
use std::fmt::Write as _;
use std::ops::Range;
@@ -641,18 +641,18 @@ impl CompletionProvider for RustStyleCompletionProvider {
_: editor::CompletionContext,
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Option<Vec<project::Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
else {
return Task::ready(Ok(Some(Vec::new())));
return Task::ready(Ok(Vec::new()));
};
self.div_inspector.update(cx, |div_inspector, _cx| {
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
});
Task::ready(Ok(Some(
STYLE_METHODS
Task::ready(Ok(vec![CompletionResponse {
completions: STYLE_METHODS
.iter()
.map(|(_, method)| Completion {
replace_range: replace_range.clone(),
@@ -667,7 +667,8 @@ impl CompletionProvider for RustStyleCompletionProvider {
confirm: None,
})
.collect(),
)))
is_incomplete: false,
}]))
}
fn resolve_completions(

View File

@@ -51,6 +51,7 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
shellexpand.workspace = true
smallvec.workspace = true
smol.workspace = true
streaming-iterator.workspace = true

View File

@@ -34,7 +34,7 @@ pub use highlight_map::HighlightMap;
use http_client::HttpClient;
pub use language_registry::{LanguageName, LoadedLanguage};
use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions};
pub use manifest::{ManifestName, ManifestProvider, ManifestQuery};
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
use parking_lot::Mutex;
use regex::Regex;
use schemars::{
@@ -323,7 +323,6 @@ pub trait LspAdapterDelegate: Send + Sync {
fn http_client(&self) -> Arc<dyn HttpClient>;
fn worktree_id(&self) -> WorktreeId;
fn worktree_root_path(&self) -> &Path;
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
fn registered_lsp_adapters(&self) -> Vec<Arc<dyn LspAdapter>>;
async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;

View File

@@ -23,6 +23,7 @@ use serde_json::Value;
use settings::{
Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true;
@@ -1331,9 +1332,10 @@ impl settings::Settings for AllLanguageSettings {
disabled_globs: completion_globs
.iter()
.filter_map(|g| {
let expanded_g = shellexpand::tilde(g).into_owned();
Some(DisabledGlob {
matcher: globset::Glob::new(g).ok()?.compile_matcher(),
is_absolute: Path::new(g).is_absolute(),
matcher: globset::Glob::new(&expanded_g).ok()?.compile_matcher(),
is_absolute: Path::new(&expanded_g).is_absolute(),
})
})
.collect(),
@@ -1712,10 +1714,12 @@ mod tests {
};
#[cfg(windows)]
let glob_str = glob_str.as_str();
let expanded_glob_str = shellexpand::tilde(glob_str).into_owned();
DisabledGlob {
matcher: globset::Glob::new(glob_str).unwrap().compile_matcher(),
is_absolute: Path::new(glob_str).is_absolute(),
matcher: globset::Glob::new(&expanded_glob_str)
.unwrap()
.compile_matcher(),
is_absolute: Path::new(&expanded_glob_str).is_absolute(),
}
})
.collect(),
@@ -1811,6 +1815,12 @@ mod tests {
let dot_env_file = make_test_file(&[".env"]);
let settings = build_settings(&[".env"]);
assert!(!settings.enabled_for_file(&dot_env_file, &cx));
// Test tilde expansion
let home = shellexpand::tilde("~").into_owned().to_string();
let home_file = make_test_file(&[&home, "test.rs"]);
let settings = build_settings(&["~/test.rs"]);
assert!(!settings.enabled_for_file(&home_file, &cx));
}
#[test]

View File

@@ -1,8 +1,7 @@
use std::{borrow::Borrow, path::Path, sync::Arc};
use gpui::SharedString;
use crate::LspAdapterDelegate;
use settings::WorktreeId;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ManifestName(SharedString);
@@ -39,10 +38,15 @@ pub struct ManifestQuery {
/// Path to the file, relative to worktree root.
pub path: Arc<Path>,
pub depth: usize,
pub delegate: Arc<dyn LspAdapterDelegate>,
pub delegate: Arc<dyn ManifestDelegate>,
}
pub trait ManifestProvider {
fn name(&self) -> ManifestName;
fn search(&self, query: ManifestQuery) -> Option<Arc<Path>>;
}
pub trait ManifestDelegate: Send + Sync {
fn worktree_id(&self) -> WorktreeId;
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
}

View File

@@ -14,7 +14,7 @@ use collections::HashMap;
use gpui::{AsyncApp, SharedString};
use settings::WorktreeId;
use crate::LanguageName;
use crate::{LanguageName, ManifestName};
/// Represents a single toolchain.
#[derive(Clone, Debug)]
@@ -44,10 +44,13 @@ pub trait ToolchainLister: Send + Sync {
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Option<Arc<Path>>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString;
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName;
}
#[async_trait(?Send)]

View File

@@ -4,6 +4,7 @@ use client::{Client, UserStore, zed_urls};
use futures::{
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
};
use google_ai::GoogleModelMode;
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, SemanticVersion, Subscription, Task,
};
@@ -750,7 +751,8 @@ impl LanguageModel for CloudLanguageModel {
let client = self.client.clone();
let llm_api_token = self.llm_api_token.clone();
let model_id = self.model.id.to_string();
let generate_content_request = into_google(request, model_id.clone());
let generate_content_request =
into_google(request, model_id.clone(), GoogleModelMode::Default);
async move {
let http_client = &client.http_client();
let token = llm_api_token.acquire(&client).await?;
@@ -922,7 +924,8 @@ impl LanguageModel for CloudLanguageModel {
}
zed_llm_client::LanguageModelProvider::Google => {
let client = self.client.clone();
let request = into_google(request, self.model.id.to_string());
let request =
into_google(request, self.model.id.to_string(), GoogleModelMode::Default);
let llm_api_token = self.llm_api_token.clone();
let future = self.request_limiter.stream(async move {
let PerformLlmCompletionResponse {

View File

@@ -1,7 +1,8 @@
use anyhow::{Context as _, Result, anyhow};
use collections::BTreeMap;
use collections::{BTreeMap, HashMap};
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::Stream;
use futures::{FutureExt, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{
AnyView, AppContext as _, AsyncApp, Entity, FontStyle, Subscription, Task, TextStyle,
@@ -12,11 +13,14 @@ use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelToolChoice, RateLimiter, Role,
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
RateLimiter, Role, StopReason,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::pin::Pin;
use std::str::FromStr;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{Icon, IconName, List, prelude::*};
@@ -28,6 +32,13 @@ const PROVIDER_ID: &str = "deepseek";
const PROVIDER_NAME: &str = "DeepSeek";
const DEEPSEEK_API_KEY_VAR: &str = "DEEPSEEK_API_KEY";
#[derive(Default)]
struct RawToolCall {
id: String,
name: String,
arguments: String,
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct DeepSeekSettings {
pub api_url: String,
@@ -280,11 +291,11 @@ impl LanguageModel for DeepSeekLanguageModel {
}
fn supports_tools(&self) -> bool {
false
true
}
fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {
false
true
}
fn supports_images(&self) -> bool {
@@ -339,35 +350,12 @@ impl LanguageModel for DeepSeekLanguageModel {
BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>,
>,
> {
let request = into_deepseek(
request,
self.model.id().to_string(),
self.max_output_tokens(),
);
let request = into_deepseek(request, &self.model, self.max_output_tokens());
let stream = self.stream_completion(request, cx);
async move {
let stream = stream.await?;
Ok(stream
.map(|result| {
result
.and_then(|response| {
response
.choices
.first()
.context("Empty response")
.map(|choice| {
choice
.delta
.content
.clone()
.unwrap_or_default()
.map(LanguageModelCompletionEvent::Text)
})
})
.map_err(LanguageModelCompletionError::Other)
})
.boxed())
let mapper = DeepSeekEventMapper::new();
Ok(mapper.map_stream(stream.await?).boxed())
}
.boxed()
}
@@ -375,69 +363,67 @@ impl LanguageModel for DeepSeekLanguageModel {
pub fn into_deepseek(
request: LanguageModelRequest,
model: String,
model: &deepseek::Model,
max_output_tokens: Option<u32>,
) -> deepseek::Request {
let is_reasoner = model == "deepseek-reasoner";
let is_reasoner = *model == deepseek::Model::Reasoner;
let len = request.messages.len();
let merged_messages =
request
.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
let mut messages = Vec::new();
for message in request.messages {
for content in message.content {
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => messages
.push(match message.role {
Role::User => deepseek::RequestMessage::User { content: text },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(text),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content: text },
}),
MessageContent::RedactedThinking(_) => {}
MessageContent::Image(_) => {}
MessageContent::ToolUse(tool_use) => {
let tool_call = deepseek::ToolCall {
id: tool_use.id.to_string(),
content: deepseek::ToolCallContent::Function {
function: deepseek::FunctionContent {
name: tool_use.name.to_string(),
arguments: serde_json::to_string(&tool_use.input)
.unwrap_or_default(),
},
},
};
if is_reasoner {
if let Some(last_msg) = acc.last_mut() {
match (last_msg, role) {
(deepseek::RequestMessage::User { content: last }, Role::User) => {
last.push(' ');
last.push_str(&content);
return acc;
}
(
deepseek::RequestMessage::Assistant {
content: last_content,
..
},
Role::Assistant,
) => {
*last_content = last_content
.take()
.map(|c| {
let mut s =
String::with_capacity(c.len() + content.len() + 1);
s.push_str(&c);
s.push(' ');
s.push_str(&content);
s
})
.or(Some(content));
return acc;
}
_ => {}
}
if let Some(deepseek::RequestMessage::Assistant { tool_calls, .. }) =
messages.last_mut()
{
tool_calls.push(tool_call);
} else {
messages.push(deepseek::RequestMessage::Assistant {
content: None,
tool_calls: vec![tool_call],
});
}
}
acc.push(match role {
Role::User => deepseek::RequestMessage::User { content },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content },
});
acc
});
MessageContent::ToolResult(tool_result) => {
match &tool_result.content {
LanguageModelToolResultContent::Text(text) => {
messages.push(deepseek::RequestMessage::Tool {
content: text.to_string(),
tool_call_id: tool_result.tool_use_id.to_string(),
});
}
LanguageModelToolResultContent::Image(_) => {}
};
}
}
}
}
deepseek::Request {
model,
messages: merged_messages,
model: model.id().to_string(),
messages,
stream: true,
max_tokens: max_output_tokens,
temperature: if is_reasoner {
@@ -460,6 +446,103 @@ pub fn into_deepseek(
}
}
pub struct DeepSeekEventMapper {
tool_calls_by_index: HashMap<usize, RawToolCall>,
}
impl DeepSeekEventMapper {
pub fn new() -> Self {
Self {
tool_calls_by_index: HashMap::default(),
}
}
pub fn map_stream(
mut self,
events: Pin<Box<dyn Send + Stream<Item = Result<deepseek::StreamResponse>>>>,
) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
{
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
})
})
}
pub fn map_event(
&mut self,
event: deepseek::StreamResponse,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.first() else {
return vec![Err(LanguageModelCompletionError::Other(anyhow!(
"Response contained no choices"
)))];
};
let mut events = Vec::new();
if let Some(content) = choice.delta.content.clone() {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
for tool_call in tool_calls {
let entry = self.tool_calls_by_index.entry(tool_call.index).or_default();
if let Some(tool_id) = tool_call.id.clone() {
entry.id = tool_id;
}
if let Some(function) = tool_call.function.as_ref() {
if let Some(name) = function.name.clone() {
entry.name = name;
}
if let Some(arguments) = function.arguments.clone() {
entry.arguments.push_str(&arguments);
}
}
}
}
match choice.finish_reason.as_deref() {
Some("stop") => {
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
}
Some("tool_calls") => {
events.extend(self.tool_calls_by_index.drain().map(|(_, tool_call)| {
match serde_json::Value::from_str(&tool_call.arguments) {
Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: tool_call.id.clone().into(),
name: tool_call.name.as_str().into(),
is_input_complete: true,
input,
raw_input: tool_call.arguments.clone(),
},
)),
Err(error) => Err(LanguageModelCompletionError::BadInputJson {
id: tool_call.id.into(),
tool_name: tool_call.name.as_str().into(),
raw_input: tool_call.arguments.into(),
json_parse_error: error.to_string(),
}),
}
}));
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
}
Some(stop_reason) => {
log::error!("Unexpected DeepSeek stop_reason: {stop_reason:?}",);
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
}
None => {}
}
events
}
}
struct ConfigurationView {
api_key_editor: Entity<Editor>,
state: Entity<State>,

View File

@@ -4,7 +4,8 @@ use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
use google_ai::{
FunctionDeclaration, GenerateContentResponse, Part, SystemInstruction, UsageMetadata,
FunctionDeclaration, GenerateContentResponse, GoogleModelMode, Part, SystemInstruction,
ThinkingConfig, UsageMetadata,
};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
@@ -45,11 +46,41 @@ pub struct GoogleSettings {
pub available_models: Vec<AvailableModel>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ModelMode {
#[default]
Default,
Thinking {
/// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`.
budget_tokens: Option<u32>,
},
}
impl From<ModelMode> for GoogleModelMode {
fn from(value: ModelMode) -> Self {
match value {
ModelMode::Default => GoogleModelMode::Default,
ModelMode::Thinking { budget_tokens } => GoogleModelMode::Thinking { budget_tokens },
}
}
}
impl From<GoogleModelMode> for ModelMode {
fn from(value: GoogleModelMode) -> Self {
match value {
GoogleModelMode::Default => ModelMode::Default,
GoogleModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens },
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AvailableModel {
name: String,
display_name: Option<String>,
max_tokens: usize,
mode: Option<ModelMode>,
}
pub struct GoogleLanguageModelProvider {
@@ -216,6 +247,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
name: model.name.clone(),
display_name: model.display_name.clone(),
max_tokens: model.max_tokens,
mode: model.mode.unwrap_or_default().into(),
},
);
}
@@ -343,7 +375,7 @@ impl LanguageModel for GoogleLanguageModel {
cx: &App,
) -> BoxFuture<'static, Result<usize>> {
let model_id = self.model.id().to_string();
let request = into_google(request, model_id.clone());
let request = into_google(request, model_id.clone(), self.model.mode());
let http_client = self.http_client.clone();
let api_key = self.state.read(cx).api_key.clone();
@@ -379,7 +411,7 @@ impl LanguageModel for GoogleLanguageModel {
>,
>,
> {
let request = into_google(request, self.model.id().to_string());
let request = into_google(request, self.model.id().to_string(), self.model.mode());
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
let response = request
@@ -394,6 +426,7 @@ impl LanguageModel for GoogleLanguageModel {
pub fn into_google(
mut request: LanguageModelRequest,
model_id: String,
mode: GoogleModelMode,
) -> google_ai::GenerateContentRequest {
fn map_content(content: Vec<MessageContent>) -> Vec<Part> {
content
@@ -504,6 +537,12 @@ pub fn into_google(
stop_sequences: Some(request.stop),
max_output_tokens: None,
temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
thinking_config: match mode {
GoogleModelMode::Thinking { budget_tokens } => {
budget_tokens.map(|thinking_budget| ThinkingConfig { thinking_budget })
}
GoogleModelMode::Default => None,
},
top_p: None,
top_k: None,
}),
@@ -620,6 +659,7 @@ impl GoogleEventMapper {
)));
}
Part::FunctionResponsePart(_) => {}
Part::ThoughtPart(_) => {}
});
}
}
@@ -685,10 +725,15 @@ fn update_usage(usage: &mut UsageMetadata, new: &UsageMetadata) {
}
fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
let prompt_tokens = usage.prompt_token_count.unwrap_or(0) as u32;
let cached_tokens = usage.cached_content_token_count.unwrap_or(0) as u32;
let input_tokens = prompt_tokens - cached_tokens;
let output_tokens = usage.candidates_token_count.unwrap_or(0) as u32;
language_model::TokenUsage {
input_tokens: usage.prompt_token_count.unwrap_or(0) as u32,
output_tokens: usage.candidates_token_count.unwrap_or(0) as u32,
cache_read_input_tokens: usage.cached_content_token_count.unwrap_or(0) as u32,
input_tokens,
output_tokens,
cache_read_input_tokens: cached_tokens,
cache_creation_input_tokens: 0,
}
}

View File

@@ -4,14 +4,11 @@ use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, Subscription, Task};
use http_client::HttpClient;
use language_model::{
AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent,
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
LanguageModelToolUseId, StopReason,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason,
};
use ollama::{
ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool,
@@ -54,6 +51,10 @@ pub struct AvailableModel {
pub keep_alive: Option<KeepAlive>,
/// Whether the model supports tools
pub supports_tools: Option<bool>,
/// Whether the model supports vision
pub supports_images: Option<bool>,
/// Whether to enable think mode
pub supports_thinking: Option<bool>,
}
pub struct OllamaLanguageModelProvider {
@@ -99,6 +100,8 @@ impl State {
None,
None,
Some(capabilities.supports_tools()),
Some(capabilities.supports_vision()),
Some(capabilities.supports_thinking()),
);
Ok(ollama_model)
}
@@ -219,6 +222,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
max_tokens: model.max_tokens,
keep_alive: model.keep_alive.clone(),
supports_tools: model.supports_tools,
supports_vision: model.supports_images,
supports_thinking: model.supports_thinking,
},
);
}
@@ -273,22 +278,59 @@ pub struct OllamaLanguageModel {
impl OllamaLanguageModel {
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let supports_vision = self.model.supports_vision.unwrap_or(false);
ChatRequest {
model: self.model.name.clone(),
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => ChatMessage::User {
content: msg.string_contents(),
},
Role::Assistant => ChatMessage::Assistant {
content: msg.string_contents(),
tool_calls: None,
},
Role::System => ChatMessage::System {
content: msg.string_contents(),
},
.map(|msg| {
let images = if supports_vision {
msg.content
.iter()
.filter_map(|content| match content {
MessageContent::Image(image) => Some(image.source.to_string()),
_ => None,
})
.collect::<Vec<String>>()
} else {
vec![]
};
match msg.role {
Role::User => ChatMessage::User {
content: msg.string_contents(),
images: if images.is_empty() {
None
} else {
Some(images)
},
},
Role::Assistant => {
let content = msg.string_contents();
let thinking =
msg.content.into_iter().find_map(|content| match content {
MessageContent::Thinking { text, .. } if !text.is_empty() => {
Some(text)
}
_ => None,
});
ChatMessage::Assistant {
content,
tool_calls: None,
images: if images.is_empty() {
None
} else {
Some(images)
},
thinking,
}
}
Role::System => ChatMessage::System {
content: msg.string_contents(),
},
}
})
.collect(),
keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
@@ -299,6 +341,7 @@ impl OllamaLanguageModel {
temperature: request.temperature.or(Some(1.0)),
..Default::default()
}),
think: self.model.supports_thinking,
tools: request.tools.into_iter().map(tool_into_ollama).collect(),
}
}
@@ -326,7 +369,7 @@ impl LanguageModel for OllamaLanguageModel {
}
fn supports_images(&self) -> bool {
false
self.model.supports_vision.unwrap_or(false)
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
@@ -424,7 +467,7 @@ fn map_to_language_model_completion_events(
let mut events = Vec::new();
match delta.message {
ChatMessage::User { content } => {
ChatMessage::User { content, images: _ } => {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
ChatMessage::System { content } => {
@@ -433,8 +476,16 @@ fn map_to_language_model_completion_events(
ChatMessage::Assistant {
content,
tool_calls,
images: _,
thinking,
} => {
// Check for tool calls
if let Some(text) = thinking {
events.push(Ok(LanguageModelCompletionEvent::Thinking {
text,
signature: None,
}));
}
if let Some(tool_call) = tool_calls.and_then(|v| v.into_iter().next()) {
match tool_call {
OllamaToolCall::Function(function) => {
@@ -455,7 +506,7 @@ fn map_to_language_model_completion_events(
state.used_tools = true;
}
}
} else {
} else if !content.is_empty() {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
}

View File

@@ -20,8 +20,8 @@ use workspace::{
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
};
const SEND_LINE: &str = "// Send:";
const RECEIVE_LINE: &str = "// Receive:";
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
pub struct LogStore {
@@ -464,8 +464,7 @@ impl LogStore {
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front();
}
let entry: &str = message.as_ref();
let entry = entry.to_string();
let entry = format!("{}\n", message.as_ref().trim());
let visible = message.should_include(current_severity);
log_lines.push_back(message);
@@ -580,7 +579,7 @@ impl LogStore {
});
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: message.to_string(),
entry: format!("{}\n\n", message),
kind: LogKind::Rpc,
});
cx.notify();
@@ -644,13 +643,7 @@ impl LspLogView {
let last_point = editor.buffer().read(cx).len(cx);
let newest_cursor_is_at_end =
editor.selections.newest::<usize>(cx).start >= last_point;
editor.edit(
vec![
(last_point..last_point, entry.trim()),
(last_point..last_point, "\n"),
],
cx,
);
editor.edit(vec![(last_point..last_point, entry.as_str())], cx);
let entry_length = entry.len();
if entry_length > 1024 {
editor.fold_ranges(

View File

@@ -379,17 +379,19 @@ impl ContextProvider for PythonContextProvider {
};
let module_target = self.build_module_target(variables);
let worktree_id = location
.file_location
.buffer
.read(cx)
.file()
.map(|f| f.worktree_id(cx));
let location_file = location.file_location.buffer.read(cx).file().cloned();
let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
cx.spawn(async move |cx| {
let raw_toolchain = if let Some(worktree_id) = worktree_id {
let file_path = location_file
.as_ref()
.and_then(|f| f.path().parent())
.map(Arc::from)
.unwrap_or_else(|| Arc::from("".as_ref()));
toolchains
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
.active_toolchain(worktree_id, file_path, "Python".into(), cx)
.await
.map_or_else(
|| String::from("python3"),
@@ -398,14 +400,16 @@ impl ContextProvider for PythonContextProvider {
} else {
String::from("python3")
};
let active_toolchain = format!("\"{raw_toolchain}\"");
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
let raw_toolchain_var = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
Ok(task::TaskVariables::from_iter(
test_target
.into_iter()
.chain(module_target.into_iter())
.chain([toolchain, raw_toolchain]),
.chain([toolchain, raw_toolchain_var]),
))
})
}
@@ -689,9 +693,13 @@ fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
#[async_trait]
impl ToolchainLister for PythonToolchainProvider {
fn manifest_name(&self) -> language::ManifestName {
ManifestName::from(SharedString::new_static("pyproject.toml"))
}
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Option<Arc<Path>>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList {
let env = project_env.unwrap_or_default();
@@ -702,7 +710,14 @@ impl ToolchainLister for PythonToolchainProvider {
&environment,
);
let mut config = Configuration::default();
config.workspace_directories = Some(vec![worktree_root.clone()]);
let mut directories = vec![worktree_root.clone()];
if let Some(subroot_relative_path) = subroot_relative_path {
debug_assert!(subroot_relative_path.is_relative());
directories.push(worktree_root.join(subroot_relative_path));
}
config.workspace_directories = Some(directories);
for locator in locators.iter() {
locator.configure(&config);
}

View File

@@ -1,7 +1,7 @@
name = "TypeScript"
grammar = "typescript"
path_suffixes = ["ts", "cts", "mts"]
first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx)\b'
first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx|[/ ]node)\b'
line_comments = ["// "]
block_comment = ["/*", "*/"]
autoclose_before = ";:.,=}])>"

View File

@@ -1690,7 +1690,9 @@ impl MultiBuffer {
last_range.context.start <= range.context.start,
"Last range: {last_range:?} Range: {range:?}"
);
if last_range.context.end >= range.context.start {
if last_range.context.end >= range.context.start
|| last_range.context.end.row + 1 == range.context.start.row
{
last_range.context.end = range.context.end.max(last_range.context.end);
*counts.last_mut().unwrap() += 1;
continue;
@@ -5780,7 +5782,7 @@ impl MultiBufferSnapshot {
// then add to the indent stack with the depth found
let mut found_indent = false;
let mut last_row = first_row;
if line_indent.is_line_empty() {
if line_indent.is_line_blank() {
while !found_indent {
let Some((target_row, new_line_indent, _)) = row_indents.next() else {
break;
@@ -5790,7 +5792,7 @@ impl MultiBufferSnapshot {
break;
}
if new_line_indent.is_line_empty() {
if new_line_indent.is_line_blank() {
continue;
}
last_row = target_row.min(end_row);

View File

@@ -1592,7 +1592,6 @@ fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) {
six
seven
eight
-----
nine
ten
eleven
@@ -1848,7 +1847,6 @@ fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
zero
one
two
-----
three
four
five

View File

@@ -38,11 +38,13 @@ pub struct Model {
pub max_tokens: usize,
pub keep_alive: Option<KeepAlive>,
pub supports_tools: Option<bool>,
pub supports_vision: Option<bool>,
pub supports_thinking: Option<bool>,
}
fn get_max_tokens(name: &str) -> usize {
/// Default context length for unknown models.
const DEFAULT_TOKENS: usize = 2048;
const DEFAULT_TOKENS: usize = 4096;
/// Magic number. Lets many Ollama models work with ~16GB of ram.
const MAXIMUM_TOKENS: usize = 16384;
@@ -67,6 +69,8 @@ impl Model {
display_name: Option<&str>,
max_tokens: Option<usize>,
supports_tools: Option<bool>,
supports_vision: Option<bool>,
supports_thinking: Option<bool>,
) -> Self {
Self {
name: name.to_owned(),
@@ -76,6 +80,8 @@ impl Model {
max_tokens: max_tokens.unwrap_or_else(|| get_max_tokens(name)),
keep_alive: Some(KeepAlive::indefinite()),
supports_tools,
supports_vision,
supports_thinking,
}
}
@@ -98,9 +104,14 @@ pub enum ChatMessage {
Assistant {
content: String,
tool_calls: Option<Vec<OllamaToolCall>>,
#[serde(skip_serializing_if = "Option::is_none")]
images: Option<Vec<String>>,
thinking: Option<String>,
},
User {
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
images: Option<Vec<String>>,
},
System {
content: String,
@@ -140,6 +151,7 @@ pub struct ChatRequest {
pub keep_alive: KeepAlive,
pub options: Option<ChatOptions>,
pub tools: Vec<OllamaTool>,
pub think: Option<bool>,
}
impl ChatRequest {
@@ -215,6 +227,14 @@ impl ModelShow {
// .contains expects &String, which would require an additional allocation
self.capabilities.iter().any(|v| v == "tools")
}
pub fn supports_vision(&self) -> bool {
self.capabilities.iter().any(|v| v == "vision")
}
pub fn supports_thinking(&self) -> bool {
self.capabilities.iter().any(|v| v == "thinking")
}
}
pub async fn complete(
@@ -459,9 +479,12 @@ mod tests {
ChatMessage::Assistant {
content,
tool_calls,
images: _,
thinking,
} => {
assert!(content.is_empty());
assert!(tool_calls.is_some_and(|v| !v.is_empty()));
assert!(thinking.is_none());
}
_ => panic!("Deserialized wrong role"),
}
@@ -523,4 +546,70 @@ mod tests {
assert!(result.capabilities.contains(&"tools".to_string()));
assert!(result.capabilities.contains(&"completion".to_string()));
}
#[test]
fn serialize_chat_request_with_images() {
let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let request = ChatRequest {
model: "llava".to_string(),
messages: vec![ChatMessage::User {
content: "What do you see in this image?".to_string(),
images: Some(vec![base64_image.to_string()]),
}],
stream: false,
keep_alive: KeepAlive::default(),
options: None,
think: None,
tools: vec![],
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("images"));
assert!(serialized.contains(base64_image));
}
#[test]
fn serialize_chat_request_without_images() {
let request = ChatRequest {
model: "llama3.2".to_string(),
messages: vec![ChatMessage::User {
content: "Hello, world!".to_string(),
images: None,
}],
stream: false,
keep_alive: KeepAlive::default(),
options: None,
think: None,
tools: vec![],
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(!serialized.contains("images"));
}
#[test]
fn test_json_format_with_images() {
let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let request = ChatRequest {
model: "llava".to_string(),
messages: vec![ChatMessage::User {
content: "What do you see?".to_string(),
images: Some(vec![base64_image.to_string()]),
}],
stream: false,
keep_alive: KeepAlive::default(),
options: None,
think: None,
tools: vec![],
};
let serialized = serde_json::to_string(&request).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap();
let message_images = parsed["messages"][0]["images"].as_array().unwrap();
assert_eq!(message_images.len(), 1);
assert_eq!(message_images[0].as_str().unwrap(), base64_image);
}
}

View File

@@ -408,6 +408,7 @@ pub fn task_file_name() -> &'static str {
}
/// Returns the relative path to a `debug.json` file within a project.
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static Path {
Path::new(".zed/debug.json")
}

View File

@@ -30,6 +30,47 @@ impl DapLocator for GoLocator {
let go_action = build_config.args.first()?;
match go_action.as_str() {
"test" => {
let binary_path = if build_config.env.contains_key("OUT_DIR") {
"${OUT_DIR}/__debug".to_string()
} else {
"__debug".to_string()
};
let build_task = TaskTemplate {
label: "go test debug".into(),
command: "go".into(),
args: vec![
"test".into(),
"-c".into(),
"-gcflags \"all=-N -l\"".into(),
"-o".into(),
binary_path,
],
env: build_config.env.clone(),
cwd: build_config.cwd.clone(),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: task::HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
Some(DebugScenario {
label: resolved_label.to_string().into(),
adapter: adapter.0,
build: Some(BuildTaskDefinition::Template {
task_template: build_task,
locator_name: Some(self.name()),
}),
config: serde_json::Value::Null,
tcp_connection: None,
})
}
"run" => {
let program = build_config
.args
@@ -91,6 +132,23 @@ impl DapLocator for GoLocator {
}
match go_action.as_str() {
"test" => {
let program = if let Some(out_dir) = build_config.env.get("OUT_DIR") {
format!("{}/__debug", out_dir)
} else {
PathBuf::from(&cwd)
.join("__debug")
.to_string_lossy()
.to_string()
};
Ok(DebugRequest::Launch(task::LaunchRequest {
program,
cwd: Some(PathBuf::from(&cwd)),
args: vec!["-test.v".into(), "-test.run=${ZED_SYMBOL}".into()],
env,
}))
}
"build" => {
let package = build_config
.args
@@ -221,6 +279,92 @@ mod tests {
assert!(scenario.is_none());
}
#[test]
fn test_create_scenario_for_go_test() {
let locator = GoLocator;
let task = TaskTemplate {
label: "go test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env: Default::default(),
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
assert_eq!(scenario.adapter, "Delve");
assert_eq!(scenario.label, "test label");
assert!(scenario.build.is_some());
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert_eq!(task_template.command, "go");
assert!(task_template.args.contains(&"test".into()));
assert!(task_template.args.contains(&"-c".into()));
assert!(
task_template
.args
.contains(&"-gcflags \"all=-N -l\"".into())
);
assert!(task_template.args.contains(&"-o".into()));
assert!(task_template.args.contains(&"__debug".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
assert!(
scenario.config.is_null(),
"Initial config should be null to ensure it's invalid"
);
}
#[test]
fn test_create_scenario_for_go_test_with_out_dir() {
let locator = GoLocator;
let mut env = FxHashMap::default();
env.insert("OUT_DIR".to_string(), "/tmp/build".to_string());
let task = TaskTemplate {
label: "go test".into(),
command: "go".into(),
args: vec!["test".into(), ".".into()],
env,
cwd: Some("${ZED_WORKTREE_ROOT}".into()),
use_new_terminal: false,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
reveal_target: RevealTarget::Dock,
hide: HideStrategy::Never,
shell: Shell::System,
tags: vec![],
show_summary: true,
show_command: true,
};
let scenario =
locator.create_scenario(&task, "test label", DebugAdapterName("Delve".into()));
assert!(scenario.is_some());
let scenario = scenario.unwrap();
if let Some(BuildTaskDefinition::Template { task_template, .. }) = &scenario.build {
assert!(task_template.args.contains(&"${OUT_DIR}/__debug".into()));
} else {
panic!("Expected BuildTaskDefinition::Template");
}
}
#[test]
fn test_skip_unsupported_go_commands() {
let locator = GoLocator;

View File

@@ -2194,4 +2194,8 @@ impl Session {
self.shutdown(cx).detach();
}
}
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
self.thread_states.thread_state(thread_id)
}
}

View File

@@ -1,10 +1,10 @@
mod signature_help;
use crate::{
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
PrepareRenameResponse, ProjectTransaction, ResolveState,
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState,
lsp_store::{LocalLspStore, LspStore},
};
use anyhow::{Context as _, Result};
@@ -2095,7 +2095,7 @@ impl LspCommand for GetHover {
#[async_trait(?Send)]
impl LspCommand for GetCompletions {
type Response = Vec<CoreCompletion>;
type Response = CoreCompletionResponse;
type LspRequest = lsp::request::Completion;
type ProtoRequest = proto::GetCompletions;
@@ -2127,19 +2127,22 @@ impl LspCommand for GetCompletions {
mut cx: AsyncApp,
) -> Result<Self::Response> {
let mut response_list = None;
let mut completions = if let Some(completions) = completions {
let (mut completions, mut is_incomplete) = if let Some(completions) = completions {
match completions {
lsp::CompletionResponse::Array(completions) => completions,
lsp::CompletionResponse::Array(completions) => (completions, false),
lsp::CompletionResponse::List(mut list) => {
let is_incomplete = list.is_incomplete;
let items = std::mem::take(&mut list.items);
response_list = Some(list);
items
(items, is_incomplete)
}
}
} else {
Vec::new()
(Vec::new(), false)
};
let unfiltered_completions_count = completions.len();
let language_server_adapter = lsp_store
.read_with(&mut cx, |lsp_store, _| {
lsp_store.language_server_adapter_for_id(server_id)
@@ -2259,11 +2262,17 @@ impl LspCommand for GetCompletions {
});
})?;
// If completions were filtered out due to errors that may be transient, mark the result
// incomplete so that it is re-queried.
if unfiltered_completions_count != completions.len() {
is_incomplete = true;
}
language_server_adapter
.process_completions(&mut completions)
.await;
Ok(completions
let completions = completions
.into_iter()
.zip(completion_edits)
.map(|(mut lsp_completion, mut edit)| {
@@ -2290,7 +2299,12 @@ impl LspCommand for GetCompletions {
},
}
})
.collect())
.collect();
Ok(CoreCompletionResponse {
completions,
is_incomplete,
})
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
@@ -2332,18 +2346,20 @@ impl LspCommand for GetCompletions {
}
fn response_to_proto(
completions: Vec<CoreCompletion>,
response: CoreCompletionResponse,
_: &mut LspStore,
_: PeerId,
buffer_version: &clock::Global,
_: &mut App,
) -> proto::GetCompletionsResponse {
proto::GetCompletionsResponse {
completions: completions
completions: response
.completions
.iter()
.map(LspStore::serialize_completion)
.collect(),
version: serialize_version(buffer_version),
can_reuse: !response.is_incomplete,
}
}
@@ -2360,11 +2376,16 @@ impl LspCommand for GetCompletions {
})?
.await?;
message
let completions = message
.completions
.into_iter()
.map(LspStore::deserialize_completion)
.collect()
.collect::<Result<Vec<_>>>()?;
Ok(CoreCompletionResponse {
completions,
is_incomplete: !message.can_reuse,
})
}
fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result<BufferId> {

View File

@@ -3,14 +3,15 @@ pub mod lsp_ext_command;
pub mod rust_analyzer_ext;
use crate::{
CodeAction, Completion, CompletionSource, CoreCompletion, Hover, InlayHint, LspAction,
ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment,
lsp_command::{self, *},
lsp_store,
manifest_tree::{
AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestTree,
AdapterQuery, LanguageServerTree, LanguageServerTreeNode, LaunchDisposition,
ManifestQueryDelegate, ManifestTree,
},
prettier_store::{self, PrettierStore, PrettierStoreEvent},
project_settings::{LspSettings, ProjectSettings},
@@ -997,7 +998,7 @@ impl LocalLspStore {
.collect::<Vec<_>>();
async move {
futures::future::join_all(shutdown_futures).await;
join_all(shutdown_futures).await;
}
}
@@ -1036,7 +1037,7 @@ impl LocalLspStore {
else {
return Vec::new();
};
let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
let root = self.lsp_tree.update(cx, |this, cx| {
this.get(
project_path,
@@ -2290,7 +2291,8 @@ impl LocalLspStore {
})
.map(|(delegate, servers)| (true, delegate, servers))
.unwrap_or_else(|| {
let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx);
let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
let servers = self
.lsp_tree
.clone()
@@ -2304,7 +2306,7 @@ impl LocalLspStore {
)
.collect::<Vec<_>>()
});
(false, delegate, servers)
(false, lsp_delegate, servers)
});
let servers = servers
.into_iter()
@@ -3585,6 +3587,7 @@ impl LspStore {
prettier_store: Entity<PrettierStore>,
toolchain_store: Entity<ToolchainStore>,
environment: Entity<ProjectEnvironment>,
manifest_tree: Entity<ManifestTree>,
languages: Arc<LanguageRegistry>,
http_client: Arc<dyn HttpClient>,
fs: Arc<dyn Fs>,
@@ -3618,7 +3621,7 @@ impl LspStore {
sender,
)
};
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
Self {
mode: LspStoreMode::Local(LocalLspStore {
weak: cx.weak_entity(),
@@ -4465,10 +4468,13 @@ impl LspStore {
)
.map(|(delegate, servers)| (true, delegate, servers))
.or_else(|| {
let delegate = adapters
let lsp_delegate = adapters
.entry(worktree_id)
.or_insert_with(|| get_adapter(worktree_id, cx))
.clone()?;
let delegate = Arc::new(ManifestQueryDelegate::new(
worktree.read(cx).snapshot(),
));
let path = file
.path()
.parent()
@@ -4483,7 +4489,7 @@ impl LspStore {
cx,
);
Some((false, delegate, nodes.collect()))
Some((false, lsp_delegate, nodes.collect()))
})
else {
continue;
@@ -5075,7 +5081,7 @@ impl LspStore {
position: PointUtf16,
context: CompletionContext,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let language_registry = self.languages.clone();
if let Some((upstream_client, project_id)) = self.upstream_client() {
@@ -5099,11 +5105,17 @@ impl LspStore {
});
cx.foreground_executor().spawn(async move {
let completions = task.await?;
let mut result = Vec::new();
populate_labels_for_completions(completions, language, lsp_adapter, &mut result)
.await;
Ok(Some(result))
let completion_response = task.await?;
let completions = populate_labels_for_completions(
completion_response.completions,
language,
lsp_adapter,
)
.await;
Ok(vec![CompletionResponse {
completions,
is_incomplete: completion_response.is_incomplete,
}])
})
} else if let Some(local) = self.as_local() {
let snapshot = buffer.read(cx).snapshot();
@@ -5117,7 +5129,7 @@ impl LspStore {
)
.completions;
if !completion_settings.lsp {
return Task::ready(Ok(None));
return Task::ready(Ok(Vec::new()));
}
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
@@ -5184,25 +5196,23 @@ impl LspStore {
}
})?;
let mut has_completions_returned = false;
let mut completions = Vec::new();
for (lsp_adapter, task) in tasks {
if let Ok(Some(new_completions)) = task.await {
has_completions_returned = true;
populate_labels_for_completions(
new_completions,
let futures = tasks.into_iter().map(async |(lsp_adapter, task)| {
let completion_response = task.await.ok()??;
let completions = populate_labels_for_completions(
completion_response.completions,
language.clone(),
lsp_adapter,
&mut completions,
)
.await;
}
}
if has_completions_returned {
Ok(Some(completions))
} else {
Ok(None)
}
Some(CompletionResponse {
completions,
is_incomplete: completion_response.is_incomplete,
})
});
let responses: Vec<Option<CompletionResponse>> = join_all(futures).await;
Ok(responses.into_iter().flatten().collect())
})
} else {
Task::ready(Err(anyhow!("No upstream client or local language server")))
@@ -6476,7 +6486,7 @@ impl LspStore {
worktree_id,
path: Arc::from("".as_ref()),
};
let delegate = LocalLspAdapterDelegate::from_local_lsp(local, &worktree, cx);
let delegate = Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot()));
local.lsp_tree.update(cx, |language_server_tree, cx| {
for node in language_server_tree.get(
path,
@@ -9541,8 +9551,7 @@ async fn populate_labels_for_completions(
new_completions: Vec<CoreCompletion>,
language: Option<Arc<Language>>,
lsp_adapter: Option<Arc<CachedLspAdapter>>,
completions: &mut Vec<Completion>,
) {
) -> Vec<Completion> {
let lsp_completions = new_completions
.iter()
.filter_map(|new_completion| {
@@ -9566,6 +9575,7 @@ async fn populate_labels_for_completions(
.into_iter()
.fuse();
let mut completions = Vec::new();
for completion in new_completions {
match completion.source.lsp_completion(true) {
Some(lsp_completion) => {
@@ -9606,6 +9616,7 @@ async fn populate_labels_for_completions(
}
}
}
completions
}
#[derive(Debug)]
@@ -10204,14 +10215,6 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
self.worktree.id()
}
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
self.worktree.entry_for_path(path).map_or(false, |entry| {
is_dir.map_or(true, |is_required_to_be_dir| {
is_required_to_be_dir == entry.is_dir()
})
})
}
fn worktree_root_path(&self) -> &Path {
self.worktree.abs_path().as_ref()
}

View File

@@ -11,16 +11,17 @@ use std::{
borrow::Borrow,
collections::{BTreeMap, hash_map::Entry},
ops::ControlFlow,
path::Path,
sync::Arc,
};
use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription};
use language::{LspAdapterDelegate, ManifestName, ManifestQuery};
use language::{ManifestDelegate, ManifestName, ManifestQuery};
pub use manifest_store::ManifestProviders;
use path_trie::{LabelPresence, RootPathTrie, TriePath};
use settings::{SettingsStore, WorktreeId};
use worktree::{Event as WorktreeEvent, Worktree};
use worktree::{Event as WorktreeEvent, Snapshot, Worktree};
use crate::{
ProjectPath,
@@ -89,7 +90,7 @@ pub(crate) enum ManifestTreeEvent {
impl EventEmitter<ManifestTreeEvent> for ManifestTree {}
impl ManifestTree {
pub(crate) fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
root_points: Default::default(),
_subscriptions: [
@@ -106,11 +107,11 @@ impl ManifestTree {
worktree_store,
})
}
fn root_for_path(
pub(crate) fn root_for_path(
&mut self,
ProjectPath { worktree_id, path }: ProjectPath,
manifests: &mut dyn Iterator<Item = ManifestName>,
delegate: Arc<dyn LspAdapterDelegate>,
delegate: Arc<dyn ManifestDelegate>,
cx: &mut App,
) -> BTreeMap<ManifestName, ProjectPath> {
debug_assert_eq!(delegate.worktree_id(), worktree_id);
@@ -218,3 +219,26 @@ impl ManifestTree {
}
}
}
pub(crate) struct ManifestQueryDelegate {
worktree: Snapshot,
}
impl ManifestQueryDelegate {
pub fn new(worktree: Snapshot) -> Self {
Self { worktree }
}
}
impl ManifestDelegate for ManifestQueryDelegate {
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool {
self.worktree.entry_for_path(path).map_or(false, |entry| {
is_dir.map_or(true, |is_required_to_be_dir| {
is_required_to_be_dir == entry.is_dir()
})
})
}
fn worktree_id(&self) -> WorktreeId {
self.worktree.id()
}
}

View File

@@ -16,7 +16,7 @@ use std::{
use collections::{HashMap, IndexMap};
use gpui::{App, AppContext as _, Entity, Subscription};
use language::{
Attach, CachedLspAdapter, LanguageName, LanguageRegistry, LspAdapterDelegate,
Attach, CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
language_settings::AllLanguageSettings,
};
use lsp::LanguageServerName;
@@ -151,7 +151,7 @@ impl LanguageServerTree {
&'a mut self,
path: ProjectPath,
query: AdapterQuery<'_>,
delegate: Arc<dyn LspAdapterDelegate>,
delegate: Arc<dyn ManifestDelegate>,
cx: &mut App,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let settings_location = SettingsLocation {
@@ -181,7 +181,7 @@ impl LanguageServerTree {
LanguageServerName,
(LspSettings, BTreeSet<LanguageName>, Arc<CachedLspAdapter>),
>,
delegate: Arc<dyn LspAdapterDelegate>,
delegate: Arc<dyn ManifestDelegate>,
cx: &mut App,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let worktree_id = path.worktree_id;
@@ -401,7 +401,7 @@ impl<'tree> ServerTreeRebase<'tree> {
&'a mut self,
path: ProjectPath,
query: AdapterQuery<'_>,
delegate: Arc<dyn LspAdapterDelegate>,
delegate: Arc<dyn ManifestDelegate>,
cx: &mut App,
) -> impl Iterator<Item = LanguageServerTreeNode> + 'a {
let settings_location = SettingsLocation {

View File

@@ -35,6 +35,7 @@ pub use git_store::{
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
};
pub use manifest_tree::ManifestTree;
use anyhow::{Context as _, Result, anyhow};
use buffer_store::{BufferStore, BufferStoreEvent};
@@ -554,6 +555,23 @@ impl std::fmt::Debug for Completion {
}
}
/// Response from a source of completions.
pub struct CompletionResponse {
pub completions: Vec<Completion>,
/// When false, indicates that the list is complete and so does not need to be re-queried if it
/// can be filtered instead.
pub is_incomplete: bool,
}
/// Response from language server completion request.
#[derive(Clone, Debug, Default)]
pub(crate) struct CoreCompletionResponse {
pub completions: Vec<CoreCompletion>,
/// When false, indicates that the list is complete and so does not need to be re-queried if it
/// can be filtered instead.
pub is_incomplete: bool,
}
/// A generic completion that can come from different sources.
#[derive(Clone, Debug)]
pub(crate) struct CoreCompletion {
@@ -874,11 +892,13 @@ impl Project {
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
let environment = cx.new(|_| ProjectEnvironment::new(env));
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
let toolchain_store = cx.new(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
manifest_tree.clone(),
cx,
)
});
@@ -946,6 +966,7 @@ impl Project {
prettier_store.clone(),
toolchain_store.clone(),
environment.clone(),
manifest_tree,
languages.clone(),
client.http_client(),
fs.clone(),
@@ -3084,16 +3105,13 @@ impl Project {
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<ToolchainList>> {
if let Some(toolchain_store) = self.toolchain_store.clone() {
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
cx.spawn(async move |cx| {
cx.update(|cx| {
toolchain_store
.read(cx)
.list_toolchains(path, language_name, cx)
})
.ok()?
.await
toolchain_store
.update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
.ok()?
.await
})
} else {
Task::ready(None)
@@ -3429,7 +3447,7 @@ impl Project {
position: T,
context: CompletionContext,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<Completion>>>> {
) -> Task<Result<Vec<CompletionResponse>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.completions(buffer, position, context, cx)

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