Compare commits

..

69 Commits

Author SHA1 Message Date
Michael Sloan
2a55062dd1 Refactor multibuffer methods for multibuffer range -> buffer range
* Renames `excerpts_for_ranges` to `disjoint_ranges_to_buffer_ranges`,
and modifies it to return `MultiBufferExcerpt` since it provides more
information and was already being built.

* `range_to_buffer_ranges` now returns an iterator, should improve
efficiency in a few places.  Now implemented by just calling
`disjoint_ranges_to_buffer_ranges`.
2025-01-06 19:05:57 -07:00
Michael Sloan
1ef638d802 Remove unnecessary lifetimes on Buffer::diagnostic_group (#22698)
Release Notes:

- N/A
2025-01-06 06:12:35 +00:00
Michael Sloan
fcb03989d2 Remove unnecessary finding of primary diagnostic for diagnostic hover (#22697)
No need to find or store the primary range ahead of time as it's found
by `activate_diagnostics`.

Not entirely sure we should still even have the special case when the
popover is visible. It does support the keyboard interaction of opening
hover followed by jumping to the primary position, but that seems pretty
undiscoverable.

Support for clicking the hover to navigate to the primary diagnostic was
removed in https://github.com/zed-industries/zed/pull/3408

Release Notes:

- N/A
2025-01-06 06:08:17 +00:00
Burak Varlı
2a9fa0e2dc Ensure end >= start in lsp::Range (#22690)
Should resolve https://github.com/zed-industries/zed/issues/21714.

In some conditions that I'm not sure of, Zed sends LSP requests with
`start > end` position, and zls has an [assertion for end >=
start](f253553b82/src/offsets.zig (L492)),
and that causes zls to crash, like:

```bash
# first `textDocument/inlayHint` request with `end >= start`
[2025-01-05T19:33:09+00:00 TRACE lsp] outgoing message:{"jsonrpc":"2.0","id":1043,"method":"textDocument/inlayHint","params":{"textDocument":{"uri":"file:///Users/burak/Code/parzig/src/parquet/decoding.zig"},"range":{"start":{"line":0,"character":0},"end":{"line":24,"character":0}}}}
# successful response 
[2025-01-05T19:33:09+00:00 TRACE lsp::input_handler] incoming message: {"jsonrpc":"2.0","id":1043,"result":[{"position":{"line":0,"character":9},"label":": type","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":1,"character":22},"label":": type","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":4,"character":13},"label":": [](unknown type)","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":4,"character":30},"label":"T:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\ncomptime type\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":4,"character":33},"label":"n:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\nusize\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":5,"character":23},"label":": bool","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":6,"character":19},"label":": usize","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":9,"character":26},"label":": [](unknown type)","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":9,"character":43},"label":"T:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\ncomptime type\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":9,"character":47},"label":"n:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\nusize\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":21,"character":13},"label":": [](unknown type)","kind":1,"paddingLeft":false,"paddingRight":false},{"position":{"line":21,"character":30},"label":"T:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\ncomptime type\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":21,"character":33},"label":"n:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\nusize\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":22,"character":33},"label":"T:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\ncomptime type\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":22,"character":36},"label":"buf:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\n[]T\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":22,"character":41},"label":"bit_width:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\nu8\n```"},"paddingLeft":false,"paddingRight":true},{"position":{"line":22,"character":52},"label":"reader:","kind":2,"tooltip":{"kind":"markdown","value":"```zig\nanytype\n```"},"paddingLeft":false,"paddingRight":true}]}
[2025-01-05T19:33:09+00:00 TRACE lsp] Took 14.855ms to receive response to "textDocument/inlayHint" id 1043
# problematic `textDocument/inlayHint` request with `start > end`
[2025-01-05T19:33:09+00:00 TRACE lsp] outgoing message:{"jsonrpc":"2.0","id":1044,"method":"textDocument/inlayHint","params":{"textDocument":{"uri":"file:///Users/burak/Code/parzig/src/parquet/decoding.zig"},"range":{"start":{"line":50,"character":25},"end":{"line":25,"character":0}}}}
# zls crashes here, and after this point, all LSP requests fail
[2025-01-05T19:33:09+00:00 TRACE lsp] incoming stderr message:thread 5391652 panic: reached unreachable code
[2025-01-05T19:33:09+00:00 ERROR lsp] cannot read LSP message headers
```

In LSP specification for
[`Range`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range)
type, it says:
> ... If you want to specify a range that contains a line including the
line ending character(s) then use an end position denoting the start of
the next line.

I feel like zls's assertion is sensible, so I've updated the generic
`range_to_lsp` function rather than doing something specific to zls. But
let me know if this seems incorrect.

zls was crashing after 5-10 minutes of working with a Zig codebase
before, and after this change, I tested for an hour and didn't
experience any crashes.

Release Notes:

- Ensure `end >= start` in `lsp::Range`, which should fix Zig/zls
crashes.

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-01-06 05:22:28 +00:00
Michael Sloan
3ae6aa0e4d Start diagnostic group_id at 1 to handle non LS diagnostics (#22694)
In particular, `DiagnosticPopover` both:

* Supports moving the selection to a diagnostic when clicked, based on
`group_id`

* Provides Diagnostic values with `group_id: 0` providing informztion on
hover about invisible characters.

So, clicking such a popover would navigate to the very first error
produced by a language server. Really not a big deal of course, but
seems good to fix as it might result in surprising behavior in other
future circumstances

Release Notes:

- N/A
2025-01-06 05:18:56 +00:00
Michael Sloan
7506c0385b Fix a doc comment typo on DiagnosticEntry::to_lsp_diagnostic_stub (#22695)
Release Notes:

- N/A
2025-01-06 04:32:30 +00:00
Michael Sloan
570e6c80a8 Fix panic on diagnostic hover (#22693)
In #22620 `diagnostic_group` was modified to return results for
multibuffers, but was returning singleton buffer points. `hover_popover`
uses it to find the jump target for clicking the popup - which doesn't
seem to be working right now but that's a separate issue. Now that
`diagnostic_group` is returning values in multibuffers converting these
to anchors was crashing.

Also resolves a potential bug - if folding in multibuffers was supported
then "Go To Diagnostics" would not properly skip diagnostics from folded
regions.

Release Notes:

- N/A
2025-01-06 02:31:02 +00:00
tims
94ee2e1811 Fix ghost files appearing in the project panel when clicking relative paths in the terminal (#22688)
Closes #15705

When opening a file from the terminal, if the file path is relative, we
attempt to guess all possible paths where the file could be. This
involves generating paths for each worktree, the current terminal
directory, etc. For example, if we have two worktrees, `dotfiles` and
`example`, and `foo.txt` in `example/a`, the generated paths might look
like this:

- `/home/tims/dotfiles/../example/a/foo.txt` from the `dotfiles`
worktree
- `/home/tims/example/../example/a/foo.txt` from the `example` worktree
- `/home/tims/example/a/foo.txt` from the current terminal directory
(This is already canonicalized)

Note that there should only be a single path, but multiple paths are
created due to missing canonicalization.

Later, when opening these paths, the worktree prefix is stripped, and
the remaining path is used to open the file in its respective worktree.

As a result, the above three paths would resolve like this:

- `../example/a/foo.txt` as the filename in the `dotfiles` worktree
(Ghost file)
- `../example/a/foo.txt` as the filename in the `example` worktree
(Ghost file)
- `foo.txt` as the filename in the `a` directory of the `example`
worktree (This opens the file)

This PR fixes the issue by canonicalizing these paths before adding them
to the HashSet.

Before:

![before](https://github.com/user-attachments/assets/7cb98b86-1adf-462f-bcc6-9bff6a8425cd)

After:

![after](https://github.com/user-attachments/assets/44568167-2a5a-4022-ba98-b359d2c6e56b)


Release Notes:

- Fixed ghost files appearing in the project panel when clicking
relative paths in the terminal.
2025-01-05 21:49:32 +00:00
Piotr Osiewicz
299ae92ffb gpui: Do not derive serde::Deserialize for automatically generated Actions (#22687)
Closes #ISSUE

Release Notes:

- N/A
2025-01-05 17:25:00 +00:00
Cole Miller
de08e47e5b Improve panic report with reentrant SlotMap use (#22667)
`double_lease_panic` already does what we want, just extend it to the
indexing operation as well.

Release Notes:

- N/A
2025-01-04 20:37:40 +00:00
Kirill Bulatov
8151dc7696 Return back Rust completion details (#22648)
Closes https://github.com/zed-industries/zed/issues/22642

In Zed, Rust's label generators expected the details to come in ` (use
std.foo.Bar)` form, but recently, r-a started to send these details
without the leading whitespace which broke the code generation.

The PR makes LSP results parsing more lenient to work with both details'
forms.

Release Notes:

- Fixed Rust completion labels not showing the imports
2025-01-04 11:15:09 +00:00
Michael Sloan
5f1eee3c66 Fix inlay hints display reverting to settings value on theme change (#22605)
Closes #4276

Release Notes:

- Fixed inlay hints that have been manually enabled disappearing when
theme selector is used.
2025-01-04 08:26:08 +00:00
Mikayla Maki
9613084f59 Move git status out of Entry (#22224)
- [x] Rewrite worktree git handling
- [x] Fix tests
- [x] Fix `test_propagate_statuses_for_repos_under_project`
- [x] Replace `WorkDirectoryEntry` with `WorkDirectory` in
`RepositoryEntry`
- [x] Add a worktree event for capturing git status changes
- [x] Confirm that the local repositories are correctly updating the new
WorkDirectory field
- [x] Implement the git statuses query as a join when pulling entries
out of worktree
- [x] Use this new join to implement the project panel and outline
panel.
- [x] Synchronize git statuses over the wire for collab and remote dev
(use the existing `worktree_repository_statuses` table, adjust as
needed)
- [x] Only send changed statuses to collab

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.com>
Co-authored-by: Nathan <nathan@zed.dev>
2025-01-04 01:00:16 +00:00
Jason Lee
72057e5716 cli: Fix support for older macOS versions (#22515)
Close #22419

Release Notes:

- Fixed `zed` cli crash by `ScreenCaptureKit` library not loaded error
on macOS lower versions.

<img width="843" alt="image"
src="https://github.com/user-attachments/assets/9e0b615e-933f-4808-bf20-3e37e9e8bc6d"
/>

--- 

The main reason is the `cli` depends on `release_channel`, and it
depends on `gpui`.

```
$ cargo tree -p cli
├── release_channel v0.1.0 (/Users/jason/github/zed/crates/release_channel)
│   └── gpui v0.1.0 (/Users/jason/github/zed/crates/gpui)
│       ├── anyhow v1.0.95
│       ├── async-task v4.7.1
│       ├── block v0.1.6
│       ├── cocoa v0.26.0
```
2025-01-04 00:42:37 +00:00
tims
e25789893d linux: Fix issue where relative symlinks were not being watched using fs watch (#22608)
Closes #22607

Symlinks can be absolute or relative. When using
[stow](https://www.gnu.org/software/stow/) to manage dotfiles, it
creates relative symlinks to the target files.

For example:  

- Original file:  `/home/tims/dotfiles/zed/setting.json`  
- Symlink path: `/home/tims/.config/zed/setting.json`  
- Target path (relative to symlink): `../../dotfiles/zed/setting.json`  

The issue is that you can’t watch the symlink path because it’s relative
and doesn't include the base path it is relative to. This PR fixes that
by converting relative symlink paths to absolute paths.

- Absolute path (after parent join):
`/home/tims/.config/zed/../../dotfiles/zed/setting.json` (This works)
- Canonicalized path (from absolute path):
`/home/tims/dotfiles/zed/setting.json` (This works too, just more
cleaner)

Release Notes:

- Fix issue where items on the Welcome page could not be toggled on
Linux when using Stow to manage dotfiles
2025-01-04 00:12:20 +00:00
tims
b46b261f11 linux: Fix process PID to window mapping for X11 (#22348)
Closes #22326

This PR adds process PID information to window created by X11, so that
window manager can identify which process this window belongs to.
Without this property, the window manager would have no reliable way to
know which process created this window.

In original issue, `robotgo` throws error on `x, y, w, h :=
robotgo.GetBounds(pid)` this method. If we go deeper into the source
code of `robotgo`, it calls `GetXidFromPid` which goes through all
windows, and tries to check for provided pid. Hence, when it tries to do
that for Zed, it fails and returns `0, err` to caller.

```go
// Robotgo source code trying to look through all windows and query pid

// GetXidFromPid get the xid from pid
func GetXidFromPid(xu *xgbutil.XUtil, pid int) (xproto.Window, error) {
	windows, err := ewmh.ClientListGet(xu)
	if err != nil {
		return 0, err
	}

	for _, window := range windows {
		wmPid, err := ewmh.WmPidGet(xu, window)
		if err != nil {
			return 0, err
		}

		if uint(pid) == wmPid {
			return window, nil
		}
	}

	return 0, errors.New("failed to find a window with a matching pid.")
}
```

Querying for pid for active Zed window:

Before:
```sh
tims@lemon ~/w/go-repro [127]> xprop -root _NET_ACTIVE_WINDOW
_NET_ACTIVE_WINDOW(WINDOW): window id # 0x4e00002
tims@lemon ~/w/go-repro> xprop -id 0x4e00002 _NET_WM_PID
_NET_WM_PID:  not found.
```

After:
```sh
tims@lemon ~/w/go-repro> xprop -root _NET_ACTIVE_WINDOW
_NET_ACTIVE_WINDOW(WINDOW): window id # 0x4e00002
tims@lemon ~/w/go-repro> xprop -id 0x4e00002 _NET_WM_PID
_NET_WM_PID(CARDINAL) = 103548
tims@lemon ~/w/go-repro>
```

Correct zed process PID (below) assosiated with zed window (shown
above):

![image](https://github.com/user-attachments/assets/8b40128b-addb-4c88-944e-b1d26b908bf5)

Release Notes:

- Fix `robotgo` failing when Zed window is open on Linux
2025-01-04 00:10:36 +00:00
tims
71a0eb3b13 windows: Fix cursor style not changing when hovering over items in the title bar (#22580)
Closes #22578

Currently, the `hovered` boolean in the window state is only updated by
the `WM_MOUSELEAVE` event, which fires when the mouse cursor leaves the
window's working area. This means that when the user moves the cursor
from the window to the title bar, `hovered` is set to `false`. Later in
the code, this flag is used to determine the cursor style and check if
the cursor is over the correct window.

The `hovered` boolean should remain active even when the mouse is over
non-client items, such as the title bar or window borders. This PR fixes
that by using `WM_NCMOUSELEAVE` event, which is triggered when the mouse
leaves non-client items. This event is used to update the `hovered`
boolean accordingly.

Now, `hovered` is `true` when the mouse is over the window's working
area, as well as non-client areas like the title bar.

More context:

- Existing: `dwFlags: TME_LEAVE` tracks window area mouse leaves, which
is used in `handle_mouse_move_msg` func.
- New: `dwFlags: TME_LEAVE | TME_NONCLIENT` tracks non-client mouse
leaves, which is used in `handle_nc_mouse_move_msg` func.

Preview:


https://github.com/user-attachments/assets/b319303f-81b9-45cb-bf0c-535a59b96561

Release Notes:

- Fix cursor style not changing on hover over items in the title bar on
Windows
2025-01-04 00:01:29 +00:00
Marshall Bowers
dd75f85ecf elm: Extract to zed-extensions/elm repository (#22637)
This PR extracts the Elm extension to the
[zed-extensions/elm](https://github.com/zed-extensions/elm) repository.

Release Notes:

- N/A
2025-01-04 00:00:45 +00:00
Roy Williams
b1a6e2427f anthropic: Allow specifying additional beta headers for custom models (#20551)
Release Notes:

- Added the ability to specify additional beta headers for custom
Anthropic models.

---------

Co-authored-by: David Soria Parra <167242713+dsp-ant@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 23:46:32 +00:00
Marshall Bowers
bbe6bf9caf assistant: Remove unused AssistantSettings::update_file (#22636)
As a follow-up to #21672, this PR removes the
`AssistantSettings::update_file` method, as it was no longer used
anywhere.

Release Notes:

- N/A
2025-01-03 23:34:15 +00:00
Torrat
53cfb578e8 python: Adjust binary path based on OS (#22587)
Closes #ISSUE

- #21452 

Describe the bug / provide steps to reproduce it

Language server error: pylsp

failed to spawn command. path:
"C:\Users\AppData\Local\Zed\languages\pylsp\pylsp-venv\bin\pylsp",
working directory: "D:\Coding\Python", args: []
-- stderr--

Environment
- Windows 11
- python

Release Notes:

- Windows: Fixed the path building used to run `pip` commands in the
venv generated on Windows 11.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 22:56:31 +00:00
Drew Ridley
e6fe12d0e1 assistant: Remove outdated settings update path (#21672)
Removed a settings update that should have been removed in the 0.148.0
release.

I am not sure if there is a tracking issue, but I identified this check
for outdated settings that should not be needed anymore. I investigated
a bit and did not find any conflicts or UB as a result of removing this
code.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 22:52:08 +00:00
Marshall Bowers
04cf19d49a Upgrade strum to v0.26 (#22633)
This PR upgrades `strum` to v0.26.

Supersedes #21896.

Release Notes:

- N/A
2025-01-03 22:23:06 +00:00
Marshall Bowers
4bd5f0d355 racket: Extract to zed-extensions/racket repository (#22630)
This PR extracts the Racket extension to the
[zed-extensions/racket](https://github.com/zed-extensions/racket)
repository.

Release Notes:

- N/A
2025-01-03 22:04:50 +00:00
Marshall Bowers
fdbf3d0f25 clojure: Extract to zed-extensions/clojure repository (#22628)
This PR extracts the Clojure extension to the
[zed-extensions/clojure](https://github.com/zed-extensions/clojure)
repository.

Release Notes:

- N/A
2025-01-03 21:14:53 +00:00
Nils Koch
a1ef1d3f76 Add syntax highlighting for character literals in Haskell, PureScript, and Zig (#22609)
Closes #22480

Release Notes:

- N/A

| Before | After |
|----------|----------|
| <img width="344" alt="before"
src="https://github.com/user-attachments/assets/37f8daf7-c9a0-4259-8c03-bd1a4479abca"
/> | <img width="344" alt="after"
src="https://github.com/user-attachments/assets/0f7e4429-e48b-4b32-9797-a0da8487e23e"
/> |

Zig, Haskel, and PureScript define a character caputure name in
`highlights.scm`, but we did not define a color for that capture name in
the themes. The new character color is the same as the string color in
all themes.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 20:51:43 +00:00
Marshall Bowers
04518b11bc language_model_selector: Refresh the models when the providers change (#22624)
This PR fixes an issue introduced in #21939 where the list of models in
the language model selector could be outdated.

Since we're no longer recreating the picker each render, we now need to
make sure we are updating the list of models accordingly when there are
changes to the language model providers.

I noticed it specifically in Assistant1.

Release Notes:

- Fixed a staleness issue with the language model selector.
2025-01-03 19:38:08 +00:00
saahityaedams
e4eef725de Add support for Claude 3.5 Haiku model (#22323)
Partly Closes #22185

Release Notes:

- Added support for the Claude 3.5 Haiku model.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2025-01-03 18:49:29 +00:00
Marshall Bowers
7c7eb98e64 astro: Extract to zed-extensions/astro repository (#22623)
This PR extracts the Astro extension to the
[zed-extensions/astro](https://github.com/zed-extensions/astro)
repository.

Release Notes:

- N/A
2025-01-03 18:34:33 +00:00
Marshall Bowers
7826d79a40 markdown: Make push_div work with Stateful<Div>s (#22622)
This PR updates the `push_div` method in the `MarkdownElementBuilder` to
support taking in a `Stateful<Div>`.

This is some groundwork for supporting horizontal scrolling in Markdown
code blocks.

Release Notes:

- N/A
2025-01-03 18:24:04 +00:00
tims
bb24c085be linux: Add keyboard shortcuts for menus (#22074)
Closes #19837

This PR is a continuation of [linux: Implement
Menus](https://github.com/zed-industries/zed/pull/21873) and should only
be reviewed once the existing PR is merged.

I created this as a separate PR as the existing PR was already reviewed
but is yet to merge, and also it was my initial plan to do it in
separate parts because of the scope of it. This will also help reviewing
code faster.

This PR adds two new types of keyboard shortcuts to make menu navigation
easier:

1. `Alt + Z` for Zed, `Alt + F` for File, `Alt + S` for Selection, and
so on to open a specific menu with this combination. This mimics VSCode
and IntelliJ.

2. `Arrow Left/Right` when any menu is open. This will trigger the
current menu to close, and the previous/next to open respectively. First
and last element cycling is handled.

`Arrow Up/Down` to navigate menu entries is already there in existing
work.



https://github.com/user-attachments/assets/976aea48-4e20-4c19-850d-4d205a4bead2


Release Notes:

- Added keyboard navigation for menus on Linux (left/right). If you wish
to open menus with keyboard shortcuts add the following to your user
keymap:
    ```json
      {
        "context": "Workspace",
        "bindings": {
          "alt-z": ["app_menu::OpenApplicationMenu", "Zed"],
          "alt-f": ["app_menu::OpenApplicationMenu", "File"],
          "alt-e": ["app_menu::OpenApplicationMenu", "Edit"],
          "alt-s": ["app_menu::OpenApplicationMenu", "Selection"],
          "alt-v": ["app_menu::OpenApplicationMenu", "View"],
          "alt-g": ["app_menu::OpenApplicationMenu", "Go"],
          "alt-w": ["app_menu::OpenApplicationMenu", "Window"],
          "alt-h": ["app_menu::OpenApplicationMenu", "Help"]
        }
      }
    ```

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-01-03 18:19:24 +00:00
Cole Miller
11ec25aedb Support diagnostic navigation in multibuffers (#22620)
cc @nathansobo 

Release Notes:

- Support diagnostic navigation in multibuffers
2025-01-03 18:07:56 +00:00
Marshall Bowers
39af06085a assistant2: Add an example thread to showcase long lines of code (#22621)
This PR adds another example thread to showcase a response with long
lines of code.

This example will be helpful when working to make the code blocks scroll
horizontally instead of wrapping.

Release Notes:

- N/A
2025-01-03 17:48:04 +00:00
Marshall Bowers
a49e394e51 assistant2: Remove single-letter variable name (#22618)
This PR removes a single-letter variable name in place of a full one,
for readability.

Release Notes:

- N/A
2025-01-03 17:04:07 +00:00
Peter Tripp
e5c3d5d626 Emacs keybinding improvements (2025-01-02) (#22590)
Various improvements to the emacs compatibility keybindings.

- See also: https://github.com/zed-industries/zed/issues/4856

Release Notes:

- Improvements to emacs keybindings:
- Better support for running emacs inside Zed terminal (e.g. `ctrl-x
ctrl-c` will quit emacs in terminal not zed)
  - `alt-^` Join Lines
  - `ctrl-/` Undo
  - `alt-.` GotoDefinition and `alt-,` GoBack
  - `ctrl-x h` SelectAll
  - `alt-<` / `alt->` Goto End/Beginning of Buffer
  - `ctrl-g` as Menu::cancel
2025-01-03 16:48:41 +00:00
Marshall Bowers
1aba459a0a csharp: Bump to v0.1.0 (#22617)
This PR bumps the C# extension to v0.1.0.

Changes:

- https://github.com/zed-industries/zed/pull/15175
- https://github.com/zed-industries/zed/pull/15885
- https://github.com/zed-industries/zed/pull/16955
- https://github.com/zed-industries/zed/pull/18869
- https://github.com/zed-industries/zed/pull/22599

Release Notes:

- N/A
2025-01-03 16:47:19 +00:00
Hossein Khosravi
8c253af451 linux: Fix regex patterns for detecting Fedora in script/linux (#22611)
In `script/linux` file, in order to install build dependencies we check
ID and VERSION_ID fields of `/etc/os-release` file for installing
os-specific packages. The regex patterns for those fields are wrong
because there's no `"` character after `ID=` or `VERSION_ID=`. This
causes `grep` to fail.
So I extended the pattern by adding `"?` after each `"` character to
bypass the cause of failure.

Release Notes:

- N/A

Signed-off-by: thehxdev <hossein.khosravi.ce@gmail.com>
2025-01-03 16:28:07 +00:00
renovate[bot]
3c207209cb Update Rust crate sea-orm to v1.1.3 (#22554)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dev-dependencies
| patch | `1.1.2` -> `1.1.3` |
| [sea-orm](https://www.sea-ql.org/SeaORM)
([source](https://redirect.github.com/SeaQL/sea-orm)) | dependencies |
patch | `1.1.2` -> `1.1.3` |

---

### Release Notes

<details>
<summary>SeaQL/sea-orm (sea-orm)</summary>

###
[`v1.1.3`](https://redirect.github.com/SeaQL/sea-orm/blob/HEAD/CHANGELOG.md#113---2024-12-24)

[Compare
Source](https://redirect.github.com/SeaQL/sea-orm/compare/1.1.2...1.1.3)

##### New Features

- \[sea-orm-codegen] register seaography entity modules & active
enums[https://github.com/SeaQL/sea-orm/pull/2403](https://redirect.github.com/SeaQL/sea-orm/pull/2403)3

```rust
pub mod prelude;

pub mod sea_orm_active_enums;

pub mod baker;
pub mod bakery;
pub mod cake;
pub mod cakes_bakers;
pub mod customer;
pub mod lineitem;
pub mod order;

seaography::register_entity_modules!([
    baker,
    bakery,
    cake,
    cakes_bakers,
    customer,
    lineitem,
    order,
]);

seaography::register_active_enums!([
    sea_orm_active_enums::Tea,
    sea_orm_active_enums::Color,
]);
```

##### Enhancements

- Insert many allow active models to have different column set
[https://github.com/SeaQL/sea-orm/pull/2433](https://redirect.github.com/SeaQL/sea-orm/pull/2433)

```rust
// this previously panics
let apple = cake_filling::ActiveModel {
    cake_id: ActiveValue::set(2),
    filling_id: ActiveValue::NotSet,
};
let orange = cake_filling::ActiveModel {
    cake_id: ActiveValue::NotSet,
    filling_id: ActiveValue::set(3),
};
assert_eq!(
    Insert::<cake_filling::ActiveModel>::new()
        .add_many([apple, orange])
        .build(DbBackend::Postgres)
        .to_string(),
    r#"INSERT INTO "cake_filling" ("cake_id", "filling_id") VALUES (2, NULL), (NULL, 3)"#,
);
```

- \[sea-orm-cli] Added `MIGRATION_DIR` environment variable
[https://github.com/SeaQL/sea-orm/pull/2419](https://redirect.github.com/SeaQL/sea-orm/pull/2419)
- Added `ColumnDef::is_unique`
[https://github.com/SeaQL/sea-orm/pull/2401](https://redirect.github.com/SeaQL/sea-orm/pull/2401)
- Postgres: quote schema in `search_path`
[https://github.com/SeaQL/sea-orm/pull/2436](https://redirect.github.com/SeaQL/sea-orm/pull/2436)

##### Bug Fixes

- MySQL: fix transaction isolation level not respected when used with
access mode
[https://github.com/SeaQL/sea-orm/pull/2450](https://redirect.github.com/SeaQL/sea-orm/pull/2450)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-03 16:22:21 +00:00
Peter Tripp
663f5244ca Deploy script: Specify remote for new Preview branch (#22614)
Set the git remote tracking branch when the new Preview branch is
created by `script/bump-zed-minor-versions`. This only impacts the local
git branch configuration of the user who runs this script.

Release Notes:

- N/A
2025-01-03 15:25:15 +00:00
Agus Zubiaga
0599f0fcb6 Fix vertical alignment when jumping from multibuffers (#22613)
Clicking buffer headers and line numbers would sometimes take you to a
disorienting scroll position. This PR improves that so the destination
line is roughly at the same Y position as it appeared in the
multibuffer.



https://github.com/user-attachments/assets/3ad71537-cf26-4136-948f-c5a96df57178


**Note**: The alignment won't always be perfect because the multibuffer
and target buffer might start at a different absolute Y position
(because of open search, breadcrumbs, etc). I wanted to compensate for
that, but that requires a fundamental change that I'd prefer to make
separately.

Release Notes:

- Fix vertical alignment when jumping from multibuffers
2025-01-03 14:37:00 +00:00
Finn Evers
6e2b6258b1 csharp: Add bracket indents (#22599)
This PR adds an initial `indents.scm` to the C#-extension in order to
support auto-indentation with brackets.

Release Notes:

- N/A
2025-01-03 13:55:25 +00:00
Marshall Bowers
82492d74a8 assistant2: Tweak "Add Context" placeholder (#22596)
This PR tweaks the "Add Context" placeholder, as the text appeared to be
vertically misaligned.

#### Before

<img width="215" alt="Screenshot 2025-01-02 at 6 03 06 PM"
src="https://github.com/user-attachments/assets/1bac0deb-bd90-4ff3-b681-ee884cbe831d"
/>

#### After

<img width="189" alt="Screenshot 2025-01-02 at 6 03 20 PM"
src="https://github.com/user-attachments/assets/c9673fb0-11d6-42ac-8fec-9af269dfc73c"
/>


Release Notes:

- N/A
2025-01-02 23:18:32 +00:00
renovate[bot]
16ead69052 Update Rust crate serde_json to v1.0.134 (#22555)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.133` -> `1.0.134` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.133` -> `1.0.134` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.134`](https://redirect.github.com/serde-rs/json/releases/tag/v1.0.134)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/v1.0.133...v1.0.134)

- Add `RawValue` associated constants for literal `null`, `true`,
`false`
([#&#8203;1221](https://redirect.github.com/serde-rs/json/issues/1221),
thanks [@&#8203;bheylin](https://redirect.github.com/bheylin))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 23:08:47 +00:00
Marshall Bowers
a53be7b4be collab_ui: Show the chat panel icon when the chat panel is active (#22593)
This PR is a follow-up to #22200 that makes it so the chat panel icon is
visible when the chat panel is active, even if not in a call (when using
the `when_in_call` setting).

Release Notes:

- N/A
2025-01-02 22:53:34 +00:00
renovate[bot]
a79def005d Update Rust crate quote to v1.0.38 (#22553)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [quote](https://redirect.github.com/dtolnay/quote) | dependencies |
patch | `1.0.37` -> `1.0.38` |

---

### Release Notes

<details>
<summary>dtolnay/quote (quote)</summary>

###
[`v1.0.38`](https://redirect.github.com/dtolnay/quote/releases/tag/1.0.38)

[Compare
Source](https://redirect.github.com/dtolnay/quote/compare/1.0.37...1.0.38)

- Support interpolating arrays inside of arrays using a repetition
([#&#8203;286](https://redirect.github.com/dtolnay/quote/issues/286))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 22:45:55 +00:00
Michael Sloan
2d431e9b51 Improve truncate efficiency and fix OBOE in truncate_and_remove_front (#22591)
* Skip walking string for truncate when byte len is <= char limit

* Fix `truncate_and_remove_front` returning string that is `max_chars +
1` in length. Now more consistent with `truncate_and_trailoff` behavior.

* Fix `truncate_and_remove_front` adding ellipsis when max_chars == char
length

Release Notes:

- N/A
2025-01-02 22:35:36 +00:00
Michael Sloan
f9df8c1729 Use the same label for both string and bag in tasks modal fuzzy match (#22022)
#22592 tracks properly doing fuzzy match within the full label

Release Notes:

- N/A
2025-01-02 22:11:14 +00:00
Justin Su
898064e6b4 Fix a typo in default.json (#22589)
Release Notes:

- N/A
2025-01-02 21:44:51 +00:00
Josef Zoller
8cb397cf6c project_panel: Open rename file editor if pasted file was disambiguated (#19975)
Closes #19974.

When a file is pasted in the project panel at a location where a file
with that name already exists, the new file's name is disambiguated by
appending " copy" at the end. This happens on the paste and the
duplicate actions, as well as when Alt-dragging files.
With this PR, this will now open the file rename editor with the
disambiguator pre-selected.

Open question:
With this PR's current implementation, this won't always work when
pasting multiple files at once. In this case, the file rename editor
only opens for the last pasted file, if that file was disambiguated. If
only other files were disambiguated instead, it won't open.
This roughly mimics the previous paste behaviour, namely that only the
last pasted file was selected.

I see two options here: If multiple files were pasted and some of them
were disambiguated, we could select and open the rename editor for the
last file that was actually disambiguated (easy), or we could open a
kind of multi-editor for all files (hard, but maybe a multi-rename
editor could actually be interesting in general...).

Release Notes:

- Open rename file editor if pasted file was disambiguated
2025-01-02 21:33:51 +00:00
Agus Zubiaga
374c298bd5 assistant2: Suggest current thread in inline assistant (#22586)
Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.com>
2025-01-02 20:36:57 +00:00
Kirill Bulatov
0e75ca8603 Fix tooltips too eager to disappear when there's a gap between the tooltip source and the tooltip itself (#22583)
Follow-up of https://github.com/zed-industries/zed/pull/22548

Release Notes:

- N/A

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-01-02 19:45:47 +00:00
Marshall Bowers
2c2ca9e370 assistant2: Wire up the directory context picker (#22582)
This PR wires up the functionality of the directory context picker.

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
2025-01-02 19:42:59 +00:00
Peter Tripp
3cf5ab16a9 racket: Bump Extension to v0.0.2 (#22584)
Includes:
- https://github.com/zed-industries/zed/pull/18728
2025-01-02 14:39:18 -05:00
Peter Tripp
53e1ab3c64 elixir: Bump to v0.1.3 (#22585)
Includes:
- https://github.com/zed-industries/zed/pull/22579
2025-01-02 14:38:51 -05:00
Peter Tripp
d0c4c0c240 elixir: Capture identifiers as @variable (#22579)
- Closes: https://github.com/zed-industries/zed/issues/19382

Before/After
<img width="329" alt="Screenshot 2025-01-02 at 13 08 46"
src="https://github.com/user-attachments/assets/ede36fd3-ed55-4436-912c-bb8b7ad9b0cd"
/><img width="329" alt="Screenshot 2025-01-02 at 13 08 18"
src="https://github.com/user-attachments/assets/eb784bdc-fd13-487d-b6ed-c960d8020d9b"
/>


Release Notes:

- N/A
2025-01-02 18:27:12 +00:00
Marshall Bowers
20c0d72fe4 ci: Make docs-only check a no-op in the merge queue (#22576)
This PR makes the docs-only check a no-op that defaults to `false` when
running in the merge queue.

I noticed that the current check did not work properly in the merge
queue, resulting in it always assuming a change was docs-only and not
running the requisite CI jobs.

Release Notes:

- N/A
2025-01-02 18:14:17 +00:00
renovate[bot]
d5f058d6e2 Update swatinem/rust-cache digest to f0deed1 (#22552)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [swatinem/rust-cache](https://redirect.github.com/swatinem/rust-cache)
| action | digest | `82a92a6` -> `f0deed1` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 17:41:35 +00:00
Bennet Bo Fenner
fbef8c2b3b docs: Update scrollbar > diagnostics setting section (#22574)
This updates the docs after the setting was changed in #22364 

Release Notes:

- N/A
2025-01-02 17:27:17 +00:00
Bennet Bo Fenner
b009e72121 terminal: Support clicking on "file://" URLs with line numbers (#22559)
Closes #10325

Release Notes:

- Fixed an issue inside the integrated terminal where clicking on URLs
that started with `file://` would sometimes not work when the path
included a line number (e.g. `file:///Users/someuser/lorem.txt:221:22`)
2025-01-02 17:24:55 +00:00
Aaron Feickert
f55a3629b0 Add fine-grained control for scrollbar diagnostics (#22364)
This PR updates the scrollbar diagnostic setting to provide fine-grained
control over which indicators to show, based on severity level. This
allows the user to hide lower-severity diagnostics that can otherwise
clutter the scrollbar (for example, unused or disabled code).

The options are set such that the existing boolean setting has the same
effect: when `true` all diagnostics are shown, and when `false` no
diagnostics are shown.

Closes #22296.

Release Notes:

- Added fine-grained control of scrollbar diagnostic indicators.
2025-01-02 17:03:00 +00:00
renovate[bot]
3ac0aef211 Update Rust crate unicase to v2.8.1 (#22558)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [unicase](https://redirect.github.com/seanmonstar/unicase) |
dependencies | patch | `2.8.0` -> `2.8.1` |
| [unicase](https://redirect.github.com/seanmonstar/unicase) |
workspace.dependencies | patch | `2.8.0` -> `2.8.1` |

---

### Release Notes

<details>
<summary>seanmonstar/unicase (unicase)</summary>

###
[`v2.8.1`](https://redirect.github.com/seanmonstar/unicase/releases/tag/v2.8.1)

[Compare
Source](https://redirect.github.com/seanmonstar/unicase/compare/v2.8.0...v2.8.1)

##### What's Changed

- fix: hash for Unicode will call write_u8 like Ascii does by
[@&#8203;seanmonstar](https://redirect.github.com/seanmonstar) in
[https://github.com/seanmonstar/unicase/pull/73](https://redirect.github.com/seanmonstar/unicase/pull/73)
- fix: provide prefix-freedom in Hash impls by
[@&#8203;seanmonstar](https://redirect.github.com/seanmonstar) in
[https://github.com/seanmonstar/unicase/pull/74](https://redirect.github.com/seanmonstar/unicase/pull/74)

**Full Changelog**:
https://github.com/seanmonstar/unicase/compare/v2.8.0...v2.8.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 14:55:59 +00:00
Agus Zubiaga
59b5b9af90 assistant2: Suggest current file as context (#22526)
Suggest adding the current file as context in the new assistant panel.



https://github.com/user-attachments/assets/62bc267b-3dfe-4a3b-a6af-c89af2c779a8


Note: This doesn't include suggesting the current thread in the inline
assistant.

Release Notes:

- N/A
2025-01-02 13:33:47 +00:00
Piotr Osiewicz
b3e36c93b4 deps: Bump indexmap to 2.0 (#22567)
Closes #ISSUE

Release Notes:

- N/A
2025-01-02 12:07:46 +00:00
Piotr Osiewicz
5447821715 gpui/perf: Use SharedString on API boundary of line layout (#22566)
This commit is all about strings, not about line layout at all. When
laying out text, we use a line layout cache to avoid roundtrips to
system layout engine where possible. This makes it so that we might end
up not needing an owned version of text to insert into the cache, as we
might get a cached version.

The API boundary of line layout accepted text to be laid out as &str. It
then performed cache lookup (which didn't require having an owned
version) and only resorted to making an owned version when needed. As it
turned out though, exact cache hits are quite rare and we end up needing
owned version more often than not. The callers of line layout either
dealt with SharedStrings or owned Strings. Due to coercing them into
&str, we were ~always copying text into a new string (unless there was a
same-frame-hit). This is a bit wasteful, thus this PR generifies the API
a bit to make it easier to reuse existing string allocations if there
are any.

Benchmark scenario: scrolling down page-by-page through editor_tests (I
ran the same scenario twice):

![1](https://github.com/user-attachments/assets/8cd09692-2699-41d9-b211-83554d93902f)

![2](https://github.com/user-attachments/assets/d11f7c22-2315-4261-8189-2356baf5d2f7)


Release Notes:

- N/A
2025-01-02 11:06:01 +00:00
Michael Sloan
665717da9a Fuzzy match performance improvements redo (#22561)
Release Notes:

- N/A
2025-01-02 05:31:06 +00:00
Danilo Leal
28d1d2d939 assistant2: Add link styles for thread messages (#22560)
<img width="700" alt="Screenshot 2025-01-02 at 1 52 30 AM"
src="https://github.com/user-attachments/assets/8d2308c8-cdea-421f-b9ff-7893479dba3c"
/>


Release Notes:

- N/A
2025-01-02 05:09:33 +00:00
Hayashi Mikihiro
44af405fb0 pane: Turn off preview mode when pinning a tab (#22501)
I opened the tab in preview mode, pinned it, then I opened another file,
but its tab unexpectedly closed.


https://github.com/user-attachments/assets/b857382e-f0ad-4d5a-9036-19de01663c97

Pinning a tab now turns off preview mode.



https://github.com/user-attachments/assets/e34b7c7f-452b-4f36-99c1-e0c68429225c


Release Notes:

- Pinning a preview tab will now turn off preview mode

---------

Signed-off-by: Hayashi Mikihiro <34ttrweoewiwe28@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-01-02 04:09:35 +00:00
Kirill Bulatov
c11bde7bf4 Remove stuck tooltips (#22548)
Closes https://github.com/zed-industries/zed/issues/21657

Follow-up of https://github.com/zed-industries/zed/pull/22488
Previous PR broke git blame tooltips, which are expected to be open when
hovered, even if the mouse cursor is moved away from the actual blame
entry that caused the tooltip to appear.

Current version moves the invalidation logic into `prepaint_tooltip`,
where the new data about the tooltip origin is used to ensure we
invalidate only tooltips that have no mouse cursor in either origin
bounds or tooltip bounds (if it's hoverable).


Release Notes:

- Fixed tooltips getting stuck
2025-01-01 18:47:10 +00:00
Peter Tripp
0d423a7b37 Bump Zed to v0.169 (#22547)
Release Notes:

-N/A
2025-01-01 12:31:37 -05:00
180 changed files with 4835 additions and 3276 deletions

View File

@@ -35,7 +35,12 @@ jobs:
- name: Check for non-docs changes
id: check_changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
if [ "${{ github.event_name }}" == "merge_group" ]; then
# When we're running in a merge queue, never assume that the changes
# are docs-only, as there could be other PRs in the group that
# contain non-docs changes.
echo "docs_only=false" >> $GITHUB_OUTPUT
elif git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -qvE '^docs/'; then
echo "docs_only=false" >> $GITHUB_OUTPUT
else
echo "docs_only=true" >> $GITHUB_OUTPUT
@@ -175,7 +180,7 @@ jobs:
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -216,7 +221,7 @@ jobs:
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
@@ -247,7 +252,7 @@ jobs:
- name: Cache dependencies
if: needs.check_docs_only.outputs.docs_only == 'false'
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"

View File

@@ -21,7 +21,7 @@ jobs:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
uses: swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"

194
Cargo.lock generated
View File

@@ -245,7 +245,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.25.0",
"strum",
"thiserror 1.0.69",
"util",
]
@@ -435,7 +435,7 @@ dependencies = [
"similar",
"smallvec",
"smol",
"strum 0.25.0",
"strum",
"telemetry_events",
"terminal",
"terminal_view",
@@ -494,7 +494,6 @@ dependencies = [
"project",
"proto",
"rand 0.8.5",
"release_channel",
"rope",
"schemars",
"serde",
@@ -2152,7 +2151,7 @@ dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes 2.0.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2180,7 +2179,7 @@ dependencies = [
"ipnet",
"maybe-owned",
"rustix 0.38.42",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"winx",
]
@@ -2275,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb"
dependencies = [
"heck 0.4.1",
"indexmap 2.7.0",
"indexmap",
"log",
"proc-macro2",
"quote",
@@ -2716,7 +2715,7 @@ dependencies = [
"settings",
"sha2",
"sqlx",
"strum 0.25.0",
"strum",
"subtle",
"supermaven_api",
"telemetry_events",
@@ -2785,7 +2784,8 @@ dependencies = [
name = "collections"
version = "0.1.0"
dependencies = [
"rustc-hash 1.1.0",
"indexmap",
"rustc-hash 2.1.0",
]
[[package]]
@@ -2994,7 +2994,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum 0.25.0",
"strum",
"task",
"theme",
"ui",
@@ -4145,7 +4145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -4812,7 +4812,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
dependencies = [
"io-lifetimes 2.0.4",
"rustix 0.38.42",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -5102,7 +5102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
dependencies = [
"fallible-iterator",
"indexmap 2.7.0",
"indexmap",
"stable_deref_trait",
]
@@ -5194,6 +5194,7 @@ dependencies = [
"util",
"windows 0.58.0",
"workspace",
"worktree",
]
[[package]]
@@ -5274,7 +5275,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.25.0",
"strum",
]
[[package]]
@@ -5374,7 +5375,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"strum 0.25.0",
"strum",
"sum_tree",
"taffy",
"thiserror 1.0.69",
@@ -5434,7 +5435,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap 2.7.0",
"indexmap",
"slab",
"tokio",
"tokio-util",
@@ -5453,7 +5454,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.2.0",
"indexmap 2.7.0",
"indexmap",
"slab",
"tokio",
"tokio-util",
@@ -6232,27 +6233,16 @@ dependencies = [
"heed",
"html_to_markdown",
"http_client",
"indexmap 1.9.3",
"indexmap",
"indoc",
"parking_lot",
"paths",
"pretty_assertions",
"serde",
"strum 0.25.0",
"strum",
"util",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.7.0"
@@ -6388,7 +6378,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes 2.0.4",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6775,7 +6765,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"strum 0.25.0",
"strum",
"ui",
"util",
]
@@ -6821,7 +6811,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum 0.25.0",
"strum",
"telemetry_events",
"theme",
"thiserror 1.0.69",
@@ -6999,7 +6989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -7741,7 +7731,7 @@ dependencies = [
"cfg_aliases 0.1.1",
"codespan-reporting",
"hexf-parse",
"indexmap 2.7.0",
"indexmap",
"log",
"rustc-hash 1.1.0",
"spirv",
@@ -8373,7 +8363,7 @@ checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [
"crc32fast",
"hashbrown 0.15.2",
"indexmap 2.7.0",
"indexmap",
"memchr",
]
@@ -8478,7 +8468,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.25.0",
"strum",
]
[[package]]
@@ -9328,7 +9318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap 2.7.0",
"indexmap",
]
[[package]]
@@ -9511,7 +9501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap 2.7.0",
"indexmap",
"quick-xml 0.32.0",
"serde",
"time",
@@ -9829,7 +9819,7 @@ dependencies = [
"file_icons",
"git",
"gpui",
"indexmap 1.9.3",
"indexmap",
"language",
"menu",
"pretty_assertions",
@@ -9933,7 +9923,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
dependencies = [
"bytes 1.9.0",
"heck 0.5.0",
"itertools 0.12.1",
"itertools 0.10.5",
"log",
"multimap 0.10.0",
"once_cell",
@@ -9966,7 +9956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
dependencies = [
"anyhow",
"itertools 0.12.1",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.90",
@@ -10148,14 +10138,14 @@ dependencies = [
"once_cell",
"socket2 0.5.8",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "quote"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
@@ -10892,7 +10882,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"strum 0.25.0",
"strum",
"tracing",
"util",
"zstd",
@@ -11048,7 +11038,7 @@ dependencies = [
"libc",
"linux-raw-sys 0.4.14",
"once_cell",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -11223,7 +11213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
dependencies = [
"dyn-clone",
"indexmap 1.9.3",
"indexmap",
"schemars_derive",
"serde",
"serde_json",
@@ -11296,9 +11286,9 @@ dependencies = [
[[package]]
name = "sea-orm"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b24d72a69e89762982c29af249542b06c59fa131f87cc9d5b94be1f692b427a"
checksum = "0dbcf83248860dc632c46c7e81a221e041b50d0006191756cb001d9e8afc60a9"
dependencies = [
"async-stream",
"async-trait",
@@ -11314,7 +11304,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum 0.26.3",
"strum",
"thiserror 1.0.69",
"time",
"tracing",
@@ -11324,9 +11314,9 @@ dependencies = [
[[package]]
name = "sea-orm-macros"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0497f4fd82ecb2a222bea5319b9048f8ab58d4e734d095b062987acbcdeecdda"
checksum = "49ce6f08134f3681b1ca92185b96fac898f26d9b4f5538d13f7032ef243d14b2"
dependencies = [
"heck 0.4.1",
"proc-macro2",
@@ -11556,11 +11546,11 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.134"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
dependencies = [
"indexmap 2.7.0",
"indexmap",
"itoa",
"memchr",
"ryu",
@@ -11573,7 +11563,7 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540"
dependencies = [
"indexmap 2.7.0",
"indexmap",
"itoa",
"memchr",
"ryu",
@@ -12146,7 +12136,7 @@ dependencies = [
"hashbrown 0.14.5",
"hashlink 0.9.1",
"hex",
"indexmap 2.7.0",
"indexmap",
"log",
"memchr",
"once_cell",
@@ -12370,7 +12360,7 @@ dependencies = [
"settings",
"simplelog",
"story",
"strum 0.25.0",
"strum",
"theme",
"title_bar",
"ui",
@@ -12430,26 +12420,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.25.0"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.25.3"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.4.1",
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
@@ -12768,7 +12752,7 @@ dependencies = [
"fd-lock",
"io-lifetimes 2.0.4",
"rustix 0.38.42",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"winx",
]
@@ -12900,7 +12884,7 @@ dependencies = [
"fastrand 2.3.0",
"once_cell",
"rustix 0.38.42",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -13025,7 +13009,7 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"indexmap 1.9.3",
"indexmap",
"log",
"palette",
"parking_lot",
@@ -13037,7 +13021,7 @@ dependencies = [
"serde_json_lenient",
"serde_repr",
"settings",
"strum 0.25.0",
"strum",
"util",
"uuid",
]
@@ -13060,7 +13044,7 @@ dependencies = [
"anyhow",
"clap",
"gpui",
"indexmap 1.9.3",
"indexmap",
"log",
"palette",
"rust-embed",
@@ -13069,7 +13053,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"simplelog",
"strum 0.25.0",
"strum",
"theme",
"vscode_theme",
]
@@ -13497,7 +13481,7 @@ version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap 2.7.0",
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
@@ -14000,7 +13984,7 @@ dependencies = [
"settings",
"smallvec",
"story",
"strum 0.25.0",
"strum",
"theme",
"ui_macros",
"windows 0.58.0",
@@ -14029,9 +14013,9 @@ dependencies = [
[[package]]
name = "unicase"
version = "2.8.0"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
@@ -14579,7 +14563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
dependencies = [
"anyhow",
"indexmap 2.7.0",
"indexmap",
"serde",
"serde_derive",
"serde_json",
@@ -14608,7 +14592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.6.0",
"indexmap 2.7.0",
"indexmap",
"semver",
]
@@ -14621,7 +14605,7 @@ dependencies = [
"ahash 0.8.11",
"bitflags 2.6.0",
"hashbrown 0.14.5",
"indexmap 2.7.0",
"indexmap",
"semver",
"serde",
]
@@ -14651,7 +14635,7 @@ dependencies = [
"cfg-if",
"encoding_rs",
"hashbrown 0.14.5",
"indexmap 2.7.0",
"indexmap",
"libc",
"libm",
"log",
@@ -14772,7 +14756,7 @@ dependencies = [
"cranelift-bitset",
"cranelift-entity",
"gimli 0.29.0",
"indexmap 2.7.0",
"indexmap",
"log",
"object",
"postcard",
@@ -14902,7 +14886,7 @@ checksum = "c58b085b2d330e5057dddd31f3ca527569b90fcdd35f6d373420c304927a5190"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.7.0",
"indexmap",
"wit-parser 0.215.0",
]
@@ -15203,7 +15187,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -15652,7 +15636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags 2.6.0",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -15699,7 +15683,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
dependencies = [
"anyhow",
"heck 0.4.1",
"indexmap 2.7.0",
"indexmap",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@@ -15727,7 +15711,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.6.0",
"indexmap 2.7.0",
"indexmap",
"log",
"serde",
"serde_derive",
@@ -15746,7 +15730,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.7.0",
"indexmap",
"log",
"semver",
"serde",
@@ -15764,7 +15748,7 @@ checksum = "935a97eaffd57c3b413aa510f8f0b550a4a9fe7d59e79cd8b89a83dcb860321f"
dependencies = [
"anyhow",
"id-arena",
"indexmap 2.7.0",
"indexmap",
"log",
"semver",
"serde",
@@ -15820,7 +15804,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"strum 0.25.0",
"strum",
"task",
"tempfile",
"theme",
@@ -16204,7 +16188,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.168.2"
version = "0.169.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -16336,24 +16320,9 @@ dependencies = [
"serde",
]
[[package]]
name = "zed_astro"
version = "0.1.2"
dependencies = [
"serde",
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_clojure"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_csharp"
version = "0.0.2"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -16372,13 +16341,6 @@ dependencies = [
"zed_extension_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elm"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_emmet"
version = "0.0.3"

View File

@@ -149,12 +149,9 @@ members = [
# Extensions
#
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
"extensions/deno",
"extensions/elixir",
"extensions/elm",
"extensions/emmet",
"extensions/erlang",
"extensions/glsl",
@@ -392,7 +389,7 @@ hyper = "0.14"
http = "1.1"
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
@@ -443,9 +440,10 @@ runtimelib = { version = "0.24.0", default-features = false, features = [
] }
rustc-demangle = "0.1.23"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = "0.21.12"
rustls-native-certs = "0.8.0"
schemars = { version = "0.8", features = ["impl_json_schema"] }
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -465,7 +463,7 @@ smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
sqlformat = "0.2"
strsim = "0.11"
strum = { version = "0.25.0", features = ["derive"] }
strum = { version = "0.26.0", features = ["derive"] }
subtle = "2.5.0"
sys-locale = "0.3.1"
sysinfo = "0.31.0"

View File

@@ -435,6 +435,13 @@
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
}
},
{
"context": "ApplicationMenu",
"bindings": {
"left": ["app_menu::NavigateApplicationMenuInDirection", "Left"],
"right": ["app_menu::NavigateApplicationMenuInDirection", "Right"]
}
},
// Bindings from Sublime Text
{
"context": "Editor",

View File

@@ -4,55 +4,70 @@
// from the command palette.
[
{
"context": "Editor",
"bindings": {
"ctrl-g": "editor::Cancel",
"ctrl-shift-g": "go_to_line::Toggle",
//"ctrl-space": "editor::SetMark",
"ctrl-x u": "editor::Undo",
"ctrl-x ctrl-u": "editor::Redo",
"ctrl-f": "editor::MoveRight",
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
"alt-d": "editor::DeleteToNextWordEnd",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-w": "editor::Cut",
"alt-w": "editor::Copy",
"ctrl-y": "editor::Paste",
"ctrl-_": "editor::Undo",
"ctrl-v": "editor::MovePageDown",
"alt-v": "editor::MovePageUp",
"ctrl-x ]": "editor::MoveToEnd",
"ctrl-x [": "editor::MoveToBeginning",
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
"ctrl-s": "buffer_search::Deploy",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-shift-r": "editor::Rename"
"ctrl-g": "menu::Cancel"
}
},
{
"context": "Workspace",
"context": "Editor",
"bindings": {
"ctrl-x k": "pane::CloseActiveItem",
"ctrl-x ctrl-c": "workspace::CloseWindow",
"ctrl-x o": "workspace::ActivateNextPane",
"ctrl-x b": "tab_switcher::Toggle",
"ctrl-x 0": "pane::CloseActiveItem",
"ctrl-x 1": "pane::CloseInactiveItems",
"ctrl-x 2": "pane::SplitVertical",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-x ctrl-s": "workspace::Save",
"ctrl-x ctrl-w": "workspace::SaveAs",
"ctrl-x s": "workspace::SaveAll",
"shift shift": "file_finder::Toggle"
"ctrl-g": "editor::Cancel",
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
"alt-g g": "go_to_line::Toggle", // goto-line
"alt-g alt-g": "go_to_line::Toggle", // goto-line
//"ctrl-space": "editor::SetMark",
"ctrl-f": "editor::MoveRight", // forward-char
"ctrl-b": "editor::MoveLeft", // backward-char
"ctrl-n": "editor::MoveDown", // next-line
"ctrl-p": "editor::MoveUp", // previous-line
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word
"alt-l": "editor::ConvertToLowerCase", // downcase-word
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save
"ctrl-y": "editor::KillRingYank", // yank
"ctrl-_": "editor::Undo", // undo
"ctrl-/": "editor::Undo", // undo
"ctrl-x u": "editor::Undo", // undo
"ctrl-v": "editor::MovePageDown", // scroll-up
"alt-v": "editor::MovePageUp", // scroll-down
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
"ctrl-x ]": "editor::MoveToEnd", // end-of-buffer
"alt-<": "editor::MoveToBeginning", // beginning-of-buffer
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
}
},
{
"context": "Workspace && !Terminal",
"bindings": {
"ctrl-x ctrl-c": "workspace::CloseWindow", // kill-emacs
"ctrl-x o": "workspace::ActivateNextPane", // other-window
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
"ctrl-x 2": "pane::SplitDown", // split-window-below
"ctrl-x 3": "pane::SplitRight", // split-window-right
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
}
},
{

View File

@@ -4,55 +4,70 @@
// from the command palette.
[
{
"context": "Editor",
"bindings": {
"ctrl-g": "editor::Cancel",
"ctrl-shift-g": "go_to_line::Toggle",
//"ctrl-space": "editor::SetMark",
"ctrl-x u": "editor::Undo",
"ctrl-x ctrl-u": "editor::Redo",
"ctrl-f": "editor::MoveRight",
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
"alt-d": "editor::DeleteToNextWordEnd",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-w": "editor::Cut",
"alt-w": "editor::Copy",
"ctrl-y": "editor::Paste",
"ctrl-_": "editor::Undo",
"ctrl-v": "editor::MovePageDown",
"alt-v": "editor::MovePageUp",
"ctrl-x ]": "editor::MoveToEnd",
"ctrl-x [": "editor::MoveToBeginning",
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
"ctrl-s": "buffer_search::Deploy",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-shift-r": "editor::Rename"
"ctrl-g": "menu::Cancel"
}
},
{
"context": "Workspace",
"context": "Editor",
"bindings": {
"ctrl-x k": "pane::CloseActiveItem",
"ctrl-x ctrl-c": "workspace::CloseWindow",
"ctrl-x o": "workspace::ActivateNextPane",
"ctrl-x b": "tab_switcher::Toggle",
"ctrl-x 0": "pane::CloseActiveItem",
"ctrl-x 1": "pane::CloseInactiveItems",
"ctrl-x 2": "pane::SplitVertical",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-x ctrl-s": "workspace::Save",
"ctrl-x ctrl-w": "workspace::SaveAs",
"ctrl-x s": "workspace::SaveAll",
"shift shift": "file_finder::Toggle"
"ctrl-g": "editor::Cancel",
"ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer
"alt-g g": "go_to_line::Toggle", // goto-line
"alt-g alt-g": "go_to_line::Toggle", // goto-line
//"ctrl-space": "editor::SetMark",
"ctrl-f": "editor::MoveRight", // forward-char
"ctrl-b": "editor::MoveLeft", // backward-char
"ctrl-n": "editor::MoveDown", // next-line
"ctrl-p": "editor::MoveUp", // previous-line
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }], // move-beginning-of-line
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }], // move-end-of-line
"alt-f": "editor::MoveToNextSubwordEnd", // forward-word
"alt-b": "editor::MoveToPreviousSubwordStart", // backward-word
"alt-u": "editor::ConvertToUpperCase", // upcase-word
"alt-l": "editor::ConvertToLowerCase", // downcase-word
"alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save
"ctrl-y": "editor::KillRingYank", // yank
"ctrl-_": "editor::Undo", // undo
"ctrl-/": "editor::Undo", // undo
"ctrl-x u": "editor::Undo", // undo
"ctrl-v": "editor::MovePageDown", // scroll-up
"alt-v": "editor::MovePageUp", // scroll-down
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer
"ctrl-x ]": "editor::MoveToEnd", // end-of-buffer
"alt-<": "editor::MoveToBeginning", // beginning-of-buffer
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
}
},
{
"context": "Workspace && !Terminal",
"bindings": {
"ctrl-x ctrl-c": "workspace::CloseWindow", // kill-emacs
"ctrl-x o": "workspace::ActivateNextPane", // other-window
"ctrl-x k": "pane::CloseActiveItem", // kill-buffer
"ctrl-x 0": "pane::CloseActiveItem", // delete-window
"ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows
"ctrl-x 2": "pane::SplitDown", // split-window-below
"ctrl-x 3": "pane::SplitRight", // split-window-right
"ctrl-x ctrl-f": "file_finder::Toggle", // find-file
"ctrl-x ctrl-s": "workspace::Save", // save-buffer
"ctrl-x ctrl-w": "workspace::SaveAs", // write-file
"ctrl-x s": "workspace::SaveAll" // save-some-buffers
}
},
{

View File

@@ -256,8 +256,13 @@
"search_results": true,
// Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true,
// Which diagnostic indicators to show in the scrollbar:
// - "none" or false: do not show diagnostics
// - "error": show only errors
// - "warning": show only errors and warnings
// - "information": show only errors, warnings, and information
// - "all" or true: show all diagnostics
"diagnostics": "all",
/// Forcefully enable or disable the scrollbar for each axis
"axes": {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
@@ -741,7 +746,7 @@
// Delay is restarted with every cursor movement.
// "delay_ms": 600
//
// Whether or not do display the git commit summary on the same line.
// Whether or not to display the git commit summary on the same line.
// "show_commit_summary": false
//
// The minimum column number to show the inline blame information at

View File

@@ -30,6 +30,8 @@ pub enum Model {
#[default]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
Claude3Opus,
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
@@ -48,6 +50,8 @@ pub enum Model {
cache_configuration: Option<AnthropicModelCacheConfiguration>,
max_output_tokens: Option<u32>,
default_temperature: Option<f32>,
#[serde(default)]
extra_beta_headers: Vec<String>,
},
}
@@ -55,6 +59,8 @@ impl Model {
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
} else if id.starts_with("claude-3-sonnet") {
@@ -69,6 +75,7 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-latest",
Model::Claude3Haiku => "claude-3-haiku-latest",
@@ -79,6 +86,7 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
@@ -90,11 +98,13 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet | Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
}),
Self::Claude3_5Sonnet | Self::Claude3_5Haiku | Self::Claude3Haiku => {
Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
})
}
Self::Custom {
cache_configuration,
..
@@ -106,6 +116,7 @@ impl Model {
pub fn max_token_count(&self) -> usize {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 200_000,
@@ -116,7 +127,7 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet => 8_192,
Self::Claude3_5Sonnet | Self::Claude3_5Haiku => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -126,6 +137,7 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 1.0,
@@ -136,6 +148,24 @@ impl Model {
}
}
pub fn beta_headers(&self) -> String {
let mut headers = vec!["prompt-caching-2024-07-31".to_string()];
if let Self::Custom {
extra_beta_headers, ..
} = self
{
headers.extend(
extra_beta_headers
.iter()
.filter(|header| !header.trim().is_empty())
.cloned(),
);
}
headers.join(",")
}
pub fn tool_model_id(&self) -> &str {
if let Self::Custom {
tool_override: Some(tool_override),
@@ -156,11 +186,12 @@ pub async fn complete(
request: Request,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let model = Model::from_id(&request.model)?;
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", "prompt-caching-2024-07-31")
.header("Anthropic-Beta", model.beta_headers())
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
@@ -271,14 +302,12 @@ pub async fn stream_completion_with_rate_limit_info(
stream: true,
};
let uri = format!("{api_url}/v1/messages");
let model = Model::from_id(&request.base.model)?;
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("Anthropic-Beta", model.beta_headers())
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =

View File

@@ -37,7 +37,7 @@ pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use settings::{Settings, SettingsStore};
use slash_command::search_command::SearchSlashCommandFeatureFlag;
use slash_command::{
auto_command, cargo_workspace_command, default_command, delta_command, diagnostics_command,
@@ -199,16 +199,6 @@ pub fn init(
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
// TODO: remove this when 0.148.0 is released.
if AssistantSettings::get_global(cx).using_outdated_settings_version {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let fs = fs.clone();
|content, cx| {
content.update_file(fs, cx);
}
});
}
cx.spawn(|mut cx| {
let client = client.clone();
async move {

View File

@@ -122,7 +122,7 @@ pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
let settings = AssistantSettings::get_global(cx);
terminal_panel.asssistant_enabled(settings.enabled, cx);
terminal_panel.set_assistant_enabled(settings.enabled, cx);
},
)
.detach();

View File

@@ -3,18 +3,12 @@ use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{AppContext, Pixels};
use language_model::{CloudModel, LanguageModel};
use language_models::{
provider::open_ai, AllLanguageModelSettings, AnthropicSettingsContent,
AnthropicSettingsContentV1, OllamaSettingsContent, OpenAiSettingsContent,
OpenAiSettingsContentV1, VersionedAnthropicSettingsContent, VersionedOpenAiSettingsContent,
};
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsSources};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -106,96 +100,6 @@ impl AssistantSettingsContent {
}
}
pub fn update_file(&mut self, fs: Arc<dyn Fs>, cx: &AppContext) {
if let AssistantSettingsContent::Versioned(settings) = self {
if let VersionedAssistantSettingsContent::V1(settings) = settings {
if let Some(provider) = settings.provider.clone() {
match provider {
AssistantProviderContentV1::Anthropic { api_url, .. } => {
update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.anthropic.is_none() {
content.anthropic =
Some(AnthropicSettingsContent::Versioned(
VersionedAnthropicSettingsContent::V1(
AnthropicSettingsContentV1 {
api_url,
available_models: None,
},
),
));
}
},
)
}
AssistantProviderContentV1::Ollama { api_url, .. } => {
update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.ollama.is_none() {
content.ollama = Some(OllamaSettingsContent {
api_url,
available_models: None,
});
}
},
)
}
AssistantProviderContentV1::OpenAi {
api_url,
available_models,
..
} => update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.openai.is_none() {
let available_models = available_models.map(|models| {
models
.into_iter()
.filter_map(|model| match model {
OpenAiModel::Custom {
name,
display_name,
max_tokens,
max_output_tokens,
max_completion_tokens: None,
} => Some(open_ai::AvailableModel {
name,
display_name,
max_tokens,
max_output_tokens,
max_completion_tokens: None,
}),
_ => None,
})
.collect::<Vec<_>>()
});
content.openai = Some(OpenAiSettingsContent::Versioned(
VersionedOpenAiSettingsContent::V1(
OpenAiSettingsContentV1 {
api_url,
available_models,
},
),
));
}
},
),
_ => {}
}
}
}
}
*self = AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
self.upgrade(),
));
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
@@ -534,6 +438,7 @@ fn merge<T>(target: &mut T, value: Option<T>) {
#[cfg(test)]
mod tests {
use fs::Fs;
use gpui::{ReadGlobal, TestAppContext};
use super::*;

View File

@@ -133,7 +133,7 @@ impl InlineAssistant {
};
let enabled = AssistantSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.asssistant_enabled(enabled, cx)
terminal_panel.set_assistant_enabled(enabled, cx)
});
})
.detach();
@@ -228,19 +228,20 @@ impl InlineAssistant {
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
for (excerpt_id, buffer, buffer_range) in
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
for (excerpt, buffer_range) in
snapshot.disjoint_ranges_to_buffer_ranges(selections.iter().map(|selection| {
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
}))
{
let buffer = excerpt.buffer();
let start = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_before(buffer_range.start),
};
let end = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
@@ -797,10 +798,12 @@ impl InlineAssistant {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
let multibuffer_snapshot = multibuffer.snapshot(cx);
let mut ranges =
multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
});
report_assistant_event(
@@ -2615,26 +2618,30 @@ impl EventEmitter<CodegenEvent> for CodegenAlternative {}
impl CodegenAlternative {
pub fn new(
buffer: Model<MultiBuffer>,
multi_buffer: Model<MultiBuffer>,
range: Range<Anchor>,
active: bool,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
let snapshot = multi_buffer.read(cx).snapshot(cx);
let (old_buffer, _, _) = buffer
.read(cx)
.range_to_buffer_ranges(range.clone(), cx)
.pop()
// TODO: Could be made more efficient by using a reverse iterator.
let (old_excerpt, _) = snapshot
.range_to_buffer_ranges(range.clone())
.last()
.unwrap();
let old_buffer = cx.new_model(|cx| {
let old_buffer = old_buffer.read(cx);
let text = old_buffer.as_rope().clone();
let line_ending = old_buffer.line_ending();
let language = old_buffer.language().cloned();
let language_registry = old_buffer.language_registry();
let text = old_excerpt.buffer().as_rope().clone();
let line_ending = old_excerpt.buffer().line_ending();
let language = old_excerpt.buffer().language().cloned();
let language_registry = multi_buffer
.read(cx)
.buffer(old_excerpt.buffer_id())
.unwrap()
.read(cx)
.language_registry();
let mut buffer = Buffer::local_normalized(text, line_ending, cx);
buffer.set_language(language, cx);
@@ -2645,7 +2652,7 @@ impl CodegenAlternative {
});
Self {
buffer: buffer.clone(),
buffer: multi_buffer.clone(),
old_buffer,
edit_position: None,
message_id: None,
@@ -2656,7 +2663,7 @@ impl CodegenAlternative {
generation: Task::ready(()),
diff: Diff::default(),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
_subscription: cx.subscribe(&multi_buffer, Self::handle_buffer_event),
builder,
active,
edits: Vec::new(),
@@ -2867,10 +2874,11 @@ impl CodegenAlternative {
let telemetry = self.telemetry.clone();
let language_name = {
let multibuffer = self.buffer.read(cx);
let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
let snapshot = multibuffer.snapshot(cx);
let mut ranges = snapshot.range_to_buffer_ranges(self.range.clone());
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
};

View File

@@ -50,7 +50,6 @@ parking_lot.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
release_channel.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -5,7 +5,7 @@ use collections::HashMap;
use gpui::{
list, AbsoluteLength, AnyElement, AppContext, DefiniteLength, EdgesRefinement, Empty, Length,
ListAlignment, ListOffset, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, View, WeakView,
TextStyleRefinement, UnderlineStyle, View, WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -22,7 +22,7 @@ pub struct ActiveThread {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
pub(crate) thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
@@ -140,6 +140,15 @@ impl ActiveThread {
background_color: Some(colors.editor_foreground.opacity(0.01)),
..Default::default()
},
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
underline: Some(UnderlineStyle {
color: Some(colors.text_accent.opacity(0.5)),
thickness: px(1.),
..Default::default()
}),
..Default::default()
},
..Default::default()
};
@@ -274,12 +283,11 @@ impl ActiveThread {
.when_some(context, |parent, context| {
if !context.is_empty() {
parent.child(
h_flex()
.flex_wrap()
.gap_1()
.px_1p5()
.pb_1p5()
.children(context.iter().map(|c| ContextPill::new(c.clone()))),
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
} else {
parent

View File

@@ -19,7 +19,7 @@ use workspace::Workspace;
use crate::active_thread::ActiveThread;
use crate::assistant_settings::{AssistantDockPosition, AssistantSettings};
use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus};
@@ -206,6 +206,10 @@ impl AssistantPanel {
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn active_thread(&self, cx: &AppContext) -> Model<Thread> {
self.thread.read(cx).thread.clone()
}
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));

View File

@@ -257,17 +257,21 @@ impl CodegenAlternative {
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
let (old_buffer, _, _) = buffer
.read(cx)
.range_to_buffer_ranges(range.clone(), cx)
.pop()
// TODO: Could be more efficient by using a reverse iterator.
let (old_excerpt, _) = snapshot
.range_to_buffer_ranges(range.clone())
.last()
.unwrap();
let old_buffer = cx.new_model(|cx| {
let old_buffer = old_buffer.read(cx);
let text = old_buffer.as_rope().clone();
let line_ending = old_buffer.line_ending();
let language = old_buffer.language().cloned();
let language_registry = old_buffer.language_registry();
let text = old_excerpt.buffer().as_rope().clone();
let line_ending = old_excerpt.buffer().line_ending();
let language = old_excerpt.buffer().language().cloned();
let language_registry = buffer
.read(cx)
.buffer(old_excerpt.buffer_id())
.unwrap()
.read(cx)
.language_registry();
let mut buffer = Buffer::local_normalized(text, line_ending, cx);
buffer.set_language(language, cx);
@@ -471,10 +475,11 @@ impl CodegenAlternative {
let telemetry = self.telemetry.clone();
let language_name = {
let multibuffer = self.buffer.read(cx);
let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
let snapshot = multibuffer.snapshot(cx);
let mut ranges = snapshot.range_to_buffer_ranges(self.range.clone());
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
};

View File

@@ -1,8 +1,11 @@
use gpui::SharedString;
use language_model::{LanguageModelRequestMessage, MessageContent};
use project::ProjectEntryId;
use serde::{Deserialize, Serialize};
use util::post_inc;
use crate::thread::ThreadId;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
@@ -21,12 +24,12 @@ pub struct Context {
pub text: SharedString,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
File(ProjectEntryId),
Directory,
FetchedUrl,
Thread,
Thread(ThreadId),
}
pub fn attach_context_to_message(
@@ -40,7 +43,7 @@ pub fn attach_context_to_message(
for context in context.into_iter() {
match context.kind {
ContextKind::File => {
ContextKind::File(_) => {
file_context.push_str(&context.text);
file_context.push('\n');
}
@@ -54,7 +57,7 @@ pub fn attach_context_to_message(
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
ContextKind::Thread => {
ContextKind::Thread(_) => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);

View File

@@ -10,12 +10,10 @@ use gpui::{
WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
@@ -54,29 +52,24 @@ impl ContextPicker {
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
kind: ContextPickerEntryKind::File,
icon: IconName::File,
});
let release_channel = ReleaseChannel::global(cx);
// The directory context picker isn't fully implemented yet, so limit it
// to development builds.
if release_channel == ReleaseChannel::Dev {
entries.push(ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
icon: IconName::Folder,
});
}
entries.push(ContextPickerEntry {
name: "Folder".into(),
kind: ContextPickerEntryKind::Directory,
icon: IconName::Folder,
});
entries.push(ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
kind: ContextPickerEntryKind::FetchedUrl,
icon: IconName::Globe,
});
if thread_store.is_some() {
entries.push(ContextPickerEntry {
name: "Thread".into(),
kind: ContextKind::Thread,
kind: ContextPickerEntryKind::Thread,
icon: IconName::MessageCircle,
});
}
@@ -140,10 +133,18 @@ impl Render for ContextPicker {
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
kind: ContextKind,
kind: ContextPickerEntryKind,
icon: IconName,
}
#[derive(Debug, Clone)]
enum ContextPickerEntryKind {
File,
Directory,
FetchedUrl,
Thread,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
@@ -183,7 +184,7 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| {
match entry.kind {
ContextKind::File => {
ContextPickerEntryKind::File => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
@@ -194,7 +195,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::Directory => {
ContextPickerEntryKind::Directory => {
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
DirectoryContextPicker::new(
self.context_picker.clone(),
@@ -205,7 +206,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::FetchedUrl => {
ContextPickerEntryKind::FetchedUrl => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
@@ -216,7 +217,7 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
ContextKind::Thread => {
ContextPickerEntryKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
ThreadContextPicker::new(

View File

@@ -1,19 +1,18 @@
// TODO: Remove this when we finish the implementation.
#![allow(unused)]
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::anyhow;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use project::{PathMatchCandidateSet, ProjectPath, Worktree, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::file_context_picker::codeblock_fence_for_path;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
@@ -193,14 +192,61 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let worktree = project.update(&mut cx, |project, cx| {
project
.worktree_for_id(worktree_id, cx)
.ok_or_else(|| anyhow!("no worktree found for {worktree_id:?}"))
})??;
let files = worktree.update(&mut cx, |worktree, _cx| {
collect_files_in_path(worktree, &path)
})?;
let open_buffer_tasks = project.update(&mut cx, |project, cx| {
files
.into_iter()
.map(|file_path| {
project.open_buffer(
ProjectPath {
worktree_id,
path: file_path.clone(),
},
cx,
)
})
.collect::<Vec<_>>()
})?;
let open_all_buffers_tasks = cx.background_executor().spawn(async move {
let mut buffers = Vec::with_capacity(open_buffer_tasks.len());
for open_buffer_task in open_buffer_tasks {
let buffer = open_buffer_task.await?;
buffers.push(buffer);
}
anyhow::Ok(buffers)
});
let buffers = open_all_buffers_tasks.await?;
this.update(&mut cx, |this, cx| {
let mut text = String::new();
// TODO: Add the files from the selected directory.
for buffer in buffers {
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
}
this.delegate
.context_store
.update(cx, |context_store, cx| {
.update(cx, |context_store, _cx| {
context_store.insert_context(
ContextKind::Directory,
path.to_string_lossy().to_string(),
@@ -247,3 +293,17 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
)
}
}
fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<Arc<Path>> {
let mut files = Vec::new();
for entry in worktree.child_entries(path) {
if entry.is_dir() {
files.extend(collect_files_in_path(worktree, &entry.path));
} else if entry.is_file() {
files.push(entry.path.clone());
}
}
files
}

View File

@@ -7,7 +7,7 @@ use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
@@ -207,11 +207,20 @@ impl PickerDelegate for FileContextPickerDelegate {
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
let Some((entry_id, open_buffer_task)) = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path.clone()), cx)
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
let entry_id = project.entry_for_path(&project_path, cx)?.id;
let task = project.open_buffer(project_path, cx);
Some((entry_id, task))
})
.ok()
.flatten()
else {
return anyhow::Ok(());
};
@@ -232,7 +241,7 @@ impl PickerDelegate for FileContextPickerDelegate {
text.push_str("```\n");
context_store.insert_context(
ContextKind::File,
ContextKind::File(entry_id),
path.to_string_lossy().to_string(),
text,
);
@@ -307,7 +316,10 @@ impl PickerDelegate for FileContextPickerDelegate {
}
}
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
pub(crate) fn codeblock_fence_for_path(
path: Option<&Path>,
row_range: Option<RangeInclusive<u32>>,
) -> String {
let mut text = String::new();
write!(text, "```").unwrap();

View File

@@ -169,25 +169,11 @@ impl PickerDelegate for ThreadContextPickerDelegate {
self.context_store
.update(cx, |context_store, cx| {
let text = thread.update(cx, |thread, _cx| {
let mut text = String::new();
for message in thread.messages() {
text.push_str(match message.role {
language_model::Role::User => "User:",
language_model::Role::Assistant => "Assistant:",
language_model::Role::System => "System:",
});
text.push('\n');
text.push_str(&message.text);
text.push('\n');
}
text
});
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
context_store.insert_context(
ContextKind::Thread(thread.read(cx).id().clone()),
entry.summary.clone(),
thread.read(cx).text(),
);
})
.ok();

View File

@@ -1,6 +1,10 @@
use gpui::SharedString;
use project::ProjectEntryId;
use crate::context::{Context, ContextId, ContextKind};
use crate::{
context::{Context, ContextId, ContextKind},
thread::ThreadId,
};
pub struct ContextStore {
context: Vec<Context>,
@@ -44,4 +48,18 @@ impl ContextStore {
pub fn remove_context(&mut self, id: &ContextId) {
self.context.retain(|context| context.id != *id);
}
pub fn contains_project_entry(&self, entry_id: ProjectEntryId) -> bool {
self.context.iter().any(|probe| match probe.kind {
ContextKind::File(probe_entry_id) => probe_entry_id == entry_id,
ContextKind::Directory | ContextKind::FetchedUrl | ContextKind::Thread(_) => false,
})
}
pub fn contains_thread(&self, thread_id: &ThreadId) -> bool {
self.context.iter().any(|probe| match probe.kind {
ContextKind::Thread(ref probe_thread_id) => probe_thread_id == thread_id,
ContextKind::File(_) | ContextKind::Directory | ContextKind::FetchedUrl => false,
})
}
}

View File

@@ -1,21 +1,27 @@
use std::rc::Rc;
use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
use editor::Editor;
use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectEntryId;
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::thread::{Thread, ThreadId};
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::ToggleContextPicker;
use settings::Settings;
use crate::{AssistantPanel, ToggleContextPicker};
pub struct ContextStrip {
context_store: Model<ContextStore>,
context_picker: View<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakView<Workspace>,
}
impl ContextStrip {
@@ -25,6 +31,7 @@ impl ContextStrip {
thread_store: Option<WeakModel<ThreadStore>>,
focus_handle: FocusHandle,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
@@ -40,16 +47,76 @@ impl ContextStrip {
}),
context_picker_menu_handle,
focus_handle,
suggest_context_kind,
workspace,
}
}
fn suggested_context(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
let entry_id = *active_item.project_entry_ids(cx).first()?;
if self.context_store.read(cx).contains_project_entry(entry_id) {
return None;
}
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
let active_buffer = editor.buffer().read(cx).as_singleton()?;
let file = active_buffer.read(cx).file()?;
let title = file.path().to_string_lossy().into_owned().into();
Some(SuggestedContext::File {
entry_id,
title,
buffer: active_buffer.downgrade(),
})
}
fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_thread = workspace
.read(cx)
.panel::<AssistantPanel>(cx)?
.read(cx)
.active_thread(cx);
let weak_active_thread = active_thread.downgrade();
let active_thread = active_thread.read(cx);
if self
.context_store
.read(cx)
.contains_thread(active_thread.id())
{
return None;
}
Some(SuggestedContext::Thread {
id: active_thread.id().clone(),
title: active_thread.summary().unwrap_or("Active Thread".into()),
thread: weak_active_thread,
})
}
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context = self.context_store.read(cx).context().clone();
let context_store = self.context_store.read(cx);
let context = context_store.context().clone();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let suggested_context = self.suggested_context(cx);
h_flex()
.flex_wrap()
.gap_1()
@@ -60,13 +127,17 @@ impl Render for ContextStrip {
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled)
.tooltip(move |cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
cx,
)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
cx,
)
}
}),
)
.attach(gpui::Corner::TopLeft)
@@ -77,26 +148,22 @@ impl Render for ContextStrip {
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.when(context.is_empty(), {
.when(context.is_empty() && suggested_context.is_none(), {
|parent| {
parent.child(
h_flex()
.id("no-content-info")
.ml_1p5()
.gap_2()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::Small.rems(cx))
.text_color(cx.theme().colors().text_muted)
.child("Add Context")
.children(
ui::KeyBinding::for_action_in(
&ToggleContextPicker,
&self.focus_handle,
cx,
)
.map(|binding| binding.into_any_element()),
.child(
Label::new("Add Context")
.size(LabelSize::Small)
.color(Color::Muted),
)
.opacity(0.5),
.opacity(0.5)
.children(
KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
),
)
}
})
@@ -112,6 +179,30 @@ impl Render for ContextStrip {
}))
})
}))
.when_some(suggested_context, |el, suggested| {
el.child(
Button::new("add-suggested-context", suggested.title().clone())
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |context_store, cx| {
suggested.accept(context_store, cx);
});
cx.notify();
})
})
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.style(ButtonStyle::Filled)
.tooltip(|cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
}),
)
})
.when(!context.is_empty(), {
move |parent| {
parent.child(
@@ -130,3 +221,63 @@ impl Render for ContextStrip {
})
}
}
pub enum SuggestContextKind {
File,
Thread,
}
#[derive(Clone)]
pub enum SuggestedContext {
File {
entry_id: ProjectEntryId,
title: SharedString,
buffer: WeakModel<Buffer>,
},
Thread {
id: ThreadId,
title: SharedString,
thread: WeakModel<Thread>,
},
}
impl SuggestedContext {
pub fn title(&self) -> &SharedString {
match self {
Self::File { title, .. } => title,
Self::Thread { title, .. } => title,
}
}
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
match self {
Self::File {
entry_id,
title,
buffer,
} => {
let Some(buffer) = buffer.upgrade() else {
return;
};
let text = buffer.read(cx).text();
context_store.insert_context(
ContextKind::File(*entry_id),
title.clone(),
text.clone(),
);
}
Self::Thread { id, title, thread } => {
let Some(thread) = thread.upgrade() else {
return;
};
context_store.insert_context(
ContextKind::Thread(id.clone()),
title.clone(),
thread.read(cx).text(),
);
}
}
}
}

View File

@@ -118,7 +118,7 @@ impl InlineAssistant {
};
let enabled = AssistantSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.asssistant_enabled(enabled, cx)
terminal_panel.set_assistant_enabled(enabled, cx)
});
})
.detach();
@@ -294,19 +294,20 @@ impl InlineAssistant {
let newest_selection = newest_selection.unwrap();
let mut codegen_ranges = Vec::new();
for (excerpt_id, buffer, buffer_range) in
snapshot.excerpts_in_ranges(selections.iter().map(|selection| {
for (excerpt, buffer_range) in
snapshot.disjoint_ranges_to_buffer_ranges(selections.iter().map(|selection| {
snapshot.anchor_before(selection.start)..snapshot.anchor_after(selection.end)
}))
{
let buffer = excerpt.buffer();
let start = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_before(buffer_range.start),
};
let end = Anchor {
buffer_id: Some(buffer.remote_id()),
excerpt_id,
excerpt_id: excerpt.id(),
text_anchor: buffer.anchor_after(buffer_range.end),
};
codegen_ranges.push(start..end);
@@ -871,10 +872,11 @@ impl InlineAssistant {
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
let language_name = assist.editor.upgrade().and_then(|editor| {
let multibuffer = editor.read(cx).buffer().read(cx);
let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
let snapshot = multibuffer.snapshot(cx);
let mut ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
ranges
.first()
.and_then(|(buffer, _, _)| buffer.read(cx).language())
.next()
.and_then(|(excerpt, _)| excerpt.buffer().language())
.map(|language| language.name())
});
report_assistant_event(

View File

@@ -2,7 +2,7 @@ use crate::assistant_model_selector::AssistantModelSelector;
use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::context_strip::{ContextStrip, SuggestContextKind};
use crate::terminal_codegen::TerminalCodegen;
use crate::thread_store::ThreadStore;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
@@ -793,6 +793,7 @@ impl PromptEditor<BufferCodegen> {
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
}),
@@ -932,6 +933,7 @@ impl PromptEditor<TerminalCodegen> {
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
cx,
)
}),

View File

@@ -20,7 +20,7 @@ use workspace::Workspace;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::context_strip::{ContextStrip, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
@@ -87,6 +87,7 @@ impl MessageEditor {
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
SuggestContextKind::File,
cx,
)
}),

View File

@@ -164,6 +164,27 @@ impl Thread {
id
}
/// Returns the representation of this [`Thread`] in a textual form.
///
/// This is the representation we use when attaching a thread as context to another thread.
pub fn text(&self) -> String {
let mut text = String::new();
for message in &self.messages {
text.push_str(match message.role {
language_model::Role::User => "User:",
language_model::Role::Assistant => "Assistant:",
language_model::Role::System => "System:",
});
text.push('\n');
text.push_str(&message.text);
text.push('\n');
}
text
}
pub fn to_completion_request(
&self,
_request_kind: RequestKind,

View File

@@ -238,5 +238,46 @@ impl ThreadStore {
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
thread
}));
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust code with long lines", cx);
thread.insert_user_message("Could you write me some Rust code with long lines?", Vec::new(), cx);
thread.insert_message(Role::Assistant, r#"Here's some Rust code with some intentionally long lines:
```rust
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let very_long_vector = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
let complicated_hashmap: HashMap<String, Vec<(i32, f64, String)>> = [("key1".to_string(), vec![(1, 1.1, "value1".to_string()), (2, 2.2, "value2".to_string())]), ("key2".to_string(), vec![(3, 3.3, "value3".to_string()), (4, 4.4, "value4".to_string())])].iter().cloned().collect();
let nested_structure = Arc::new(Mutex::new(HashMap::new()));
let long_closure = |x: i32, y: i32, z: i32| -> i32 { let result = x * y + z; println!("The result of the long closure calculation is: {}", result); result };
let thread_handles: Vec<_> = (0..10).map(|i| {
let nested_structure_clone = Arc::clone(&nested_structure);
thread::spawn(move || {
let mut lock = nested_structure_clone.lock().unwrap();
lock.entry(format!("thread_{}", i)).or_insert_with(|| HashSet::new()).insert(i * i);
})
}).collect();
for handle in thread_handles {
handle.join().unwrap();
}
println!("The final state of the nested structure is: {:?}", nested_structure.lock().unwrap());
let complex_expression = very_long_vector.iter().filter(|&&x| x % 2 == 0).map(|&x| x * x).fold(0, |acc, x| acc + x) + long_closure(5, 10, 15);
println!("The result of the complex expression is: {}", complex_expression);
}
```"#.unindent(), cx);
thread
}));
}
}

View File

@@ -33,10 +33,10 @@ impl RenderOnce for ContextPill {
px(4.)
};
let icon = match self.context.kind {
ContextKind::File => IconName::File,
ContextKind::File(_) => IconName::File,
ContextKind::Directory => IconName::Folder,
ContextKind::FetchedUrl => IconName::Globe,
ContextKind::Thread => IconName::MessageCircle,
ContextKind::Thread(_) => IconName::MessageCircle,
};
h_flex()

View File

@@ -2,4 +2,10 @@ fn main() {
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
}
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
// Weakly link ScreenCaptureKit to ensure can be used on macOS 10.15+.
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ScreenCaptureKit");
}
}

View File

@@ -79,7 +79,7 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
let curdir = env::current_dir().context("retrieving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {

View File

@@ -106,6 +106,22 @@ CREATE TABLE "worktree_repositories" (
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,

View File

@@ -1,4 +1,5 @@
use anyhow::Context as _;
use util::ResultExt;
use super::*;
@@ -274,8 +275,8 @@ impl Database {
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
canonical_path: ActiveValue::set(entry.canonical_path.clone()),
is_ignored: ActiveValue::set(entry.is_ignored),
git_status: ActiveValue::set(None),
is_external: ActiveValue::set(entry.is_external),
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
is_deleted: ActiveValue::set(false),
scan_id: ActiveValue::set(update.scan_id as i64),
is_fifo: ActiveValue::set(entry.is_fifo),
@@ -295,7 +296,6 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::GitStatus,
worktree_entry::Column::ScanId,
])
.to_owned(),
@@ -349,6 +349,79 @@ impl Database {
)
.exec(&*tx)
.await?;
let has_any_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.updated_statuses.is_empty());
if has_any_statuses {
worktree_repository_statuses::Entity::insert_many(
update.updated_repositories.iter().flat_map(
|repository: &proto::RepositoryEntry| {
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
}
})
},
),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
let has_any_removed_statuses = update
.updated_repositories
.iter()
.any(|repository| !repository.removed_statuses.is_empty());
if has_any_removed_statuses {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::RepoPath.is_in(
update.updated_repositories.iter().flat_map(|repository| {
repository.removed_statuses.iter()
}),
),
),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
if !update.removed_repositories.is_empty() {
@@ -643,7 +716,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@@ -657,23 +729,49 @@ impl Database {
// Populate repository entries.
{
let mut db_repository_entries = worktree_repository::Entity::find()
let db_repository_entries = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::IsDeleted.eq(false)),
)
.stream(tx)
.all(tx)
.await?;
while let Some(db_repository_entry) = db_repository_entries.next().await {
let db_repository_entry = db_repository_entry?;
for db_repository_entry in db_repository_entries {
if let Some(worktree) = worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{
let mut repository_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project.id))
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository_entry.work_directory_id),
)
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(tx)
.await?;
let mut updated_statuses = Vec::new();
while let Some(status_entry) = repository_statuses.next().await {
let status_entry: worktree_repository_statuses::Model = status_entry?;
updated_statuses.push(proto::StatusEntry {
repo_path: status_entry.repo_path,
status: status_entry.status as i32,
});
}
worktree.repository_entries.insert(
db_repository_entry.work_directory_id as u64,
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
updated_statuses,
removed_statuses: Vec::new(),
},
);
}

View File

@@ -662,7 +662,6 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@@ -682,26 +681,69 @@ impl Database {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
let db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(tx)
.all(tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
for db_repository in db_repositories.into_iter() {
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
let status_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree
{
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_statuses = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(db_repository.work_directory_id),
)
.add(status_entry_filter),
)
.stream(tx)
.await?;
let mut removed_statuses = Vec::new();
let mut updated_statuses = Vec::new();
while let Some(db_status) = db_statuses.next().await {
let db_status: worktree_repository_statuses::Model = db_status?;
if db_status.is_deleted {
removed_statuses.push(db_status.repo_path);
} else {
updated_statuses.push(proto::StatusEntry {
repo_path: db_status.repo_path,
status: db_status.status as i32,
});
}
}
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
updated_statuses,
removed_statuses,
});
}
}

View File

@@ -2925,8 +2925,6 @@ async fn test_git_status_sync(
assert_eq!(snapshot.status_for_file(file), status);
}
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
@@ -4067,7 +4065,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticEntry {
range: Point::new(0, 4)..Point::new(0, 7),
diagnostic: Diagnostic {
group_id: 2,
group_id: 3,
message: "message 1".to_string(),
severity: lsp::DiagnosticSeverity::ERROR,
is_primary: true,
@@ -4077,7 +4075,7 @@ async fn test_collaborating_with_diagnostics(
DiagnosticEntry {
range: Point::new(0, 10)..Point::new(0, 13),
diagnostic: Diagnostic {
group_id: 3,
group_id: 4,
severity: lsp::DiagnosticSeverity::WARNING,
message: "message 2".to_string(),
is_primary: true,
@@ -6669,6 +6667,10 @@ async fn test_remote_git_branches(
client_a
.fs()
.insert_branches(Path::new("/project/.git"), &branches);
let branches_set = branches
.into_iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
let project_id = active_call_a
@@ -6690,10 +6692,10 @@ async fn test_remote_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&branches_b, &branches);
assert_eq!(branches_b, branches_set);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {

View File

@@ -229,6 +229,10 @@ async fn test_ssh_collaboration_git_branches(
.await;
let branches = ["main", "dev", "feature-1"];
let branches_set = branches
.iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
// User A connects to the remote project via SSH.
@@ -281,10 +285,10 @@ async fn test_ssh_collaboration_git_branches(
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&branches_b, &branches);
assert_eq!(&branches_b, &branches_set);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {

View File

@@ -1135,15 +1135,20 @@ impl Panel for ChatPanel {
}
fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => None,
ChatPanelButton::Always => Some(ui::IconName::MessageBubbles),
ChatPanelButton::WhenInCall => ActiveCall::global(cx)
.read(cx)
.room()
.filter(|room| room.read(cx).contains_guests())
.map(|_| ui::IconName::MessageBubbles),
}
let show_icon = match ChatPanelSettings::get_global(cx).button {
ChatPanelButton::Never => false,
ChatPanelButton::Always => true,
ChatPanelButton::WhenInCall => {
let is_in_call = ActiveCall::global(cx)
.read(cx)
.room()
.map_or(false, |room| room.read(cx).contains_guests());
self.active || is_in_call
}
};
show_icon.then(|| ui::IconName::MessageBubbles)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View File

@@ -16,4 +16,5 @@ doctest = false
test-support = []
[dependencies]
rustc-hash = "1.1"
indexmap.workspace = true
rustc-hash.workspace = true

View File

@@ -4,12 +4,24 @@ pub type HashMap<K, V> = FxHashMap<K, V>;
#[cfg(feature = "test-support")]
pub type HashSet<T> = FxHashSet<T>;
#[cfg(feature = "test-support")]
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, rustc_hash::FxBuildHasher>;
#[cfg(feature = "test-support")]
pub type IndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
#[cfg(not(feature = "test-support"))]
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
#[cfg(not(feature = "test-support"))]
pub type HashSet<T> = std::collections::HashSet<T>;
#[cfg(not(feature = "test-support"))]
pub type IndexMap<K, V> = indexmap::IndexMap<K, V>;
#[cfg(not(feature = "test-support"))]
pub type IndexSet<T> = indexmap::IndexSet<T>;
pub use rustc_hash::FxHasher;
pub use rustc_hash::{FxHashMap, FxHashSet};
pub use std::collections::*;

View File

@@ -82,7 +82,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -95,7 +95,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -106,7 +106,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -117,7 +117,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::INFORMATION,
is_primary: false,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -128,7 +128,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -139,7 +139,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -241,7 +241,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
}],
@@ -348,7 +348,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 0,
group_id: 1,
..Default::default()
},
},
@@ -359,7 +359,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
severity: DiagnosticSeverity::ERROR,
is_primary: true,
is_disk_based: true,
group_id: 1,
group_id: 2,
..Default::default()
},
},
@@ -775,7 +775,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
assert!(view.focus_handle.is_focused(cx));
});
let mut next_group_id = 0;
let mut next_group_id = 1;
let mut next_filename = 0;
let mut language_server_ids = vec![LanguageServerId(0)];
let mut updated_language_servers = HashSet::default();

View File

@@ -1,11 +1,11 @@
use std::time::Duration;
use editor::Editor;
use editor::{AnchorRangeExt, Editor};
use gpui::{
EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
ViewContext, WeakView,
};
use language::Diagnostic;
use language::{Diagnostic, DiagnosticEntry};
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
@@ -148,7 +148,11 @@ impl DiagnosticIndicator {
(buffer, cursor_position)
});
let new_diagnostic = buffer
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
.diagnostics_in_range(cursor_position..cursor_position, false)
.map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: range.to_offset(&buffer),
})
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
.map(|entry| entry.diagnostic);

View File

@@ -2748,7 +2748,7 @@ mod tests {
.iter()
.filter(|(_, block)| matches!(block, Block::FoldedBuffer { .. }))
.count(),
"Should have one folded block, prodicing a header of the second buffer"
"Should have one folded block, producing a header of the second buffer"
);
assert_eq!(
blocks_snapshot.text(),
@@ -2994,7 +2994,7 @@ mod tests {
}
})
.count(),
"Should have one folded block, prodicing a header of the second buffer"
"Should have one folded block, producing a header of the second buffer"
);
assert_eq!(blocks_snapshot.text(), "\n");
assert_eq!(

View File

@@ -99,8 +99,8 @@ use itertools::Itertools;
use language::{
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
CursorShape, Diagnostic, DiagnosticEntry, Documentation, IndentKind, IndentSize, Language,
OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
@@ -109,7 +109,7 @@ pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use similar::{ChangeTag, TextDiff};
use std::iter::Peekable;
use std::iter::{self, Peekable};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
@@ -3549,13 +3549,11 @@ impl Editor {
Bias::Left,
);
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
multi_buffer
.range_to_buffer_ranges(multi_buffer_visible_range, cx)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
let buffer = buffer_handle.read(cx);
let buffer_file = project::File::from_dyn(buffer.file())?;
multi_buffer_snapshot
.disjoint_ranges_to_buffer_ranges(iter::once(multi_buffer_visible_range))
.filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty())
.filter_map(|(excerpt, excerpt_visible_range)| {
let buffer_file = project::File::from_dyn(excerpt.buffer().file())?;
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
let worktree_entry = buffer_worktree
.read(cx)
@@ -3564,17 +3562,17 @@ impl Editor {
return None;
}
let language = buffer.language()?;
let language = excerpt.buffer().language()?;
if let Some(restrict_to_languages) = restrict_to_languages {
if !restrict_to_languages.contains(language) {
return None;
}
}
Some((
excerpt_id,
excerpt.id(),
(
buffer_handle,
buffer.version().clone(),
multi_buffer.buffer(excerpt.buffer_id()).unwrap(),
excerpt.buffer().version().clone(),
excerpt_visible_range,
),
))
@@ -9149,11 +9147,12 @@ impl Editor {
// If there is an active Diagnostic Popover jump to its diagnostic instead.
if direction == Direction::Next {
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
let (group_id, jump_to) = popover.activation_info();
if self.activate_diagnostics(group_id, cx) {
self.activate_diagnostics(popover.group_id(), cx);
if let Some(active_diagnostics) = self.active_diagnostics.as_ref() {
let primary_range_start = active_diagnostics.primary_range.start;
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut new_selection = s.newest_anchor().clone();
new_selection.collapse_to(jump_to, SelectionGoal::None);
new_selection.collapse_to(primary_range_start, SelectionGoal::None);
s.select_anchors(vec![new_selection.clone()]);
});
}
@@ -9179,10 +9178,23 @@ impl Editor {
let snapshot = self.snapshot(cx);
loop {
let diagnostics = if direction == Direction::Prev {
buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
buffer
.diagnostics_in_range(0..search_start, true)
.map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: range.to_offset(&buffer),
})
.collect::<Vec<_>>()
} else {
buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
buffer
.diagnostics_in_range(search_start..buffer.len(), false)
.map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: range.to_offset(&buffer),
})
.collect::<Vec<_>>()
}
.into_iter()
.filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start));
let group = diagnostics
// relies on diagnostics_in_range to return diagnostics with the same starting range to
@@ -9212,7 +9224,8 @@ impl Editor {
});
if let Some((primary_range, group_id)) = group {
if self.activate_diagnostics(group_id, cx) {
self.activate_diagnostics(group_id, cx);
if self.active_diagnostics.is_some() {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(vec![Selection {
id: selection.id,
@@ -10289,11 +10302,12 @@ impl Editor {
let buffer = self.buffer.read(cx).snapshot(cx);
let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
let is_valid = buffer
.diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone(), false)
.diagnostics_in_range(active_diagnostics.primary_range.clone(), false)
.any(|entry| {
let range = entry.range.to_offset(&buffer);
entry.diagnostic.is_primary
&& !entry.range.is_empty()
&& entry.range.start == primary_range_start
&& !range.is_empty()
&& range.start == primary_range_start
&& entry.diagnostic.message == active_diagnostics.primary_message
});
@@ -10313,7 +10327,7 @@ impl Editor {
}
}
fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) -> bool {
fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) {
self.dismiss_diagnostics(cx);
let snapshot = self.snapshot(cx);
self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
@@ -10323,16 +10337,18 @@ impl Editor {
let mut primary_message = None;
let mut group_end = Point::zero();
let diagnostic_group = buffer
.diagnostic_group::<MultiBufferPoint>(group_id)
.diagnostic_group(group_id)
.filter_map(|entry| {
if snapshot.is_line_folded(MultiBufferRow(entry.range.start.row))
&& (entry.range.start.row == entry.range.end.row
|| snapshot.is_line_folded(MultiBufferRow(entry.range.end.row)))
let start = entry.range.start.to_point(&buffer);
let end = entry.range.end.to_point(&buffer);
if snapshot.is_line_folded(MultiBufferRow(start.row))
&& (start.row == end.row
|| snapshot.is_line_folded(MultiBufferRow(end.row)))
{
return None;
}
if entry.range.end > group_end {
group_end = entry.range.end;
if end > group_end {
group_end = end;
}
if entry.diagnostic.is_primary {
primary_range = Some(entry.range.clone());
@@ -10343,8 +10359,6 @@ impl Editor {
.collect::<Vec<_>>();
let primary_range = primary_range?;
let primary_message = primary_message?;
let primary_range =
buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end);
let blocks = display_map
.insert_blocks(
@@ -10375,7 +10389,6 @@ impl Editor {
is_valid: true,
})
});
self.active_diagnostics.is_some()
}
fn dismiss_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
@@ -10484,13 +10497,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut toggled_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
@@ -10570,13 +10583,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut folded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
}
@@ -10736,13 +10749,13 @@ impl Editor {
} else {
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let mut unfolded_buffers = HashSet::default();
for (_, buffer_snapshot, _) in multi_buffer_snapshot.excerpts_in_ranges(
for (excerpt, _) in multi_buffer_snapshot.disjoint_ranges_to_buffer_ranges(
self.selections
.disjoint_anchors()
.into_iter()
.map(|selection| selection.range()),
) {
let buffer_id = buffer_snapshot.remote_id();
let buffer_id = excerpt.buffer().remote_id();
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
@@ -11486,34 +11499,36 @@ impl Editor {
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<url::Url>> {
let buffer_and_selection = maybe!({
let selection = self.selections.newest::<Point>(cx);
let buffer_and_selection_rows = maybe!({
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let selection = self.selections.newest_anchor();
let selection_range = selection.range();
let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
(buffer, selection_range.start.row..selection_range.end.row)
let (buffer, selection_rows) = if let Some(buffer) = multi_buffer.as_singleton() {
(
buffer,
selection_range.start.to_point(&multi_buffer_snapshot).row
..selection_range.end.to_point(&multi_buffer_snapshot).row,
)
} else {
let buffer_ranges = self
.buffer()
.read(cx)
.range_to_buffer_ranges(selection_range, cx);
let (buffer, range, _) = if selection.reversed {
buffer_ranges.first()
} else {
buffer_ranges.last()
}?;
let snapshot = buffer.read(cx).snapshot();
let selection = text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row;
(buffer.clone(), selection)
let selection_head = selection.head();
let excerpt =
multi_buffer_snapshot.excerpt_containing(selection_head..selection_head)?;
let range =
excerpt.map_range_to_buffer(selection_range.to_offset(&multi_buffer_snapshot));
let snapshot = excerpt.buffer();
(
multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(),
text::ToPoint::to_point(&range.start, &snapshot).row
..text::ToPoint::to_point(&range.end, &snapshot).row,
)
};
Some((buffer, selection))
Some((buffer, selection_rows))
});
let Some((buffer, selection)) = buffer_and_selection else {
let Some((buffer, selection_rows)) = buffer_and_selection_rows else {
return Task::ready(Err(anyhow!("failed to determine buffer and selection")));
};
@@ -11522,7 +11537,7 @@ impl Editor {
};
project.update(cx, |project, cx| {
project.get_permalink_to_line(&buffer, selection, cx)
project.get_permalink_to_line(&buffer, selection_rows, cx)
})
}
@@ -11765,7 +11780,7 @@ impl Editor {
}
/// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict.
/// Rerturns a map of display rows that are highlighted and their corresponding highlight color.
/// Returns a map of display rows that are highlighted and their corresponding highlight color.
/// Allows to ignore certain kinds of highlights.
pub fn highlighted_display_rows(
&mut self,
@@ -12399,17 +12414,18 @@ impl Editor {
};
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
let multi_buffer = self.buffer.read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let mut new_selections_by_buffer = HashMap::default();
for selection in selections {
for (buffer, range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
for (excerpt, range) in
multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end)
{
let mut range = range.to_point(buffer.read(cx));
let mut range = range.to_point(excerpt.buffer());
range.start.column = 0;
range.end.column = buffer.read(cx).line_len(range.end.row);
range.end.column = excerpt.buffer().line_len(range.end.row);
new_selections_by_buffer
.entry(buffer)
.entry(multi_buffer.buffer(excerpt.buffer_id()).unwrap())
.or_insert(Vec::new())
.push(range)
}
@@ -12508,13 +12524,15 @@ impl Editor {
}
None => {
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
let multi_buffer = self.buffer.read(cx);
for selection in selections {
for (mut buffer_handle, mut range, _) in
buffer.range_to_buffer_ranges(selection.range(), cx)
for (excerpt, mut range) in multi_buffer
.snapshot(cx)
.range_to_buffer_ranges(selection.range())
{
// When editing branch buffers, jump to the corresponding location
// in their base buffer.
let mut buffer_handle = multi_buffer.buffer(excerpt.buffer_id()).unwrap();
let buffer = buffer_handle.read(cx);
if let Some(base_buffer) = buffer.base_buffer() {
range = buffer.range_to_version(range, &base_buffer.read(cx).version());
@@ -12555,7 +12573,7 @@ impl Editor {
.file()
.is_none()
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a paroject path or entity id,
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index) =

View File

@@ -105,7 +105,7 @@ pub struct Scrollbar {
pub git_diff: bool,
pub selected_symbol: bool,
pub search_results: bool,
pub diagnostics: bool,
pub diagnostics: ScrollbarDiagnostics,
pub cursors: bool,
pub axes: ScrollbarAxes,
}
@@ -150,6 +150,73 @@ pub struct ScrollbarAxes {
pub vertical: bool,
}
/// Which diagnostic indicators to show in the scrollbar.
///
/// Default: all
#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ScrollbarDiagnostics {
/// Show all diagnostic levels: hint, information, warnings, error.
All,
/// Show only the following diagnostic levels: information, warning, error.
Information,
/// Show only the following diagnostic levels: warning, error.
Warning,
/// Show only the following diagnostic level: error.
Error,
/// Do not show diagnostics.
None,
}
impl<'de> Deserialize<'de> for ScrollbarDiagnostics {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ScrollbarDiagnostics;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
r#"a boolean or one of "all", "information", "warning", "error", "none""#
)
}
fn visit_bool<E>(self, b: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match b {
false => Ok(ScrollbarDiagnostics::None),
true => Ok(ScrollbarDiagnostics::All),
}
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match s {
"all" => Ok(ScrollbarDiagnostics::All),
"information" => Ok(ScrollbarDiagnostics::Information),
"warning" => Ok(ScrollbarDiagnostics::Warning),
"error" => Ok(ScrollbarDiagnostics::Error),
"none" => Ok(ScrollbarDiagnostics::None),
_ => Err(E::unknown_variant(
s,
&["all", "information", "warning", "error", "none"],
)),
}
}
}
deserializer.deserialize_any(Visitor)
}
}
/// The key to use for adding multiple cursors
///
/// Default: alt
@@ -348,10 +415,10 @@ pub struct ScrollbarContent {
///
/// Default: true
pub selected_symbol: Option<bool>,
/// Whether to show diagnostic indicators in the scrollbar.
/// Which diagnostic indicators to show in the scrollbar:
///
/// Default: true
pub diagnostics: Option<bool>,
/// Default: all
pub diagnostics: Option<ScrollbarDiagnostics>,
/// Whether to show cursor positions in the scrollbar.
///
/// Default: true

View File

@@ -6,7 +6,7 @@ use crate::{
},
editor_settings::{
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
ShowScrollbar,
ScrollbarDiagnostics, ShowScrollbar,
},
git::blame::{CommitDetails, GitBlame},
hover_popover::{
@@ -45,7 +45,7 @@ use language::{
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
ShowWhitespaceSetting,
},
ChunkRendererContext,
ChunkRendererContext, DiagnosticEntry,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -1235,7 +1235,7 @@ impl EditorElement {
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
||
// Diagnostics
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
||
// Cursors out of sight
non_visible_cursors
@@ -4738,13 +4738,39 @@ impl EditorElement {
}
}
if scrollbar_settings.diagnostics {
if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
let diagnostics = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, Point>(
Point::zero()..max_point,
false,
)
.diagnostics_in_range(Point::zero()..max_point, false)
.map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: range.to_point(&snapshot.buffer_snapshot),
})
// Don't show diagnostics the user doesn't care about
.filter(|diagnostic| {
match (
scrollbar_settings.diagnostics,
diagnostic.diagnostic.severity,
) {
(ScrollbarDiagnostics::All, _) => true,
(
ScrollbarDiagnostics::Error,
DiagnosticSeverity::ERROR,
) => true,
(
ScrollbarDiagnostics::Warning,
DiagnosticSeverity::ERROR
| DiagnosticSeverity::WARNING,
) => true,
(
ScrollbarDiagnostics::Information,
DiagnosticSeverity::ERROR
| DiagnosticSeverity::WARNING
| DiagnosticSeverity::INFORMATION,
) => true,
(_, _) => false,
}
})
// We want to sort by severity, in order to paint the most severe diagnostics last.
.sorted_by_key(|diagnostic| {
std::cmp::Reverse(diagnostic.diagnostic.severity)

View File

@@ -194,14 +194,24 @@ impl ProjectDiffEditor {
let open_tasks = project
.update(&mut cx, |project, cx| {
let worktree = project.worktree_for_id(id, cx)?;
let applicable_entries = worktree
.read(cx)
.entries(false, 0)
.filter(|entry| !entry.is_external)
.filter(|entry| entry.is_file())
.filter_map(|entry| Some((entry.git_status?, entry)))
.filter_map(|(git_status, entry)| {
Some((git_status, entry.id, project.path_for_entry(entry.id, cx)?))
let snapshot = worktree.read(cx).snapshot();
let applicable_entries = snapshot
.repositories()
.flat_map(|entry| {
entry.status().map(|git_entry| {
(git_entry.status, entry.join(git_entry.repo_path))
})
})
.filter_map(|(status, path)| {
let id = snapshot.entry_for_path(&path)?.id;
Some((
status,
id,
ProjectPath {
worktree_id: snapshot.id(),
path: path.into(),
},
))
})
.collect::<Vec<_>>();
Some(

View File

@@ -3,7 +3,7 @@ use crate::{
hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
Hover, RangeToAnchorExt,
Hover,
};
use gpui::{
div, px, AnyElement, AsyncWindowContext, FontWeight, Hsla, InteractiveElement, IntoElement,
@@ -263,29 +263,7 @@ fn show_hover(
delay.await;
}
// If there's a diagnostic, assign it on the hover state and notify
let mut local_diagnostic = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, usize>(anchor..anchor, false)
// Find the entry with the most specific range
.min_by_key(|entry| entry.range.end - entry.range.start)
.map(|entry| DiagnosticEntry {
diagnostic: entry.diagnostic,
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
});
// Pull the primary diagnostic out so we can jump to it if the popover is clicked
let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
snapshot
.buffer_snapshot
.diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
.find(|diagnostic| diagnostic.diagnostic.is_primary)
.map(|entry| DiagnosticEntry {
diagnostic: entry.diagnostic,
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
})
});
if let Some(invisible) = snapshot
let local_diagnostic = if let Some(invisible) = snapshot
.buffer_snapshot
.chars_at(anchor)
.next()
@@ -294,7 +272,7 @@ fn show_hover(
let after = snapshot.buffer_snapshot.anchor_after(
anchor.to_offset(&snapshot.buffer_snapshot) + invisible.len_utf8(),
);
local_diagnostic = Some(DiagnosticEntry {
Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
@@ -311,7 +289,7 @@ fn show_hover(
let before = snapshot.buffer_snapshot.anchor_before(
anchor.to_offset(&snapshot.buffer_snapshot) - invisible.len_utf8(),
);
local_diagnostic = Some(DiagnosticEntry {
Some(DiagnosticEntry {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: format!("Unicode character U+{:02X}", invisible as u32),
@@ -319,7 +297,16 @@ fn show_hover(
},
range: before..anchor,
})
}
} else {
snapshot
.buffer_snapshot
.diagnostics_in_range(anchor..anchor, false)
// Find the entry with the most specific range
.min_by_key(|entry| {
let range = entry.range.to_offset(&snapshot.buffer_snapshot);
range.end - range.start
})
};
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
let text = match local_diagnostic.diagnostic.source {
@@ -388,7 +375,6 @@ fn show_hover(
Some(DiagnosticPopover {
local_diagnostic,
primary_diagnostic,
parsed_content,
border_color,
background_color,
@@ -783,7 +769,6 @@ impl InfoPopover {
#[derive(Debug, Clone)]
pub struct DiagnosticPopover {
local_diagnostic: DiagnosticEntry<Anchor>,
primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
parsed_content: Option<View<Markdown>>,
border_color: Option<Hsla>,
background_color: Option<Hsla>,
@@ -837,13 +822,8 @@ impl DiagnosticPopover {
diagnostic_div.into_any_element()
}
pub fn activation_info(&self) -> (usize, Anchor) {
let entry = self
.primary_diagnostic
.as_ref()
.unwrap_or(&self.local_diagnostic);
(entry.diagnostic.group_id, entry.range.start)
pub fn group_id(&self) -> usize {
self.local_diagnostic.diagnostic.group_id
}
}

View File

@@ -456,16 +456,16 @@ impl Editor {
range: Range<Anchor>,
cx: &mut ViewContext<Editor>,
) -> Option<()> {
let (buffer, range, _) = self
.buffer
.read(cx)
.range_to_buffer_ranges(range, cx)
.into_iter()
.next()?;
let multi_buffer = self.buffer.read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let (excerpt, range) = multi_buffer_snapshot.range_to_buffer_ranges(range).next()?;
buffer.update(cx, |branch_buffer, cx| {
branch_buffer.merge_into_base(vec![range], cx);
});
multi_buffer
.buffer(excerpt.buffer_id())
.unwrap()
.update(cx, |branch_buffer, cx| {
branch_buffer.merge_into_base(vec![range], cx);
});
if let Some(project) = self.project.clone() {
self.save(true, project, cx).detach_and_log_err(cx);

View File

@@ -36,6 +36,7 @@ pub struct InlayHintCache {
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
version: usize,
pub(super) enabled: bool,
enabled_in_settings: bool,
update_tasks: HashMap<ExcerptId, TasksForRanges>,
refresh_task: Option<Task<()>>,
invalidate_debounce: Option<Duration>,
@@ -268,6 +269,7 @@ impl InlayHintCache {
Self {
allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
enabled: inlay_hint_settings.enabled,
enabled_in_settings: inlay_hint_settings.enabled,
hints: HashMap::default(),
update_tasks: HashMap::default(),
refresh_task: None,
@@ -288,10 +290,21 @@ impl InlayHintCache {
visible_hints: Vec<Inlay>,
cx: &mut ViewContext<Editor>,
) -> ControlFlow<Option<InlaySplice>> {
let old_enabled = self.enabled;
// If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay
// hint visibility changes when other settings change (such as theme).
//
// Another option might be to store whether the user has manually toggled inlay hint
// visibility, and prefer this. This could lead to confusion as it means inlay hint
// visibility would not change when updating the setting if they were ever toggled.
if new_hint_settings.enabled != self.enabled_in_settings {
self.enabled = new_hint_settings.enabled;
};
self.enabled_in_settings = new_hint_settings.enabled;
self.invalidate_debounce = debounce_value(new_hint_settings.edit_debounce_ms);
self.append_debounce = debounce_value(new_hint_settings.scroll_debounce_ms);
let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
match (self.enabled, new_hint_settings.enabled) {
match (old_enabled, self.enabled) {
(false, false) => {
self.allowed_hint_kinds = new_allowed_hint_kinds;
ControlFlow::Break(None)
@@ -314,7 +327,6 @@ impl InlayHintCache {
}
}
(true, false) => {
self.enabled = new_hint_settings.enabled;
self.allowed_hint_kinds = new_allowed_hint_kinds;
if self.hints.is_empty() {
ControlFlow::Break(None)
@@ -327,7 +339,6 @@ impl InlayHintCache {
}
}
(false, true) => {
self.enabled = new_hint_settings.enabled;
self.allowed_hint_kinds = new_allowed_hint_kinds;
ControlFlow::Continue(())
}

View File

@@ -615,9 +615,20 @@ impl Item for Editor {
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).project_path(cx))
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
.and_then(|path| {
let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&path, cx)?;
let git_status = project
.worktree_for_id(path.worktree_id, cx)?
.read(cx)
.snapshot()
.status_for_file(path.path);
Some(entry_git_aware_label_color(
git_status,
entry.is_ignored,
params.selected,
))
})
.unwrap_or_else(|| entry_label_color(params.selected))
} else {
@@ -1457,10 +1468,11 @@ impl SearchableItem for Editor {
search_within_ranges
};
for (excerpt_id, search_buffer, search_range) in
buffer.excerpts_in_ranges(search_within_ranges)
for (excerpt, search_range) in
buffer.disjoint_ranges_to_buffer_ranges(search_within_ranges)
{
if !search_range.is_empty() {
let search_buffer = excerpt.buffer();
ranges.extend(
query
.search(search_buffer, Some(search_range.clone()))
@@ -1471,8 +1483,8 @@ impl SearchableItem for Editor {
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
buffer.anchor_in_excerpt(excerpt.id(), start).unwrap()
..buffer.anchor_in_excerpt(excerpt.id(), end).unwrap()
}),
);
}
@@ -1559,10 +1571,10 @@ pub fn entry_git_aware_label_color(
Color::Ignored
} else {
match git_status {
Some(GitFileStatus::Added) => Color::Created,
Some(GitFileStatus::Added) | Some(GitFileStatus::Untracked) => Color::Created,
Some(GitFileStatus::Modified) => Color::Modified,
Some(GitFileStatus::Conflict) => Color::Conflict,
None => entry_label_color(selected),
Some(GitFileStatus::Deleted) | None => entry_label_color(selected),
}
}
}

View File

@@ -257,7 +257,8 @@ impl EditorLspTestContext {
Self::new(language, Default::default(), cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
/// Constructs lsp range using a marked string with '[', ']' range delimiters
#[track_caller]
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())

View File

@@ -230,6 +230,7 @@ impl EditorTestContext {
self.cx.background_executor.run_until_parked();
}
#[track_caller]
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);

View File

@@ -720,7 +720,16 @@ impl Fs for RealFs {
}
// Check if path is a symlink and follow the target parent
if let Some(target) = self.read_link(&path).await.ok() {
if let Some(mut target) = self.read_link(&path).await.ok() {
// Check if symlink target is relative path, if so make it absolute
if target.is_relative() {
if let Some(parent) = path.parent() {
target = parent.join(target);
if let Ok(canonical) = self.canonicalize(&target).await {
target = canonical;
}
}
}
watcher.add(&target).ok();
if let Some(parent) = target.parent() {
watcher.add(parent).log_err();

View File

@@ -14,7 +14,6 @@ pub struct Matcher<'a> {
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
min_score: f64,
match_positions: Vec<usize>,
last_positions: Vec<usize>,
@@ -22,11 +21,6 @@ pub struct Matcher<'a> {
best_position_matrix: Vec<usize>,
}
pub trait Match: Ord {
fn score(&self) -> f64;
fn set_positions(&mut self, positions: Vec<usize>);
}
pub trait MatchCandidate {
fn has_chars(&self, bag: CharBag) -> bool;
fn to_string(&self) -> Cow<'_, str>;
@@ -38,7 +32,6 @@ impl<'a> Matcher<'a> {
lowercase_query: &'a [char],
query_char_bag: CharBag,
smart_case: bool,
max_results: usize,
) -> Self {
Self {
query,
@@ -50,10 +43,11 @@ impl<'a> Matcher<'a> {
score_matrix: Vec::new(),
best_position_matrix: Vec::new(),
smart_case,
max_results,
}
}
/// Filter and score fuzzy match candidates. Results are returned unsorted, in the same order as
/// the input candidates.
pub fn match_candidates<C: MatchCandidate, R, F>(
&mut self,
prefix: &[char],
@@ -63,8 +57,7 @@ impl<'a> Matcher<'a> {
cancel_flag: &AtomicBool,
build_match: F,
) where
R: Match,
F: Fn(&C, f64) -> R,
F: Fn(&C, f64, &Vec<usize>) -> R,
{
let mut candidate_chars = Vec::new();
let mut lowercase_candidate_chars = Vec::new();
@@ -103,20 +96,7 @@ impl<'a> Matcher<'a> {
);
if score > 0.0 {
let mut mat = build_match(&candidate, score);
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
if results.len() < self.max_results {
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
} else if i < results.len() {
results.pop();
mat.set_positions(self.match_positions.clone());
results.insert(i, mat);
}
if results.len() == self.max_results {
self.min_score = results.last().unwrap().score();
}
}
results.push(build_match(&candidate, score, &self.match_positions));
}
}
}
@@ -325,18 +305,18 @@ mod tests {
#[test]
fn test_get_last_positions() {
let mut query: &[char] = &['d', 'c'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let mut matcher = Matcher::new(query, query, query.into(), false);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(!result);
query = &['c', 'd'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let mut matcher = Matcher::new(query, query, query.into(), false);
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![2, 4]);
query = &['z', '/', 'z', 'f'];
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
let mut matcher = Matcher::new(query, query, query.into(), false);
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
assert!(result);
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
@@ -451,7 +431,7 @@ mod tests {
});
}
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case);
let cancel_flag = AtomicBool::new(false);
let mut results = Vec::new();
@@ -462,16 +442,17 @@ mod tests {
path_entries.into_iter(),
&mut results,
&cancel_flag,
|candidate, score| PathMatch {
|candidate, score, positions| PathMatch {
score,
worktree_id: 0,
positions: Vec::new(),
positions: positions.clone(),
path: Arc::from(candidate.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
is_dir: false,
},
);
results.sort_by(|a, b| b.cmp(a));
results
.into_iter()

View File

@@ -3,11 +3,14 @@ use std::{
borrow::Cow,
cmp::{self, Ordering},
path::Path,
sync::{atomic::AtomicBool, Arc},
sync::{
atomic::{self, AtomicBool},
Arc,
},
};
use crate::{
matcher::{Match, MatchCandidate, Matcher},
matcher::{MatchCandidate, Matcher},
CharBag,
};
@@ -42,16 +45,6 @@ pub trait PathMatchCandidateSet<'a>: Send + Sync {
fn candidates(&'a self, start: usize) -> Self::Candidates;
}
impl Match for PathMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
fn has_chars(&self, bag: CharBag) -> bool {
self.char_bag.is_superset(bag)
@@ -102,13 +95,7 @@ pub fn match_fixed_path_set(
let query = query.chars().collect::<Vec<_>>();
let query_char_bag = CharBag::from(&lowercase_query[..]);
let mut matcher = Matcher::new(
&query,
&lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut matcher = Matcher::new(&query, &lowercase_query, query_char_bag, smart_case);
let mut results = Vec::new();
matcher.match_candidates(
@@ -117,16 +104,17 @@ pub fn match_fixed_path_set(
candidates.into_iter(),
&mut results,
&AtomicBool::new(false),
|candidate, score| PathMatch {
|candidate, score, positions| PathMatch {
score,
worktree_id,
positions: Vec::new(),
positions: positions.clone(),
is_dir: candidate.is_dir,
path: Arc::from(candidate.path),
path_prefix: Arc::default(),
distance_to_relative_ancestor: usize::MAX,
},
);
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
results
}
@@ -164,16 +152,15 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
scope.spawn(async move {
let segment_start = segment_idx * segment_size;
let segment_end = segment_start + segment_size;
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut matcher =
Matcher::new(query, lowercase_query, query_char_bag, smart_case);
let mut tree_start = 0;
for candidate_set in candidate_sets {
if cancel_flag.load(atomic::Ordering::Relaxed) {
break;
}
let tree_end = tree_start + candidate_set.len();
if tree_start < segment_end && segment_start < tree_end {
@@ -193,10 +180,10 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
candidates,
results,
cancel_flag,
|candidate, score| PathMatch {
|candidate, score, positions| PathMatch {
score,
worktree_id,
positions: Vec::new(),
positions: positions.clone(),
path: Arc::from(candidate.path),
is_dir: candidate.is_dir,
path_prefix: candidate_set.prefix(),
@@ -222,14 +209,12 @@ pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
return Vec::new();
}
let mut results = segment_results.concat();
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
results
}

View File

@@ -1,5 +1,5 @@
use crate::{
matcher::{Match, MatchCandidate, Matcher},
matcher::{MatchCandidate, Matcher},
CharBag,
};
use gpui::BackgroundExecutor;
@@ -8,7 +8,7 @@ use std::{
cmp::{self, Ordering},
iter,
ops::Range,
sync::atomic::AtomicBool,
sync::atomic::{self, AtomicBool},
};
#[derive(Clone, Debug)]
@@ -46,16 +46,6 @@ pub struct StringMatch {
pub string: String,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatch {
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable();
@@ -167,13 +157,8 @@ pub async fn match_strings(
scope.spawn(async move {
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new(
query,
lowercase_query,
query_char_bag,
smart_case,
max_results,
);
let mut matcher =
Matcher::new(query, lowercase_query, query_char_bag, smart_case);
matcher.match_candidates(
&[],
@@ -181,10 +166,10 @@ pub async fn match_strings(
candidates[segment_start..segment_end].iter(),
results,
cancel_flag,
|candidate, score| StringMatch {
|candidate, score, positions| StringMatch {
candidate_id: candidate.id,
score,
positions: Vec::new(),
positions: positions.clone(),
string: candidate.string.to_string(),
},
);
@@ -193,13 +178,11 @@ pub async fn match_strings(
})
.await;
let mut results = Vec::new();
for segment_result in segment_results {
if results.is_empty() {
results = segment_result;
} else {
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
}
if cancel_flag.load(atomic::Ordering::Relaxed) {
return Vec::new();
}
let mut results = segment_results.concat();
util::truncate_to_bottom_n_sorted_by(&mut results, max_results, &|a, b| b.cmp(a));
results
}

View File

@@ -16,6 +16,7 @@ use std::sync::LazyLock;
pub use crate::hosting_provider::*;
pub use crate::remote::*;
pub use git2 as libgit;
pub use repository::WORK_DIRECTORY_REPO_PATH;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));

View File

@@ -7,6 +7,8 @@ use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::sync::LazyLock;
use std::{
cmp::Ordering,
path::{Component, Path, PathBuf},
@@ -37,7 +39,8 @@ pub trait GitRepository: Send + Sync {
/// Returns the SHA of the current HEAD.
fn head_sha(&self) -> Option<String>;
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus>;
/// Returns the list of git statuses, sorted by path
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
@@ -132,7 +135,7 @@ impl GitRepository for RealGitRepository {
Some(self.repository.lock().head().ok()?.target()?.to_string())
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
let working_directory = self
.repository
.lock()
@@ -289,8 +292,9 @@ impl GitRepository for FakeGitRepository {
state.dot_git_dir.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
let state = self.state.lock();
let mut entries = state
.worktree_statuses
.iter()
@@ -306,6 +310,7 @@ impl GitRepository for FakeGitRepository {
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Ok(GitStatus {
entries: entries.into(),
})
@@ -394,6 +399,8 @@ pub enum GitFileStatus {
Added,
Modified,
Conflict,
Deleted,
Untracked,
}
impl GitFileStatus {
@@ -421,20 +428,34 @@ impl GitFileStatus {
}
}
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
LazyLock::new(|| RepoPath(Path::new("").into()));
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(pub PathBuf);
pub struct RepoPath(pub Arc<Path>);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path)
RepoPath(path.into())
}
pub fn from_str(path: &str) -> Self {
let path = Path::new(path);
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path.into())
}
pub fn to_proto(&self) -> String {
self.0.to_string_lossy().to_string()
}
}
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.to_path_buf())
RepoPath::new(value.into())
}
}
@@ -444,9 +465,15 @@ impl From<PathBuf> for RepoPath {
}
}
impl From<&str> for RepoPath {
fn from(value: &str) -> Self {
Self::from_str(value)
}
}
impl Default for RepoPath {
fn default() -> Self {
RepoPath(PathBuf::new())
RepoPath(Path::new("").into())
}
}
@@ -457,13 +484,19 @@ impl AsRef<Path> for RepoPath {
}
impl std::ops::Deref for RepoPath {
type Target = PathBuf;
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Borrow<Path> for RepoPath {
fn borrow(&self) -> &Path {
self.0.as_ref()
}
}
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a Path);

View File

@@ -1,10 +1,6 @@
use crate::repository::{GitFileStatus, RepoPath};
use anyhow::{anyhow, Result};
use std::{
path::{Path, PathBuf},
process::Stdio,
sync::Arc,
};
use std::{path::Path, process::Stdio, sync::Arc};
#[derive(Clone)]
pub struct GitStatus {
@@ -15,7 +11,7 @@ impl GitStatus {
pub(crate) fn new(
git_binary: &Path,
working_directory: &Path,
path_prefixes: &[PathBuf],
path_prefixes: &[RepoPath],
) -> Result<Self> {
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
@@ -27,7 +23,7 @@ impl GitStatus {
"-z",
])
.args(path_prefixes.iter().map(|path_prefix| {
if *path_prefix == Path::new("") {
if path_prefix.0.as_ref() == Path::new("") {
Path::new(".")
} else {
path_prefix
@@ -55,10 +51,12 @@ impl GitStatus {
let (status, path) = entry.split_at(3);
let status = status.trim();
Some((
RepoPath(PathBuf::from(path)),
RepoPath(Path::new(path).into()),
match status {
"A" | "??" => GitFileStatus::Added,
"A" => GitFileStatus::Added,
"M" => GitFileStatus::Modified,
"D" => GitFileStatus::Deleted,
"??" => GitFileStatus::Untracked,
_ => return None,
},
))
@@ -75,7 +73,7 @@ impl GitStatus {
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
self.entries
.binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
.binary_search_by(|(repo_path, _)| repo_path.0.as_ref().cmp(path))
.ok()
.map(|index| self.entries[index].1)
}

View File

@@ -14,9 +14,11 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
@@ -29,8 +31,7 @@ settings.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
git.workspace = true
collections.workspace = true
worktree.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -1,11 +1,16 @@
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
use anyhow::{Context as _, Result};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::{
scroll::{Autoscroll, AutoscrollStrategy},
Editor, MultiBuffer, DEFAULT_MULTIBUFFER_CONTEXT,
};
use git::{diff::DiffHunk, repository::GitFileStatus};
use git::{
diff::DiffHunk,
repository::{GitFileStatus, RepoPath},
};
use gpui::*;
use gpui::{
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
@@ -14,7 +19,7 @@ use gpui::{
};
use language::{Buffer, BufferRow, OffsetRangeExt};
use menu::{SelectNext, SelectPrev};
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use project::{EntryKind, Fs, Project, ProjectEntryId, ProjectPath, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{
@@ -22,7 +27,7 @@ use std::{
collections::HashSet,
ffi::OsStr,
ops::{Deref, Range},
path::{Path, PathBuf},
path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@@ -37,9 +42,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent},
ItemHandle, Workspace,
};
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
use worktree::StatusEntry;
actions!(git_panel, [ToggleFocus]);
@@ -69,7 +72,7 @@ pub struct GitStatusEntry {}
struct EntryDetails {
filename: String,
display_name: String,
path: Arc<Path>,
path: RepoPath,
kind: EntryKind,
depth: usize,
is_expanded: bool,
@@ -101,7 +104,8 @@ pub struct GitPanel {
scrollbar_state: ScrollbarState,
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
// TODO Reintroduce expanded directories, once we're deriving directories from paths
// expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
@@ -115,18 +119,20 @@ pub struct GitPanel {
#[derive(Debug, Clone)]
struct WorktreeEntries {
worktree_id: WorktreeId,
// TODO support multiple repositories per worktree
work_directory: worktree::WorkDirectory,
visible_entries: Vec<GitPanelEntry>,
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
paths: Rc<OnceCell<HashSet<RepoPath>>>,
}
#[derive(Debug, Clone)]
struct GitPanelEntry {
entry: Entry,
entry: worktree::StatusEntry,
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
}
impl Deref for GitPanelEntry {
type Target = Entry;
type Target = worktree::StatusEntry;
fn deref(&self) -> &Self::Target {
&self.entry
@@ -134,11 +140,11 @@ impl Deref for GitPanelEntry {
}
impl WorktreeEntries {
fn paths(&self) -> &HashSet<Arc<Path>> {
fn paths(&self) -> &HashSet<RepoPath> {
self.paths.get_or_init(|| {
self.visible_entries
.iter()
.map(|e| (e.entry.path.clone()))
.map(|e| (e.entry.repo_path.clone()))
.collect()
})
}
@@ -165,8 +171,11 @@ impl GitPanel {
})
.detach();
cx.subscribe(&project, |this, _, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
project::Event::GitRepositoryUpdated => {
this.update_visible_entries(None, None, cx);
}
project::Event::WorktreeRemoved(_id) => {
// this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, None, cx);
cx.notify();
}
@@ -183,7 +192,7 @@ impl GitPanel {
project::Event::Closed => {
this.git_diff_editor_updates = Task::ready(());
this.reveal_in_editor = Task::ready(());
this.expanded_dir_ids.clear();
// this.expanded_dir_ids.clear();
this.visible_entries.clear();
this.git_diff_editor = None;
}
@@ -200,8 +209,7 @@ impl GitPanel {
pending_serialization: Task::ready(None),
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
// expanded_dir_ids: Default::default(),
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
scroll_handle,
@@ -288,16 +296,16 @@ impl GitPanel {
}
fn calculate_depth_and_difference(
entry: &Entry,
visible_worktree_entries: &HashSet<Arc<Path>>,
entry: &StatusEntry,
visible_worktree_entries: &HashSet<RepoPath>,
) -> (usize, usize) {
let (depth, difference) = entry
.path
.repo_path
.ancestors()
.skip(1) // Skip the entry itself
.find_map(|ancestor| {
if let Some(parent_entry) = visible_worktree_entries.get(ancestor) {
let entry_path_components_count = entry.path.components().count();
let entry_path_components_count = entry.repo_path.components().count();
let parent_path_components_count = parent_entry.components().count();
let difference = entry_path_components_count - parent_path_components_count;
let depth = parent_entry
@@ -432,13 +440,7 @@ impl GitPanel {
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
.map(|worktree_entries| {
worktree_entries
.visible_entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
})
.map(|worktree_entries| worktree_entries.visible_entries.len())
.sum()
}
@@ -446,7 +448,7 @@ impl GitPanel {
&self,
range: Range<usize>,
cx: &mut ViewContext<Self>,
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
mut callback: impl FnMut(usize, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
for worktree_entries in &self.visible_entries {
@@ -468,11 +470,11 @@ impl GitPanel {
{
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
.expanded_dir_ids
.get(&snapshot.id())
.map(Vec::as_slice)
.unwrap_or(&[]);
// let expanded_entry_ids = self
// .expanded_dir_ids
// .get(&snapshot.id())
// .map(Vec::as_slice)
// .unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = worktree_entries.paths();
@@ -483,22 +485,22 @@ impl GitPanel {
.enumerate()
{
let index = index_start + i;
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let status = entry.status;
let is_expanded = true; //expanded_entry_ids.binary_search(&entry.id).is_ok();
let (depth, difference) = Self::calculate_depth_and_difference(entry, entries);
let filename = match difference {
diff if diff > 1 => entry
.path
.repo_path
.iter()
.skip(entry.path.components().count() - diff)
.skip(entry.repo_path.components().count() - diff)
.collect::<PathBuf>()
.to_str()
.unwrap_or_default()
.to_string(),
_ => entry
.path
.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
@@ -506,16 +508,17 @@ impl GitPanel {
let details = EntryDetails {
filename,
display_name: entry.path.to_string_lossy().into_owned(),
kind: entry.kind,
display_name: entry.repo_path.to_string_lossy().into_owned(),
// TODO get it from StatusEntry?
kind: EntryKind::File,
is_expanded,
path: entry.path.clone(),
status,
path: entry.repo_path.clone(),
status: Some(status),
hunks: entry.hunks.clone(),
depth,
index,
};
callback(entry.id, details, cx);
callback(ix, details, cx);
}
}
ix = end_ix;
@@ -527,7 +530,7 @@ impl GitPanel {
fn update_visible_entries(
&mut self,
for_worktree: Option<WorktreeId>,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
_new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
@@ -549,24 +552,36 @@ impl GitPanel {
None => false,
});
for worktree in project.visible_worktrees(cx) {
let worktree_id = worktree.read(cx).id();
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
if for_worktree.is_some() && for_worktree != Some(worktree_id) {
continue;
}
let snapshot = worktree.read(cx).snapshot();
let mut visible_worktree_entries = snapshot
.entries(false, 0)
.filter(|entry| !entry.is_external)
.filter(|entry| entry.git_status.is_some())
.cloned()
.collect::<Vec<_>>();
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
let mut visible_worktree_entries = Vec::new();
// Only use the first repository for now
let repositories = snapshot.repositories().take(1);
let mut work_directory = None;
for repository in repositories {
visible_worktree_entries.extend(repository.status());
work_directory = Some(worktree::WorkDirectory::clone(repository));
}
// TODO use the GitTraversal
// let mut visible_worktree_entries = snapshot
// .entries(false, 0)
// .filter(|entry| !entry.is_external)
// .filter(|entry| entry.git_status.is_some())
// .cloned()
// .collect::<Vec<_>>();
// snapshot.propagate_git_statuses(&mut visible_worktree_entries);
// project::sort_worktree_entries(&mut visible_worktree_entries);
if !visible_worktree_entries.is_empty() {
self.visible_entries.push(WorktreeEntries {
worktree_id,
work_directory: work_directory.unwrap(),
visible_entries: visible_worktree_entries
.into_iter()
.map(|entry| GitPanelEntry {
@@ -580,24 +595,25 @@ impl GitPanel {
}
self.visible_entries.extend(after_update);
if let Some((worktree_id, entry_id)) = new_selected_entry {
self.selected_item = self.visible_entries.iter().enumerate().find_map(
|(worktree_index, worktree_entries)| {
if worktree_entries.worktree_id == worktree_id {
worktree_entries
.visible_entries
.iter()
.position(|entry| entry.id == entry_id)
.map(|entry_index| {
worktree_index * worktree_entries.visible_entries.len()
+ entry_index
})
} else {
None
}
},
);
}
// TODO re-implement this
// if let Some((worktree_id, entry_id)) = new_selected_entry {
// self.selected_item = self.visible_entries.iter().enumerate().find_map(
// |(worktree_index, worktree_entries)| {
// if worktree_entries.worktree_id == worktree_id {
// worktree_entries
// .visible_entries
// .iter()
// .position(|entry| entry.id == entry_id)
// .map(|entry_index| {
// worktree_index * worktree_entries.visible_entries.len()
// + entry_index
// })
// } else {
// None
// }
// },
// );
// }
let project = self.project.downgrade();
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
@@ -612,12 +628,14 @@ impl GitPanel {
.visible_entries
.iter()
.filter_map(|entry| {
let git_status = entry.git_status()?;
let git_status = entry.status;
let entry_hunks = entry.hunks.clone();
let (entry_path, unstaged_changes_task) =
project.update(cx, |project, cx| {
let entry_path =
project.path_for_entry(entry.id, cx)?;
let entry_path = ProjectPath {
worktree_id: worktree_entries.worktree_id,
path: worktree_entries.work_directory.unrelativize(&entry.repo_path)?,
};
let open_task =
project.open_path(entry_path.clone(), cx);
let unstaged_changes_task =
@@ -682,8 +700,8 @@ impl GitPanel {
)
.collect()
}
// TODO support conflicts display
GitFileStatus::Conflict => Vec::new(),
// TODO support these
GitFileStatus::Conflict | GitFileStatus::Deleted | GitFileStatus::Untracked => Vec::new(),
}
}).clone()
})?;
@@ -992,18 +1010,17 @@ impl GitPanel {
fn render_entry(
&self,
id: ProjectEntryId,
ix: usize,
selected: bool,
details: EntryDetails,
cx: &ViewContext<Self>,
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let checkbox_id = ElementId::Name(format!("checkbox_{}", ix).into());
let is_staged = ToggleState::Selected;
let handle = cx.view().downgrade();
h_flex()
.id(id)
.id(("git-panel-entry", ix))
.h(px(28.))
.w_full()
.pl(px(12. + 12. * details.depth as f32))
@@ -1019,7 +1036,7 @@ impl GitPanel {
this.child(git_status_icon(status))
})
.child(
ListItem::new(("label", id))
ListItem::new(details.path.0.clone())
.toggle_state(selected)
.child(h_flex().gap_1p5().child(details.display_name.clone()))
.on_click(move |e, cx| {

View File

@@ -44,10 +44,13 @@ const REMOVED_COLOR: Hsla = Hsla {
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
match status {
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),
GitFileStatus::Added | GitFileStatus::Untracked => {
Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR))
}
GitFileStatus::Modified => {
Icon::new(IconName::SquareDot).color(Color::Custom(MODIFIED_COLOR))
}
GitFileStatus::Conflict => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
GitFileStatus::Deleted => Icon::new(IconName::Warning).color(Color::Custom(REMOVED_COLOR)),
}
}

View File

@@ -185,8 +185,7 @@ macro_rules! actions {
#[doc = "The `"]
#[doc = stringify!($name)]
#[doc = "` action, see [`gpui::actions!`]"]
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
#[serde(crate = "gpui::private::serde")]
#[derive(::std::clone::Clone,::std::cmp::PartialEq, ::std::default::Default)]
pub struct $name;
gpui::__impl_action!($namespace, $name, $name,
@@ -211,14 +210,7 @@ macro_rules! action_as {
#[doc = "The `"]
#[doc = stringify!($name)]
#[doc = "` action, see [`gpui::actions!`]"]
#[derive(
::std::cmp::PartialEq,
::std::clone::Clone,
::std::default::Default,
::std::fmt::Debug,
gpui::private::serde_derive::Deserialize,
)]
#[serde(crate = "gpui::private::serde")]
#[derive(::std::clone::Clone, ::std::cmp::PartialEq, ::std::default::Default)]
pub struct $name;
gpui::__impl_action!(

View File

@@ -117,8 +117,9 @@ impl EntityMap {
pub fn read<T: 'static>(&self, model: &Model<T>) -> &T {
self.assert_valid_context(model);
self.entities[model.entity_id]
.downcast_ref()
self.entities
.get(model.entity_id)
.and_then(|entity| entity.downcast_ref())
.unwrap_or_else(|| double_lease_panic::<T>("read"))
}

View File

@@ -1580,7 +1580,7 @@ impl LinuxClient for X11Client {
}
}
// Adatpted from:
// Adapted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {

View File

@@ -55,6 +55,7 @@ x11rb::atom_manager! {
WM_PROTOCOLS,
WM_DELETE_WINDOW,
WM_CHANGE_STATE,
_NET_WM_PID,
_NET_WM_NAME,
_NET_WM_STATE,
_NET_WM_STATE_MAXIMIZED_VERT,
@@ -436,6 +437,18 @@ impl X11WindowState {
// Collect errors during setup, so that window can be destroyed on failure.
let setup_result = maybe!({
let pid = std::process::id();
check_reply(
|| "X11 ChangeProperty for _NET_WM_PID failed.",
xcb.change_property32(
xproto::PropMode::REPLACE,
x_window,
atoms._NET_WM_PID,
xproto::AtomEnum::CARDINAL,
&[pid],
),
)?;
if let Some(size) = params.window_min_size {
let mut size_hints = WmSizeHints::new();
let min_size = (size.width.0 as i32, size.height.0 as i32);

View File

@@ -322,7 +322,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
let always_use_cmd_layout = always_use_command_layout();
// Handle Dvorak+QWERTY / Russian / Armeniam
// Handle Dvorak+QWERTY / Russian / Armenian
if command || always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
let chars_with_both =

View File

@@ -47,6 +47,7 @@ pub(crate) fn handle_msg(
WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr),
WM_MOUSELEAVE => handle_mouse_leave_msg(state_ptr),
WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr),
WM_NCMOUSELEAVE => handle_nc_mouse_leave_msg(state_ptr),
WM_NCLBUTTONDOWN => {
handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr)
}
@@ -314,6 +315,18 @@ fn handle_mouse_move_msg(
Some(1)
}
fn handle_nc_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
lock.hovered = false;
if let Some(mut callback) = lock.callbacks.hovered_status_change.take() {
drop(lock);
callback(false);
state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback);
}
Some(0)
}
fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
lock.hovered = false;
@@ -971,6 +984,27 @@ fn handle_nc_mouse_move_msg(
return None;
}
let mut lock = state_ptr.state.borrow_mut();
if !lock.hovered {
lock.hovered = true;
unsafe {
TrackMouseEvent(&mut TRACKMOUSEEVENT {
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
dwFlags: TME_LEAVE | TME_NONCLIENT,
hwndTrack: handle,
dwHoverTime: HOVER_DEFAULT,
})
.log_err()
};
if let Some(mut callback) = lock.callbacks.hovered_status_change.take() {
drop(lock);
callback(true);
state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback);
}
} else {
drop(lock);
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;

View File

@@ -81,6 +81,12 @@ impl<'a> PartialEq<&'a str> for SharedString {
}
}
impl From<&SharedString> for SharedString {
fn from(value: &SharedString) -> Self {
value.clone()
}
}
impl From<SharedString> for Arc<str> {
fn from(val: SharedString) -> Self {
match val.0 {

View File

@@ -356,7 +356,7 @@ impl WindowTextSystem {
});
}
let layout = self.layout_line(text.as_ref(), font_size, runs)?;
let layout = self.layout_line(&text, font_size, runs)?;
Ok(ShapedLine {
layout,
@@ -483,12 +483,16 @@ impl WindowTextSystem {
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
pub fn layout_line<Text>(
&self,
text: &str,
text: Text,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
) -> Result<Arc<LineLayout>>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);

View File

@@ -1,4 +1,4 @@
use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString, Size};
use collections::FxHashMap;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
@@ -420,15 +420,19 @@ impl LineLayoutCache {
curr_frame.used_wrapped_lines.clear();
}
pub fn layout_wrapped_line(
pub fn layout_wrapped_line<Text>(
&self,
text: &str,
text: Text,
font_size: Pixels,
runs: &[FontRun],
wrap_width: Option<Pixels>,
) -> Arc<WrappedLineLayout> {
) -> Arc<WrappedLineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
let key = &CacheKeyRef {
text,
text: text.as_ref(),
font_size,
runs,
wrap_width,
@@ -449,8 +453,8 @@ impl LineLayoutCache {
layout
} else {
drop(current_frame);
let unwrapped_layout = self.layout_line(text, font_size, runs);
let text = SharedString::from(text);
let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs);
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
} else {
@@ -462,7 +466,7 @@ impl LineLayoutCache {
wrap_width,
});
let key = Arc::new(CacheKey {
text: text.into(),
text,
font_size,
runs: SmallVec::from(runs),
wrap_width,
@@ -478,9 +482,18 @@ impl LineLayoutCache {
}
}
pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
pub fn layout_line<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[FontRun],
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
let key = &CacheKeyRef {
text,
text: text.as_ref(),
font_size,
runs,
wrap_width: None,
@@ -497,9 +510,13 @@ impl LineLayoutCache {
current_frame.used_lines.push(key);
layout
} else {
let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
let text = SharedString::from(text);
let layout = Arc::new(
self.platform_text_system
.layout_line(&text, font_size, runs),
);
let key = Arc::new(CacheKey {
text: text.into(),
text,
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
@@ -524,7 +541,7 @@ trait AsCacheKeyRef {
#[derive(Clone, Debug, Eq)]
struct CacheKey {
text: String,
text: SharedString,
font_size: Pixels,
runs: SmallVec<[FontRun; 1]>,
wrap_width: Option<Pixels>,

View File

@@ -4880,6 +4880,8 @@ pub enum ElementId {
FocusHandle(FocusId),
/// A combination of a name and an integer.
NamedInteger(SharedString, usize),
/// A path
Path(Arc<std::path::Path>),
}
impl Display for ElementId {
@@ -4891,6 +4893,7 @@ impl Display for ElementId {
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
ElementId::Path(path) => write!(f, "{}", path.display())?,
}
Ok(())
@@ -4927,6 +4930,12 @@ impl From<SharedString> for ElementId {
}
}
impl From<Arc<std::path::Path>> for ElementId {
fn from(path: Arc<std::path::Path>) -> Self {
ElementId::Path(path)
}
}
impl From<&'static str> for ElementId {
fn from(name: &'static str) -> Self {
ElementId::Name(name.into())

View File

@@ -96,12 +96,18 @@ impl Item for ImageView {
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let project_path = self.image_item.read(cx).project_path(cx);
let label_color = if ItemSettings::get_global(cx).git_status {
let git_status = self
.project
.read(cx)
.project_path_git_status(&project_path, cx);
self.project
.read(cx)
.entry_for_path(&project_path, cx)
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
entry_git_aware_label_color(git_status, entry.is_ignored, params.selected)
})
.unwrap_or_else(|| params.text_color())
} else {

View File

@@ -208,10 +208,10 @@ pub struct Diagnostic {
/// The human-readable message associated with this diagnostic.
pub message: String,
/// An id that identifies the group to which this diagnostic belongs.
/// 0 is used for diagnostics that do not come from a language server.
///
/// When a language server produces a diagnostic with
/// one or more associated diagnostics, those diagnostics are all
/// assigned a single group ID.
/// When a language server produces a diagnostic with one or more associated diagnostics, those
/// diagnostics are all assigned a single group ID.
pub group_id: usize,
/// Whether this diagnostic is the primary diagnostic for its group.
///
@@ -3943,14 +3943,14 @@ impl BufferSnapshot {
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
where
T: 'a + Clone + ToOffset,
O: 'a + FromAnchor + Ord,
O: 'a + FromAnchor,
{
let mut iterators: Vec<_> = self
.diagnostics
.iter()
.map(|(_, collection)| {
collection
.range::<T, O>(search_range.clone(), self, true, reversed)
.range::<T, text::Anchor>(search_range.clone(), self, true, reversed)
.peekable()
})
.collect();
@@ -3964,7 +3964,7 @@ impl BufferSnapshot {
let cmp = a
.range
.start
.cmp(&b.range.start)
.cmp(&b.range.start, self)
// when range is equal, sort by diagnostic severity
.then(a.diagnostic.severity.cmp(&b.diagnostic.severity))
// and stabilize order with group_id
@@ -3975,7 +3975,13 @@ impl BufferSnapshot {
cmp
}
})?;
iterators[next_ix].next()
iterators[next_ix]
.next()
.map(|DiagnosticEntry { range, diagnostic }| DiagnosticEntry {
diagnostic,
range: FromAnchor::from_anchor(&range.start, self)
..FromAnchor::from_anchor(&range.end, self),
})
})
}
@@ -4013,12 +4019,12 @@ impl BufferSnapshot {
}
/// Returns an iterator over the diagnostics for the given group.
pub fn diagnostic_group<'a, O>(
&'a self,
pub fn diagnostic_group<O>(
&self,
group_id: usize,
) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
) -> impl Iterator<Item = DiagnosticEntry<O>> + '_
where
O: 'a + FromAnchor,
O: FromAnchor + 'static,
{
self.diagnostics
.iter()

View File

@@ -52,7 +52,7 @@ pub struct Summary {
}
impl DiagnosticEntry<PointUtf16> {
/// Returns a raw LSP diagnostic ssed to provide diagnostic context to LSP
/// Returns a raw LSP diagnostic used to provide diagnostic context to LSP
/// codeAction request
pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
let code = self

View File

@@ -1850,16 +1850,20 @@ pub fn point_from_lsp(point: lsp::Position) -> Unclipped<PointUtf16> {
}
pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
lsp::Range {
start: point_to_lsp(range.start),
end: point_to_lsp(range.end),
let mut start = point_to_lsp(range.start);
let mut end = point_to_lsp(range.end);
if start > end {
log::error!("range_to_lsp called with inverted range {start:?}-{end:?}");
mem::swap(&mut start, &mut end);
}
lsp::Range { start, end }
}
pub fn range_from_lsp(range: lsp::Range) -> Range<Unclipped<PointUtf16>> {
let mut start = point_from_lsp(range.start);
let mut end = point_from_lsp(range.end);
if start > end {
log::warn!("range_from_lsp called with inverted range {start:?}-{end:?}");
mem::swap(&mut start, &mut end);
}
start..end

View File

@@ -68,6 +68,7 @@ impl CloudModel {
anthropic::Model::Claude3Opus
| anthropic::Model::Claude3Sonnet
| anthropic::Model::Claude3Haiku
| anthropic::Model::Claude3_5Haiku
| anthropic::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}

View File

@@ -52,6 +52,8 @@ pub struct AvailableModel {
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
pub max_output_tokens: Option<u32>,
pub default_temperature: Option<f32>,
#[serde(default)]
pub extra_beta_headers: Vec<String>,
}
pub struct AnthropicLanguageModelProvider {
@@ -202,6 +204,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
}),
max_output_tokens: model.max_output_tokens,
default_temperature: model.default_temperature,
extra_beta_headers: model.extra_beta_headers.clone(),
},
);
}

View File

@@ -94,6 +94,9 @@ pub struct AvailableModel {
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
/// The default temperature to use for this model.
pub default_temperature: Option<f32>,
/// Any extra beta headers to provide when using the model.
#[serde(default)]
pub extra_beta_headers: Vec<String>,
}
struct GlobalRefreshLlmTokenListener(Model<RefreshLlmTokenListener>);
@@ -323,6 +326,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}),
default_temperature: model.default_temperature,
max_output_tokens: model.max_output_tokens,
extra_beta_headers: model.extra_beta_headers.clone(),
}),
AvailableProvider::OpenAi => CloudModel::OpenAi(open_ai::Model::Custom {
name: model.name.clone(),

View File

@@ -97,6 +97,7 @@ impl AnthropicSettingsContent {
cache_configuration,
max_output_tokens,
default_temperature,
extra_beta_headers,
} => Some(provider::anthropic::AvailableModel {
name,
display_name,
@@ -111,6 +112,7 @@ impl AnthropicSettingsContent {
),
max_output_tokens,
default_temperature,
extra_beta_headers,
}),
_ => None,
})

View File

@@ -128,13 +128,20 @@ impl SyntaxTreeView {
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
let editor_state = self.editor.as_mut()?;
let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
let snapshot = editor_state
.editor
.update(cx, |editor, cx| editor.snapshot(cx));
let (excerpt, buffer, range) = editor_state.editor.update(cx, |editor, cx| {
let selection = editor.selections.last::<usize>(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
editor
.buffer()
.read(cx)
.range_to_buffer_ranges(selection_range, cx)
.pop()
let selection_head = selection.head();
let multi_buffer = editor.buffer().read(cx);
let excerpt = snapshot
.buffer_snapshot
.excerpt_containing(selection_head..selection_head)?;
let range = excerpt.map_range_to_buffer(selection_range);
let buffer = multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone();
Some((excerpt, buffer, range))
})?;
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
@@ -143,16 +150,16 @@ impl SyntaxTreeView {
.active_buffer
.get_or_insert_with(|| BufferState {
buffer: buffer.clone(),
excerpt_id,
excerpt_id: excerpt.id(),
active_layer: None,
});
let mut prev_layer = None;
if did_reparse {
prev_layer = buffer_state.active_layer.take();
}
if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt.id() {
buffer_state.buffer = buffer.clone();
buffer_state.excerpt_id = excerpt_id;
buffer_state.excerpt_id = excerpt.id();
buffer_state.active_layer = None;
}

View File

@@ -770,6 +770,12 @@ impl PyLspAdapter {
}
}
const BINARY_DIR: &str = if cfg!(target_os = "windows") {
"Scripts"
} else {
"bin"
};
#[async_trait(?Send)]
impl LspAdapter for PyLspAdapter {
fn name(&self) -> LanguageServerName {
@@ -811,7 +817,7 @@ impl LspAdapter for PyLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
let pip_path = venv.join("bin").join("pip3");
let pip_path = venv.join(BINARY_DIR).join("pip3");
ensure!(
util::command::new_smol_command(pip_path.as_path())
.arg("install")
@@ -842,7 +848,7 @@ impl LspAdapter for PyLspAdapter {
.success(),
"pylsp-mypy installation failed"
);
let pylsp = venv.join("bin").join("pylsp");
let pylsp = venv.join(BINARY_DIR).join("pylsp");
Ok(LanguageServerBinary {
path: pylsp,
env: None,
@@ -856,7 +862,7 @@ impl LspAdapter for PyLspAdapter {
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let venv = self.base_venv(delegate).await.ok()?;
let pylsp = venv.join("bin").join("pylsp");
let pylsp = venv.join(BINARY_DIR).join("pylsp");
Some(LanguageServerBinary {
path: pylsp,
env: None,

View File

@@ -6,8 +6,8 @@ use gpui::{
actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
Point, Render, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
TextStyle, TextStyleRefinement, View,
Point, Render, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout,
TextRun, TextStyle, TextStyleRefinement, View,
};
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
@@ -785,8 +785,52 @@ impl IntoElement for MarkdownElement {
}
}
enum AnyDiv {
Div(Div),
Stateful(Stateful<Div>),
}
impl AnyDiv {
fn into_any_element(self) -> AnyElement {
match self {
Self::Div(div) => div.into_any_element(),
Self::Stateful(div) => div.into_any_element(),
}
}
}
impl From<Div> for AnyDiv {
fn from(value: Div) -> Self {
Self::Div(value)
}
}
impl From<Stateful<Div>> for AnyDiv {
fn from(value: Stateful<Div>) -> Self {
Self::Stateful(value)
}
}
impl Styled for AnyDiv {
fn style(&mut self) -> &mut StyleRefinement {
match self {
Self::Div(div) => div.style(),
Self::Stateful(div) => div.style(),
}
}
}
impl ParentElement for AnyDiv {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
match self {
Self::Div(div) => div.extend(elements),
Self::Stateful(div) => div.extend(elements),
}
}
}
struct MarkdownElementBuilder {
div_stack: Vec<Div>,
div_stack: Vec<AnyDiv>,
rendered_lines: Vec<RenderedLine>,
pending_line: PendingLine,
rendered_links: Vec<RenderedLink>,
@@ -812,7 +856,7 @@ struct ListStackEntry {
impl MarkdownElementBuilder {
fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
Self {
div_stack: vec![div().debug_selector(|| "inner".into())],
div_stack: vec![div().debug_selector(|| "inner".into()).into()],
rendered_lines: Vec::new(),
pending_line: PendingLine::default(),
rendered_links: Vec::new(),
@@ -841,11 +885,12 @@ impl MarkdownElementBuilder {
self.text_style_stack.pop();
}
fn push_div(&mut self, mut div: Div, range: &Range<usize>, markdown_end: usize) {
fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
let mut div = div.into();
self.flush_text();
if range.start == 0 {
//first element, remove top margin
// Remove the top margin on the first element.
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(px(0.).into())),
@@ -856,6 +901,7 @@ impl MarkdownElementBuilder {
..Default::default()
});
}
if range.end == markdown_end {
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
@@ -867,12 +913,13 @@ impl MarkdownElementBuilder {
..Default::default()
});
}
self.div_stack.push(div);
}
fn pop_div(&mut self) {
self.flush_text();
let div = self.div_stack.pop().unwrap().into_any();
let div = self.div_stack.pop().unwrap().into_any_element();
self.div_stack.last_mut().unwrap().extend(iter::once(div));
}
@@ -973,7 +1020,7 @@ impl MarkdownElementBuilder {
debug_assert_eq!(self.div_stack.len(), 1);
self.flush_text();
RenderedMarkdown {
element: self.div_stack.pop().unwrap().into_any(),
element: self.div_stack.pop().unwrap().into_any_element(),
text: RenderedText {
lines: self.rendered_lines.into(),
links: self.rendered_links.into(),

View File

@@ -1667,42 +1667,6 @@ impl MultiBuffer {
})
}
pub fn range_to_buffer_ranges<T: ToOffset>(
&self,
range: Range<T>,
cx: &AppContext,
) -> Vec<(Model<Buffer>, Range<usize>, ExcerptId)> {
let snapshot = self.read(cx);
let start = range.start.to_offset(&snapshot);
let end = range.end.to_offset(&snapshot);
let mut result = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>(&());
cursor.seek(&start, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
while let Some(excerpt) = cursor.item() {
if *cursor.start() > end {
break;
}
let mut end_before_newline = cursor.end(&());
if excerpt.has_trailing_newline {
end_before_newline -= 1;
}
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
result.push((buffer, start..end, excerpt.id));
cursor.next(&());
}
result
}
pub fn remove_excerpts(
&mut self,
excerpt_ids: impl IntoIterator<Item = ExcerptId>,
@@ -3907,35 +3871,43 @@ impl MultiBufferSnapshot {
.any(|excerpt| excerpt.buffer.has_diagnostics())
}
pub fn diagnostic_group<'a, O>(
&'a self,
pub fn diagnostic_group(
&self,
group_id: usize,
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
where
O: text::FromAnchor + 'a,
{
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id))
) -> impl Iterator<Item = DiagnosticEntry<Anchor>> + '_ {
self.all_excerpts().flat_map(move |excerpt| {
excerpt.buffer().diagnostic_group(group_id).map(
move |DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: self.anchor_in_excerpt(excerpt.id(), range.start).unwrap()
..self.anchor_in_excerpt(excerpt.id(), range.end).unwrap(),
},
)
})
}
pub fn diagnostics_in_range<'a, T, O>(
pub fn diagnostics_in_range<'a, T>(
&'a self,
range: Range<T>,
reversed: bool,
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
) -> impl Iterator<Item = DiagnosticEntry<Anchor>> + 'a
where
T: 'a + ToOffset,
O: 'a + text::FromAnchor + Ord,
{
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| {
buffer.diagnostics_in_range(
range.start.to_offset(self)..range.end.to_offset(self),
reversed,
)
})
let mut ranges = self.range_to_buffer_ranges(range).collect::<Vec<_>>();
if reversed {
ranges.reverse();
}
ranges.into_iter().flat_map(move |(excerpt, range)| {
let excerpt_id = excerpt.id();
excerpt.buffer().diagnostics_in_range(range, reversed).map(
move |DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
diagnostic,
range: self.anchor_in_excerpt(excerpt_id, range.start).unwrap()
..self.anchor_in_excerpt(excerpt_id, range.end).unwrap(),
},
)
})
}
pub fn syntax_ancestor<T: ToOffset>(
@@ -4185,44 +4157,60 @@ impl MultiBufferSnapshot {
})
}
/// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt
/// Returns excerpts overlapping the given range. If range spans multiple excerpts returns one
/// range for each excerpt.
///
/// The ranges are specified in the coordinate space of the multibuffer, not the individual excerpted buffers.
/// Each returned excerpt's range is in the coordinate space of its source buffer.
pub fn excerpts_in_ranges(
pub fn range_to_buffer_ranges<T: ToOffset>(
&self,
ranges: impl IntoIterator<Item = Range<Anchor>>,
) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, Range<usize>)> {
let mut ranges = ranges.into_iter().map(|range| range.to_offset(self));
range: Range<T>,
) -> impl Iterator<Item = (MultiBufferExcerpt<'_>, Range<usize>)> {
self.disjoint_ranges_to_buffer_ranges(iter::once(range))
}
/// Returns excerpts overlapping the given ranges, which must be disjoint and sorted by start position.
/// If range spans multiple excerpts returns one range for each excerpt
///
/// The ranges are specified in the coordinate space of the multibuffer, not the individual excerpted buffers.
/// Each returned excerpt's range is in the coordinate space of its source buffer.
pub fn disjoint_ranges_to_buffer_ranges<T: ToOffset>(
&self,
ranges: impl IntoIterator<Item = Range<T>>,
) -> impl Iterator<Item = (MultiBufferExcerpt<'_>, Range<usize>)> {
let mut ranges = ranges.into_iter();
let mut cursor = self.excerpts.cursor::<(usize, Point)>(&());
cursor.next(&());
let mut current_range = ranges.next();
iter::from_fn(move || {
let range = current_range.clone()?;
if range.start >= cursor.end(&()).0 {
cursor.seek_forward(&range.start, Bias::Right, &());
if range.start == self.len() {
let range = current_range.as_ref()?;
let start = range.start.to_offset(self);
let end = range.end.to_offset(self);
if start >= cursor.end(&()).0 {
cursor.seek_forward(&start, Bias::Right, &());
if start == self.len() {
cursor.prev(&());
}
}
let excerpt = cursor.item()?;
let range_start_in_excerpt = cmp::max(range.start, cursor.start().0);
let range_start_in_excerpt = cmp::max(start, cursor.start().0);
let range_end_in_excerpt = if excerpt.has_trailing_newline {
cmp::min(range.end, cursor.end(&()).0 - 1)
cmp::min(end, cursor.end(&()).0 - 1)
} else {
cmp::min(range.end, cursor.end(&()).0)
cmp::min(end, cursor.end(&()).0)
};
let buffer_range = MultiBufferExcerpt::new(excerpt, *cursor.start())
let multi_buffer_excerpt = MultiBufferExcerpt::new(excerpt, *cursor.start());
let buffer_range = multi_buffer_excerpt
.map_range_to_buffer(range_start_in_excerpt..range_end_in_excerpt);
if range.end > cursor.end(&()).0 {
if end > cursor.end(&()).0 {
cursor.next(&());
} else {
current_range = ranges.next();
}
Some((excerpt.id, &excerpt.buffer, buffer_range))
Some((multi_buffer_excerpt, buffer_range))
})
}
@@ -4664,6 +4652,10 @@ impl<'a> MultiBufferExcerpt<'a> {
self.excerpt.id
}
pub fn buffer_id(&self) -> BufferId {
self.excerpt.buffer_id
}
pub fn start_anchor(&self) -> Anchor {
Anchor {
buffer_id: Some(self.excerpt.buffer_id),

View File

@@ -1234,14 +1234,15 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) {
start_ix..end_ix
);
let excerpted_buffer_ranges = multibuffer
.read(cx)
.range_to_buffer_ranges(start_ix..end_ix, cx);
let snapshot = multibuffer.read(cx).snapshot(cx);
let excerpted_buffer_ranges = snapshot
.range_to_buffer_ranges(start_ix..end_ix)
.collect::<Vec<_>>();
let excerpted_buffers_text = excerpted_buffer_ranges
.iter()
.map(|(buffer, buffer_range, _)| {
buffer
.read(cx)
.map(|(excerpt, buffer_range)| {
excerpt
.buffer()
.text_for_range(buffer_range.clone())
.collect::<String>()
})
@@ -1489,7 +1490,7 @@ fn test_excerpts_in_ranges_no_ranges(cx: &mut AppContext) {
let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx));
let mut excerpts = snapshot.excerpts_in_ranges(iter::from_fn(|| None));
let mut excerpts = snapshot.disjoint_ranges_to_buffer_ranges::<usize>(iter::from_fn(|| None));
assert!(excerpts.next().is_none());
}
@@ -1588,12 +1589,12 @@ fn test_excerpts_in_ranges_range_inside_the_excerpt(cx: &mut AppContext) {
)];
let excerpts = snapshot
.excerpts_in_ranges(vec![range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
.disjoint_ranges_to_buffer_ranges(vec![range.clone()].into_iter())
.map(|(excerpt, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
excerpt.id(),
excerpt.buffer().remote_id(),
map_range_from_excerpt(&snapshot, excerpt.id(), excerpt.buffer(), actual_range),
)
})
.collect_vec();
@@ -1653,12 +1654,12 @@ fn test_excerpts_in_ranges_range_crosses_excerpts_boundary(cx: &mut AppContext)
];
let excerpts = snapshot
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
.disjoint_ranges_to_buffer_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
excerpt.id(),
excerpt.buffer().remote_id(),
map_range_from_excerpt(&snapshot, excerpt.id(), excerpt.buffer(), actual_range),
)
})
.collect_vec();
@@ -1729,12 +1730,12 @@ fn test_excerpts_in_ranges_range_encloses_excerpt(cx: &mut AppContext) {
];
let excerpts = snapshot
.excerpts_in_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
.disjoint_ranges_to_buffer_ranges(vec![expected_range.clone()].into_iter())
.map(|(excerpt, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
excerpt.id(),
excerpt.buffer().remote_id(),
map_range_from_excerpt(&snapshot, excerpt.id(), excerpt.buffer(), actual_range),
)
})
.collect_vec();
@@ -1795,12 +1796,12 @@ fn test_excerpts_in_ranges_multiple_ranges(cx: &mut AppContext) {
});
let excerpts = snapshot
.excerpts_in_ranges(ranges)
.map(|(excerpt_id, buffer, actual_range)| {
.disjoint_ranges_to_buffer_ranges(ranges)
.map(|(excerpt, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
excerpt.id(),
excerpt.buffer().remote_id(),
map_range_from_excerpt(&snapshot, excerpt.id(), excerpt.buffer(), actual_range),
)
})
.collect_vec();
@@ -1861,12 +1862,12 @@ fn test_excerpts_in_ranges_range_ends_at_excerpt_end(cx: &mut AppContext) {
];
let excerpts = snapshot
.excerpts_in_ranges(ranges.into_iter())
.map(|(excerpt_id, buffer, actual_range)| {
.disjoint_ranges_to_buffer_ranges(ranges.into_iter())
.map(|(excerpt, actual_range)| {
(
excerpt_id,
buffer.remote_id(),
map_range_from_excerpt(&snapshot, excerpt_id, buffer, actual_range),
excerpt.id(),
excerpt.buffer().remote_id(),
map_range_from_excerpt(&snapshot, excerpt.id(), excerpt.buffer(), actual_range),
)
})
.collect_vec();

View File

@@ -19,8 +19,8 @@ db.workspace = true
editor.workspace = true
file_icons.workspace = true
fuzzy.workspace = true
itertools.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
@@ -36,8 +36,8 @@ smol.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
worktree.workspace = true
workspace.workspace = true
worktree.workspace = true
[dev-dependencies]
search = { workspace = true, features = ["test-support"] }

File diff suppressed because it is too large Load Diff

View File

@@ -569,9 +569,9 @@ impl LocalBufferStore {
buffer_change_sets
.into_iter()
.filter_map(|(change_set, buffer_snapshot, path)| {
let (repo_entry, local_repo_entry) = snapshot.repo_for_path(&path)?;
let relative_path = repo_entry.relativize(&snapshot, &path).ok()?;
let base_text = local_repo_entry.repo().load_index_text(&relative_path);
let local_repo = snapshot.local_repo_for_path(&path)?;
let relative_path = local_repo.relativize(&path).ok()?;
let base_text = local_repo.repo().load_index_text(&relative_path);
Some((change_set, buffer_snapshot, base_text))
})
.collect::<Vec<_>>()
@@ -1161,16 +1161,16 @@ impl BufferStore {
Worktree::Local(worktree) => {
let worktree = worktree.snapshot();
let blame_params = maybe!({
let (repo_entry, local_repo_entry) = match worktree.repo_for_path(&file.path) {
let local_repo = match worktree.local_repo_for_path(&file.path) {
Some(repo_for_path) => repo_for_path,
None => return Ok(None),
};
let relative_path = repo_entry
.relativize(&worktree, &file.path)
let relative_path = local_repo
.relativize(&file.path)
.context("failed to relativize buffer path")?;
let repo = local_repo_entry.repo().clone();
let repo = local_repo.repo().clone();
let content = match version {
Some(version) => buffer.rope_for_version(&version).clone(),
@@ -1247,7 +1247,7 @@ impl BufferStore {
});
};
let path = match repo_entry.relativize(worktree, file.path()) {
let path = match repo_entry.relativize(file.path()) {
Ok(RepoPath(path)) => path,
Err(e) => return Task::ready(Err(e)),
};

View File

@@ -87,9 +87,8 @@ pub use language::Location;
#[cfg(any(test, feature = "test-support"))]
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
pub use worktree::{
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
FS_WATCH_LATENCY,
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
};
const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
@@ -2961,7 +2960,7 @@ impl LspStore {
http_client,
fs,
yarn,
next_diagnostic_group_id: Default::default(),
next_diagnostic_group_id: 1,
diagnostics: Default::default(),
_subscription: cx.on_app_quit(|this, cx| {
this.as_local_mut().unwrap().shutdown_language_servers(cx)
@@ -3109,6 +3108,7 @@ impl LspStore {
WorktreeStoreEvent::WorktreeUpdateSent(worktree) => {
worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree));
}
WorktreeStoreEvent::GitRepositoryUpdated => {}
}
}
@@ -5586,13 +5586,13 @@ impl LspStore {
<R::LspRequest as lsp::request::Request>::Result: Send,
<R::LspRequest as lsp::request::Request>::Params: Send,
{
let Some(local) = self.as_local() else {
return Task::ready(Vec::new());
};
debug_assert!(self.upstream_client().is_none());
let snapshot = buffer.read(cx).snapshot();
let scope = position.and_then(|position| snapshot.language_scope_at(position));
let server_ids = local
let server_ids = self
.as_local()
.unwrap()
.language_servers_for_buffer(buffer.read(cx), cx)
.filter(|(adapter, _)| {
scope

View File

@@ -39,7 +39,10 @@ use futures::{
pub use image_store::{ImageItem, ImageStore};
use image_store::{ImageItemEvent, ImageStoreEvent};
use git::{blame::Blame, repository::GitRepository};
use git::{
blame::Blame,
repository::{GitFileStatus, GitRepository},
};
use gpui::{
AnyModel, AppContext, AsyncAppContext, BorrowAppContext, Context as _, EventEmitter, Hsla,
Model, ModelContext, SharedString, Task, WeakModel, WindowContext,
@@ -95,9 +98,8 @@ pub use task_inventory::{
BasicContextProvider, ContextProviderWithTasks, Inventory, TaskSourceKind,
};
pub use worktree::{
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, RepositoryEntry,
UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings,
FS_WATCH_LATENCY,
Entry, EntryKind, File, LocalWorktree, PathChange, ProjectEntryId, UpdatedEntriesSet,
UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, FS_WATCH_LATENCY,
};
pub use buffer_store::ProjectTransaction;
@@ -242,6 +244,7 @@ pub enum Event {
ActivateProjectPanel,
WorktreeAdded(WorktreeId),
WorktreeOrderChanged,
GitRepositoryUpdated,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
WorktreeUpdatedGitRepositories(WorktreeId),
@@ -1433,6 +1436,15 @@ impl Project {
.unwrap_or(false)
}
pub fn project_path_git_status(
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<GitFileStatus> {
self.worktree_for_id(project_path.worktree_id, cx)
.and_then(|worktree| worktree.read(cx).status_for_file(&project_path.path))
}
pub fn visibility_for_paths(&self, paths: &[PathBuf], cx: &AppContext) -> Option<bool> {
paths
.iter()
@@ -2295,6 +2307,7 @@ impl Project {
}
WorktreeStoreEvent::WorktreeOrderChanged => cx.emit(Event::WorktreeOrderChanged),
WorktreeStoreEvent::WorktreeUpdateSent(_) => {}
WorktreeStoreEvent::GitRepositoryUpdated => cx.emit(Event::GitRepositoryUpdated),
}
}
@@ -3516,17 +3529,6 @@ impl Project {
)
}
pub fn get_repo(
&self,
project_path: &ProjectPath,
cx: &AppContext,
) -> Option<Arc<dyn GitRepository>> {
self.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.as_local()?
.local_git_repo(&project_path.path)
}
pub fn get_first_worktree_root_repo(&self, cx: &AppContext) -> Option<Arc<dyn GitRepository>> {
let worktree = self.visible_worktrees(cx).next()?.read(cx).as_local()?;
let root_entry = worktree.root_git_entry()?;
@@ -4426,8 +4428,10 @@ impl Completion {
}
}
pub fn sort_worktree_entries(entries: &mut [Entry]) {
pub fn sort_worktree_entries(entries: &mut [impl AsRef<Entry>]) {
entries.sort_by(|entry_a, entry_b| {
let entry_a = entry_a.as_ref();
let entry_b = entry_b.as_ref();
compare_paths(
(&entry_a.path, entry_a.is_file()),
(&entry_b.path, entry_b.is_file()),

View File

@@ -1312,7 +1312,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: lsp::DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
group_id: 0,
group_id: 1,
is_primary: true,
..Default::default()
}
@@ -1828,7 +1828,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 1,
group_id: 2,
is_primary: true,
..Default::default()
},
@@ -1840,7 +1840,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'CCC'".to_string(),
is_disk_based: true,
group_id: 2,
group_id: 3,
is_primary: true,
..Default::default()
}
@@ -1906,7 +1906,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::WARNING,
message: "unreachable statement".to_string(),
is_disk_based: true,
group_id: 4,
group_id: 5,
is_primary: true,
..Default::default()
}
@@ -1918,7 +1918,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 3,
group_id: 4,
is_primary: true,
..Default::default()
},
@@ -1998,7 +1998,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::WARNING,
message: "undefined variable 'A'".to_string(),
is_disk_based: true,
group_id: 6,
group_id: 7,
is_primary: true,
..Default::default()
}
@@ -2010,7 +2010,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
severity: DiagnosticSeverity::ERROR,
message: "undefined variable 'BB'".to_string(),
is_disk_based: true,
group_id: 5,
group_id: 6,
is_primary: true,
..Default::default()
},
@@ -3838,7 +3838,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 1,
group_id: 2,
is_primary: true,
..Default::default()
}
@@ -3848,27 +3848,27 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 2,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 0,
group_id: 1,
is_primary: false,
..Default::default()
}
@@ -3878,43 +3878,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 0,
is_primary: true,
..Default::default()
}
}
]
);
assert_eq!(
buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 0,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(2, 8)..Point::new(2, 17),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 0,
group_id: 1,
is_primary: true,
..Default::default()
}
@@ -3924,13 +3888,49 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
assert_eq!(
buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 1".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(1, 13)..Point::new(1, 15),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 2 hint 2".to_string(),
group_id: 1,
is_primary: false,
..Default::default()
}
},
DiagnosticEntry {
range: Point::new(2, 8)..Point::new(2, 17),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
message: "error 2".to_string(),
group_id: 1,
is_primary: true,
..Default::default()
}
}
]
);
assert_eq!(
buffer.diagnostic_group::<Point>(2).collect::<Vec<_>>(),
&[
DiagnosticEntry {
range: Point::new(1, 8)..Point::new(1, 9),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::WARNING,
message: "error 1".to_string(),
group_id: 1,
group_id: 2,
is_primary: true,
..Default::default()
}
@@ -3940,7 +3940,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
diagnostic: Diagnostic {
severity: DiagnosticSeverity::HINT,
message: "error 1 hint 1".to_string(),
group_id: 1,
group_id: 2,
is_primary: false,
..Default::default()
}

View File

@@ -109,7 +109,7 @@ impl Inventory {
/// Pulls its task sources relevant to the worktree and the language given and resolves them with the [`TaskContext`] given.
/// Joins the new resolutions with the resolved tasks that were used (spawned) before,
/// orders them so that the most recently used come first, all equally used ones are ordered so that the most specific tasks come first.
/// Deduplicates the tasks by their labels and contenxt and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
/// Deduplicates the tasks by their labels and context and splits the ordered list into two: used tasks and the rest, newly resolved tasks.
pub fn used_and_current_resolved_tasks(
&self,
worktree: Option<WorktreeId>,

View File

@@ -62,6 +62,7 @@ pub enum WorktreeStoreEvent {
WorktreeReleased(EntityId, WorktreeId),
WorktreeOrderChanged,
WorktreeUpdateSent(Model<Worktree>),
GitRepositoryUpdated,
}
impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
@@ -322,6 +323,7 @@ impl WorktreeStore {
let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
let worktree = worktree?;
this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
if visible {
@@ -374,6 +376,17 @@ impl WorktreeStore {
this.send_project_updates(cx);
})
.detach();
cx.subscribe(
worktree,
|_this, _, event: &worktree::Event, cx| match event {
worktree::Event::UpdatedGitRepositories(_) => {
cx.emit(WorktreeStoreEvent::GitRepositoryUpdated);
}
worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedEntries(_) => {}
},
)
.detach();
}
pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
@@ -583,11 +596,11 @@ impl WorktreeStore {
pub fn shared(
&mut self,
remote_id: u64,
downsteam_client: AnyProtoClient,
downstream_client: AnyProtoClient,
cx: &mut ModelContext<Self>,
) {
self.retain_worktrees = true;
self.downstream_client = Some((downsteam_client, remote_id));
self.downstream_client = Some((downstream_client, remote_id));
// When shared, retain all worktrees
for worktree_handle in self.worktrees.iter_mut() {

View File

@@ -63,7 +63,7 @@ use workspace::{
notifications::{DetachAndPromptErr, NotifyTaskExt},
DraggedSelection, OpenInTerminal, PreviewTabsSettings, SelectedEntry, Workspace,
};
use worktree::CreatedEntry;
use worktree::{CreatedEntry, GitEntry, GitEntryRef};
const PROJECT_PANEL_KEY: &str = "ProjectPanel";
const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
@@ -76,7 +76,7 @@ pub struct ProjectPanel {
// An update loop that keeps incrementing/decrementing scroll offset while there is a dragged entry that's
// hovered over the start/end of a list.
hover_scroll_task: Option<Task<()>>,
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
visible_entries: Vec<(WorktreeId, Vec<GitEntry>, OnceCell<HashSet<Arc<Path>>>)>,
/// Maps from leaf project entry ID to the currently selected ancestor.
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
/// project entries (and all non-leaf nodes are guaranteed to be directories).
@@ -311,7 +311,8 @@ impl ProjectPanel {
this.update_visible_entries(None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
project::Event::GitRepositoryUpdated
| project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
@@ -1159,7 +1160,7 @@ impl ProjectPanel {
}
}
fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
fn rename_impl(&mut self, selection: Option<Range<usize>>, cx: &mut ViewContext<Self>) {
if let Some(SelectedEntry {
worktree_id,
entry_id,
@@ -1183,13 +1184,16 @@ impl ProjectPanel {
.map(|s| s.to_string_lossy())
.unwrap_or_default()
.to_string();
let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
let selection_end =
file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
let selection = selection.unwrap_or_else(|| {
let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
let selection_end =
file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
0..selection_end
});
self.filename_editor.update(cx, |editor, cx| {
editor.set_text(file_name, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([0..selection_end])
s.select_ranges([selection])
});
editor.focus(cx);
});
@@ -1201,6 +1205,10 @@ impl ProjectPanel {
}
}
fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
self.rename_impl(None, cx);
}
fn trash(&mut self, action: &Trash, cx: &mut ViewContext<Self>) {
self.remove(true, action.skip_prompt, cx);
}
@@ -1359,9 +1367,10 @@ impl ProjectPanel {
let parent_entry = worktree.entry_for_path(parent_path)?;
// Remove all siblings that are being deleted except the last marked entry
let mut siblings: Vec<Entry> = worktree
let mut siblings: Vec<_> = worktree
.snapshot()
.child_entries(parent_path)
.with_git_statuses()
.filter(|sibling| {
sibling.id == latest_entry.id
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
@@ -1369,7 +1378,7 @@ impl ProjectPanel {
entry_id: sibling.id,
})
})
.cloned()
.map(|entry| entry.to_owned())
.collect();
project::sort_worktree_entries(&mut siblings);
@@ -1762,7 +1771,7 @@ impl ProjectPanel {
source: &SelectedEntry,
(worktree, target_entry): (Model<Worktree>, &Entry),
cx: &AppContext,
) -> Option<PathBuf> {
) -> Option<(PathBuf, Option<Range<usize>>)> {
let mut new_path = target_entry.path.to_path_buf();
// If we're pasting into a file, or a directory into itself, go up one level.
if target_entry.is_file() || (target_entry.is_dir() && target_entry.id == source.entry_id) {
@@ -1778,6 +1787,8 @@ impl ProjectPanel {
new_path.push(&clipboard_entry_file_name);
let extension = new_path.extension().map(|e| e.to_os_string());
let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
let file_name_len = file_name_without_extension.to_string_lossy().len();
let mut disambiguation_range = None;
let mut ix = 0;
{
let worktree = worktree.read(cx);
@@ -1785,9 +1796,17 @@ impl ProjectPanel {
new_path.pop();
let mut new_file_name = file_name_without_extension.to_os_string();
new_file_name.push(" copy");
let disambiguation = " copy";
let mut disambiguation_len = disambiguation.len();
new_file_name.push(disambiguation);
if ix > 0 {
new_file_name.push(format!(" {}", ix));
let extra_disambiguation = format!(" {}", ix);
disambiguation_len += extra_disambiguation.len();
new_file_name.push(extra_disambiguation);
}
if let Some(extension) = extension.as_ref() {
new_file_name.push(".");
@@ -1795,10 +1814,11 @@ impl ProjectPanel {
}
new_path.push(new_file_name);
disambiguation_range = Some(file_name_len..(file_name_len + disambiguation_len));
ix += 1;
}
}
Some(new_path)
Some((new_path, disambiguation_range))
}
fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
@@ -1816,9 +1836,10 @@ impl ProjectPanel {
}
let mut paste_entry_tasks: IndexMap<(ProjectEntryId, bool), PasteTask> =
IndexMap::default();
let mut disambiguation_range = None;
let clip_is_cut = clipboard_entries.is_cut();
for clipboard_entry in clipboard_entries.items() {
let new_path =
let (new_path, new_disambiguation_range) =
self.create_paste_path(clipboard_entry, self.selected_sub_entry(cx)?, cx)?;
let clip_entry_id = clipboard_entry.entry_id;
let is_same_worktree = clipboard_entry.worktree_id == worktree_id;
@@ -1855,8 +1876,11 @@ impl ProjectPanel {
};
let needs_delete = !is_same_worktree && clip_is_cut;
paste_entry_tasks.insert((clip_entry_id, needs_delete), task);
disambiguation_range = new_disambiguation_range.or(disambiguation_range);
}
let item_count = paste_entry_tasks.len();
cx.spawn(|project_panel, mut cx| async move {
let mut last_succeed = None;
let mut need_delete_ids = Vec::new();
@@ -1877,17 +1901,6 @@ impl ProjectPanel {
}
}
}
// update selection
if let Some(entry_id) = last_succeed {
project_panel
.update(&mut cx, |project_panel, _cx| {
project_panel.selection = Some(SelectedEntry {
worktree_id,
entry_id,
});
})
.ok();
}
// remove entry for cut in difference worktree
for entry_id in need_delete_ids {
project_panel
@@ -1899,6 +1912,22 @@ impl ProjectPanel {
})??
.await?;
}
// update selection
if let Some(entry_id) = last_succeed {
project_panel
.update(&mut cx, |project_panel, cx| {
project_panel.selection = Some(SelectedEntry {
worktree_id,
entry_id,
});
// if only one entry was pasted and it was disambiguated, open the rename editor
if item_count == 1 && disambiguation_range.is_some() {
project_panel.rename_impl(disambiguation_range, cx);
}
})
.ok();
}
anyhow::Ok(())
})
@@ -2307,7 +2336,7 @@ impl ProjectPanel {
}
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
let mut entry_iter = snapshot.entries(true, 0).with_git_statuses();
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
if auto_collapse_dirs && entry.kind.is_dir() {
@@ -2349,7 +2378,7 @@ impl ProjectPanel {
}
}
auto_folded_ancestors.clear();
visible_worktree_entries.push(entry.clone());
visible_worktree_entries.push(entry.to_owned());
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
entry.id == new_entry_id || {
self.ancestors.get(&entry.id).map_or(false, |entries| {
@@ -2363,25 +2392,27 @@ impl ProjectPanel {
false
};
if precedes_new_entry {
visible_worktree_entries.push(Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
visible_worktree_entries.push(GitEntry {
entry: Entry {
id: NEW_ENTRY_ID,
kind: new_entry_kind,
path: entry.path.join("\0").into(),
inode: 0,
mtime: entry.mtime,
size: entry.size,
is_ignored: entry.is_ignored,
is_external: false,
is_private: false,
is_always_included: entry.is_always_included,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
},
git_status: entry.git_status,
canonical_path: entry.canonical_path.clone(),
char_bag: entry.char_bag,
is_fifo: entry.is_fifo,
});
}
let worktree_abs_path = worktree.read(cx).abs_path();
let (depth, path) = if Some(entry) == worktree.read(cx).root_entry() {
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
let Some(path_name) = worktree_abs_path
.file_name()
.with_context(|| {
@@ -2458,8 +2489,8 @@ impl ProjectPanel {
entry_iter.advance();
}
snapshot.propagate_git_statuses(&mut visible_worktree_entries);
project::sort_worktree_entries(&mut visible_worktree_entries);
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
}
@@ -2606,23 +2637,55 @@ impl ProjectPanel {
let _ = maybe!({
let project = self.project.read(cx);
let target_worktree = project.worktree_for_entry(target_entry_id, cx)?;
let worktree_id = target_worktree.read(cx).id();
let target_entry = target_worktree
.read(cx)
.entry_for_id(target_entry_id)?
.clone();
let mut copy_tasks = Vec::new();
let mut disambiguation_range = None;
for selection in selections.items() {
let new_path = self.create_paste_path(
let (new_path, new_disambiguation_range) = self.create_paste_path(
selection,
(target_worktree.clone(), &target_entry),
cx,
)?;
self.project
.update(cx, |project, cx| {
project.copy_entry(selection.entry_id, None, new_path, cx)
})
.detach_and_log_err(cx)
let task = self.project.update(cx, |project, cx| {
project.copy_entry(selection.entry_id, None, new_path, cx)
});
copy_tasks.push(task);
disambiguation_range = new_disambiguation_range.or(disambiguation_range);
}
let item_count = copy_tasks.len();
cx.spawn(|project_panel, mut cx| async move {
let mut last_succeed = None;
for task in copy_tasks.into_iter() {
if let Some(Some(entry)) = task.await.log_err() {
last_succeed = Some(entry.id);
}
}
// update selection
if let Some(entry_id) = last_succeed {
project_panel
.update(&mut cx, |project_panel, cx| {
project_panel.selection = Some(SelectedEntry {
worktree_id,
entry_id,
});
// if only one entry was dragged and it was disambiguated, open the rename editor
if item_count == 1 && disambiguation_range.is_some() {
project_panel.rename_impl(disambiguation_range, cx);
}
})
.ok();
}
})
.detach();
Some(())
});
} else {
@@ -2655,13 +2718,13 @@ impl ProjectPanel {
None
}
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, &Entry)> {
fn entry_at_index(&self, index: usize) -> Option<(WorktreeId, GitEntryRef)> {
let mut offset = 0;
for (worktree_id, visible_worktree_entries, _) in &self.visible_entries {
if visible_worktree_entries.len() > offset + index {
return visible_worktree_entries
.get(index)
.map(|entry| (*worktree_id, entry));
.map(|entry| (*worktree_id, entry.to_ref()));
}
offset += visible_worktree_entries.len();
}
@@ -2694,7 +2757,7 @@ impl ProjectPanel {
.collect()
});
for entry in visible_worktree_entries[entry_range].iter() {
callback(entry, entries, cx);
callback(&entry, entries, cx);
}
ix = end_ix;
}
@@ -2763,7 +2826,7 @@ impl ProjectPanel {
};
let (depth, difference) =
ProjectPanel::calculate_depth_and_difference(entry, entries);
ProjectPanel::calculate_depth_and_difference(&entry, entries);
let filename = match difference {
diff if diff > 1 => entry
@@ -2892,9 +2955,9 @@ impl ProjectPanel {
worktree_id: WorktreeId,
reverse_search: bool,
only_visible_entries: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<Entry> {
) -> Option<GitEntry> {
if only_visible_entries {
let entries = self
.visible_entries
@@ -2909,15 +2972,18 @@ impl ProjectPanel {
.clone();
return utils::ReversibleIterable::new(entries.iter(), reverse_search)
.find(|ele| predicate(ele, worktree_id))
.find(|ele| predicate(ele.to_ref(), worktree_id))
.cloned();
}
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
worktree.update(cx, |tree, _| {
utils::ReversibleIterable::new(tree.entries(true, 0usize), reverse_search)
.find_single_ended(|ele| predicate(ele, worktree_id))
.cloned()
utils::ReversibleIterable::new(
tree.entries(true, 0usize).with_git_statuses(),
reverse_search,
)
.find_single_ended(|ele| predicate(*ele, worktree_id))
.map(|ele| ele.to_owned())
})
}
@@ -2925,7 +2991,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@@ -2947,7 +3013,9 @@ impl ProjectPanel {
let root_entry = tree.root_entry()?;
let tree_id = tree.id();
let mut first_iter = tree.traverse_from_path(true, true, true, entry.path.as_ref());
let mut first_iter = tree
.traverse_from_path(true, true, true, entry.path.as_ref())
.with_git_statuses();
if reverse_search {
first_iter.next();
@@ -2955,25 +3023,25 @@ impl ProjectPanel {
let first = first_iter
.enumerate()
.take_until(|(count, ele)| *ele == root_entry && *count != 0usize)
.map(|(_, ele)| ele)
.find(|ele| predicate(ele, tree_id))
.cloned();
.take_until(|(count, entry)| entry.entry == root_entry && *count != 0usize)
.map(|(_, entry)| entry)
.find(|ele| predicate(*ele, tree_id))
.map(|ele| ele.to_owned());
let second_iter = tree.entries(true, 0usize);
let second_iter = tree.entries(true, 0usize).with_git_statuses();
let second = if reverse_search {
second_iter
.take_until(|ele| ele.id == start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
} else {
second_iter
.take_while(|ele| ele.id != start.entry_id)
.filter(|ele| predicate(ele, tree_id))
.filter(|ele| predicate(*ele, tree_id))
.last()
.cloned()
.map(|ele| ele.to_owned())
};
if reverse_search {
@@ -3030,7 +3098,7 @@ impl ProjectPanel {
&self,
start: Option<&SelectedEntry>,
reverse_search: bool,
predicate: impl Fn(&Entry, WorktreeId) -> bool,
predicate: impl Fn(GitEntryRef, WorktreeId) -> bool,
cx: &mut ViewContext<Self>,
) -> Option<SelectedEntry> {
let mut worktree_ids: Vec<_> = self
@@ -3072,8 +3140,8 @@ impl ProjectPanel {
)
};
let first_search = first_iter.find(|ele| predicate(ele, start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele, start.worktree_id));
let first_search = first_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
let second_search = second_iter.find(|ele| predicate(ele.to_ref(), start.worktree_id));
if first_search.is_some() {
return first_search.map(|entry| SelectedEntry {
@@ -5283,11 +5351,22 @@ mod tests {
//
"v root1",
" one.txt",
" one copy.txt <== selected",
" [EDITOR: 'one copy.txt'] <== selected",
" one.two.txt",
]
);
panel.update(cx, |panel, cx| {
panel.filename_editor.update(cx, |editor, cx| {
let file_name_selections = editor.selections.all::<usize>(cx);
assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
let file_name_selection = &file_name_selections[0];
assert_eq!(file_name_selection.start, "one".len(), "Should select the file name disambiguation after the original file name");
assert_eq!(file_name_selection.end, "one copy".len(), "Should select the file name disambiguation until the extension");
});
assert!(panel.confirm_edit(cx).is_none());
});
panel.update(cx, |panel, cx| {
panel.paste(&Default::default(), cx);
});
@@ -5300,10 +5379,12 @@ mod tests {
"v root1",
" one.txt",
" one copy.txt",
" one copy 1.txt <== selected",
" [EDITOR: 'one copy 1.txt'] <== selected",
" one.two.txt",
]
);
panel.update(cx, |panel, cx| assert!(panel.confirm_edit(cx).is_none()));
}
#[gpui::test]
@@ -5495,11 +5576,14 @@ mod tests {
" four.txt",
" one.txt",
" three.txt",
" three copy.txt <== selected",
" [EDITOR: 'three copy.txt'] <== selected",
" two.txt",
]
);
panel.update(cx, |panel, cx| panel.cancel(&menu::Cancel {}, cx));
cx.executor().run_until_parked();
select_path(&panel, "root1/a", cx);
panel.update(cx, |panel, cx| {
panel.copy(&Default::default(), cx);
@@ -5603,6 +5687,48 @@ mod tests {
select_path(&panel, "root", cx);
panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
//
"v root",
" > a",
" > [EDITOR: 'a copy'] <== selected",
" v b",
" v a",
" v inner_dir",
" four.txt",
" three.txt",
" one.txt",
" two.txt"
]
);
let confirm = panel.update(cx, |panel, cx| {
panel
.filename_editor
.update(cx, |editor, cx| editor.set_text("c", cx));
panel.confirm_edit(cx).unwrap()
});
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
//
"v root",
" > a",
" > [PROCESSING: 'c'] <== selected",
" v b",
" v a",
" v inner_dir",
" four.txt",
" three.txt",
" one.txt",
" two.txt"
]
);
confirm.await.unwrap();
panel.update(cx, |panel, cx| panel.paste(&Default::default(), cx));
cx.executor().run_until_parked();
assert_eq!(
@@ -5611,18 +5737,18 @@ mod tests {
//
"v root",
" > a",
" v a copy",
" > a <== selected",
" > inner_dir",
" one.txt",
" two.txt",
" v b",
" v a",
" v inner_dir",
" four.txt",
" three.txt",
" one.txt",
" two.txt"
" two.txt",
" v c",
" > a <== selected",
" > inner_dir",
" one.txt",
" two.txt",
]
);
}
@@ -5703,6 +5829,33 @@ mod tests {
],
"Should copy dir1 as well as c.txt into dir2"
);
// Disambiguating multiple files should not open the rename editor.
select_path(&panel, "test/dir2", cx);
panel.update(cx, |panel, cx| {
panel.paste(&Default::default(), cx);
});
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..15, cx),
&[
"v test",
" v dir1 <== marked",
" a.txt",
" b.txt",
" v dir2",
" v dir1",
" a.txt",
" b.txt",
" > dir1 copy <== selected",
" c.txt",
" c copy.txt",
" c.txt <== marked",
" d.txt",
],
"Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor"
);
}
#[gpui::test]

View File

@@ -1768,7 +1768,7 @@ message Entry {
bool is_ignored = 7;
bool is_external = 8;
reserved 6;
optional GitStatus git_status = 9;
reserved 9;
bool is_fifo = 10;
optional uint64 size = 11;
optional string canonical_path = 12;
@@ -1777,6 +1777,8 @@ message Entry {
message RepositoryEntry {
uint64 work_directory_id = 1;
optional string branch = 2;
repeated StatusEntry updated_statuses = 3;
repeated string removed_statuses = 4;
}
message StatusEntry {
@@ -1788,6 +1790,7 @@ enum GitStatus {
Added = 0;
Modified = 1;
Conflict = 2;
Deleted = 3;
}
message BufferState {

View File

@@ -20,6 +20,7 @@ use serde_json::json;
use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
use smol::stream::StreamExt;
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
};
@@ -1150,6 +1151,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
let (project, headless_project) = init_test(&fs, cx, server_cx).await;
let branches = ["main", "dev", "feature-1"];
let branches_set = branches
.iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
fs.insert_branches(Path::new("/code/project1/.git"), &branches);
let (worktree, _) = project
@@ -1173,10 +1178,10 @@ async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestA
let remote_branches = remote_branches
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
.map(|branch| branch.name.to_string())
.collect::<HashSet<_>>();
assert_eq!(&remote_branches, &branches);
assert_eq!(&remote_branches, &branches_set);
cx.update(|cx| {
project.update(cx, |project, cx| {

View File

@@ -39,7 +39,7 @@ pub async fn launch_remote_kernel(
let kernel_launch_request = KernelLaunchRequest {
name: kernel_name.to_string(),
// Note: since the path we have locally may not be the same as the one on the remote server,
// we don't send it. We'll have to evaluate this decisiion along the way.
// we don't send it. We'll have to evaluate this decision along the way.
path: None,
};

View File

@@ -1,7 +1,7 @@
use crate::proto;
use serde::{Deserialize, Serialize};
use serde_json::{map, Value};
use strum::{EnumVariantNames, VariantNames as _};
use strum::VariantNames;
const KIND: &str = "kind";
const ENTITY_ID: &str = "entity_id";
@@ -15,7 +15,7 @@ const ENTITY_ID: &str = "entity_id";
/// Most notification types have a special field which is aliased to
/// `entity_id`. This field is stored in its own database column, and can
/// be used to query the notification.
#[derive(Debug, Clone, PartialEq, Eq, EnumVariantNames, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, VariantNames, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum Notification {
ContactRequest {

View File

@@ -5,7 +5,7 @@ use collections::HashMap;
// for those users.
//
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
//
// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves

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