Compare commits

...

46 Commits

Author SHA1 Message Date
Conrad Irwin
404a268412 Add docs 2025-01-06 20:19:06 -07:00
0x2CA
7aede4a348 add subword 2024-12-24 11:47:02 +08:00
Danilo Leal
8a724acc4d Add new CSS logo (#22382)
CSS has a brand new logo :) For context, see:

- https://github.com/CSS-Next/logo.css
- https://github.com/CSS-Next/logo.css/issues/54
- https://github.com/CSS-Next/logo.css/pull/53
- https://github.com/CSS-Next/logo.css/pull/55

<img width="1412" alt="Screenshot 2024-12-23 at 6 17 07 PM"
src="https://github.com/user-attachments/assets/7352a921-06fa-4e50-8aad-44c974c57ed4"
/>

Release Notes:

- Add new CSS logo
2024-12-23 21:42:39 +00:00
Danilo Leal
7595d36943 assistant2: Refine buffer inline assistant styles (#22377)
A lot of spacing tweaks. But, most notably, using the buffer font for
the inline assistant.

<img width="800" alt="Screenshot 2024-12-23 at 12 46 16 PM"
src="https://github.com/user-attachments/assets/ee2908a7-7515-4244-83fc-791172b29364"
/>

Release Notes:

- N/A
2024-12-23 21:29:39 +00:00
Danilo Leal
d25c2ff866 assistant2: Improve markdown rendering design (#22321)
This PR visually balances code blocks within thread messages a bit more.

<img width="800" alt="Screenshot 2024-12-23 at 11 26 14 AM"
src="https://github.com/user-attachments/assets/6d459aac-5d94-4021-8289-0125bc82e77c"
/>

Release Notes:

- N/A
2024-12-23 18:18:20 +00:00
plyght
7f33f31ebe docs: Use higher-quality image in the header (#22195)
Improved image quality of docs site logo at top.

Prior Change: 
<img width="159" alt="SCR-20241218-konx"
src="https://github.com/user-attachments/assets/18c936e8-958d-4970-98e2-8dd1ad3a0a89"
/>
Post Change: 
<img width="177" alt="SCR-20241218-kojm"
src="https://github.com/user-attachments/assets/0b874ac2-7193-44ec-9634-b91a71c53864"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-23 13:39:28 +00:00
Kirill Bulatov
5df409971c Use more elaborate messages for permission denied errors on startup (#22368)
Also show all paths with issues, grouping them by the issue kind.

Inspired by https://github.com/zed-industries/zed/issues/22365 ,
https://github.com/zed-industries/zed/issues/20162,
https://github.com/zed-industries/zed/issues/13426, ...

Attempts to provide example `chown` and `chmod` commands on this error,
instead of encouraging to submit another issue.
For now, uses a hardcoded command examples, but we might provide a
better workflow later, e.g. try to automatically run those commands.

I seem to be unable to select the size of the modal, but for something
that's supposed to appear only once, it seems to be ok.


![regular](https://github.com/user-attachments/assets/484feb95-b9c5-445c-87f8-cd1db9337529)


![also_regular](https://github.com/user-attachments/assets/680ea95d-91fe-4b16-b3bc-e241ecd841d4)


![many_kinds](https://github.com/user-attachments/assets/7103161f-754d-413a-94df-62a44e353674)


![exaggerated](https://github.com/user-attachments/assets/b6e70be7-9ab4-4861-afae-5285a554fdba)



Release Notes:

- N/A
2024-12-23 12:34:29 +00:00
Cole Miller
8ccc7b81c8 Restore crates/zed dependency on libc (#22354)
This somehow got lost during or after merge of #22202.

Release Notes:

- N/A
2024-12-22 15:53:16 +00:00
teapo
a0e4464a33 Update Luau docs (#22351)
The domain has changed and the old StyLua arguments work again.

Release Notes:

- N/A
2024-12-22 13:54:50 +00:00
tims
204af9cac5 linux: Fix "Failed to start language server" errors when starting Zed from .desktop file (#22335)
Closes #21406

Context:

A few weeks ago on Linux, we resolved an
[issue](https://github.com/zed-industries/zed/issues/20070) where users
could not open more than one file from the file explorer. This was fixed
by replacing `zed-editor` (zed binary in the code) with `zed` (cli
binary in the code) in the `.desktop` file. The reason for this change
was that using the cli to open files is more convenient - it determines
weather to spawn a new Zed instance or use an existing one, if we use
main binary instead it would throw error `Zed is already running`.

You can read the complete PR here: [linux: Fix file not opening from
file explorer](https://github.com/zed-industries/zed/pull/21137).

While this fix resolved the original issue, it introduced a new one.

Problem:

When the cli binary is used, it assumes it is always being invoked from
a terminal and relies on `std::env::vars()` to retrieve the environment
variables needed to spawn Zed. These env vars are then passed to the
worktree, and eventually, languages use the `PATH` from this env to find
binaries. This leads to the "Failed to start language server" error when
the `.desktop` entry is used on Linux.

Solution:

When the `zed-editor` binary is used, it uses some clever Unix-specific
logic to retrieve the default shell (`load_shell_from_passwd`) and then
fetch the env vars from that shell (`load_login_shell_environment`).
This same logic should be used in the cli binary when it is invoked via
a `.desktop` entry rather than from a terminal.

Approach:

I moved these two functions mentioned above to a utils file and reused
them in cli binary to fetch env vars only on Linux when it is not run
from a terminal. This provides missing paths, and fix the issue.

It is also possible to handle this in the `zed-editor` binary by
modifying the logic in `handle_cli_connection`, where `CliRequest::Open`
is processed. There we can discard incoming env, and use our logic. But
discarding incoming envs felt weird, and I thought it's better to handle
this at source.

Release Notes:

- Fixed `Failed to start language server` errors when starting from
dekstop entry on Linux
2024-12-22 10:05:52 +00:00
Cole Miller
a2022d7da3 Improve Linux panic reporting (#22202)
- [x] Upload separate debug symbols for Linux binaries to DigitalOcean
- [x] Send raw offsets with panic report JSON on Linux
- [x] Update `symbolicate` script to handle Linux crashes
- [x] Demangle backtraces 🎉 
- [x] Check that it works
- [x] Improve deduplication (?)
 
Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-22 08:20:17 +00:00
Kirill Bulatov
b51a28b75f Return back Windows menu (#22339)
Follow-up of https://github.com/zed-industries/zed/pull/21873

Release Notes:

- N/A
2024-12-21 22:03:17 +00:00
Michael Sloan
dbb76100e5 Improve layout of completions doc popover (#22337)
* Now more often displayed to the right. Resizes docs width if more
space than min width is available.

* If constrained in horizontal space and so displayed above/below resize
docs height to fit.

* Makes space for scrollbar and gap.

Layout is imperfect for viewport sizes smaller than the context menu,
left TODOs in the code for handling this. Wanted to get this change out
for feedback first.

Release Notes:

- N/A
2024-12-21 21:22:15 +00:00
Cole Miller
6b92e0b5da In terminal context, open new terminals with cmd-n instead of new files (#22253)
Release Notes:

- In terminal context, `cmd-n` and `ctrl-n` now open a new terminal
instead of a new file
2024-12-21 19:03:58 +00:00
Kirill Bulatov
2930211af9 Allow disabling editor scrollbars programmatically (#22333)
Disable them in the diff editors

Closes https://github.com/zed-industries/zed/issues/22271

Release Notes:

- N/A
2024-12-21 16:58:26 +00:00
Marshall Bowers
1449377278 assistant2: Fix panics when confirming nonexistent entries in the context picker (#22332)
This PR fixes a panic that could occur when confirming when the context
picker had no matching entries.

Release Notes:

- N/A
2024-12-21 16:11:29 +00:00
Kirill Bulatov
831930aad0 Make git panel entries clickable (#22329)
Makes a first pass over git panel UI, making it more interactive.


![image](https://github.com/user-attachments/assets/4d43b086-4ef2-4913-9783-2b9467d99c9a)


* every item can be selected, the selection is shown in the panel
* every item can be clicked, which changes the selection and
creates/focuses the editor with a project changes multi buffer
* the editor is scrolled so that the clicked item is in the center
* it's possible to nagivate up and down the panel, selecting
next/previous items in it, triggering the editor scroll

Known issues:

* entries are updated oddly sometimes (should become better after
DiffMap improvements land?)
* only unstaged diffs are shown currently (entry status storage should
help with this)
* no deleted files are displayed (the underlying work is done by others
now)
* added files have no diff hunks shown (DiffMap will have it?)
* performance story has not improved (again, DiffMap and status storage
should help with this)

Release Notes:

- N/A
2024-12-21 14:20:08 +00:00
Piotr Osiewicz
bc32b4d016 zeta: Compute diff on background thread (#22328)
@iamnbutler noticed slowness in assistant panel which I've pinned down
to the fact that we're calculating buffer diff on foreground thread.
This PR moves this computation into the background; I don't know much
about Zeta but it seems fine to do, as the call-site is asynchronous
anyways.

Closes #ISSUE

Release Notes:

- N/A
2024-12-21 13:44:18 +00:00
Piotr Osiewicz
fac5118f10 python: Fix decorated test detection (#22327)
Follow-up to #22325, where I missed a couple points in the review that
were actually quite relevant.

Closes #ISSUE

Release Notes:

- N/A
2024-12-21 13:11:52 +00:00
Thomas
7184b15f48 Add decorated function to pytest runnables (#22325)
Fixes: #22324

Closes #ISSUE

Release Notes:

- Fixed pytest decoracted function discovery
2024-12-21 11:31:35 +00:00
Danilo Leal
e82af55d64 assistant2: Adjust role info colors on the thread header (#22318)
<img width="800" alt="Screenshot 2024-12-20 at 9 01 29 PM"
src="https://github.com/user-attachments/assets/5e9e9bf2-c84e-4215-b658-9b668dd151b2"
/>

Release Notes:

- N/A
2024-12-21 00:19:50 +00:00
Danilo Leal
dcd21e6f23 assistant2: Use SwitchWithLabel for the tool toggle (#22317)
<img width="800" alt="Screenshot 2024-12-20 at 8 52 27 PM"
src="https://github.com/user-attachments/assets/4b4f62a3-eb23-489b-a459-2117baaa37b4"
/>

Release Notes:

- N/A
2024-12-21 00:15:47 +00:00
Danilo Leal
9efa13116d assistant2: Use a label to render the thread menu item (#22316)
The thread menu items were rendering with a larger font that the file &
directory counterparts. Now, they're consistent!

| Before | After | Reference |
|--------|--------|--------|
| <img width="800" alt="Screenshot 2024-12-20 at 8 39 47 PM"
src="https://github.com/user-attachments/assets/d196adcb-aea6-4399-8fb1-f5b771e76c1f"
/> | <img width="800" alt="Screenshot 2024-12-20 at 8 35 43 PM"
src="https://github.com/user-attachments/assets/a1e22c13-fd7e-4665-b09b-f3b583e7ce24"
/> | <img width="800" alt="Screenshot 2024-12-20 at 8 40 38 PM"
src="https://github.com/user-attachments/assets/d509ac6f-1251-4240-a369-906faebed8c0"
/> |


Release Notes:

- N/A
2024-12-20 23:53:51 +00:00
Danilo Leal
6dbc12f6af Add the SwitchWithLabel component (#22314)
<img width="800" alt="Screenshot 2024-12-20 at 8 31 14 PM"
src="https://github.com/user-attachments/assets/1d7bd10a-0db3-41e4-9f59-977cc2ab137c"
/>

Release Notes:

- N/A
2024-12-20 23:44:47 +00:00
Marshall Bowers
a8afc63a91 gpui: Remove use of use gpui::* in examples (#22311)
This PR removes the use of `use gpui::*` in the GPUI examples, as this
is not how consumers should be importing GPUI.

Release Notes:

- N/A
2024-12-20 23:09:30 +00:00
Marshall Bowers
7c6feeb3a8 Remove usage of use gpui::* (#22310)
This PR removes usages of `use gpui::*;` in the codebase (excluding
examples).

We should only use glob imports for `gpui::prelude`, and then import
everything else individually.

Release Notes:

- N/A
2024-12-20 22:52:11 +00:00
Danilo Leal
fadf9ff4f4 Make breadcrumb content scrollable (#22309)
Part of https://github.com/zed-industries/zed/issues/5363


https://github.com/user-attachments/assets/66c90cb7-1b36-4436-ad1e-344fbcd4befc

Release Notes:

- N/A
2024-12-20 22:50:41 +00:00
Michael Sloan
9b2bc458e3 Expand diagnostic excerpts using heuristics on syntactic information from TreeSitter (#21942)
This is quite experimental and untested in languages other than Rust.
It's written to attempt to do something sensible in many languages. Due
to its experimental nature, just releasing to staff, and so not
including it in release notes. Future release note might be "Improved
diagnostic excerpts by using syntactic info to determine the context
lines to show."

Release Notes:

- N/A
2024-12-20 22:42:18 +00:00
tims
ca9cee85e1 linux: Fix non-maximized Zed windows growing larger across sessions (#22301)
Closes #17870

Context:

On Linux, when creating a new window, bounds are either pulled from
existing workspace data (serialized in an SQLite DB) or fall back to
some default constants if no data exists.

These bounds include the full dimensions of the window (width and
height), which already account for insets. However, properties like
`inset` (Wayland) or `last_insets` (X11) exist only at the platform
level and are not part of the window bounds themselves.

During rendering, we call `set_client_inset`, which updates the inset
values and also adjusts the window bounds, increasing their dimensions.
In Zed's case, the inset is 10px, which adds 20px to both the width and
height (10px from each side).

Problem:

When quitting, the full window bounds (which already account for inset)
are saved to the DB. On reopening, these saved bounds are used to create
the window. `set_client_inset` runs again and inflates the dimensions
even more.

Solution:

Store window bounds *without* the inset-inflated dimensions. On the next
session, `set_client_inset` will take care of applying the inset,
resulting window dimensions matching the previous session. This fix is
in the PR.

Alternative Solution:

Another option is to save the inset explicitly in the DB and apply it
during window creation. But this means storing more data, and the inset
would need to be platform-agnostic, which adds complexity. Doesn’t seem
worth it for no real gain.

X11 Before:
```sh
saving window bounds with width: 1136, height: 784
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/zed`
saving window bounds with width: 1156, height: 804   <---- +20px
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1176, height: 824  <---- +20px
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
     Running `target/debug/zed`
saving window bounds with width: 1196, height: 844  <---- +20px
```

X11 After:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764     <---- same
tims@lemon ~/w/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
     Running `target/debug/zed`
saving window bounds with width: 1116, height: 764   <---- same
```

On Wayland, saving occurs only when you actually resize the window (on
X11, saving happens both on init and while dragging the window). To
trigger saving, I manually resized the window by ~1px to make it print.

Wayland Before:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 36s
     Running `target/debug/zed`
saving window bounds with width: 945, height: 577
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.77s
     Running `target/debug/zed`
saving window bounds with width: 966, height: 597    <--- +20px on both  (1px increase in width is me resizing)
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.87s
     Running `target/debug/zed`
saving window bounds with width: 987, height: 618    <--- +20px on both  (1px increase in width and height is me resizing)
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running `target/debug/zed`
saving window bounds with width: 1006, height: 638   <--- +20px on both  (1px decrease in width is me resizing)
```

Wayland After:
```sh
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.82s
     Running `target/debug/zed`
saving window bounds with width: 925, height: 558
tims@orange ~/zed (fix-window-growing-larger)> cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.84s
     Running `target/debug/zed`
saving window bounds with width: 925, height: 557     <--- same (1px decrease in height is me resizing)
saving window bounds with width: 925, height: 558
```

Release Notes:

- Fix non-maximized zed windows growing larger across sessions on Linux

---------

Co-authored-by: mgsloan@gmail.com <michael@zed.dev>
2024-12-20 22:23:25 +00:00
Michael Sloan
c01403b4b1 Reapply completion docs prefetch (#22306)
Leaving release notes as N/A because it had release notes in the past in
#21705

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

This change resolves this by attempting to fetch all the completions
that will be shown. It also mostly avoids re-resolving completions. It
intentionally re-resolves the current selection on navigation, as some
language servers will respond with more information later on.

Release Notes:

- N/A
2024-12-20 22:16:07 +00:00
Danilo Leal
8ee04bf04a Fix remote dev project name label (#22307)
Closes https://github.com/zed-industries/zed/issues/21877

<img width="800" alt="Screenshot 2024-12-20 at 6 45 50 PM"
src="https://github.com/user-attachments/assets/2069465f-1a00-47de-b691-585bb2603279"
/>

Release Notes:

- N/A
2024-12-20 22:02:12 +00:00
Richard Feldman
4ed0e5160f Inline assistant v2 layout (#22305)
Makes the inline assistant look like @danilo-leal's prototype.

Also fixes the bug with indent guides that @maxdeviant found!

<img width="1059" alt="Screenshot 2024-12-20 at 4 24 56 PM"
src="https://github.com/user-attachments/assets/5e55a3c2-768a-4e9d-bad5-d4ebbe9db9ce"
/>

Release Notes:

- N/A
2024-12-20 22:00:52 +00:00
Kirill Bulatov
72e56eee7a Use stable Alacritty crate version (#22304)
Release Notes:

- N/A
2024-12-20 21:40:22 +00:00
tims
306fc19739 Fix knockout icon background to match hovered entry background in project panel (#22258)
No issue attached, as this is something trivial and should have been
part of my merged PR
[here](https://github.com/zed-industries/zed/pull/22073).

For context, in the above-mentioned PR, I removed the hover color from
selected/marked entries in the Project Panel. The reasoning behind this
change is already explained in that PR, but to reiterate:

> This change was inspired by the behavior in VSCode, the Firefox
sidebar, and Dolphin (KDE File Manager). When an item is selected, it
doesn’t display a separate hover state to avoid confusing users about
whether the item is selected or merely hovered over.

@nilskch mentioned in the comments of the above PR that I should have
also changed the background of the knockout icon, which appears on
entries, to match the entry background color on hover. This PR addresses
that.

Before:

![Screenshot_20241219_230005](https://github.com/user-attachments/assets/fd517e01-1a1e-4e63-9320-ba24bf3d4c64)

After:

![Screenshot_20241219_225853](https://github.com/user-attachments/assets/35fb146f-83e4-409b-8061-0efbdbba5cf9)

Release Notes:

- N/A
2024-12-20 21:24:38 +00:00
Peter Tripp
4fbb568f42 Improve script/bump-zed-minor-version (#22199)
- Follow up to: https://github.com/zed-industries/zed/pull/22170

CC: @mgsloan 

Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2024-12-20 21:02:57 +00:00
Marshall Bowers
7913b6a5a2 assistant2: Restrict directory context picker to development builds (#22302)
This PR temporarily restricts the directory context picker to
development builds until the implementation is finished.

Release Notes:

- N/A
2024-12-20 19:08:29 +00:00
Marshall Bowers
d566792ae1 assistant2: List directories in directory context picker (#22300)
This PR updates the directory context picker in Assistant2 to show the
available directories.

Release Notes:

- N/A
2024-12-20 18:42:36 +00:00
Piotr Osiewicz
62f5ca562e editor: Improve performance of lsp_ext validation (#22299)
We've received a complaint on Discord about bad multicursor performance.
I too run 15k cursor simultaneously. The gist of the issue was in the
lsp_ext; whenever we gather up actions to be registered on a buffer, we
need to know whether a buffer has any of the languages for which we have
LSP extensions. The problem stemed from the fact that we did a two-phase
filtering. For each selection we'd first check whether this selection
lies in a part of a file that is part of a language for which we have
LSP extensions. Then, we'd check whether we're running a language server
of interest for this buffer.

This is not optimal, because it would often do the redundant work:
1. We resolve selections for buffer that are known to not contain a
given language server.
2. We look up language server in the LspStore once per each matching
selection.

In case where the file is not related at all, we end up resolving all of
the selections which is pretty bad. This PR makes us skip buffers which
are known to not match the criteria. It also caches the result of
language server lookup for the buffers.

Closes #ISSUE

Release Notes:

- Improved performance with large quantity of cursors
2024-12-20 18:41:23 +00:00
Piotr Osiewicz
4eb8492308 chore: Remove stray println (#22297)
Follow-up to #22292

Release Notes:

- N/A
2024-12-20 18:18:44 +00:00
Thorsten Ball
d824baeece Fix lang servers status set to Downloading when checking version (#22292)
This message has confused me many times too: we printed the status as
"Downloading" when we were only checking whether we need to install a
given version of a language server.

This fixes the issue for Node-based language servers where we had the
same check in all implementations.

Closes  #22241

Release Notes:

- Fixed some language servers reporting status as "Downloading..." when
only a version check was being done.
2024-12-20 16:59:10 +00:00
Marshall Bowers
8a858fee7c Enable Assistant2 outside of development builds (#22294)
This PR removes the gate that limited Assistant2 to development builds,
so that we can start testing it out in Nightly.

Note that currently this still requires explicit opt-in to the
`assistant2` feature flag.

Release Notes:

- N/A
2024-12-20 16:43:24 +00:00
Alejandro Gómez-Londoño
cbd2e81a7e Add arrow key movements to terminal vi mode (#22103)
Expands #18715

Release Notes:

- Added arrow keys movement to the built-in terminal's [vi
mode](https://github.com/alacritty/alacritty/blob/master/docs/features.md#vi-mode)
(which is using Alacritty under the hood).

Details
--
A minuscule improvement on #18715 to allow user with alternative
keyboard layouts to use the terminal's vi mode with the arrow keys.
2024-12-20 13:49:12 +00:00
Thorsten Ball
b25d8ecb75 vim: Fix deletion with sentence-motion (#22289)
Fixes #22151.

Turns out Vim also has some weird behavior with sentence deletion in
case it's on the first character of a line.

Release Notes:

- vim: Fixed deleting sentence-wise (i.e. `d(` and `d)`), which would
previously delete the whole line instead of just a sentence.
2024-12-20 12:48:38 +00:00
Aaron Feickert
7c03e11cfc Fix blank line cursor width (#22275)
This PR ensures that, for fixed-width fonts, the cursor width is the
same on blank lines as on non-blank lines, as well as at the end of a
line. It does so by using the em advance to define the cursor width
instead of the em width in these cases.

Note that this can look... bizarre on non-fixed-width fonts:
<img width="93" alt="Screenshot 2024-12-19 at 21 43 11"
src="https://github.com/user-attachments/assets/a4c9b26c-98ea-4a1d-947b-51f1acd3c2f8"
/>
However, this is arguably reasonably consistent with how (terminal) Vim
handles it:
<img width="45" alt="Screenshot 2024-12-19 at 21 46 42"
src="https://github.com/user-attachments/assets/ec3ff614-7a15-4cc3-8d14-3d15ce62f2b8"
/>

Closes #22260.

Release Notes:

- N/A
2024-12-20 10:26:27 +00:00
Michael Sloan
f3fc4d6279 Add a CI check for todo! and FIXME comments (#21950)
Motivation for this is to support writing comments that will certainly
be revisited before merge.

Release Notes:

- N/A
2024-12-20 08:38:50 +00:00
tims
e4493d60dc linux: Fix wrong cursor theme for arrow cursor style (#22276)
Closes #22264

On Linux, the arrow cursor style currently used by Zed is `arrow`.
However, this style might not be available in most themes, causing the
cursor to fall back to system default theme. Note cursor style are
platform (X11 and Wayland) agnostic.

Most themes use `left_ptr` as their arrow cursor style instead of
`arrow`. In some cases, `left_ptr` and `arrow` are symlinks pointing to
the `default` style, but the `default` style is not guaranteed to be
available across all themes.

After inspecting the available cursor themes on popular desktop
environments, changing the default from `arrow` to `left_ptr` seems to
be available in all of them. `left_ptr` as default cursor style is also
mentioned in [Arch Wiki: Cursor
themes](https://wiki.archlinux.org/title/Cursor_themes#Change_X_shaped_default_cursor).

KDE:
```sh
tims@lemon /u/s/icons> find . -name "arrow"
./Breeze_Snow/cursors/arrow
./breeze_cursors/cursors/arrow
./Adwaita/cursors/arrow

tims@lemon /u/s/icons> find . -name "default"
./default
./Breeze_Snow/cursors/default
./breeze_cursors/cursors/default
./Adwaita/cursors/default

tims@lemon /u/s/icons> find . -name "left_ptr"
./Oxygen_White/cursors/left_ptr
./KDE_Classic/cursors/left_ptr
./Oxygen_Yellow/cursors/left_ptr
./Oxygen_Blue/cursors/left_ptr
./Oxygen_Black/cursors/left_ptr
./Breeze_Snow/cursors/left_ptr
./breeze_cursors/cursors/left_ptr
./Adwaita/cursors/left_ptr
./Oxygen_Zion/cursors/left_ptr

```

Gnome:
```sh
tims@orange:/usr/share/icons$ find . -name "arrow"
./DMZ-Black/cursors/arrow
./Adwaita/cursors/arrow
./redglass/cursors/arrow
./whiteglass/cursors/arrow
./handhelds/cursors/arrow
./Yaru/cursors/arrow
./DMZ-White/cursors/arrow

tims@orange:/usr/share/icons$ find . -name "default"
./Adwaita/cursors/default
./default
./Yaru/cursors/default

tims@orange:/usr/share/icons$ find . -name "left_ptr"
./DMZ-Black/cursors/left_ptr
./Adwaita/cursors/left_ptr
./redglass/cursors/left_ptr
./whiteglass/cursors/left_ptr
./handhelds/cursors/left_ptr
./Yaru/cursors/left_ptr
./DMZ-White/cursors/left_ptr
```

My theme is set to Oxygen Yellow here.

Before:
<img
src="https://github.com/user-attachments/assets/7485f1e7-5936-45b4-96bd-399525bad95d"
alt="before" width="450px" />

After:
<img
src="https://github.com/user-attachments/assets/56090735-6a1f-4652-ad3e-075ff4c3f9ab"
alt="after" width="450px" />


Release Notes:

- Fixed wrong cursor theme for arrow cursor style on Linux.
2024-12-20 05:57:47 +00:00
107 changed files with 2949 additions and 1128 deletions

View File

@@ -94,6 +94,10 @@ jobs:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Run style checks
uses: ./.github/actions/check_style
@@ -360,6 +364,8 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -406,6 +412,8 @@ jobs:
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

18
Cargo.lock generated
View File

@@ -83,8 +83,9 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=91d034ff8b53867143c005acfaa14609147c9a2c#91d034ff8b53867143c005acfaa14609147c9a2c"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b9241459831fee2f22fcda52ddaf9e449b6627334a0f40f13a1b3344018060"
dependencies = [
"base64 0.22.1",
"bitflags 2.6.0",
@@ -493,6 +494,7 @@ dependencies = [
"project",
"proto",
"rand 0.8.5",
"release_channel",
"rope",
"schemars",
"serde",
@@ -3680,6 +3682,7 @@ dependencies = [
"ctor",
"editor",
"env_logger 0.11.5",
"feature_flags",
"gpui",
"language",
"log",
@@ -5167,8 +5170,12 @@ dependencies = [
"anyhow",
"collections",
"db",
"editor",
"futures 0.3.31",
"git",
"gpui",
"language",
"menu",
"project",
"schemars",
"serde",
@@ -6982,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]]
@@ -7695,6 +7702,7 @@ dependencies = [
"sum_tree",
"text",
"theme",
"tree-sitter",
"util",
]
@@ -13975,6 +13983,8 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"libc",
"log",
"rand 0.8.5",
"regex",
@@ -14979,7 +14989,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]]

View File

@@ -337,7 +337,7 @@ zeta = { path = "crates/zeta" }
#
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91d034ff8b53867143c005acfaa14609147c9a2c" }
alacritty_terminal = "0.24"
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }

View File

@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.7633 4.2078H4.23674L4.3551 5.5189H10.1429L9.99592 6.87645H6.20408L6.33877 8.16255H9.86939L9.66122 9.92379L8 10.3275L6.3102 9.92021L6.20408 8.86633H4.7102L4.87755 10.7955L8 11.6457L11.0694 10.8812L11.7633 4.2078ZM2 2H14L12.9061 12.7818L7.98775 14L3.09388 12.7818L2 2Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.58 2H2.5V12.08C2.5 12.5892 2.70229 13.0776 3.06235 13.4376C3.42242 13.7977 3.91078 14 4.42 14H12.58C13.0892 14 13.5776 13.7977 13.9376 13.4376C14.2977 13.0776 14.5 12.5892 14.5 12.08V3.92C14.5 3.41078 14.2977 2.92242 13.9376 2.56235C13.5776 2.20229 13.0892 2 12.58 2ZM3.358 11.6285C3.34615 12.6668 3.96437 13.2311 4.96636 13.232H4.96621C6.06429 13.2456 6.70951 12.4798 6.63088 11.3867H5.48085C5.4899 11.601 5.47243 11.8974 5.36026 12.0313C5.27992 12.1441 5.16183 12.2005 5.00645 12.2005C4.67402 12.1952 4.50788 11.9534 4.50788 11.4753V9.19488C4.50788 8.94247 4.54407 8.75168 4.61645 8.62283C4.73423 8.38524 5.17961 8.3584 5.34825 8.58663C5.47804 8.71252 5.48974 9.04683 5.48101 9.26757H6.63104C6.66099 8.70582 6.53494 8.10381 6.20079 7.80913C5.65853 7.23521 4.37403 7.26765 3.82039 7.80102C3.51213 8.07495 3.358 8.47525 3.358 9.00159V11.6285ZM7.04116 11.3867C7.01043 12.4573 7.50713 13.2473 8.61739 13.232L8.61723 13.2317C10.1571 13.2967 10.5874 11.592 9.96023 10.4759C9.74995 10.1097 9.16994 9.80702 8.71379 9.62981C8.36155 9.46772 8.21038 9.3086 8.20711 8.92079C8.20711 8.55559 8.35983 8.37291 8.66543 8.37291C8.83688 8.37291 8.95357 8.42939 9.01519 8.54217C9.10317 8.6754 9.12454 9.0409 9.11565 9.26742H10.1612C10.1866 8.71627 10.0554 8.11739 9.75509 7.81303C9.26822 7.22257 7.99791 7.24909 7.5115 7.82504C7.0109 8.29179 6.97783 9.4437 7.3346 9.96848C7.49278 10.205 7.75143 10.409 8.1107 10.5809C8.15897 10.6051 8.21552 10.6314 8.27633 10.6598C8.53247 10.7792 8.86416 10.9338 8.97119 11.1046C9.16073 11.3241 9.13593 11.8913 9.00333 12.0877C8.9336 12.1952 8.81285 12.2489 8.64141 12.2489C8.25703 12.2785 8.09666 11.8534 8.12677 11.3867H7.04116ZM10.5474 11.3867C10.5167 12.4573 11.0134 13.2473 12.1236 13.232L12.1235 13.2317C13.6634 13.2967 14.0936 11.592 13.4665 10.4759C13.2562 10.1097 12.6762 9.80702 12.2201 9.62981C11.8678 9.46772 11.7166 9.3086 11.7134 8.92079C11.7134 8.55559 11.8661 8.37291 12.1717 8.37291C12.3431 8.37291 12.4598 8.42939 12.5214 8.54217C12.6094 8.6754 12.6308 9.0409 12.6219 9.26742H13.6674C13.6928 8.71627 13.5617 8.11739 13.2614 7.81303C12.7745 7.22257 11.5042 7.24909 11.0178 7.82504C10.5172 8.29179 10.4841 9.4437 10.8409 9.96848C10.999 10.205 11.2577 10.409 11.617 10.5809C11.6652 10.6051 11.7218 10.6314 11.7826 10.6598C12.0387 10.7792 12.3704 10.9338 12.4775 11.1046C12.667 11.3241 12.6422 11.8913 12.5096 12.0877C12.4399 12.1952 12.3191 12.2489 12.1477 12.2489C11.7633 12.2785 11.6029 11.8534 11.633 11.3867H10.5474Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -448,7 +448,6 @@
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
"cmd-shift-s": "workspace::SaveAs",
"cmd-n": "workspace::NewFile",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"cmd-1": ["workspace::ActivatePane", 0],
@@ -495,6 +494,7 @@
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "workspace::NewFile",
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
@@ -761,6 +761,7 @@
"cmd-v": "terminal::Paste",
"cmd-a": "editor::SelectAll",
"cmd-k": "terminal::Clear",
"cmd-n": "workspace::NewTerminal",
"ctrl-enter": "assistant::InlineAssist",
// Some nice conveniences
"cmd-backspace": ["terminal::SendText", "\u0015"],

View File

@@ -389,6 +389,9 @@
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
// Subword TextObject
// "w": "vim::Subword",
// "shift-w": ["vim::Subword", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",

View File

@@ -1556,6 +1556,7 @@ impl ContextEditor {
let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);

View File

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

@@ -3,8 +3,9 @@ use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
Subscription, TextStyleRefinement, View, WeakView,
list, AbsoluteLength, AnyElement, AppContext, CornersRefinement, DefiniteLength,
EdgesRefinement, Empty, Length, ListAlignment, ListState, Model, StyleRefinement, Subscription,
TextStyleRefinement, View, WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
@@ -89,10 +90,11 @@ impl ActiveThread {
self.list_state.splice(old_len..old_len, 1);
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
@@ -105,6 +107,32 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
margin: EdgesRefinement {
top: Some(Length::Definite(rems(0.5).into())),
left: Some(Length::Definite(rems(0.).into())),
right: Some(Length::Definite(rems(0.).into())),
bottom: Some(Length::Definite(rems(1.).into())),
},
padding: EdgesRefinement {
top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
},
background: Some(colors.editor_foreground.opacity(0.01).into()),
border_color: Some(colors.border_variant.opacity(0.25)),
border_widths: EdgesRefinement {
top: Some(AbsoluteLength::Pixels(Pixels(1.0))),
left: Some(AbsoluteLength::Pixels(Pixels(1.))),
right: Some(AbsoluteLength::Pixels(Pixels(1.))),
bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
},
corner_radii: CornersRefinement {
top_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
top_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
bottom_right: Some(AbsoluteLength::Pixels(Pixels(2.))),
bottom_left: Some(AbsoluteLength::Pixels(Pixels(2.))),
},
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
@@ -114,8 +142,8 @@ impl ActiveThread {
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(ui_font_size.into()),
background_color: Some(cx.theme().colors().editor_background),
font_size: Some(buffer_font_size.into()),
background_color: Some(colors.editor_foreground.opacity(0.01)),
..Default::default()
},
..Default::default()
@@ -204,11 +232,12 @@ impl ActiveThread {
};
let context = self.thread.read(cx).context_for_message(message_id);
let colors = cx.theme().colors();
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
let (role_icon, role_name, role_color) = match message.role {
Role::User => (IconName::Person, "You", Color::Muted),
Role::Assistant => (IconName::ZedAssistant, "Assistant", Color::Accent),
Role::System => (IconName::Settings, "System", Color::Default),
};
div()
@@ -218,31 +247,35 @@ impl ActiveThread {
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.border_color(colors.border_variant)
.bg(colors.editor_background)
.rounded_md()
.child(
h_flex()
.justify_between()
.py_1()
.px_2()
.py_1p5()
.px_2p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.border_color(colors.border_variant)
.justify_between()
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(role_icon)
.size(IconSize::XSmall)
.color(Color::Muted),
.color(role_color),
)
.child(Label::new(role_name).size(LabelSize::XSmall)),
.child(
Label::new(role_name)
.size(LabelSize::XSmall)
.color(role_color),
),
),
)
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
.child(v_flex().p_2p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
h_flex().flex_wrap().gap_1().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),

View File

@@ -1,4 +1,5 @@
mod active_thread;
mod assistant_model_selector;
mod assistant_panel;
mod assistant_settings;
mod buffer_codegen;

View File

@@ -0,0 +1,85 @@
use fs::Fs;
use gpui::View;
use language_model::LanguageModelRegistry;
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::update_settings_file;
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
use crate::{assistant_settings::AssistantSettings, ToggleModelSelector};
pub struct AssistantModelSelector {
selector: View<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
}
impl AssistantModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
cx: &mut WindowContext,
) -> Self {
Self {
selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
}),
menu_handle,
}
}
}
impl Render for AssistantModelSelector {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.selector.focus_handle(cx).clone();
LanguageModelSelectorPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.menu_handle.clone())
}
}

View File

@@ -10,6 +10,7 @@ use gpui::{
WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use release_channel::ReleaseChannel;
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
@@ -50,23 +51,27 @@ impl ContextPicker {
confirm_behavior: ConfirmBehavior,
cx: &mut ViewContext<Self>,
) -> Self {
let mut entries = vec![
ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
},
ContextPickerEntry {
let mut entries = Vec::new();
entries.push(ContextPickerEntry {
name: "File".into(),
kind: ContextKind::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,
},
ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
},
];
});
}
entries.push(ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
});
if thread_store.is_some() {
entries.push(ContextPickerEntry {

View File

@@ -1,6 +1,8 @@
// TODO: Remove this once we've implemented the functionality.
// TODO: Remove this when we finish the implementation.
#![allow(unused)]
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
@@ -11,6 +13,7 @@ use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
@@ -75,6 +78,65 @@ impl DirectoryContextPickerDelegate {
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let directory_matches = project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<str> = worktree.root_name().into();
worktree.directories(false, 0).map(move |entry| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: true,
})
});
Task::ready(directory_matches.collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Directories,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for DirectoryContextPickerDelegate {
@@ -88,7 +150,7 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
@@ -96,17 +158,67 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
"Search folders…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
// TODO: Implement this once we fix the issues with the file context picker.
Task::ready(())
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(|this, mut cx| async move {
let mut paths = search_task.await;
let empty_path = Path::new("");
paths.retain(|path_match| path_match.path.as_ref() != empty_path);
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
// TODO: Implement this once we fix the issues with the file context picker.
match self.confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => self.dismissed(cx),
}
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
let confirm_behavior = self.confirm_behavior;
cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |this, cx| {
let mut text = String::new();
// TODO: Add the files from the selected directory.
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.insert_context(
ContextKind::Directory,
path.to_string_lossy().to_string(),
text,
);
})?;
match confirm_behavior {
ConfirmBehavior::KeepOpen => {}
ConfirmBehavior::Close => this.delegate.dismissed(cx),
}
anyhow::Ok(())
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
@@ -120,10 +232,18 @@ impl PickerDelegate for DirectoryContextPickerDelegate {
fn render_match(
&self,
_ix: usize,
_selected: bool,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
None
let path_match = &self.matches[ix];
let directory_name = path_match.path.to_string_lossy().to_string();
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(h_flex().gap_2().child(Label::new(directory_name))),
)
}
}

View File

@@ -192,7 +192,9 @@ impl PickerDelegate for FileContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let mat = &self.matches[self.selected_index];
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let workspace = self.workspace.clone();
let Some(project) = workspace

View File

@@ -154,7 +154,9 @@ impl PickerDelegate for ThreadContextPickerDelegate {
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let entry = &self.matches[self.selected_index];
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
let Some(thread_store) = self.thread_store.upgrade() else {
return;
@@ -216,7 +218,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(thread.summary.clone()),
.child(Label::new(thread.summary.clone())),
)
}
}

View File

@@ -1,13 +1,12 @@
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::terminal_codegen::TerminalCodegen;
use crate::thread_store::ThreadStore;
use crate::ToggleContextPicker;
use crate::{
assistant_settings::AssistantSettings, CycleNextInlineAssist, CyclePreviousInlineAssist,
};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{ToggleContextPicker, ToggleModelSelector};
use client::ErrorExt;
use collections::VecDeque;
use editor::{
@@ -22,9 +21,9 @@ use gpui::{
WeakModel, WeakView, WindowContext,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
use parking_lot::Mutex;
use settings::{update_settings_file, Settings};
use settings::Settings;
use std::cmp;
use std::sync::Arc;
use theme::ThemeSettings;
@@ -39,7 +38,8 @@ pub struct PromptEditor<T> {
mode: PromptEditorMode,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
model_selector: View<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -56,7 +56,7 @@ impl<T: 'static> Render for PromptEditor<T> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let mut buttons = Vec::new();
let spacing = match &self.mode {
let left_gutter_spacing = match &self.mode {
PromptEditorMode::Buffer {
id: _,
codegen,
@@ -72,23 +72,36 @@ impl<T: 'static> Render for PromptEditor<T> {
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
}
PromptEditorMode::Terminal { .. } => Pixels::ZERO,
PromptEditorMode::Terminal { .. } => {
// Give the equivalent of the same left-padding that we're using on the right
Pixels::from(40.0)
}
};
let bottom_padding = match &self.mode {
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
};
buttons.extend(self.render_buttons(cx));
v_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.gap_0p5()
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.py(cx.line_height() / 2.5)
.pt_0p5()
.pb(bottom_padding)
.pr_6()
.child(
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.items_start()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
@@ -97,30 +110,11 @@ impl<T: 'static> Render for PromptEditor<T> {
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.w(spacing)
.h_full()
.w(left_gutter_spacing)
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.child(self.render_close_button(cx))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen_status(cx) else {
return el;
@@ -172,13 +166,24 @@ impl<T: 'static> Render for PromptEditor<T> {
}
}),
)
.child(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_2().pr_6().children(buttons)),
.child(
h_flex()
.w_full()
.justify_between()
.child(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_1().children(buttons)),
),
)
.child(
h_flex()
.child(h_flex().w(spacing).justify_center().gap_2())
.child(self.context_strip.clone()),
h_flex().child(div().w(left_gutter_spacing)).child(
h_flex()
.w_full()
.pl_1()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.child(self.model_selector.clone()),
),
)
}
}
@@ -311,6 +316,10 @@ impl<T: 'static> PromptEditor<T> {
self.context_picker_menu_handle.toggle(cx);
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.model_selector_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen_status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
@@ -400,73 +409,45 @@ impl<T: 'static> PromptEditor<T> {
match codegen_status {
CodegenStatus::Idle => {
vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
)
.into_any_element(),
Button::new("start", mode.start_label())
.icon(IconName::Return)
.icon_color(Color::Muted)
.on_click(
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
)
.into_any_element(),
]
vec![Button::new("start", mode.start_label())
.label_size(LabelSize::Small)
.icon(IconName::Return)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
.into_any_element()]
}
CodegenStatus::Pending => vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Cancel Assist", cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element(),
IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::with_meta(
mode.tooltip_interrupt(),
Some(&menu::Cancel),
"Changes won't be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element(),
],
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
.icon_color(Color::Error)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::with_meta(
mode.tooltip_interrupt(),
Some(&menu::Cancel),
"Changes won't be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
.into_any_element()],
CodegenStatus::Done | CodegenStatus::Error(_) => {
let cancel = IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element();
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
if has_error || self.edited_since_done {
vec![
cancel,
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::with_meta(
mode.tooltip_restart(),
Some(&menu::Confirm),
"Changes will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element(),
]
vec![IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::with_meta(
mode.tooltip_restart(),
Some(&menu::Confirm),
"Changes will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element()]
} else {
let accept = IconButton::new("accept", IconName::Check)
.icon_color(Color::Info)
@@ -482,7 +463,6 @@ impl<T: 'static> PromptEditor<T> {
match &self.mode {
PromptEditorMode::Terminal { .. } => vec![
accept,
cancel,
IconButton::new("confirm", IconName::Play)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
@@ -498,7 +478,7 @@ impl<T: 'static> PromptEditor<T> {
}))
.into_any_element(),
],
PromptEditorMode::Buffer { .. } => vec![accept, cancel],
PromptEditorMode::Buffer { .. } => vec![accept],
}
}
}
@@ -527,6 +507,15 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn render_close_button(&self, cx: &ViewContext<Self>) -> AnyElement {
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| Tooltip::text("Close Assistant", cx))
.on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
.into_any_element()
}
fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &ViewContext<Self>) -> AnyElement {
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
@@ -690,20 +679,18 @@ impl<T: 'static> PromptEditor<T> {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
v_flex()
div()
.key_context("MessageEditor")
.size_full()
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
@@ -795,6 +782,7 @@ impl PromptEditor<BufferCodegen> {
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
@@ -809,19 +797,10 @@ impl PromptEditor<BufferCodegen> {
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
}),
model_selector_menu_handle,
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
@@ -942,6 +921,7 @@ impl PromptEditor<TerminalCodegen> {
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
editor: prompt_editor.clone(),
@@ -956,19 +936,10 @@ impl PromptEditor<TerminalCodegen> {
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
}),
model_selector_menu_handle,
edited_since_done: false,
prompt_history,
prompt_history_ix: None,

View File

@@ -7,17 +7,17 @@ use gpui::{
WeakView,
};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_model_selector::LanguageModelSelector;
use rope::Point;
use settings::{update_settings_file, Settings};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, Tooltip,
prelude::*, ButtonLike, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
SwitchWithLabel,
};
use workspace::Workspace;
use crate::assistant_settings::AssistantSettings;
use crate::assistant_model_selector::AssistantModelSelector;
use crate::context_picker::{ConfirmBehavior, ContextPicker};
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
@@ -33,8 +33,8 @@ pub struct MessageEditor {
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
inline_context_picker: View<ContextPicker>,
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector: View<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
_subscriptions: Vec<Subscription>,
}
@@ -50,6 +50,7 @@ impl MessageEditor {
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_picker_menu_handle = PopoverMenuHandle::default();
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(10, cx);
@@ -92,27 +93,17 @@ impl MessageEditor {
context_picker_menu_handle,
inline_context_picker,
inline_context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
model_selector: cx.new_view(|cx| {
AssistantModelSelector::new(fs, model_selector_menu_handle.clone(), cx)
}),
language_model_selector_menu_handle: PopoverMenuHandle::default(),
model_selector_menu_handle,
use_tools: false,
_subscriptions: subscriptions,
}
}
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.language_model_selector_menu_handle.toggle(cx);
self.model_selector_menu_handle.toggle(cx)
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
@@ -203,50 +194,6 @@ impl MessageEditor {
let editor_focus_handle = self.editor.focus_handle(cx);
cx.focus(&editor_focus_handle);
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone())
}
}
impl FocusableView for MessageEditor {
@@ -309,7 +256,7 @@ impl Render for MessageEditor {
.child(
h_flex()
.justify_between()
.child(CheckboxWithLabel::new(
.child(SwitchWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
@@ -321,22 +268,19 @@ impl Render for MessageEditor {
}),
))
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
h_flex().gap_1().child(self.model_selector.clone()).child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
),
)
}

View File

@@ -39,10 +39,17 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui(cx);
let element = h_flex()
.id("breadcrumb-container")
.flex_grow()
.overflow_x_scroll()
.text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
return element;
};
let Some(mut segments) = active_item.breadcrumbs(cx.theme(), cx) else {
return element;
};
@@ -52,6 +59,7 @@ impl Render for Breadcrumbs {
prefix_end_ix,
segments.len().saturating_sub(MAX_SEGMENTS / 2),
);
if suffix_start_ix > prefix_end_ix {
segments.splice(
prefix_end_ix..suffix_start_ix,
@@ -82,6 +90,7 @@ impl Render for Breadcrumbs {
});
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
@@ -102,14 +111,14 @@ impl Render for Breadcrumbs {
if let Some(editor) = editor.upgrade() {
let focus_handle = editor.read(cx).focus_handle(cx);
Tooltip::for_action_in(
"Show symbol outline",
"Show Symbol Outline",
&editor::actions::ToggleOutline,
&focus_handle,
cx,
)
} else {
Tooltip::for_action(
"Show symbol outline",
"Show Symbol Outline",
&editor::actions::ToggleOutline,
cx,
)

View File

@@ -18,6 +18,12 @@ use std::{
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
use {
std::io::IsTerminal,
util::{load_login_shell_environment, load_shell_from_passwd, ResultExt},
};
struct Detect;
trait InstalledApp {
@@ -161,7 +167,16 @@ fn main() -> Result<()> {
None
};
// On Linux, desktop entry uses `cli` to spawn `zed`, so we need to load env vars from the shell
// since it doesn't inherit env vars from the terminal.
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if !std::io::stdout().is_terminal() {
load_shell_from_passwd().log_err();
load_login_shell_environment().log_err();
}
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];

View File

@@ -252,7 +252,7 @@ spec:
value: "${AUTO_JOIN_CHANNEL_ID}"
securityContext:
capabilities:
# FIXME - Switch to the more restrictive `PERFMON` capability.
# TODO - Switch to the more restrictive `PERFMON` capability.
# This capability isn't yet available in a stable version of Debian.
add: ["SYS_ADMIN"]
terminationGracePeriodSeconds: 10

View File

@@ -279,6 +279,7 @@ pub async fn post_panic(
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let incident_id = uuid::Uuid::new_v4().to_string();
let panic = report.panic;
if panic.os_name == "Linux" && panic.os_version == Some("1.0.0".to_string()) {
@@ -288,11 +289,37 @@ pub async fn post_panic(
))?;
}
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
let response = blob_store_client
.head_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.send()
.await;
if response.is_ok() {
log::info!("We've already uploaded this crash");
return Ok(());
}
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
tracing::error!(
service = "client",
version = %panic.app_version,
os_name = %panic.os_name,
os_version = %panic.os_version.clone().unwrap_or_default(),
incident_id = %incident_id,
installation_id = %panic.installation_id.clone().unwrap_or_default(),
description = %panic.payload,
backtrace = %panic.backtrace.join("\n"),
@@ -331,10 +358,19 @@ pub async fn post_panic(
panic.app_version
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*OS:*\n{} {}",
"*{} {}:*\n<https://{}.{}/{}.json|{}…>",
panic.os_name,
panic.os_version.unwrap_or_default()
panic.os_version.unwrap_or_default(),
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
@@ -361,6 +397,12 @@ pub async fn post_panic(
}
fn report_to_slack(panic: &Panic) -> bool {
// Panics on macOS should make their way to Slack as a crash report,
// so we don't need to send them a second time via this channel.
if panic.os_name == "macOS" {
return false;
}
if panic.payload.contains("ERROR_SURFACE_LOST_KHR") {
return false;
}

View File

@@ -18,6 +18,7 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
feature_flags.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -14,6 +14,7 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, div, svg, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, Global, HighlightStyle, InteractiveElement, IntoElement, Model, ParentElement,
@@ -21,7 +22,8 @@ use gpui::{
WeakView, WindowContext,
};
use language::{
Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
Point, Selection, SelectionGoal, ToTreeSitterPoint,
};
use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath};
@@ -29,9 +31,10 @@ use project_diagnostics_settings::ProjectDiagnosticsSettings;
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp,
cmp::Ordering,
mem,
ops::Range,
ops::{Range, RangeInclusive},
sync::Arc,
time::Duration,
};
@@ -422,31 +425,28 @@ impl ProjectDiagnosticsEditor {
blocks: Default::default(),
block_count: 0,
};
let mut pending_range: Option<(Range<Point>, usize)> = None;
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
let mut is_first_excerpt_for_group = true;
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
if let Some((range, start_ix)) = &mut pending_range {
if let Some(entry) = resolved_entry.as_ref() {
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
range.end = range.end.max(entry.range.end);
let expanded_range = resolved_entry.as_ref().map(|entry| {
context_range_for_entry(entry, self.context, &snapshot, cx)
});
if let Some((range, context_range, start_ix)) = &mut pending_range {
if let Some(expanded_range) = expanded_range.clone() {
// If the entries are overlapping or next to each-other, merge them into one excerpt.
if context_range.end.row + 1 >= expanded_range.start.row {
context_range.end = context_range.end.max(expanded_range.end);
continue;
}
}
let excerpt_start =
Point::new(range.start.row.saturating_sub(self.context), 0);
let excerpt_end = snapshot.clip_point(
Point::new(range.end.row + self.context, u32::MAX),
Bias::Left,
);
let excerpt_id = excerpts
.insert_excerpts_after(
prev_excerpt_id,
buffer.clone(),
[ExcerptRange {
context: excerpt_start..excerpt_end,
context: context_range.clone(),
primary: Some(range.clone()),
}],
cx,
@@ -503,8 +503,9 @@ impl ProjectDiagnosticsEditor {
pending_range.take();
}
if let Some(entry) = resolved_entry {
pending_range = Some((entry.range.clone(), ix));
if let Some(entry) = resolved_entry.as_ref() {
let range = entry.range.clone();
pending_range = Some((range, expanded_range.unwrap(), ix));
}
}
@@ -923,3 +924,169 @@ fn compare_diagnostics(
})
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
}
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
fn context_range_for_entry(
entry: &DiagnosticEntry<Point>,
context: u32,
snapshot: &BufferSnapshot,
cx: &AppContext,
) -> Range<Point> {
if cx.is_staff() {
if let Some(rows) = heuristic_syntactic_expand(
entry.range.clone(),
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
snapshot,
cx,
) {
return Range {
start: Point::new(*rows.start(), 0),
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
};
}
}
Range {
start: Point::new(entry.range.start.row.saturating_sub(context), 0),
end: snapshot.clip_point(
Point::new(entry.range.end.row + context, u32::MAX),
Bias::Left,
),
}
}
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
/// to the specified `max_row_count`.
///
/// If there is a containing outline item that is less than `max_row_count`, it will be returned.
/// Otherwise fairly arbitrary heuristics are applied to attempt to return a logical block of code.
fn heuristic_syntactic_expand<'a>(
input_range: Range<Point>,
max_row_count: u32,
snapshot: &'a BufferSnapshot,
cx: &'a AppContext,
) -> Option<RangeInclusive<BufferRow>> {
let input_row_count = input_range.end.row - input_range.start.row;
if input_row_count > max_row_count {
return None;
}
// If the outline node contains the diagnostic and is small enough, just use that.
let outline_range = snapshot.outline_range_containing(input_range.clone());
if let Some(outline_range) = outline_range.clone() {
// Remove blank lines from start and end
if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
{
if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
.rev()
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
{
let row_count = end_row.saturating_sub(start_row);
if row_count <= max_row_count {
return Some(RangeInclusive::new(
outline_range.start.row,
outline_range.end.row,
));
}
}
}
}
let mut node = snapshot.syntax_ancestor(input_range.clone())?;
loop {
let node_start = Point::from_ts_point(node.start_position());
let node_end = Point::from_ts_point(node.end_position());
let node_range = node_start..node_end;
let row_count = node_end.row - node_start.row + 1;
// Stop if we've exceeded the row count or reached an outline node. Then, find the interval
// of node children which contains the query range. For example, this allows just returning
// the header of a declaration rather than the entire declaration.
if row_count > max_row_count || outline_range == Some(node_range.clone()) {
let mut cursor = node.walk();
let mut included_child_start = None;
let mut included_child_end = None;
let mut previous_end = node_start;
if cursor.goto_first_child() {
loop {
let child_node = cursor.node();
let child_range = previous_end..Point::from_ts_point(child_node.end_position());
if included_child_start.is_none() && child_range.contains(&input_range.start) {
included_child_start = Some(child_range.start);
}
if child_range.contains(&input_range.end) {
included_child_end = Some(child_range.end);
}
previous_end = child_range.end;
if !cursor.goto_next_sibling() {
break;
}
}
}
let end = included_child_end.unwrap_or(node_range.end);
if let Some(start) = included_child_start {
let row_count = end.row - start.row;
if row_count < max_row_count {
return Some(RangeInclusive::new(start.row, end.row));
}
}
log::info!(
"Expanding to ancestor started on {} node exceeding row limit of {max_row_count}.",
node.grammar_name()
);
return None;
}
let node_name = node.grammar_name();
let node_row_range = RangeInclusive::new(node_range.start.row, node_range.end.row);
if node_name.ends_with("block") {
return Some(node_row_range);
} else if node_name.ends_with("statement") || node_name.ends_with("declaration") {
// Expand to the nearest dedent or blank line for statements and declarations.
let tab_size = snapshot.settings_at(node_range.start, cx).tab_size.get();
let indent_level = snapshot
.line_indent_for_row(node_range.start.row)
.len(tab_size);
let rows_remaining = max_row_count.saturating_sub(row_count);
let Some(start_row) = (node_range.start.row.saturating_sub(rows_remaining)
..node_range.start.row)
.rev()
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
else {
return Some(node_row_range);
};
let rows_remaining = max_row_count.saturating_sub(node_range.end.row - start_row);
let Some(end_row) = (node_range.end.row + 1
..cmp::min(
node_range.end.row + rows_remaining + 1,
snapshot.row_count(),
))
.find(|row| is_line_blank_or_indented_less(indent_level, *row, tab_size, snapshot))
else {
return Some(node_row_range);
};
return Some(RangeInclusive::new(start_row, end_row));
}
// TODO: doing this instead of walking a cursor as that doesn't work - why?
let Some(parent) = node.parent() else {
log::info!(
"Expanding to ancestor reached the top node, so using default context line count.",
);
return None;
};
node = parent;
}
}
fn is_line_blank_or_indented_less(
indent_level: u32,
row: u32,
tab_size: u32,
snapshot: &BufferSnapshot,
) -> bool {
let line_indent = snapshot.line_indent_for_row(row);
line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level
}

View File

@@ -1,11 +1,8 @@
use std::cell::RefCell;
use std::{cmp::Reverse, ops::Range, rc::Rc};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
ViewContext, WeakView,
Model, ScrollStrategy, SharedString, Size, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
@@ -13,6 +10,13 @@ use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use project::{CodeAction, Completion, TaskSourceKind};
use std::{
cell::RefCell,
cmp::{min, Reverse},
iter,
ops::Range,
rc::Rc,
};
use task::ResolvedTask;
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
use util::ResultExt;
@@ -26,7 +30,10 @@ use crate::{
};
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
pub const MAX_COMPLETIONS_ASIDE_WIDTH: Pixels = px(500.);
pub const MENU_GAP: Pixels = px(4.);
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
pub enum CodeContextMenu {
Completions(CompletionsMenu),
@@ -127,14 +134,12 @@ impl CodeContextMenu {
pub fn render_aside(
&self,
style: &EditorStyle,
max_height: Pixels,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
match self {
CodeContextMenu::Completions(menu) => {
menu.render_aside(style, max_height, workspace, cx)
}
CodeContextMenu::Completions(menu) => menu.render_aside(style, max_size, workspace, cx),
CodeContextMenu::CodeActions(_) => None,
}
}
@@ -158,6 +163,7 @@ pub struct CompletionsMenu {
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
}
#[derive(Clone, Debug)]
@@ -193,6 +199,7 @@ impl CompletionsMenu {
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
}
}
@@ -249,6 +256,7 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
}
}
@@ -257,11 +265,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(0, provider, cx);
}
fn select_prev(
@@ -269,15 +273,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.entries.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.prev_match_index(), provider, cx);
}
fn select_next(
@@ -285,15 +281,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.entries.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.next_match_index(), provider, cx);
}
fn select_last(
@@ -301,11 +289,38 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.entries.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
self.update_selection_index(self.entries.len() - 1, provider, cx);
}
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.entries.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.entries.len() {
self.selected_item + 1
} else {
0
}
}
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
@@ -330,7 +345,7 @@ impl CompletionsMenu {
}
}
pub fn resolve_selected_completion(
pub fn resolve_visible_completions(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
@@ -342,24 +357,76 @@ impl CompletionsMenu {
return;
};
match &self.entries[self.selected_item] {
CompletionEntry::Match(entry) => {
let completion_index = entry.candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.borrow().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let entry_range = if self.selected_item == 0 {
0..min(visible_count, self.entries.len())
} else if self.selected_item == self.entries.len() - 1 {
self.entries.len().saturating_sub(visible_count)..self.entries.len()
} else {
last_rendered_range.map_or(0..0, |range| {
min(range.start, self.entries.len())..min(range.end, self.entries.len())
})
};
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
.detach();
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let entry_indices = util::iterate_expanded_and_wrapped_usize_range(
entry_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.entries.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let completions = self.completions.borrow();
let candidate_ids = entry_indices
.flat_map(|i| Self::entry_candidate_id(&self.entries[i]))
.filter(|i| completions[*i].documentation.is_none());
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let candidate_ids = match Self::entry_candidate_id(&self.entries[self.selected_item]) {
None => candidate_ids.collect::<Vec<usize>>(),
Some(selected_candidate_id) => iter::once(selected_candidate_id)
.chain(candidate_ids.filter(|id| *id != selected_candidate_id))
.collect::<Vec<usize>>(),
};
if candidate_ids.is_empty() {
return;
}
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
candidate_ids,
self.completions.clone(),
cx,
);
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
}
CompletionEntry::InlineCompletionHint { .. } => {}
})
.detach();
}
fn entry_candidate_id(entry: &CompletionEntry) -> Option<usize> {
match entry {
CompletionEntry::Match(entry) => Some(entry.candidate_id),
CompletionEntry::InlineCompletionHint { .. } => None,
}
}
@@ -408,12 +475,14 @@ impl CompletionsMenu {
let selected_item = self.selected_item;
let completions = self.completions.clone();
let matches = self.entries.clone();
let last_rendered_range = self.last_rendered_range.clone();
let style = style.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.borrow_mut().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.borrow_mut();
@@ -545,7 +614,7 @@ impl CompletionsMenu {
fn render_aside(
&self,
style: &EditorStyle,
max_height: Pixels,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
@@ -595,10 +664,9 @@ impl CompletionsMenu {
.child(
multiline_docs
.id("multiline_docs")
.max_h(max_height)
.px_2()
.min_w(px(260.))
.max_w(MAX_COMPLETIONS_ASIDE_WIDTH)
.px(MENU_ASIDE_X_PADDING / 2.)
.max_w(max_size.width)
.max_h(max_size.height)
.overflow_y_scroll()
.occlude(),
)

View File

@@ -128,6 +128,7 @@ use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
};
use project::{
buffer_store::BufferChangeSet,
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
@@ -605,6 +606,7 @@ pub struct Editor {
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
show_scrollbars: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -1234,6 +1236,7 @@ impl Editor {
project,
blink_manager: blink_manager.clone(),
show_local_selections: true,
show_scrollbars: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
@@ -3742,7 +3745,7 @@ impl Editor {
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
if editor.show_inline_completions_in_menu(cx) {
if let Some(hint) = editor.inline_completion_menu_hint(cx) {
@@ -5130,14 +5133,14 @@ impl Editor {
fn render_context_menu_aside(
&self,
style: &EditorStyle,
max_height: Pixels,
max_size: Size<Pixels>,
cx: &mut ViewContext<Editor>,
) -> Option<AnyElement> {
self.context_menu.borrow().as_ref().and_then(|menu| {
if menu.visible() {
menu.render_aside(
style,
max_height,
max_size,
self.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
@@ -8774,9 +8777,10 @@ impl Editor {
.map(|selection| {
let old_range = selection.start..selection.end;
let mut new_range = old_range.clone();
while let Some(containing_range) =
buffer.range_for_syntax_ancestor(new_range.clone())
let mut new_node = None;
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
{
new_node = Some(node);
new_range = containing_range;
if !display_map.intersects_fold(new_range.start)
&& !display_map.intersects_fold(new_range.end)
@@ -8785,6 +8789,17 @@ impl Editor {
}
}
if let Some(node) = new_node {
// Log the ancestor, to support using this action as a way to explore TreeSitter
// nodes. Parent and grandparent are also logged because this operation will not
// visit nodes that have the same range as their parent.
log::info!("Node: {node:?}");
let parent = node.parent();
log::info!("Parent: {parent:?}");
let grandparent = parent.and_then(|x| x.parent());
log::info!("Grandparent: {grandparent:?}");
}
selected_larger_node |= new_range != old_range;
Selection {
id: selection.id,
@@ -11247,6 +11262,11 @@ impl Editor {
cx.notify();
}
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut ViewContext<Self>) {
self.show_scrollbars = show_scrollbars;
cx.notify();
}
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
@@ -12938,6 +12958,14 @@ impl Editor {
.and_then(|item| item.to_any().downcast_ref::<T>())
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Self>,
) {
self.diff_map.add_change_set(change_set, cx);
}
fn character_size(&self, cx: &mut ViewContext<Self>) -> gpui::Point<Pixels> {
let text_layout_details = self.text_layout_details(cx);
let style = &text_layout_details.editor_style;

View File

@@ -25,14 +25,18 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use unindent::Unindent;
use util::{
@@ -10787,6 +10791,62 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -10803,138 +10863,15 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
items,
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10951,6 +10888,21 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
@@ -10959,39 +10911,56 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
match menu.as_ref().expect("should have the completions menu") {
CodeContextMenu::Completions(completions_menu) => {
assert_eq!(
completion_menu_entries(&completions_menu.entries),
vec!["Some(2)", "vec![2]"]
completions_menu
.entries
.iter()
.flat_map(|c| match c {
CompletionEntry::Match(mat) => Some(mat.string.clone()),
_ => None,
})
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
);
}
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_first(&ContextMenuFirst, cx);
editor.context_menu_prev(&ContextMenuPrev, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
);
resolved_items.lock().clear();
}
#[gpui::test]

View File

@@ -1,6 +1,6 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
code_context_menus::{CodeActionsMenu, MAX_COMPLETIONS_ASIDE_WIDTH},
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
},
@@ -1032,6 +1032,7 @@ impl EditorElement {
scroll_pixel_position: gpui::Point<Pixels>,
line_height: Pixels,
em_width: Pixels,
em_advance: Pixels,
autoscroll_containing_element: bool,
cx: &mut WindowContext,
) -> Vec<CursorLayout> {
@@ -1058,7 +1059,7 @@ impl EditorElement {
let mut block_width =
cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
if block_width == Pixels::ZERO {
block_width = em_width;
block_width = em_advance;
}
let block_text = if let CursorShape::Block = selection.cursor_shape {
snapshot
@@ -1191,12 +1192,13 @@ impl EditorElement {
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let show_scrollbars = match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
// Git
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
let show_scrollbars = self.editor.read(cx).show_scrollbars
&& match scrollbar_settings.show {
ShowScrollbar::Auto => {
let editor = self.editor.read(cx);
let is_singleton = editor.is_singleton(cx);
// Git
(is_singleton && scrollbar_settings.git_diff && !snapshot.diff_map.is_empty())
||
// Buffer Search Results
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
@@ -1212,11 +1214,11 @@ impl EditorElement {
||
// Scrollmanager
editor.scroll_manager.scrollbars_visible()
}
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
}
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
ShowScrollbar::Always => true,
ShowScrollbar::Never => false,
};
let axes: AxisPair<bool> = scrollbar_settings.axes.into();
@@ -2929,6 +2931,11 @@ impl EditorElement {
}
};
let viewport_bounds = Bounds::new(Default::default(), cx.viewport_size()).extend(Edges {
right: -Self::SCROLLBAR_WIDTH - MENU_GAP,
..Default::default()
});
// If the context menu's max height won't fit below, then flip it above the line and display
// it in reverse order. If the available space above is less than below.
let unconstrained_max_height = line_height * 12. + POPOVER_Y_PADDING;
@@ -2950,7 +2957,7 @@ impl EditorElement {
// If less than 3 lines fit within the text bounds, instead fit within the window.
if height < min_height {
let available_above = bottom_y_when_flipped;
let available_below = cx.viewport_size().height - target_position.y;
let available_below = viewport_bounds.bottom() - target_position.y;
if available_below > 3. * line_height {
y_is_flipped = false;
height = min_height;
@@ -2968,6 +2975,7 @@ impl EditorElement {
let max_height_in_lines = ((height - POPOVER_Y_PADDING) / line_height).floor() as u32;
// TODO(mgsloan): use viewport_bounds.width as a max width when rendering menu.
let Some(mut menu_element) = self.editor.update(cx, |editor, cx| {
editor.render_context_menu(&self.style, max_height_in_lines, cx)
}) else {
@@ -2976,13 +2984,11 @@ impl EditorElement {
let menu_size = menu_element.layout_as_root(AvailableSpace::min_size(), cx);
let menu_position = gpui::Point {
x: if target_position.x + menu_size.width > cx.viewport_size().width {
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
// overflow.
(cx.viewport_size().width - menu_size.width).max(Pixels::ZERO)
} else {
target_position.x
},
// Snap the right edge of the list to the right edge of the window if its horizontal bounds
// overflow. Include space for the scrollbar.
x: target_position
.x
.min((viewport_bounds.right() - menu_size.width).max(Pixels::ZERO)),
y: if y_is_flipped {
bottom_y_when_flipped - menu_size.height
} else {
@@ -2991,43 +2997,31 @@ impl EditorElement {
};
cx.defer_draw(menu_element, menu_position, 1);
let aside_element = self.editor.update(cx, |editor, cx| {
editor.render_context_menu_aside(&self.style, unconstrained_max_height, cx)
});
if let Some(aside_element) = aside_element {
let menu_bounds = Bounds::new(menu_position, menu_size);
let max_menu_size = size(menu_size.width, unconstrained_max_height);
let max_menu_bounds = if y_is_flipped {
Bounds::new(
point(
menu_position.x,
bottom_y_when_flipped - max_menu_size.height,
),
max_menu_size,
)
} else {
Bounds::new(target_position, max_menu_size)
};
// Pad the target by 4 pixels to create a gap.
let mut extend_amount = Edges::all(px(4.));
// Extend to include the cursored line to avoid overlapping it.
if y_is_flipped {
extend_amount.bottom = line_height;
} else {
extend_amount.top = line_height;
}
self.layout_context_menu_aside(
text_hitbox,
y_is_flipped,
menu_position,
menu_bounds.extend(extend_amount),
max_menu_bounds.extend(extend_amount),
unconstrained_max_height,
aside_element,
cx,
);
}
// Layout documentation aside
let menu_bounds = Bounds::new(menu_position, menu_size);
let max_menu_size = size(menu_size.width, unconstrained_max_height);
let max_menu_bounds = if y_is_flipped {
Bounds::new(
point(
menu_position.x,
bottom_y_when_flipped - max_menu_size.height,
),
max_menu_size,
)
} else {
Bounds::new(target_position, max_menu_size)
};
self.layout_context_menu_aside(
text_hitbox,
y_is_flipped,
menu_position,
menu_bounds,
max_menu_bounds,
unconstrained_max_height,
line_height,
viewport_bounds,
cx,
);
}
#[allow(clippy::too_many_arguments)]
@@ -3036,66 +3030,106 @@ impl EditorElement {
text_hitbox: &Hitbox,
y_is_flipped: bool,
menu_position: gpui::Point<Pixels>,
target_bounds: Bounds<Pixels>,
max_target_bounds: Bounds<Pixels>,
menu_bounds: Bounds<Pixels>,
max_menu_bounds: Bounds<Pixels>,
max_height: Pixels,
aside: AnyElement,
line_height: Pixels,
viewport_bounds: Bounds<Pixels>,
cx: &mut WindowContext,
) {
let mut aside = aside;
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), cx);
// Snap to right side of window if it would overflow.
let aside_x = cmp::min(
menu_position.x,
cx.viewport_size().width - actual_size.width,
);
if aside_x < px(0.) {
// Not enough space, skip drawing.
return;
let mut extend_amount = Edges::all(MENU_GAP);
// Extend to include the cursored line to avoid overlapping it.
if y_is_flipped {
extend_amount.bottom = line_height;
} else {
extend_amount.top = line_height;
}
let target_bounds = menu_bounds.extend(extend_amount);
let max_target_bounds = max_menu_bounds.extend(extend_amount);
let top_position = point(aside_x, target_bounds.top() - actual_size.height);
let bottom_position = point(aside_x, target_bounds.bottom());
let right_position = point(target_bounds.right(), menu_position.y);
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH {
let max_width = cmp::min(
available_within_viewport.right - px(1.),
MENU_ASIDE_MAX_WIDTH,
);
let Some(mut aside) =
self.render_context_menu_aside(size(max_width, max_height - POPOVER_Y_PADDING), cx)
else {
return;
};
aside.layout_as_root(AvailableSpace::min_size(), cx);
let right_position = point(target_bounds.right(), menu_position.y);
Some((aside, right_position))
} else {
let max_size = size(
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
// won't be needed here.
cmp::min(
cmp::max(menu_bounds.size.width - px(2.), MENU_ASIDE_MIN_WIDTH),
viewport_bounds.right(),
),
cmp::min(
max_height,
cmp::max(
available_within_viewport.top,
available_within_viewport.bottom,
),
) - POPOVER_Y_PADDING,
);
let Some(mut aside) = self.render_context_menu_aside(max_size, cx) else {
return;
};
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), cx);
let fit_horizontally_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
// Prefer to fit to the right, then on the same side of the line as the menu, then on
// the other side of the line.
if wanted.width < available.right {
Some(right_position)
} else if !y_is_flipped && wanted.height < available.bottom {
Some(bottom_position)
} else if !y_is_flipped && wanted.height < available.top {
Some(top_position)
} else if y_is_flipped && wanted.height < available.top {
Some(top_position)
} else if y_is_flipped && wanted.height < available.bottom {
Some(bottom_position)
} else {
None
}
let top_position = point(menu_position.x, target_bounds.top() - actual_size.height);
let bottom_position = point(menu_position.x, target_bounds.bottom());
let fit_within = |available: Edges<Pixels>, wanted: Size<Pixels>| {
// Prefer to fit on the same side of the line as the menu, then on the other side of
// the line.
if !y_is_flipped && wanted.height < available.bottom {
Some(bottom_position)
} else if !y_is_flipped && wanted.height < available.top {
Some(top_position)
} else if y_is_flipped && wanted.height < available.top {
Some(top_position)
} else if y_is_flipped && wanted.height < available.bottom {
Some(bottom_position)
} else {
None
}
};
// Prefer choosing a direction using max sizes rather than actual size for stability.
let available_within_text = max_target_bounds.space_within(&text_hitbox.bounds);
let wanted = size(MENU_ASIDE_MAX_WIDTH, max_height);
let aside_position = fit_within(available_within_text, wanted)
// Fallback: fit max size in window.
.or_else(|| fit_within(max_target_bounds.space_within(&viewport_bounds), wanted))
// Fallback: fit actual size in window.
.or_else(|| fit_within(available_within_viewport, actual_size));
aside_position.map(|position| (aside, position))
};
// Prefer choosing a direction using max sizes rather than actual size for stability.
let mut available = max_target_bounds.space_within(&text_hitbox.bounds);
let mut wanted = size(MAX_COMPLETIONS_ASIDE_WIDTH, max_height);
let aside_position = fit_horizontally_within(available, wanted)
.or_else(|| {
// Fallback: fit max size in window.
available = max_target_bounds
.space_within(&Bounds::new(Default::default(), cx.viewport_size()));
fit_horizontally_within(available, wanted)
})
.or_else(|| {
// Fallback: fit actual size in window.
wanted = actual_size;
fit_horizontally_within(available, wanted)
});
// Skip drawing if it doesn't fit anywhere.
if let Some(aside_position) = aside_position {
cx.defer_draw(aside, aside_position, 1);
if let Some((aside, position)) = positioned_aside {
cx.defer_draw(aside, position, 1);
}
}
fn render_context_menu_aside(
&self,
max_size: Size<Pixels>,
cx: &mut WindowContext,
) -> Option<AnyElement> {
if max_size.width < px(100.) || max_size.height < px(12.) {
None
} else {
self.editor.update(cx, |editor, cx| {
editor.render_context_menu_aside(&self.style, max_size, cx)
})
}
}
@@ -6246,6 +6280,7 @@ impl Element for EditorElement {
scroll_pixel_position,
line_height,
em_width,
em_advance,
autoscroll_containing_element,
cx,
);

View File

@@ -429,7 +429,7 @@ fn show_hover(
})
.or_else(|| {
let snapshot = &snapshot.buffer_snapshot;
let offset_range = snapshot.range_for_syntax_ancestor(anchor..anchor)?;
let offset_range = snapshot.syntax_ancestor(anchor..anchor)?.1;
Some(
snapshot.anchor_before(offset_range.start)
..snapshot.anchor_after(offset_range.end),

View File

@@ -1155,6 +1155,11 @@ fn editor_with_deleted_text(
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_runnables(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_read_only(true);
editor.set_show_inline_completions(Some(false), cx);
@@ -1166,7 +1171,7 @@ fn editor_with_deleted_text(
false,
cx,
);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); //
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor
._subscriptions
.extend([cx.on_blur(&editor.focus_handle, |editor, cx| {

View File

@@ -1,6 +1,8 @@
use std::collections::hash_map::Entry;
use std::sync::Arc;
use crate::Editor;
use collections::HashMap;
use gpui::{Model, WindowContext};
use language::Buffer;
use language::Language;
@@ -20,6 +22,7 @@ where
return None;
};
let multibuffer = editor.buffer().read(cx);
let mut language_servers_for = HashMap::default();
editor
.selections
.disjoint_anchors()
@@ -28,27 +31,36 @@ where
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let server_id = *match language_servers_for.entry(buffer_id) {
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
Entry::Vacant(vacant_entry) => {
let language_server_id = project
.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some(server.server_id())
} else {
None
}
});
vacant_entry.insert(language_server_id)
}
}
.as_ref()?;
Some((buffer, trigger_anchor, server_id))
})
.find_map(|(buffer, trigger_anchor, server_id)| {
let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !filter_language(&language) {
return None;
}
Some((trigger_anchor, language, buffer))
})
.find_map(|(trigger_anchor, language, buffer)| {
project
.read(cx)
.language_servers_for_local_buffer(buffer.read(cx), cx)
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == language_server_name {
Some((
trigger_anchor,
Arc::clone(&language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
Some((
trigger_anchor,
Arc::clone(&language),
server_id,
buffer.clone(),
))
})
}

View File

@@ -51,8 +51,8 @@ impl FileIcons {
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<SharedString> {
let this = cx.try_global::<Self>()?;
// FIXME: Associate a type with the languages and have the file's language
// override these associations
// TODO: Associate a type with the languages and have the file's language
// override these associations
maybe!({
let suffix = path.icon_stem_or_suffix()?;

View File

@@ -15,7 +15,11 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
db.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -1,28 +1,42 @@
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 gpui::{
actions, prelude::*, uniform_list, Action, AppContext, AsyncWindowContext, ClickEvent,
CursorStyle, EventEmitter, FocusHandle, FocusableView, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Model, Modifiers, ModifiersChangedEvent,
MouseButton, ScrollStrategy, Stateful, Task, UniformListScrollHandle, View, WeakView,
};
use language::{Buffer, BufferRow, OffsetRangeExt};
use menu::{SelectNext, SelectPrev};
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::{
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
ops::Range,
ops::{Deref, Range},
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
time::Duration,
usize,
};
use git::repository::GitFileStatus;
use util::{ResultExt, TryFutureExt};
use db::kvp::KEY_VALUE_STORE;
use gpui::*;
use project::{Entry, EntryKind, Fs, Project, ProjectEntryId, WorktreeId};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use ui::{
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, Scrollbar, ScrollbarState, Tooltip,
prelude::*, Checkbox, Divider, DividerColor, ElevationIndex, ListItem, Scrollbar,
ScrollbarState, Tooltip,
};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
ItemHandle, Workspace,
};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
@@ -31,6 +45,8 @@ actions!(git_panel, [ToggleFocus]);
const GIT_PANEL_KEY: &str = "GitPanel";
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -58,6 +74,8 @@ struct EntryDetails {
depth: usize,
is_expanded: bool,
status: Option<GitFileStatus>,
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
index: usize,
}
impl EntryDetails {
@@ -72,7 +90,7 @@ struct SerializedGitPanel {
}
pub struct GitPanel {
_workspace: WeakView<Workspace>,
workspace: WeakView<Workspace>,
current_modifiers: Modifiers,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
@@ -87,8 +105,43 @@ pub struct GitPanel {
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
visible_entries: Vec<WorktreeEntries>,
width: Option<Pixels>,
git_diff_editor: View<Editor>,
git_diff_editor_updates: Task<()>,
reveal_in_editor: Task<()>,
}
#[derive(Debug, Clone)]
struct WorktreeEntries {
worktree_id: WorktreeId,
visible_entries: Vec<GitPanelEntry>,
paths: Rc<OnceCell<HashSet<Arc<Path>>>>,
}
#[derive(Debug, Clone)]
struct GitPanelEntry {
entry: Entry,
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
}
impl Deref for GitPanelEntry {
type Target = Entry;
fn deref(&self) -> &Self::Target {
&self.entry
}
}
impl WorktreeEntries {
fn paths(&self) -> &HashSet<Arc<Path>> {
self.paths.get_or_init(|| {
self.visible_entries
.iter()
.map(|e| (e.entry.path.clone()))
.collect()
})
}
}
impl GitPanel {
@@ -115,18 +168,28 @@ impl GitPanel {
this.hide_scrollbar(cx);
})
.detach();
cx.subscribe(&project, |this, _project, event, cx| match event {
cx.subscribe(&project, |this, project, event, cx| match event {
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx);
this.update_visible_entries(None, None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(_, _)
| project::Event::WorktreeAdded(_)
| project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, cx);
project::Event::WorktreeOrderChanged => {
this.update_visible_entries(None, None, cx);
cx.notify();
}
project::Event::WorktreeUpdatedEntries(id, _)
| project::Event::WorktreeAdded(id)
| project::Event::WorktreeUpdatedGitRepositories(id) => {
this.update_visible_entries(Some(*id), None, cx);
cx.notify();
}
project::Event::Closed => {
this.git_diff_editor_updates = Task::ready(());
this.expanded_dir_ids.clear();
this.visible_entries.clear();
this.git_diff_editor = diff_display_editor(project.clone(), cx);
}
_ => {}
})
.detach();
@@ -134,11 +197,10 @@ impl GitPanel {
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
_workspace: weak_workspace,
workspace: weak_workspace,
focus_handle: cx.focus_handle(),
fs,
pending_serialization: Task::ready(None),
project,
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
@@ -149,8 +211,12 @@ impl GitPanel {
selected_item: None,
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
git_diff_editor: diff_display_editor(project.clone(), cx),
git_diff_editor_updates: Task::ready(()),
reveal_in_editor: Task::ready(()),
project,
};
this.update_visible_entries(None, cx);
this.update_visible_entries(None, None, cx);
this
});
@@ -251,6 +317,82 @@ impl GitPanel {
(depth, difference)
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let item_count = self
.visible_entries
.iter()
.map(|worktree_entries| worktree_entries.visible_entries.len())
.sum::<usize>();
if item_count == 0 {
return;
}
let selection = match self.selected_item {
Some(i) => {
if i < item_count - 1 {
self.selected_item = Some(i + 1);
i + 1
} else {
self.selected_item = Some(0);
0
}
}
None => {
self.selected_item = Some(0);
0
}
};
self.scroll_handle
.scroll_to_item(selection, ScrollStrategy::Center);
let mut hunks = None;
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
hunks = Some(entry.hunks.clone());
});
if let Some(hunks) = hunks {
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
}
cx.notify();
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
let item_count = self
.visible_entries
.iter()
.map(|worktree_entries| worktree_entries.visible_entries.len())
.sum::<usize>();
if item_count == 0 {
return;
}
let selection = match self.selected_item {
Some(i) => {
if i > 0 {
self.selected_item = Some(i - 1);
i - 1
} else {
self.selected_item = Some(item_count - 1);
item_count - 1
}
}
None => {
self.selected_item = Some(0);
0
}
};
self.scroll_handle
.scroll_to_item(selection, ScrollStrategy::Center);
let mut hunks = None;
self.for_each_visible_entry(selection..selection + 1, cx, |_, entry, _| {
hunks = Some(entry.hunks.clone());
});
if let Some(hunks) = hunks {
self.reveal_entry_in_git_editor(hunks, false, Some(UPDATE_DEBOUNCE), cx);
}
cx.notify();
}
}
impl GitPanel {
@@ -293,8 +435,9 @@ impl GitPanel {
fn entry_count(&self) -> usize {
self.visible_entries
.iter()
.map(|(_, entries, _)| {
entries
.map(|worktree_entries| {
worktree_entries
.visible_entries
.iter()
.filter(|entry| entry.git_status.is_some())
.count()
@@ -309,19 +452,23 @@ impl GitPanel {
mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<Self>),
) {
let mut ix = 0;
for (worktree_id, visible_worktree_entries, entries_paths) in &self.visible_entries {
for worktree_entries in &self.visible_entries {
if ix >= range.end {
return;
}
if ix + visible_worktree_entries.len() <= range.start {
ix += visible_worktree_entries.len();
if ix + worktree_entries.visible_entries.len() <= range.start {
ix += worktree_entries.visible_entries.len();
continue;
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
let end_ix = range.end.min(ix + worktree_entries.visible_entries.len());
// let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
if let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(worktree_entries.worktree_id, cx)
{
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
let expanded_entry_ids = self
@@ -331,14 +478,14 @@ impl GitPanel {
.unwrap_or(&[]);
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
let entries = entries_paths.get_or_init(|| {
visible_worktree_entries
.iter()
.map(|e| (e.path.clone()))
.collect()
});
let entries = worktree_entries.paths();
for entry in visible_worktree_entries[entry_range].iter() {
let index_start = entry_range.start;
for (i, entry) in worktree_entries.visible_entries[entry_range]
.iter()
.enumerate()
{
let index = index_start + i;
let status = entry.git_status;
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
@@ -360,16 +507,16 @@ impl GitPanel {
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
};
let display_name = entry.path.to_string_lossy().into_owned();
let details = EntryDetails {
filename,
display_name,
display_name: entry.path.to_string_lossy().into_owned(),
kind: entry.kind,
is_expanded,
path: entry.path.clone(),
status,
hunks: entry.hunks.clone(),
depth,
index,
};
callback(entry.id, details, cx);
}
@@ -379,44 +526,75 @@ impl GitPanel {
}
// TODO: Update expanded directory state
// TODO: Updates happen in the main loop, could be long for large workspaces
fn update_visible_entries(
&mut self,
for_worktree: Option<WorktreeId>,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let project = self.project.read(cx);
self.visible_entries.clear();
for worktree in project.visible_worktrees(cx) {
let snapshot = worktree.read(cx).snapshot();
let worktree_id = snapshot.id();
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true, 0);
while let Some(entry) = entry_iter.entry() {
// Only include entries with a git status
if entry.git_status.is_some() {
visible_worktree_entries.push(entry.clone());
let mut old_entries_removed = false;
let mut after_update = Vec::new();
self.visible_entries
.retain(|worktree_entries| match for_worktree {
Some(for_worktree) => {
if worktree_entries.worktree_id == for_worktree {
old_entries_removed = true;
false
} else if old_entries_removed {
after_update.push(worktree_entries.clone());
false
} else {
true
}
}
entry_iter.advance();
None => false,
});
for worktree in project.visible_worktrees(cx) {
let worktree_id = worktree.read(cx).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);
if !visible_worktree_entries.is_empty() {
self.visible_entries
.push((worktree_id, visible_worktree_entries, OnceCell::new()));
self.visible_entries.push(WorktreeEntries {
worktree_id,
visible_entries: visible_worktree_entries
.into_iter()
.map(|entry| GitPanelEntry {
entry,
hunks: Rc::default(),
})
.collect(),
paths: Rc::default(),
});
}
}
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, (id, entries, _))| {
if *id == worktree_id {
entries
|(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 * entries.len() + entry_index)
.map(|entry_index| {
worktree_index * worktree_entries.visible_entries.len()
+ entry_index
})
} else {
None
}
@@ -424,6 +602,163 @@ impl GitPanel {
);
}
let project = self.project.clone();
self.git_diff_editor_updates = cx.spawn(|git_panel, mut cx| async move {
cx.background_executor()
.timer(UPDATE_DEBOUNCE)
.await;
let Some(project_buffers) = git_panel
.update(&mut cx, |git_panel, cx| {
futures::future::join_all(git_panel.visible_entries.iter_mut().flat_map(
move |worktree_entries| {
worktree_entries
.visible_entries
.iter()
.filter_map(|entry| {
let git_status = entry.git_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 open_task =
project.open_path(entry_path.clone(), cx);
let unstaged_changes_task =
cx.spawn(|project, mut cx| async move {
let (_, opened_model) = open_task
.await
.context("opening buffer")?;
let buffer = opened_model
.downcast::<Buffer>()
.map_err(|_| {
anyhow::anyhow!(
"accessing buffer for entry"
)
})?;
// TODO added files have noop changes and those are not expanded properly in the multi buffer
let unstaged_changes = project
.update(&mut cx, |project, cx| {
project.open_unstaged_changes(
buffer.clone(),
cx,
)
})?
.await
.context("opening unstaged changes")?;
let hunks = cx.update(|cx| {
entry_hunks
.get_or_init(|| {
match git_status {
GitFileStatus::Added => {
let buffer_snapshot = buffer.read(cx).snapshot();
let entire_buffer_range =
buffer_snapshot.anchor_after(0)
..buffer_snapshot
.anchor_before(
buffer_snapshot.len(),
);
let entire_buffer_point_range =
entire_buffer_range
.clone()
.to_point(&buffer_snapshot);
vec![DiffHunk {
row_range: entire_buffer_point_range
.start
.row
..entire_buffer_point_range
.end
.row,
buffer_range: entire_buffer_range,
diff_base_byte_range: 0..0,
}]
}
GitFileStatus::Modified => {
let buffer_snapshot =
buffer.read(cx).snapshot();
unstaged_changes.read(cx)
.diff_to_buffer
.hunks_in_row_range(
0..BufferRow::MAX,
&buffer_snapshot,
)
.collect()
}
// TODO support conflicts display
GitFileStatus::Conflict => Vec::new(),
}
}).clone()
})?;
anyhow::Ok((buffer, unstaged_changes, hunks))
});
Some((entry_path, unstaged_changes_task))
})?;
Some((entry_path, unstaged_changes_task))
})
.map(|(entry_path, open_task)| async move {
(entry_path, open_task.await)
})
.collect::<Vec<_>>()
},
))
})
.ok()
else {
return;
};
let project_buffers = project_buffers.await;
if project_buffers.is_empty() {
return;
}
let mut change_sets = Vec::with_capacity(project_buffers.len());
if let Some(buffer_update_task) = git_panel
.update(&mut cx, |git_panel, cx| {
let editor = git_panel.git_diff_editor.clone();
let multi_buffer = editor.read(cx).buffer().clone();
let mut buffers_with_ranges = Vec::with_capacity(project_buffers.len());
for (buffer_path, open_result) in project_buffers {
if let Some((buffer, unstaged_changes, diff_hunks)) = open_result
.with_context(|| format!("opening buffer {buffer_path:?}"))
.log_err()
{
change_sets.push(unstaged_changes);
buffers_with_ranges.push((
buffer,
diff_hunks
.into_iter()
.map(|hunk| hunk.buffer_range)
.collect(),
));
}
}
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.clear(cx);
multi_buffer.push_multiple_excerpts_with_context_lines(
buffers_with_ranges,
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
)
})
})
.ok()
{
buffer_update_task.await;
git_panel
.update(&mut cx, |git_panel, cx| {
git_panel.git_diff_editor.update(cx, |editor, cx| {
for change_set in change_sets {
editor.add_change_set(change_set, cx);
}
})
})
.ok();
}
});
cx.notify();
}
}
@@ -626,17 +961,23 @@ impl GitPanel {
let item_count = self
.visible_entries
.iter()
.map(|(_, worktree_entries, _)| worktree_entries.len())
.map(|worktree_entries| worktree_entries.visible_entries.len())
.sum();
let selected_entry = self.selected_item;
h_flex()
.size_full()
.overflow_hidden()
.child(
uniform_list(cx.view().clone(), "entries", item_count, {
|this, range, cx| {
move |git_panel, range, cx| {
let mut items = Vec::with_capacity(range.end - range.start);
this.for_each_visible_entry(range, cx, |id, details, cx| {
items.push(this.render_entry(id, details, cx));
git_panel.for_each_visible_entry(range, cx, |id, details, cx| {
items.push(git_panel.render_entry(
id,
Some(details.index) == selected_entry,
details,
cx,
));
});
items
}
@@ -653,12 +994,14 @@ impl GitPanel {
fn render_entry(
&self,
id: ProjectEntryId,
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 is_staged = ToggleState::Selected;
let handle = cx.view().clone();
h_flex()
.id(id)
@@ -676,7 +1019,113 @@ impl GitPanel {
.when_some(details.status, |this, status| {
this.child(git_status_icon(status))
})
.child(h_flex().gap_1p5().child(details.display_name.clone()))
.child(
ListItem::new(("label", id))
.toggle_state(selected)
.child(h_flex().gap_1p5().child(details.display_name.clone()))
.on_click(move |e, cx| {
handle.update(cx, |git_panel, cx| {
git_panel.selected_item = Some(details.index);
let change_focus = e.down.click_count > 1;
git_panel.reveal_entry_in_git_editor(
details.hunks.clone(),
change_focus,
None,
cx,
);
});
}),
)
}
fn reveal_entry_in_git_editor(
&mut self,
hunks: Rc<OnceCell<Vec<DiffHunk>>>,
change_focus: bool,
debounce: Option<Duration>,
cx: &mut ViewContext<'_, Self>,
) {
let workspace = self.workspace.clone();
let diff_editor = self.git_diff_editor.clone();
self.reveal_in_editor = cx.spawn(|_, mut cx| async move {
if let Some(debounce) = debounce {
cx.background_executor().timer(debounce).await;
}
let Some(editor) = workspace
.update(&mut cx, |workspace, cx| {
let git_diff_editor = workspace
.items_of_type::<Editor>(cx)
.find(|editor| &diff_editor == editor);
match git_diff_editor {
Some(existing_editor) => {
workspace.activate_item(&existing_editor, true, change_focus, cx);
existing_editor
}
None => {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
diff_editor.boxed_clone(),
true,
change_focus,
None,
cx,
)
});
diff_editor.clone()
}
}
})
.ok()
else {
return;
};
if let Some(first_hunk) = hunks.get().and_then(|hunks| hunks.first()) {
let hunk_buffer_range = &first_hunk.buffer_range;
if let Some(buffer_id) = hunk_buffer_range
.start
.buffer_id
.or_else(|| first_hunk.buffer_range.end.buffer_id)
{
editor
.update(&mut cx, |editor, cx| {
let multi_buffer = editor.buffer().read(cx);
let buffer = multi_buffer.buffer(buffer_id)?;
let buffer_snapshot = buffer.read(cx).snapshot();
let (excerpt_id, _) = multi_buffer
.excerpts_for_buffer(&buffer, cx)
.into_iter()
.find(|(_, excerpt)| {
hunk_buffer_range
.start
.cmp(&excerpt.context.start, &buffer_snapshot)
.is_ge()
&& hunk_buffer_range
.end
.cmp(&excerpt.context.end, &buffer_snapshot)
.is_le()
})?;
let multi_buffer_hunk_start = multi_buffer
.snapshot(cx)
.anchor_in_excerpt(excerpt_id, hunk_buffer_range.start)?;
editor.change_selections(
Some(Autoscroll::Strategy(AutoscrollStrategy::Center)),
cx,
|s| {
s.select_ranges(Some(
multi_buffer_hunk_start..multi_buffer_hunk_start,
))
},
);
cx.notify();
Some(())
})
.ok()
.flatten();
}
}
});
}
}
@@ -704,6 +1153,8 @@ impl Render for GitPanel {
this.commit_all_changes(&CommitAllChanges, cx)
}))
})
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev))
.on_hover(cx.listener(|this, hovered, cx| {
if *hovered {
this.show_scrollbar = true;
@@ -784,3 +1235,14 @@ impl Panel for GitPanel {
Box::new(ToggleFocus)
}
}
fn diff_display_editor(project: Model<Project>, cx: &mut WindowContext) -> View<Editor> {
cx.new_view(|cx| {
let multi_buffer = cx.new_model(|cx| {
MultiBuffer::new(project.read(cx).capability()).with_title("Project diff".to_string())
});
let mut editor = Editor::for_multibuffer(multi_buffer, Some(project), true, cx);
editor.set_expand_all_diff_hunks();
editor
})
}

View File

@@ -1,6 +1,11 @@
use std::time::Duration;
use gpui::*;
use anyhow::Result;
use gpui::{
black, bounce, div, ease_in_out, percentage, prelude::*, px, rgb, size, svg, Animation,
AnimationExt as _, App, AppContext, AssetSource, Bounds, SharedString, Transformation,
ViewContext, WindowBounds, WindowOptions,
};
struct Assets {}
@@ -37,7 +42,7 @@ impl Render for AnimationExample {
div()
.flex()
.bg(rgb(0x2e7d32))
.size(Length::Definite(Pixels(300.0).into()))
.size(px(300.0))
.justify_center()
.items_center()
.shadow_lg()

View File

@@ -1,4 +1,7 @@
use gpui::*;
use gpui::{
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, ViewContext,
WindowBounds, WindowOptions,
};
struct HelloWorld {
text: SharedString,
@@ -11,7 +14,7 @@ impl Render for HelloWorld {
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(Length::Definite(Pixels(500.0).into()))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()

View File

@@ -1,9 +1,14 @@
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use gpui::*;
use std::fs;
use anyhow::Result;
use gpui::{
actions, div, img, prelude::*, px, rgb, size, App, AppContext, AssetSource, Bounds,
ImageSource, KeyBinding, Menu, MenuItem, Point, SharedString, SharedUri, TitlebarOptions,
ViewContext, WindowBounds, WindowContext, WindowOptions,
};
struct Assets {
base: PathBuf,
@@ -55,7 +60,7 @@ impl RenderOnce for ImageContainer {
.size_full()
.gap_4()
.child(self.text)
.child(img(self.src).w(px(256.0)).h(px(256.0))),
.child(img(self.src).size(px(256.0))),
)
}
}
@@ -75,7 +80,7 @@ impl Render for ImageShowcase {
.justify_center()
.items_center()
.gap_8()
.bg(rgb(0xFFFFFF))
.bg(rgb(0xffffff))
.child(
div()
.flex()

View File

@@ -1,6 +1,13 @@
use std::ops::Range;
use gpui::*;
use gpui::{
actions, black, div, fill, hsla, opaque_grey, point, prelude::*, px, relative, rgb, rgba, size,
white, yellow, App, AppContext, Bounds, ClipboardItem, CursorStyle, ElementId,
ElementInputHandler, FocusHandle, FocusableView, GlobalElementId, KeyBinding, Keystroke,
LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, View, ViewContext,
ViewInputHandler, WindowBounds, WindowContext, WindowOptions,
};
use unicode_segmentation::*;
actions!(
@@ -463,7 +470,7 @@ impl Element for TextElement {
bounds.bottom(),
),
),
rgba(0x3311FF30),
rgba(0x3311ff30),
)),
None,
)

View File

@@ -1,6 +1,10 @@
use std::{fs, path::PathBuf, time::Duration};
use gpui::*;
use anyhow::Result;
use gpui::{
div, hsla, img, point, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds,
BoxShadow, ClickEvent, SharedString, Task, Timer, ViewContext, WindowBounds, WindowOptions,
};
struct Assets {
base: PathBuf,
@@ -76,7 +80,7 @@ impl Render for HelloWorld {
.flex()
.flex_row()
.size_full()
.bg(rgb(0xE0E0E0))
.bg(rgb(0xe0e0e0))
.text_xl()
.child(
div()

View File

@@ -1,4 +1,6 @@
use gpui::*;
use gpui::{
actions, div, prelude::*, rgb, App, AppContext, Menu, MenuItem, ViewContext, WindowOptions,
};
struct SetMenus;

View File

@@ -1,4 +1,7 @@
use gpui::*;
use gpui::{
div, prelude::*, px, rgb, size, App, AppContext, Bounds, ViewContext, WindowBounds,
WindowOptions,
};
struct Shadow {}

View File

@@ -1,7 +1,11 @@
use std::fs;
use std::path::PathBuf;
use gpui::*;
use std::fs;
use anyhow::Result;
use gpui::{
div, prelude::*, px, rgb, size, svg, App, AppContext, AssetSource, Bounds, SharedString,
ViewContext, WindowBounds, WindowOptions,
};
struct Assets {
base: PathBuf,

View File

@@ -1,4 +1,6 @@
use gpui::*;
use gpui::{
div, prelude::*, px, size, App, AppContext, Bounds, ViewContext, WindowBounds, WindowOptions,
};
struct HelloWorld {}

View File

@@ -1,4 +1,7 @@
use gpui::*;
use gpui::{
div, prelude::*, px, rgb, size, uniform_list, App, AppContext, Bounds, ViewContext,
WindowBounds, WindowOptions,
};
struct UniformListExample {}

View File

@@ -1,5 +1,7 @@
use gpui::*;
use prelude::FluentBuilder as _;
use gpui::{
div, prelude::*, px, rgb, size, App, AppContext, Bounds, SharedString, Timer, ViewContext,
WindowBounds, WindowContext, WindowKind, WindowOptions,
};
struct SubWindow {
custom_titlebar: bool,

View File

@@ -1,4 +1,8 @@
use gpui::*;
use gpui::{
div, point, prelude::*, px, rgb, App, AppContext, Bounds, DisplayId, Hsla, Pixels,
SharedString, Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowKind,
WindowOptions,
};
struct WindowContent {
text: SharedString,

View File

@@ -1,15 +1,16 @@
use gpui::*;
use prelude::FluentBuilder;
use gpui::{
black, canvas, div, green, point, prelude::*, px, rgb, size, transparent_black, white, App,
AppContext, Bounds, CursorStyle, Decorations, Hsla, MouseButton, Pixels, Point, ResizeEdge,
Size, ViewContext, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowOptions,
};
struct WindowShadow {}
/*
Things to do:
1. We need a way of calculating which edge or corner the mouse is on,
and then dispatch on that
2. We need to improve the shadow rendering significantly
3. We need to implement the techniques in here in Zed
*/
// Things to do:
// 1. We need a way of calculating which edge or corner the mouse is on,
// and then dispatch on that
// 2. We need to improve the shadow rendering significantly
// 3. We need to implement the techniques in here in Zed
impl Render for WindowShadow {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
@@ -128,7 +129,7 @@ impl Render for WindowShadow {
div()
.flex()
.bg(white())
.size(Length::Definite(Pixels(300.0).into()))
.size(px(300.0))
.justify_center()
.items_center()
.shadow_lg()

View File

@@ -420,6 +420,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn get_raw_handle(&self) -> windows::HWND;
// Linux specific methods
fn inner_window_bounds(&self) -> WindowBounds {
self.window_bounds()
}
fn request_decorations(&self, _decorations: WindowDecorations) {}
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}

View File

@@ -651,7 +651,7 @@ impl CursorStyle {
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
match self {
CursorStyle::Arrow => "arrow",
CursorStyle::Arrow => "left_ptr",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",

View File

@@ -781,6 +781,19 @@ impl PlatformWindow for WaylandWindow {
}
}
fn inner_window_bounds(&self) -> WindowBounds {
let state = self.borrow();
if state.fullscreen {
WindowBounds::Fullscreen(state.window_bounds)
} else if state.maximized {
WindowBounds::Maximized(state.window_bounds)
} else {
let inset = state.inset.unwrap_or(px(0.));
drop(state);
WindowBounds::Windowed(self.bounds().inset(inset))
}
}
fn content_size(&self) -> Size<Pixels> {
self.borrow().bounds.size
}

View File

@@ -1100,6 +1100,30 @@ impl PlatformWindow for X11Window {
}
}
fn inner_window_bounds(&self) -> WindowBounds {
let state = self.0.state.borrow();
if self.is_maximized() {
WindowBounds::Maximized(state.bounds)
} else {
let mut bounds = state.bounds;
let [left, right, top, bottom] = state.last_insets;
let [left, right, top, bottom] = [
Pixels((left as f32) / state.scale_factor),
Pixels((right as f32) / state.scale_factor),
Pixels((top as f32) / state.scale_factor),
Pixels((bottom as f32) / state.scale_factor),
];
bounds.origin.x += left;
bounds.origin.y += top;
bounds.size.width -= left + right;
bounds.size.height -= top + bottom;
WindowBounds::Windowed(bounds)
}
}
fn content_size(&self) -> Size<Pixels> {
// We divide by the scale factor here because this value is queried to determine how much to draw,
// but it will be multiplied later by the scale to adjust for scaling.

View File

@@ -357,7 +357,7 @@ impl From<ImageFormat> for image::ImageFormat {
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
ImageFormat::Webp => image::ImageFormat::WebP,
ImageFormat::Gif => image::ImageFormat::Gif,
// ImageFormat::Svg => todo!(),
// TODO: ImageFormat::Svg
ImageFormat::Bmp => image::ImageFormat::Bmp,
ImageFormat::Tiff => image::ImageFormat::Tiff,
_ => unreachable!(),

View File

@@ -48,6 +48,7 @@ pub(crate) struct WindowsPlatform {
pub(crate) struct WindowsPlatformState {
callbacks: PlatformCallbacks,
menus: Vec<OwnedMenu>,
// NOTE: standard cursor handles don't need to close.
pub(crate) current_cursor: HCURSOR,
}
@@ -70,6 +71,7 @@ impl WindowsPlatformState {
Self {
callbacks,
current_cursor,
menus: Vec::new(),
}
}
}
@@ -449,8 +451,15 @@ impl Platform for WindowsPlatform {
self.state.borrow_mut().callbacks.reopen = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
self.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect();
}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
Some(self.state.borrow().menus.clone())
}
// todo(windows)
fn set_menus(&self, _menus: Vec<Menu>, _keymap: &Keymap) {}
fn set_dock_menu(&self, _menus: Vec<MenuItem>, _keymap: &Keymap) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {

View File

@@ -999,6 +999,11 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.window_bounds()
}
/// Return the `WindowBounds` excluding insets (Wayland and X11)
pub fn inner_window_bounds(&self) -> WindowBounds {
self.window.platform_window.inner_window_bounds()
}
/// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused();

View File

@@ -68,7 +68,7 @@ pub use text::{
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
use util::RandomCharIter;
use util::{debug_panic, RangeExt};
use util::{debug_panic, maybe, RangeExt};
#[cfg(any(test, feature = "test-support"))]
pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -2923,10 +2923,13 @@ impl BufferSnapshot {
(start..end, word_kind)
}
/// Returns the range for the closes syntax node enclosing the given range.
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
/// Returns the closest syntax node enclosing the given range.
pub fn syntax_ancestor<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<tree_sitter::Node<'a>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<Range<usize>> = None;
let mut result: Option<tree_sitter::Node<'a>> = None;
'outer: for layer in self
.syntax
.layers_for_range(range.clone(), &self.text, true)
@@ -2956,7 +2959,7 @@ impl BufferSnapshot {
}
let left_node = cursor.node();
let mut layer_result = left_node.byte_range();
let mut layer_result = left_node;
// For an empty range, try to find another node immediately to the right of the range.
if left_node.end_byte() == range.start {
@@ -2979,13 +2982,13 @@ impl BufferSnapshot {
// If both nodes are the same in that regard, favor the right one.
if let Some(right_node) = right_node {
if right_node.is_named() || !left_node.is_named() {
layer_result = right_node.byte_range();
layer_result = right_node;
}
}
}
if let Some(previous_result) = &result {
if previous_result.len() < layer_result.len() {
if previous_result.byte_range().len() < layer_result.byte_range().len() {
continue;
}
}
@@ -3028,6 +3031,48 @@ impl BufferSnapshot {
Some(items)
}
pub fn outline_range_containing<T: ToOffset>(&self, range: Range<T>) -> Option<Range<Point>> {
let range = range.to_offset(self);
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
grammar.outline_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|g| g.outline_config.as_ref().unwrap())
.collect::<Vec<_>>();
while let Some(mat) = matches.peek() {
let config = &configs[mat.grammar_index];
let containing_item_node = maybe!({
let item_node = mat.captures.iter().find_map(|cap| {
if cap.index == config.item_capture_ix {
Some(cap.node)
} else {
None
}
})?;
let item_byte_range = item_node.byte_range();
if item_byte_range.end < range.start || item_byte_range.start > range.end {
None
} else {
Some(item_node)
}
});
if let Some(item_node) = containing_item_node {
return Some(
Point::from_ts_point(item_node.start_position())
..Point::from_ts_point(item_node.end_position()),
);
}
matches.advance();
}
None
}
pub fn outline_items_containing<T: ToOffset>(
&self,
range: Range<T>,

View File

@@ -1104,20 +1104,32 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
let snapshot = buffer.snapshot();
assert_eq!(
snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")),
Some(range_of(text, "|"))
snapshot
.syntax_ancestor(empty_range_at(text, "|"))
.unwrap()
.byte_range(),
range_of(text, "|")
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|")),
Some(range_of(text, "|c|"))
snapshot
.syntax_ancestor(range_of(text, "|"))
.unwrap()
.byte_range(),
range_of(text, "|c|")
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|c|")),
Some(range_of(text, "|c| {}"))
snapshot
.syntax_ancestor(range_of(text, "|c|"))
.unwrap()
.byte_range(),
range_of(text, "|c| {}")
);
assert_eq!(
snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")),
Some(range_of(text, "(|c| {})"))
snapshot
.syntax_ancestor(range_of(text, "|c| {}"))
.unwrap()
.byte_range(),
range_of(text, "(|c| {})")
);
buffer

View File

@@ -78,7 +78,7 @@ pub use language_registry::{
};
pub use lsp::LanguageServerId;
pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, TreeSitterOptions};
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, ToTreeSitterPoint, TreeSitterOptions};
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
@@ -385,6 +385,15 @@ pub trait LspAdapter: 'static + Send + Sync {
None
}
async fn check_if_version_installed(
&self,
_version: &(dyn 'static + Send + Any),
_container_dir: &PathBuf,
_delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
None
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
@@ -516,14 +525,23 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
.fetch_latest_server_version(delegate.as_ref())
.await?;
log::info!("downloading language server {:?}", name.0);
delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
let binary = adapter
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
.await;
if let Some(binary) = adapter
.check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref())
.await
{
log::info!("language server {:?} is already installed", name.0);
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
Ok(binary)
} else {
log::info!("downloading language server {:?}", name.0);
delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
let binary = adapter
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
.await;
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
binary
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
binary
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]

View File

@@ -1845,7 +1845,7 @@ impl Drop for QueryCursorHandle {
}
}
pub(crate) trait ToTreeSitterPoint {
pub trait ToTreeSitterPoint {
fn to_ts_point(self) -> tree_sitter::Point;
fn from_ts_point(point: tree_sitter::Point) -> Self;
}

View File

@@ -26,6 +26,7 @@ pub struct CssLspAdapter {
}
impl CssLspAdapter {
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
pub fn new(node: NodeRuntime) -> Self {
CssLspAdapter { node }
}
@@ -56,18 +57,13 @@ impl LspAdapter for CssLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "vscode-langservers-extracted";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.await?;
}
self.node
.npm_install_packages(
&container_dir,
&[(Self::PACKAGE_NAME, latest_version.as_str())],
)
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
@@ -76,6 +72,31 @@ impl LspAdapter for CssLspAdapter {
})
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,

View File

@@ -64,6 +64,8 @@ pub struct JsonLspAdapter {
}
impl JsonLspAdapter {
const PACKAGE_NAME: &str = "vscode-langservers-extracted";
pub fn new(node: NodeRuntime, languages: Arc<LanguageRegistry>) -> Self {
Self {
node,
@@ -142,11 +144,36 @@ impl LspAdapter for JsonLspAdapter {
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(
self.node
.npm_package_latest_version("vscode-langservers-extracted")
.npm_package_latest_version(Self::PACKAGE_NAME)
.await?,
) as Box<_>)
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
@@ -155,18 +182,13 @@ impl LspAdapter for JsonLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "vscode-langservers-extracted";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.await?;
}
self.node
.npm_install_packages(
&container_dir,
&[(Self::PACKAGE_NAME, latest_version.as_str())],
)
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,

View File

@@ -117,24 +117,12 @@ impl LspAdapter for PythonLspAdapter {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(
Self::SERVER_NAME.as_ref(),
&server_path,
self.node
.npm_install_packages(
&container_dir,
&latest_version,
&[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
)
.await;
if should_install_language_server {
self.node
.npm_install_packages(
&container_dir,
&[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
)
.await?;
}
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
@@ -143,6 +131,36 @@ impl LspAdapter for PythonLspAdapter {
})
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(
Self::SERVER_NAME.as_ref(),
&server_path,
&container_dir,
&version,
)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,

View File

@@ -36,11 +36,25 @@
(function_definition
name: (identifier) @run @_pytest_method_name
(#match? @_pytest_method_name "^test_")
) @python-pytest-method
) @_python-pytest-method
)
(#set! tag python-pytest-method)
)
; decorated pytest functions
(
(module
(decorated_definition
(decorator)+ @_decorator
definition: (function_definition
name: (identifier) @run @_pytest_method_name
(#match? @_pytest_method_name "^test_")
)
) @_python-pytest-method
)
(#set! tag python-pytest-method)
)
; pytest classes
(
(module
@@ -62,7 +76,7 @@
(function_definition
name: (identifier) @run @_pytest_method_name
(#match? @_pytest_method_name "^test")
) @python-pytest-method
) @_python-pytest-method
(#set! tag python-pytest-method)
)
)

View File

@@ -34,6 +34,7 @@ pub struct TailwindLspAdapter {
impl TailwindLspAdapter {
const SERVER_NAME: LanguageServerName =
LanguageServerName::new_static("tailwindcss-language-server");
const PACKAGE_NAME: &str = "@tailwindcss/language-server";
pub fn new(node: NodeRuntime) -> Self {
TailwindLspAdapter { node }
@@ -52,7 +53,7 @@ impl LspAdapter for TailwindLspAdapter {
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
.npm_package_latest_version("@tailwindcss/language-server")
.npm_package_latest_version(Self::PACKAGE_NAME)
.await?,
) as Box<_>)
}
@@ -65,18 +66,13 @@ impl LspAdapter for TailwindLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "@tailwindcss/language-server";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.await?;
}
self.node
.npm_install_packages(
&container_dir,
&[(Self::PACKAGE_NAME, latest_version.as_str())],
)
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
@@ -85,6 +81,31 @@ impl LspAdapter for TailwindLspAdapter {
})
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,

View File

@@ -73,6 +73,7 @@ impl TypeScriptLspAdapter {
const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
const SERVER_NAME: LanguageServerName =
LanguageServerName::new_static("typescript-language-server");
const PACKAGE_NAME: &str = "typescript";
pub fn new(node: NodeRuntime) -> Self {
TypeScriptLspAdapter { node }
}
@@ -114,6 +115,36 @@ impl LspAdapter for TypeScriptLspAdapter {
}) as Box<_>)
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
&container_dir,
version.typescript_version.as_str(),
)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: typescript_server_binary_arguments(&server_path),
})
}
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
@@ -122,32 +153,22 @@ impl LspAdapter for TypeScriptLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<TypeScriptVersions>().unwrap();
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
let package_name = "typescript";
let should_install_language_server = self
.node
.should_install_npm_package(
package_name,
&server_path,
self.node
.npm_install_packages(
&container_dir,
latest_version.typescript_version.as_str(),
&[
(
Self::PACKAGE_NAME,
latest_version.typescript_version.as_str(),
),
(
"typescript-language-server",
latest_version.server_version.as_str(),
),
],
)
.await;
if should_install_language_server {
self.node
.npm_install_packages(
&container_dir,
&[
(package_name, latest_version.typescript_version.as_str()),
(
"typescript-language-server",
latest_version.server_version.as_str(),
),
],
)
.await?;
}
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,

View File

@@ -31,6 +31,7 @@ pub struct YamlLspAdapter {
impl YamlLspAdapter {
const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("yaml-language-server");
const PACKAGE_NAME: &str = "yaml-language-server";
pub fn new(node: NodeRuntime) -> Self {
YamlLspAdapter { node }
}
@@ -61,18 +62,13 @@ impl LspAdapter for YamlLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "yaml-language-server";
let should_install_language_server = self
.node
.should_install_npm_package(package_name, &server_path, &container_dir, &latest_version)
.await;
if should_install_language_server {
self.node
.npm_install_packages(&container_dir, &[(package_name, latest_version.as_str())])
.await?;
}
self.node
.npm_install_packages(
&container_dir,
&[(Self::PACKAGE_NAME, latest_version.as_str())],
)
.await?;
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
@@ -81,6 +77,31 @@ impl LspAdapter for YamlLspAdapter {
})
}
async fn check_if_version_installed(
&self,
version: &(dyn 'static + Send + Any),
container_dir: &PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let version = version.downcast_ref::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let should_install_language_server = self
.node
.should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version)
.await;
if should_install_language_server {
None
} else {
Some(LanguageServerBinary {
path: self.node.binary_path().await.ok()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,

View File

@@ -54,7 +54,7 @@ They can also be detected automatically, for example https://zed.dev/blog.
## Images
Images are like links, but with an exclamation mark `!` in front.
```todo!
```markdown
![This is an image](/images/logo.png)
```

View File

@@ -1,5 +1,5 @@
use assets::Assets;
use gpui::*;
use gpui::{rgb, App, KeyBinding, Length, StyleRefinement, View, WindowOptions};
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::NodeRuntime;

View File

@@ -39,6 +39,7 @@ smallvec.workspace = true
sum_tree.workspace = true
text.workspace = true
theme.workspace = true
tree-sitter.workspace = true
util.workspace = true
[dev-dependencies]

View File

@@ -3920,15 +3920,16 @@ impl MultiBufferSnapshot {
})
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
pub fn syntax_ancestor<T: ToOffset>(
&self,
range: Range<T>,
) -> Option<(tree_sitter::Node, Range<usize>)> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let excerpt = self.excerpt_containing(range.clone())?;
let ancestor_buffer_range = excerpt
let node = excerpt
.buffer()
.range_for_syntax_ancestor(excerpt.map_range_to_buffer(range))?;
Some(excerpt.map_range_from_buffer(ancestor_buffer_range))
.syntax_ancestor(excerpt.map_range_to_buffer(range))?;
Some((node, excerpt.map_range_from_buffer(node.byte_range())))
}
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {

View File

@@ -1668,7 +1668,7 @@ impl BufferStore {
.log_err();
}
// todo!(max): do something
// TODO(max): do something
// client
// .send(proto::UpdateStagedText {
// project_id,

View File

@@ -3142,7 +3142,7 @@ impl LspStore {
fn request_workspace_config_refresh(&mut self) {
*self._maintain_workspace_config.1.borrow_mut() = ();
}
// todo!
pub fn prettier_store(&self) -> Option<Model<PrettierStore>> {
self.as_local().map(|local| local.prettier_store.clone())
}

View File

@@ -3200,7 +3200,7 @@ impl ProjectPanel {
item_colors.default
};
let bg_hover_color = if self.mouse_down {
let bg_hover_color = if self.mouse_down || is_marked || is_active {
item_colors.marked_active
} else {
item_colors.hover
@@ -3224,9 +3224,7 @@ impl ProjectPanel {
.border_1()
.border_r_2()
.border_color(border_color)
.when(!is_marked && !is_active, |div| {
div.hover(|style| style.bg(bg_hover_color))
})
.hover(|style| style.bg(bg_hover_color))
.when(is_local, |div| {
div.on_drag_move::<ExternalPaths>(cx.listener(
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {

View File

@@ -9,6 +9,10 @@ fn main() {
"cargo:rustc-env=ZED_PKG_VERSION={}",
zed_cargo_toml.package.unwrap().version.unwrap()
);
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
// If we're building this for nightly, we want to set the ZED_COMMIT_SHA
if let Some(release_channel) = std::env::var("ZED_RELEASE_CHANNEL").ok() {

View File

@@ -160,6 +160,7 @@ fn init_panic_hook() {
option_env!("ZED_COMMIT_SHA").unwrap_or(&env!("ZED_PKG_VERSION"))
),
release_channel: release_channel::RELEASE_CHANNEL.display_name().into(),
target: env!("TARGET").to_owned().into(),
os_name: telemetry::os_name(),
os_version: Some(telemetry::os_version()),
architecture: env::consts::ARCH.into(),

View File

@@ -515,7 +515,7 @@ impl project::ProjectItem for NotebookItem {
Ok(nbformat::Notebook::V4(notebook)) => notebook,
// 4.1 - 4.4 are converted to 4.5
Ok(nbformat::Notebook::Legacy(legacy_notebook)) => {
// todo!(): Decide if we want to mutate the notebook by including Cell IDs
// TODO: Decide if we want to mutate the notebook by including Cell IDs
// and any other conversions
let notebook = nbformat::upgrade_legacy_notebook(legacy_notebook)?;
notebook

View File

@@ -57,7 +57,7 @@ impl OutputContent for MarkdownView {
fn buffer_content(&mut self, cx: &mut WindowContext) -> Option<Model<Buffer>> {
let buffer = cx.new_model(|cx| {
// todo!(): Bring in the language registry so we can set the language to markdown
// TODO: Bring in the language registry so we can set the language to markdown
let mut buffer = Buffer::local(self.raw_text.clone(), cx)
.with_language(language::PLAIN_TEXT.clone(), cx);
buffer.set_capability(language::Capability::ReadOnly, cx);

View File

@@ -610,7 +610,7 @@ impl Session {
// Start a new kernel
this.update(&mut cx, |session, cx| {
// todo!(): Differentiate between restart and restart+clear-outputs
// TODO: Differentiate between restart and restart+clear-outputs
session.clear_outputs(cx);
session.start_kernel(cx);
})

View File

@@ -269,6 +269,7 @@ pub struct Panic {
pub app_version: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: String,
pub target: Option<String>,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,

View File

@@ -1183,10 +1183,10 @@ impl Terminal {
}
let motion: Option<ViMotion> = match key.as_str() {
"h" => Some(ViMotion::Left),
"j" => Some(ViMotion::Down),
"k" => Some(ViMotion::Up),
"l" => Some(ViMotion::Right),
"h" | "left" => Some(ViMotion::Left),
"j" | "down" => Some(ViMotion::Down),
"k" | "up" => Some(ViMotion::Up),
"l" | "right" => Some(ViMotion::Right),
"w" => Some(ViMotion::WordRight),
"b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft),
"e" => Some(ViMotion::WordRightEnd),

View File

@@ -134,18 +134,19 @@ impl Render for TitleBar {
.child(
h_flex()
.gap_1()
.when_some(self.application_menu.clone(), |this, menu| {
let is_any_menu_deployed = menu.read(cx).is_any_deployed();
this.child(menu).when(!is_any_menu_deployed, |this| {
this.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.children(self.render_project_branch(cx))
})
})
.when(self.application_menu.is_none(), |this| {
this.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.children(self.render_project_branch(cx))
.map(|title_bar| {
let mut render_project_items = true;
title_bar
.when_some(self.application_menu.clone(), |title_bar, menu| {
render_project_items = !menu.read(cx).is_any_deployed();
title_bar.child(menu)
})
.when(render_project_items, |title_bar| {
title_bar
.children(self.render_project_host(cx))
.child(self.render_project_name(cx))
.children(self.render_project_branch(cx))
})
})
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()),
)
@@ -318,21 +319,24 @@ impl TitleBar {
Some(
ButtonLike::new("ssh-server-icon")
.child(
IconWithIndicator::new(
Icon::new(IconName::Server)
.size(IconSize::XSmall)
.color(icon_color),
Some(Indicator::dot().color(indicator_color)),
)
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
.into_any_element(),
)
.child(
div()
h_flex()
.gap_2()
.max_w_32()
.overflow_hidden()
.text_ellipsis()
.child(Label::new(nickname.clone()).size(LabelSize::Small)),
.child(
IconWithIndicator::new(
Icon::new(IconName::Server)
.size(IconSize::XSmall)
.color(icon_color),
Some(Indicator::dot().color(indicator_color)),
)
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
.into_any_element(),
)
.child(
Label::new(nickname.clone())
.size(LabelSize::Small)
.text_ellipsis(),
),
)
.tooltip(move |cx| {
Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)

View File

@@ -282,6 +282,52 @@ impl RenderOnce for Switch {
}
}
/// A [`Switch`] that has a [`Label`].
#[derive(IntoElement)]
pub struct SwitchWithLabel {
id: ElementId,
label: Label,
checked: ToggleState,
on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
}
impl SwitchWithLabel {
pub fn new(
id: impl Into<ElementId>,
label: Label,
checked: ToggleState,
on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
) -> Self {
Self {
id: id.into(),
label,
checked,
on_click: Arc::new(on_click),
}
}
}
impl RenderOnce for SwitchWithLabel {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(Switch::new(self.id.clone(), self.checked).on_click({
let on_click = self.on_click.clone();
move |checked, cx| {
(on_click)(checked, cx);
}
}))
.child(
div()
.id(SharedString::from(format!("{}-label", self.id)))
.on_click(move |_event, cx| {
(self.on_click)(&self.checked.inverse(), cx);
})
.child(self.label),
)
}
}
impl ComponentPreview for Checkbox {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
@@ -407,3 +453,32 @@ impl ComponentPreview for CheckboxWithLabel {
])]
}
}
impl ComponentPreview for SwitchWithLabel {
fn description() -> impl Into<Option<&'static str>> {
"A switch with an associated label, allowing users to select an option while providing a descriptive text."
}
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Off",
SwitchWithLabel::new(
"switch_with_label_unselected",
Label::new("Always save on quit"),
ToggleState::Unselected,
|_, _| {},
),
),
single_example(
"On",
SwitchWithLabel::new(
"switch_with_label_selected",
Label::new("Always save on quit"),
ToggleState::Selected,
|_, _| {},
),
),
])]
}
}

View File

@@ -1,7 +1,8 @@
#![allow(missing_docs)]
use gpui::Axis;
use crate::prelude::*;
use gpui::*;
#[derive(IntoElement)]
pub struct ToolStrip {

View File

@@ -5,11 +5,11 @@
//! It can't be located in the `ui` crate because it depends on `editor`.
//!
use editor::*;
use gpui::*;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusHandle, FocusableView, FontStyle, Hsla, TextStyle, View};
use settings::Settings;
use theme::ThemeSettings;
use ui::*;
use ui::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FieldLabelLayout {

View File

@@ -24,6 +24,7 @@ futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
@@ -35,6 +36,9 @@ take-until = "0.2.0"
tempfile = { workspace = true, optional = true }
unicase.workspace = true
[target.'cfg(unix)'.dependencies]
libc.workspace = true
[target.'cfg(windows)'.dependencies]
tendril = "0.4.3"
dunce = "1.0"

View File

@@ -6,8 +6,10 @@ pub mod serde;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use anyhow::{anyhow, Context as _, Result};
use futures::Future;
use itertools::Either;
use regex::Regex;
use std::sync::{LazyLock, OnceLock};
use std::{
@@ -109,6 +111,106 @@ where
}
}
#[cfg(unix)]
pub fn load_shell_from_passwd() -> Result<()> {
let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
n if n < 0 => 1024,
n => n as usize,
};
let mut buffer = Vec::with_capacity(buflen);
let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
let mut result: *mut libc::passwd = std::ptr::null_mut();
let uid = unsafe { libc::getuid() };
let status = unsafe {
libc::getpwuid_r(
uid,
pwd.as_mut_ptr(),
buffer.as_mut_ptr() as *mut libc::c_char,
buflen,
&mut result,
)
};
let entry = unsafe { pwd.assume_init() };
anyhow::ensure!(
status == 0,
"call to getpwuid_r failed. uid: {}, status: {}",
uid,
status
);
anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
anyhow::ensure!(
entry.pw_uid == uid,
"passwd entry has different uid ({}) than getuid ({}) returned",
entry.pw_uid,
uid,
);
let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
if env::var("SHELL").map_or(true, |shell_env| shell_env != shell) {
log::info!(
"updating SHELL environment variable to value from passwd entry: {:?}",
shell,
);
env::set_var("SHELL", shell);
}
Ok(())
}
pub fn load_login_shell_environment() -> Result<()> {
let marker = "ZED_LOGIN_SHELL_START";
let shell = env::var("SHELL").context(
"SHELL environment variable is not assigned so we can't source login environment variables",
)?;
// If possible, we want to `cd` in the user's `$HOME` to trigger programs
// such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
// into shell's `cd` command (and hooks) to manipulate env.
// We do this so that we get the env a user would have when spawning a shell
// in home directory.
let shell_cmd_prefix = std::env::var_os("HOME")
.and_then(|home| home.into_string().ok())
.map(|home| format!("cd '{home}';"));
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
let shell_cmd = format!(
"{}printf '%s' {marker}; /usr/bin/env; exit 0;",
shell_cmd_prefix.as_deref().unwrap_or("")
);
let output = std::process::Command::new(&shell)
.args(["-l", "-i", "-c", &shell_cmd])
.output()
.context("failed to spawn login shell to source login environment variables")?;
if !output.status.success() {
Err(anyhow!("login shell exited with error"))?;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(env_output_start) = stdout.find(marker) {
let env_output = &stdout[env_output_start + marker.len()..];
parse_env_output(env_output, |key, value| env::set_var(key, value));
log::info!(
"set environment variables from shell:{}, path:{}",
shell,
env::var("PATH").unwrap_or_default(),
);
}
Ok(())
}
/// Parse the result of calling `usr/bin/env` with no arguments
pub fn parse_env_output(env: &str, mut f: impl FnMut(String, String)) {
let mut current_key: Option<String> = None;
@@ -199,6 +301,35 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
}
}
pub fn iterate_expanded_and_wrapped_usize_range(
range: Range<usize>,
additional_before: usize,
additional_after: usize,
wrap_length: usize,
) -> impl Iterator<Item = usize> {
let start_wraps = range.start < additional_before;
let end_wraps = wrap_length < range.end + additional_after;
if start_wraps && end_wraps {
Either::Left(0..wrap_length)
} else if start_wraps {
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
if wrapped_start <= range.end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
}
} else if end_wraps {
let wrapped_end = range.end + additional_after - wrap_length;
if range.start <= wrapped_end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
}
} else {
Either::Left((range.start - additional_before)..(range.end + additional_after))
}
}
pub trait ResultExt<E> {
type Ok;
@@ -734,4 +865,48 @@ Line 2
Line 3"#
);
}
#[test]
fn test_iterate_expanded_and_wrapped_usize_range() {
// Neither wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
(1..5).collect::<Vec<usize>>()
);
// Start wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
((0..5).chain(7..8)).collect::<Vec<usize>>()
);
// Start wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Start wraps all the way around and past 0
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
(0..1).chain(2..8).collect::<Vec<usize>>()
);
// End wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps all the way around and past the end
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Both start and end wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
}
}

View File

@@ -585,8 +585,6 @@ impl Motion {
| NextLineStart
| PreviousLineStart
| StartOfLineDownward
| SentenceBackward
| SentenceForward
| StartOfParagraph
| EndOfParagraph
| WindowTop
@@ -611,6 +609,8 @@ impl Motion {
| Left
| Backspace
| Right
| SentenceBackward
| SentenceForward
| Space
| StartOfLine { .. }
| EndOfLineDownward

View File

@@ -28,23 +28,27 @@ impl Vim {
original_columns.insert(selection.id, original_head.column());
motion.expand_selection(map, selection, times, true, &text_layout_details);
let start_point = selection.start.to_point(map);
let next_line = map
.buffer_snapshot
.clip_point(Point::new(start_point.row + 1, 0), Bias::Left)
.to_display_point(map);
match motion {
// Motion::NextWordStart on an empty line should delete it.
Motion::NextWordStart { .. } => {
Motion::NextWordStart { .. }
if selection.is_empty()
&& map
.buffer_snapshot
.line_len(MultiBufferRow(selection.start.to_point(map).row))
== 0
{
selection.end = map
.buffer_snapshot
.clip_point(
Point::new(selection.start.to_point(map).row + 1, 0),
Bias::Left,
)
.to_display_point(map)
}
.line_len(MultiBufferRow(start_point.row))
== 0 =>
{
selection.end = next_line
}
// Sentence motions, when done from start of line, include the newline
Motion::SentenceForward | Motion::SentenceBackward
if selection.start.column() == 0 =>
{
selection.end = next_line
}
Motion::EndOfDocument {} => {
// Deleting until the end of the document includes the last line, including
@@ -604,4 +608,62 @@ mod test {
cx.simulate("d t x", "ˇax").await.assert_matches();
cx.simulate("d t x", "aˇx").await.assert_matches();
}
#[gpui::test]
async fn test_delete_sentence(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.simulate(
"d )",
indoc! {"
Fiˇrst. Second. Third.
Fourth.
"},
)
.await
.assert_matches();
cx.simulate(
"d )",
indoc! {"
First. Secˇond. Third.
Fourth.
"},
)
.await
.assert_matches();
// Two deletes
cx.simulate(
"d ) d )",
indoc! {"
First. Second. Thirˇd.
Fourth.
"},
)
.await
.assert_matches();
// Should delete whole line if done on first column
cx.simulate(
"d )",
indoc! {"
ˇFirst.
Fourth.
"},
)
.await
.assert_matches();
// Backwards it should also delete the whole first line
cx.simulate(
"d (",
indoc! {"
First.
ˇSecond.
Fourth.
"},
)
.await
.assert_matches();
}
}

View File

@@ -21,6 +21,7 @@ use serde::Deserialize;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Object {
Word { ignore_punctuation: bool },
Subword { ignore_punctuation: bool },
Sentence,
Paragraph,
Quotes,
@@ -47,12 +48,18 @@ struct Word {
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Subword {
#[serde(default)]
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct IndentObj {
#[serde(default)]
include_below: bool,
}
impl_actions!(vim, [Word, IndentObj]);
impl_actions!(vim, [Word, Subword, IndentObj]);
actions!(
vim,
@@ -83,6 +90,13 @@ pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
vim.object(Object::Word { ignore_punctuation }, cx)
},
);
Vim::action(
editor,
cx,
|vim, &Subword { ignore_punctuation }: &Subword, cx| {
vim.object(Object::Subword { ignore_punctuation }, cx)
},
);
Vim::action(editor, cx, |vim, _: &Tag, cx| vim.object(Object::Tag, cx));
Vim::action(editor, cx, |vim, _: &Sentence, cx| {
vim.object(Object::Sentence, cx)
@@ -154,6 +168,7 @@ impl Object {
pub fn is_multiline(self) -> bool {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Quotes
| Object::BackQuotes
| Object::VerticalBars
@@ -176,6 +191,7 @@ impl Object {
pub fn always_expands_both_ways(self) -> bool {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Sentence
| Object::Paragraph
| Object::Argument
@@ -198,6 +214,7 @@ impl Object {
pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode {
match self {
Object::Word { .. }
| Object::Subword { .. }
| Object::Sentence
| Object::Quotes
| Object::BackQuotes
@@ -243,6 +260,13 @@ impl Object {
in_word(map, relative_to, ignore_punctuation)
}
}
Object::Subword { ignore_punctuation } => {
if around {
around_subword(map, relative_to, ignore_punctuation)
} else {
in_subword(map, relative_to, ignore_punctuation)
}
}
Object::Sentence => sentence(map, relative_to, around),
Object::Paragraph => paragraph(map, relative_to, around),
Object::Quotes => {
@@ -350,6 +374,63 @@ fn in_word(
Some(start..end)
}
fn in_subword(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
let offset = relative_to.to_offset(map, Bias::Left);
// Use motion::right so that we consider the character under the cursor when looking for the start
let classifier = map
.buffer_snapshot
.char_classifier_at(relative_to.to_point(map))
.ignore_punctuation(ignore_punctuation);
let in_subword = map
.buffer_chars_at(offset)
.next()
.map(|(c, _)| {
if classifier.is_word('-') {
!classifier.is_whitespace(c) && c != '_' && c != '-'
} else {
!classifier.is_whitespace(c) && c != '_'
}
})
.unwrap_or(false);
let start = if in_subword {
movement::find_preceding_boundary_display_point(
map,
right(map, relative_to, 1),
movement::FindRange::SingleLine,
|left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
},
)
} else {
movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
})
};
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_end = classifier.kind(left) != classifier.kind(right);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end
});
Some(start..end)
}
pub fn surrounding_html_tag(
map: &DisplaySnapshot,
head: DisplayPoint,
@@ -461,6 +542,40 @@ fn around_word(
}
}
fn around_subword(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
let classifier = map
.buffer_snapshot
.char_classifier_at(relative_to.to_point(map))
.ignore_punctuation(ignore_punctuation);
let start = movement::find_preceding_boundary_display_point(
map,
right(map, relative_to, 1),
movement::FindRange::SingleLine,
|left, right| {
let is_word_start = classifier.kind(left) != classifier.kind(right);
let is_subword_start = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
},
);
let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| {
let is_word_end = classifier.kind(left) != classifier.kind(right);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end
});
Some(start..end)
}
fn around_containing_word(
map: &DisplaySnapshot,
relative_to: DisplayPoint,

View File

@@ -0,0 +1,22 @@
{"Put":{"state":"Fiˇrst. Second. Third.\nFourth.\n"}}
{"Key":"d"}
{"Key":")"}
{"Get":{"state":"FiˇSecond. Third.\nFourth.\n","mode":"Normal"}}
{"Put":{"state":"First. Secˇond. Third.\nFourth.\n"}}
{"Key":"d"}
{"Key":")"}
{"Get":{"state":"First. SecˇThird.\nFourth.\n","mode":"Normal"}}
{"Put":{"state":"First. Second. Thirˇd.\nFourth.\n"}}
{"Key":"d"}
{"Key":")"}
{"Key":"d"}
{"Key":")"}
{"Get":{"state":"First. Second. Thˇi\n","mode":"Normal"}}
{"Put":{"state":"ˇFirst.\nFourth.\n"}}
{"Key":"d"}
{"Key":")"}
{"Get":{"state":"ˇFourth.\n","mode":"Normal"}}
{"Put":{"state":"First.\nˇSecond.\nFourth.\n"}}
{"Key":"d"}
{"Key":"("}
{"Get":{"state":"ˇSecond.\nFourth.\n","mode":"Normal"}}

View File

@@ -6,7 +6,7 @@ use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
IconDecoration, Indicator, Switch, Table, TintColor, Tooltip,
IconDecoration, Indicator, Switch, SwitchWithLabel, Table, TintColor, Tooltip,
};
use crate::{Item, Workspace};
@@ -369,16 +369,17 @@ impl ThemePreview {
.overflow_scroll()
.size_full()
.gap_2()
.child(Switch::render_component_previews(cx))
.child(ContentGroup::render_component_previews(cx))
.child(IconDecoration::render_component_previews(cx))
.child(DecoratedIcon::render_component_previews(cx))
.child(Button::render_component_previews(cx))
.child(Checkbox::render_component_previews(cx))
.child(CheckboxWithLabel::render_component_previews(cx))
.child(ContentGroup::render_component_previews(cx))
.child(DecoratedIcon::render_component_previews(cx))
.child(Facepile::render_component_previews(cx))
.child(Button::render_component_previews(cx))
.child(Indicator::render_component_previews(cx))
.child(Icon::render_component_previews(cx))
.child(IconDecoration::render_component_previews(cx))
.child(Indicator::render_component_previews(cx))
.child(Switch::render_component_previews(cx))
.child(SwitchWithLabel::render_component_previews(cx))
.child(Table::render_component_previews(cx))
}

View File

@@ -995,7 +995,7 @@ impl Workspace {
this.update(&mut cx, |this, cx| {
if let Some(display) = cx.display() {
if let Ok(display_uuid) = display.uuid() {
let window_bounds = cx.window_bounds();
let window_bounds = cx.inner_window_bounds();
if let Some(database_id) = workspace_id {
cx.background_executor()
.spawn(DB.set_window_open_status(

View File

@@ -33,6 +33,10 @@ fn main() {
// Populate git sha environment variable if git is available
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
if output.status.success() {
let git_sha = String::from_utf8_lossy(&output.stdout);

View File

@@ -10,6 +10,7 @@ use clap::{command, Parser};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{parse_zed_link, Client, ProxySettings, UserStore};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
use editor::Editor;
use env_logger::Builder;
@@ -37,18 +38,17 @@ use settings::{
handle_settings_file_changes, watch_config_file, InvalidSettingsError, Settings, SettingsStore,
};
use simplelog::ConfigBuilder;
use smol::process::Command;
use std::{
env,
fs::OpenOptions,
io::{IsTerminal, Write},
io::{self, IsTerminal, Write},
path::{Path, PathBuf},
process,
sync::Arc,
};
use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
use time::UtcOffset;
use util::{maybe, parse_env_output, ResultExt, TryFutureExt};
use util::{load_login_shell_environment, maybe, ResultExt, TryFutureExt};
use uuid::Uuid;
use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
use workspace::{
@@ -63,26 +63,66 @@ use zed::{
use crate::zed::inline_completion_registry;
#[cfg(unix)]
use util::load_shell_from_passwd;
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
fn fail_to_launch(e: anyhow::Error) {
eprintln!("Zed failed to launch: {e:?}");
App::new().run(move |cx| {
if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| cx.new_view(|_| gpui::Empty)) {
window.update(cx, |_, cx| {
let response = cx.prompt(gpui::PromptLevel::Critical, "Zed failed to launch", Some(&format!("{e}\n\nFor help resolving this, please open an issue on https://github.com/zed-industries/zed")), &["Exit"]);
fn files_not_createad_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
let message = "Zed failed to launch";
let error_details = errors
.into_iter()
.flat_map(|(kind, paths)| {
#[allow(unused_mut)] // for non-unix platforms
let mut error_kind_details = match paths.len() {
0 => return None,
1 => format!(
"{kind} when creating directory {:?}",
paths.first().expect("match arm checks for a single entry")
),
_many => format!("{kind} when creating directories {paths:?}"),
};
cx.spawn(|_, mut cx| async move {
response.await?;
cx.update(|cx| {
cx.quit()
#[cfg(unix)]
{
match kind {
io::ErrorKind::PermissionDenied => {
error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\
\nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`");
}
_ => {}
}
}
Some(error_kind_details)
})
.collect::<Vec<_>>().join("\n\n");
eprintln!("{message}: {error_details}");
App::new().run(move |cx| {
if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |cx| {
cx.new_view(|_| gpui::Empty)
}) {
window
.update(cx, |_, cx| {
let response = cx.prompt(
gpui::PromptLevel::Critical,
message,
Some(&error_details),
&["Exit"],
);
cx.spawn(|_, mut cx| async move {
response.await?;
cx.update(|cx| cx.quit())
})
}).detach_and_log_err(cx);
}).log_err();
.detach_and_log_err(cx);
})
.log_err();
} else {
fail_to_open_window(e, cx)
fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx)
}
})
}
@@ -137,8 +177,9 @@ fn main() {
menu::init();
zed_actions::init();
if let Err(e) = init_paths() {
fail_to_launch(e);
let file_errors = init_paths();
if !file_errors.is_empty() {
files_not_createad_on_launch(file_errors);
return;
}
@@ -219,9 +260,9 @@ fn main() {
.spawn(async {
#[cfg(unix)]
{
load_shell_from_passwd().await.log_err();
load_shell_from_passwd().log_err();
}
load_login_shell_environment().await.log_err();
load_login_shell_environment().log_err();
})
.detach()
};
@@ -905,8 +946,8 @@ pub(crate) async fn restorable_workspace_locations(
}
}
fn init_paths() -> anyhow::Result<()> {
for path in [
fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
[
paths::config_dir(),
paths::extensions_dir(),
paths::languages_dir(),
@@ -914,12 +955,13 @@ fn init_paths() -> anyhow::Result<()> {
paths::logs_dir(),
paths::temp_dir(),
]
.iter()
{
std::fs::create_dir_all(path)
.map_err(|e| anyhow!("Could not create directory {:?}: {}", path, e))?;
}
Ok(())
.into_iter()
.fold(HashMap::default(), |mut errors, path| {
if let Err(e) = std::fs::create_dir_all(path) {
errors.entry(e.kind()).or_insert_with(Vec::new).push(path);
}
errors
})
}
fn init_logger() {
@@ -998,109 +1040,8 @@ fn init_stdout_logger() {
.init();
}
#[cfg(unix)]
async fn load_shell_from_passwd() -> Result<()> {
let buflen = match unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) } {
n if n < 0 => 1024,
n => n as usize,
};
let mut buffer = Vec::with_capacity(buflen);
let mut pwd: std::mem::MaybeUninit<libc::passwd> = std::mem::MaybeUninit::uninit();
let mut result: *mut libc::passwd = std::ptr::null_mut();
let uid = unsafe { libc::getuid() };
let status = unsafe {
libc::getpwuid_r(
uid,
pwd.as_mut_ptr(),
buffer.as_mut_ptr() as *mut libc::c_char,
buflen,
&mut result,
)
};
let entry = unsafe { pwd.assume_init() };
anyhow::ensure!(
status == 0,
"call to getpwuid_r failed. uid: {}, status: {}",
uid,
status
);
anyhow::ensure!(!result.is_null(), "passwd entry for uid {} not found", uid);
anyhow::ensure!(
entry.pw_uid == uid,
"passwd entry has different uid ({}) than getuid ({}) returned",
entry.pw_uid,
uid,
);
let shell = unsafe { std::ffi::CStr::from_ptr(entry.pw_shell).to_str().unwrap() };
if env::var("SHELL").map_or(true, |shell_env| shell_env != shell) {
log::info!(
"updating SHELL environment variable to value from passwd entry: {:?}",
shell,
);
env::set_var("SHELL", shell);
}
Ok(())
}
async fn load_login_shell_environment() -> Result<()> {
let marker = "ZED_LOGIN_SHELL_START";
let shell = env::var("SHELL").context(
"SHELL environment variable is not assigned so we can't source login environment variables",
)?;
// If possible, we want to `cd` in the user's `$HOME` to trigger programs
// such as direnv, asdf, mise, ... to adjust the PATH. These tools often hook
// into shell's `cd` command (and hooks) to manipulate env.
// We do this so that we get the env a user would have when spawning a shell
// in home directory.
let shell_cmd_prefix = std::env::var_os("HOME")
.and_then(|home| home.into_string().ok())
.map(|home| format!("cd '{home}';"));
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
let shell_cmd = format!(
"{}printf '%s' {marker}; /usr/bin/env; exit 0;",
shell_cmd_prefix.as_deref().unwrap_or("")
);
let output = Command::new(&shell)
.args(["-l", "-i", "-c", &shell_cmd])
.output()
.await
.context("failed to spawn login shell to source login environment variables")?;
if !output.status.success() {
Err(anyhow!("login shell exited with error"))?;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(env_output_start) = stdout.find(marker) {
let env_output = &stdout[env_output_start + marker.len()..];
parse_env_output(env_output, |key, value| env::set_var(key, value));
log::info!(
"set environment variables from shell:{}, path:{}",
shell,
env::var("PATH").unwrap_or_default(),
);
}
Ok(())
}
fn stdout_is_a_pty() -> bool {
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal()
std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
}
#[derive(Parser, Debug)]

View File

@@ -1,31 +1,26 @@
use crate::stdout_is_a_pty;
use anyhow::{Context, Result};
use backtrace::{self, Backtrace};
use chrono::Utc;
use client::{telemetry, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use gpui::{AppContext, SemanticVersion};
use http_client::{HttpRequestExt, Method};
use http_client::{self, HttpClient, HttpClientWithUrl};
use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method};
use paths::{crashes_dir, crashes_retired_dir};
use project::Project;
use release_channel::ReleaseChannel;
use release_channel::RELEASE_CHANNEL;
use release_channel::{ReleaseChannel, RELEASE_CHANNEL};
use settings::Settings;
use smol::stream::StreamExt;
use std::{
env,
ffi::OsStr,
ffi::{c_void, OsStr},
sync::{atomic::Ordering, Arc},
};
use std::{io::Write, panic, sync::atomic::AtomicU32, thread};
use telemetry_events::LocationData;
use telemetry_events::Panic;
use telemetry_events::PanicRequest;
use telemetry_events::{LocationData, Panic, PanicRequest};
use url::Url;
use util::ResultExt;
use crate::stdout_is_a_pty;
static PANIC_COUNT: AtomicU32 = AtomicU32::new(0);
pub fn init_panic_hook(
@@ -69,25 +64,35 @@ pub fn init_panic_hook(
);
std::process::exit(-1);
}
let main_module_base_address = get_main_module_base_address();
let backtrace = Backtrace::new();
let mut backtrace = backtrace
let mut symbols = backtrace
.frames()
.iter()
.flat_map(|frame| {
frame
.symbols()
.iter()
.filter_map(|frame| Some(format!("{:#}", frame.name()?)))
let base = frame
.module_base_address()
.unwrap_or(main_module_base_address);
frame.symbols().iter().map(move |symbol| {
format!(
"{}+{}",
symbol
.name()
.as_ref()
.map_or("<unknown>".to_owned(), <_>::to_string),
(frame.ip() as isize).saturating_sub(base as isize)
)
})
})
.collect::<Vec<_>>();
// Strip out leading stack frames for rust panic-handling.
if let Some(ix) = backtrace
if let Some(ix) = symbols
.iter()
.position(|name| name == "rust_begin_unwind" || name == "_rust_begin_unwind")
{
backtrace.drain(0..=ix);
symbols.drain(0..=ix);
}
let panic_data = telemetry_events::Panic {
@@ -98,12 +103,13 @@ pub fn init_panic_hook(
line: location.line(),
}),
app_version: app_version.to_string(),
release_channel: RELEASE_CHANNEL.display_name().into(),
release_channel: RELEASE_CHANNEL.dev_name().into(),
target: env!("TARGET").to_owned().into(),
os_name: telemetry::os_name(),
os_version: Some(telemetry::os_version()),
architecture: env::consts::ARCH.into(),
panicked_on: Utc::now().timestamp_millis(),
backtrace,
backtrace: symbols,
system_id: system_id.clone(),
installation_id: installation_id.clone(),
session_id: session_id.clone(),
@@ -133,6 +139,25 @@ pub fn init_panic_hook(
}));
}
#[cfg(not(target_os = "windows"))]
fn get_main_module_base_address() -> *mut c_void {
let mut dl_info = libc::Dl_info {
dli_fname: std::ptr::null(),
dli_fbase: std::ptr::null_mut(),
dli_sname: std::ptr::null(),
dli_saddr: std::ptr::null_mut(),
};
unsafe {
libc::dladdr(get_main_module_base_address as _, &mut dl_info);
}
dl_info.dli_fbase
}
#[cfg(target_os = "windows")]
fn get_main_module_base_address() -> *mut c_void {
std::ptr::null_mut()
}
pub fn init(
http_client: Arc<HttpClientWithUrl>,
system_id: Option<String>,

View File

@@ -300,7 +300,6 @@ fn show_software_emulation_warning_if_needed(
}
fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let assistant2_feature_flag = cx.wait_for_flag::<feature_flags::Assistant2FeatureFlag>();
let git_ui_feature_flag = cx.wait_for_flag::<feature_flags::GitUiFeatureFlag>();
@@ -361,7 +360,7 @@ fn initialize_panels(prompt_builder: Arc<PromptBuilder>, cx: &mut ViewContext<Wo
}
})?;
let is_assistant2_enabled = if cfg!(test) || release_channel != ReleaseChannel::Dev {
let is_assistant2_enabled = if cfg!(test) {
false
} else {
assistant2_feature_flag.await

View File

@@ -386,7 +386,7 @@ fn session_state(session: View<Session>, cx: &WindowContext) -> ReplMenuState {
indicator: None,
kernel_name: kernel_name.clone(),
kernel_language: kernel_language.clone(),
// todo!(): Technically not shutdown, but indeterminate
// TODO: Technically not shutdown, but indeterminate
status: KernelStatus::Shutdown,
// current_delta: Duration::default(),
}

View File

@@ -269,6 +269,7 @@ impl RateCompletionModal {
let mut editor = Editor::multi_line(cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);

View File

@@ -298,14 +298,21 @@ impl Zeta {
cx.spawn(|this, mut cx| async move {
let request_sent_at = Instant::now();
let mut input_events = String::new();
for event in events {
if !input_events.is_empty() {
input_events.push('\n');
input_events.push('\n');
}
input_events.push_str(&event.to_prompt());
}
let input_events = cx
.background_executor()
.spawn(async move {
let mut input_events = String::new();
for event in events {
if !input_events.is_empty() {
input_events.push('\n');
input_events.push('\n');
}
input_events.push_str(&event.to_prompt());
}
input_events
})
.await;
let input_excerpt = prompt_for_excerpt(&snapshot, &excerpt_range, offset);
let input_outline = prompt_for_outline(&snapshot);

View File

@@ -1,6 +1,6 @@
# Luau
[Luau](https://luau-lang.org/) is a fast, small, safe, gradually typed embeddable scripting language derived from Lua. Luau was developed by Roblox and available under the MIT license.
[Luau](https://luau.org/) is a fast, small, safe, gradually typed, embeddable scripting language derived from Lua. Luau was developed by Roblox and is available under the MIT license.
Luau language support in Zed is provided by the community-maintained [Luau extension](https://github.com/4teapo/zed-luau).
Report issues to: [https://github.com/4teapo/zed-luau/issues](https://github.com/4teapo/zed-luau/issues)
@@ -33,7 +33,7 @@ Then add the following to your Zed `settings.json`:
"formatter": {
"external": {
"command": "stylua",
"arguments": ["--stdin-filepath", "{buffer_path}", "-"]
"arguments": ["-"]
}
}
}

View File

@@ -392,6 +392,20 @@ Subword motion, which allows you to navigate and select individual words in came
]
```
Similarly subword text objects are available, and can be bound with:
```json
[
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"bindings": {
"w": "vim::Subword",
"shift-w": ["vim::Subword", { "ignorePunctuation": true }],
}
}
]
```
Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap.
```json

View File

@@ -327,7 +327,7 @@ pre > code {
padding: 1rem;
}
/* FIXME: ACE editors overlap their buttons because ACE does absolute
/* TODO: ACE editors overlap their buttons because ACE does absolute
positioning within the code block which breaks padding. The only solution I
can think of is to move the padding to the outer pre tag (or insert a div
wrapper), but that would require fixing a whole bunch of CSS rules.

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