Compare commits

..

88 Commits

Author SHA1 Message Date
Nathan Sobo
9445013dd6 Merge branch 'main' into gemini
Co-Authored-By: Antonio <antonio@zed.dev>
2024-07-15 12:02:43 +02:00
llogick
684d9dde56 zig: Wire up LSP settings and binary.{path/arguments} for zls (#14379)
Enables the  LSP `settings` and `binary.{path/arguments}` functionality

Example:
```
"lsp": {
    "zls": {
      "settings": {
        "semantic_tokens": "none"
      },
      "binary": {
        "path": "/home/user/zls/zig-out/bin/zls",
        "arguments": ["--enable-debug-log"]
      }
    }
  },
```

Release Notes:

- N/A
2024-07-15 09:35:03 +02:00
张小白
315692d112 windows: Refactor clipboard implementation (#14347)
This PR provides a similar implementation to the macOS clipboard
implementation, adds support for metadata and includes tests.

Release Notes:

- N/A
2024-07-14 19:40:41 -07:00
张小白
ba09eabfba windows: Make window creation failable (#14395)
Release Notes:

- N/A
2024-07-14 19:37:46 -07:00
lea
70d983abe3 Include stable package in docs, mention UM, and link to sources for the Fedora package (#14432)
Hello, I'm one of the maintainers of the Zed package on Terra. I made
the following changes:

- Mention the Terra stable package, instead of only preview and nightly.
- Link to sources for Terra packages instead of pkgs.org.
- Mention Ultramarine in addition to Fedora (one of Terra's targets).

Release Notes:

- N/A
2024-07-14 19:34:08 -07:00
Fernando Tagawa
4a3097d4dd x11: Fix capitalization with neo 2 (#14466)
Fixed #14282

Release Notes:

- N/A
2024-07-14 18:19:20 -07:00
Kirill Bulatov
59ce3535d3 Turn off use_on_type_format too, for languages that have format_on_save disabled (#14413)
Based on the discussion in
https://github.com/zed-industries/zed/issues/14400


Release Notes:

- N/A
2024-07-13 22:04:15 +03:00
Kirill Bulatov
f8b5e42070 Do not send textDocument/didSave message if server does not declare its support (#14412)
Release Notes:

- Improved Zed logic for sending `textDocument/didSave` request
([14286](https://github.com/zed-industries/zed/issues/14286))
2024-07-13 21:59:21 +03:00
Kirill Bulatov
88c5eb550e Lookup prettier more leniently (#14403)
Do not require the `prettier` dependency name to be in package.json's
[dev]Dependencies, instead just checking the `node_modules` contents.

Release Notes:

- Improved `prettier` detection to pick up its installation from
transitive dependencies
([12731](https://github.com/zed-industries/zed/issues/12731)
2024-07-13 21:59:14 +03:00
Bedis Nbiba
e5dc6beace deno: wire up LSP settings (#14410)
Currently deno lsp only works because deno have a workaround when it
detects deno.json it gets activated, but without a deno.json it won't
work
With this change now it works correctly regardless of a deno.json
presence, it only require enable:true:


```json
{
  "lsp": {
    "deno": {
      "settings": {
        "deno": {
          "enable": true
        }
      }
    }
  }
}
```


Release Notes:

- Improved initial Deno set-up to enable it without explicit deno.json present in the file system
2024-07-13 21:10:36 +03:00
Zak Johnson
3a410942b4 Apply terminal.foreground and terminal.background from theme (#14281)
Release Notes:

- Fixed terminal colors not respecting the theme
([#11418](https://github.com/zed-industries/zed/discussions/11418)).
2024-07-13 14:41:44 +03:00
Kirill Bulatov
89fbd6528f Do not fold excerpts by default in the outline panel (#14378)
Release Notes:

- N/A
2024-07-13 04:08:21 +03:00
Kirill Bulatov
9ce989a704 Tidy up collab-related signature help data (#14377)
Follow-up of https://github.com/zed-industries/zed/pull/12909

* Fully preserve LSP data when sending it via collab, and only strip it
on the client.
* Avoid extra custom request handlers, and extend multi LSP server query
protocol instead.


Release Notes:

- N/A
2024-07-13 04:06:01 +03:00
Kirill Bulatov
dd63e25f23 Revert hold: true for macOS tasks (#14376)
Otherwise, ctrl-c makes them stuck being held from time to time

Follow-up of https://github.com/zed-industries/zed/pull/13898 that
reverts the macOS-related part of the PR.

Release Notes:

- N/A
2024-07-13 04:02:38 +03:00
Max Brunsfeld
489077befc Extract a BufferStore object from Project (#14037)
This is a ~small~ pure refactor that's a step toward SSH remoting. I've
extracted the Project's buffer state management into a smaller, separate
struct called `BufferStore`, currently in the same crate. I did this as
a separate PR to reduce conflicts between main and `remoting-over-ssh`.

The idea is to make use of this struct (and other smaller structs that
make up `Project`) in a dedicated, simpler `HeadlessProject` type that
we will use in the SSH server to model the remote end of a project. With
this approach, as we develop the headless project, we can avoid adding
more conditional logic to `Project` itself (which is already very
complex), and actually make `Project` a bit smaller by extracting out
helper objects.

Release Notes:

- N/A
2024-07-12 15:25:54 -07:00
FilipeBisinella
21c5ce2bbd Add pyright workspace configuration (#14265)
Release Notes:

- Added support for pyright workspace configuration, as described in
https://microsoft.github.io/pyright/#/settings .
2024-07-12 15:13:09 -07:00
Marshall Bowers
3deb000f70 assistant: Add basic glob support for expanding items in /docs (#14370)
This PR updates the `/docs` slash command with basic globbing support
for expanding docs.

A `*` can be added to the item path to signify the end of a prefix
match.

For example:

```
# This will match any documentation items starting with `auk::`.
# In this case, it will pull in the docs for each item in the crate.
/docs docs-rs auk::*

# This will match any documentation items starting with `auk::visitor::`,
# which will pull in docs for the `visitor` module.
/docs docs-rs auk::visitor::*
```


https://github.com/user-attachments/assets/5e1e21f1-241b-483f-9cd1-facc3aa76365

Release Notes:

- N/A
2024-07-12 17:57:50 -04:00
Mikayla Maki
fe3fe945a9 linux: Indicate when the window is focused (#14266)
fixes #14202

Release Notes:

- Added a representation of the current focus state to Zed's window
style ([#14202](https://github.com/zed-industries/zed/issues/14202))
2024-07-12 14:20:58 -07:00
Stanislav Alekseev
11178eacc7 Fix diagnostic popover not overflowing when necessary (#14322)
It was broken after #13996 moved rendering text one level deeper,
causing `max_h` and `overflow_y_scroll` to apply to different widgets
Release Notes:

- Fixed large diagnostic popovers not overflowing when nessesary

Before:
<img width="814" alt="Screenshot 2024-07-12 at 15 25 46"
src="https://github.com/user-attachments/assets/4f615600-2857-4470-8b77-864e3a9e38d5">

After:
<img width="813" alt="Screenshot 2024-07-12 at 15 26 10"
src="https://github.com/user-attachments/assets/83c1f344-b3b1-4929-8197-4b24a0e9c65e">
2024-07-12 14:14:11 -07:00
Stanislav Alekseev
59bc027750 Fix direnv option being named direnv and not load_direnv in the docs (#14309)
This is a quick followup to #13902 that fixes a mistake with the setting
naming in the docs, I accidentally made
Release Notes:

- N/A
2024-07-12 14:12:02 -07:00
张小白
0a718c65e2 windows: Return client size and position from window_bounds (#14228)
This is a follow up of #14218 , since we open the window based on the
size of the client area, `window_bounds` should also return the size of
the client area to maintain consistency.

Release Notes:

- N/A
2024-07-12 13:19:36 -07:00
Marshall Bowers
85d77a3eec Clarify /docs error message when target/doc does not exist (#14364)
This PR improves the error message shown by the `/docs` slash command
when indexing fails due to the absence of `target/doc`.

We now distinguish between the overall `target/doc` directory missing
and an individual crate directory missing beneath it.

Release Notes:

- N/A
2024-07-12 16:09:16 -04:00
Marshall Bowers
ca80343486 assistant: Add docs provider for docs.rs (#14356)
This PR adds an indexed docs provider for retrieving docs from `docs.rs`
using the `/docs` slash command.

Release Notes:

- N/A
2024-07-12 13:22:52 -04:00
Semen Fomchenkov
739038ddaf docs: Add ALT Linux (Sisyphus) (#14351)
Added ALT Linux (Sisyphus) as one of the ways to install via the package
manager in linux.md.

Release Notes:

- N/A
2024-07-12 12:59:17 -04:00
Peter Tripp
106e0623dd PlainText language: Default to SoftWrap::EditorWidth (#14331)
- Remove wrap guide / vertical ruler in untitled buffers
- Fixes https://github.com/zed-industries/zed/issues/12473
2024-07-12 11:10:59 -04:00
Peter Tripp
607ad6de3c zig: Improve indentation (#14332)
- Fixes https://github.com/zed-industries/zed/issues/14140
2024-07-12 10:24:07 -04:00
Kirill Bulatov
ea26a01f5f Do not render a signature popover when its location is before the visible range (#14307)
Follow-up of https://github.com/zed-industries/zed/pull/12909

Release Notes:

- N/A
2024-07-12 11:31:52 +03:00
Stanislav Alekseev
8abc000553 Fix nushell local env detection by using direnv export (#13902)
I don't intend fully on getting this merged, this is just an experiment
on using `direnv` directly without relying on shell-specific behaviours.
It works though, so this finally closes #8633
Release Notes:

- Fixed nushell not picking up `direnv` environments by directly
interfacing with it using `direnv export`

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-07-12 10:29:32 +02:00
Kirill Bulatov
9f5309cedd Remove non-default wrap setting for markdown (#14304)
With this setting, markdown files are one of the few that get a line
wrap indicator, a vertical line on the right, which confuses people.

Release Notes:

- N/A
2024-07-12 11:15:57 +03:00
Conrad Irwin
adf74fdc14 linux: Fix panic handling unknown keys (#14274)
Pulls in https://github.com/rust-x-bindings/xkbcommon-rs/pull/54 to
avoid
panicking.

Release Notes:

- linux: Fix a panic in keyboard handling
2024-07-11 17:03:19 -06:00
sherwyn
e402d7e96a vim: Add support for vim::PreviousLineStart motion (#14193)
Release Notes:

- vim: Added `-`/`+` to go to beginning of line above/below
([#14183](https://github.com/zed-industries/zed/issues/14183)).
- vim: (Breaking) Removed non-standard builtin binding from `-` to open
the project panel. You can re-add it to your keymap file with:
`{"context":"VimControl", "bindings":{ "-":
"pane::RevealInProjectPanel"}}`


Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.


https://github.com/zed-industries/zed/assets/32429059/0e9e9348-265e-4a81-a45a-4739034dc5d9

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-11 16:36:07 -06:00
Conrad Irwin
12dfd4a2c2 Don't panic on unknown cursor style on x11 (#14264)
Release Notes:

- linux: Fixed a panic if we request a cursor style your system doesn't
support
2024-07-11 16:05:01 -06:00
Conrad Irwin
b87d1eabcc linux: Panic less on window init (#14255)
This change pulls in https://github.com/kvark/blade/pull/135 and updates
the simplelog dependency for compatibility with that.


Release Notes:

- linux: Show link to troubleshooting docs when we can't open a window
2024-07-11 16:04:46 -06:00
Max Brunsfeld
ac528dda64 Fix panic when evaluating a code snippet containing multi-byte characters (#14269)
Also, don't retrieve code snippets when rendering the repl quick action
button

Release Notes:

- N/A

---------

Co-authored-by: Kyle Kelley <kylek@zed.dev>
Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
2024-07-11 15:04:13 -07:00
Marshall Bowers
906688f012 assistant: Show a warning indicator when the user needs to run cargo doc (#14262)
This PR updates the `/docs` slash command to show a warning to the user
if a crate's docs cannot be indexed due to the target directory not
containing docs:

<img width="782" alt="Screenshot 2024-07-11 at 5 11 46 PM"
src="https://github.com/user-attachments/assets/2f54f7a1-97f4-4d2d-b51f-57ba31e50a2f">

Release Notes:

- N/A
2024-07-11 17:37:31 -04:00
Nate Butler
c18e9aedcd Add items_baseline to Styled (#14238)
Add support for aligning items to the baseline.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2024-07-11 16:38:21 -04:00
Marshall Bowers
cd4847ca22 assistant: Use a more generic icon for the /docs command (#14247)
This PR updates the `/docs` slash command to use a more generic icon to
convey docs.

It was still using the Rust icon, a relic of when it was still
`/rustdoc`.

Release Notes:

- N/A
2024-07-11 15:46:33 -04:00
Brad Pitcher
4c63e8b203 docs: Fix Linux aarch64 tarball links (#14245)
Fixed tarball documentation links for linux aarch64 (they were pointing
at x86_64 tarballs)

Release Notes:

- N/A
2024-07-11 15:43:10 -04:00
TC
d9d8c1f6d9 assistant: Handle http:// links in /fetch (#14243)
Previously http://google.com would get modified to
https://http://google.com which doesn't work. I assume http links should
be supported.

Release Notes:

- N/A
2024-07-11 15:30:45 -04:00
Conrad Irwin
b0dbc80575 vim: (BREAKING) clean up keymap contexts (#14233)
Release Notes:

- vim: (BREAKING) Improved vim keymap contexts.

Previously `vim_mode == normal` was true even when operators were
pending, which led to bugs like #13789 and a requirement for custom
keymaps to exclude various conditions like (`!VimObject` and
`!VimWaiting`) to avoid bugs.

Now `vim_mode` will be set to `operator` or `waiting` in these cases as
described in [the docs](https://zed.dev/docs/vim#keybindings). For most
custom keymaps this change will be a no-op or an improvement, but if you
were deliberately relying on the old behaviour (if you were relying on
`VimObject` or `VimWaiting` becoming true) you will need to update your
keymap.

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-07-11 13:16:26 -06:00
Mikayla Maki
8e853e2b56 Update linux.md 2024-07-11 12:05:59 -07:00
Mikayla Maki
47a78907d6 Update system-requirements.md 2024-07-11 11:59:52 -07:00
Mikayla Maki
0c1a3db87d Update getting-started.md 2024-07-11 11:50:36 -07:00
Omer Tuchfeld
3541a1175f Disrupt blink for immediate feedback on cursor shape changes (#14177)
# Issue

When a user does something that changes the cursor shape, such as when
switching between vim modes, there may be an up to 500ms (cursor blink
interval) delay until the user receives feedback for their action. This
happens when the shape change happens during the invisible phase of a
blink - the user will not see the cursor shape change until the next
phase, which could be 500ms away.

# Solution

Cursor shape changes should disrupt blinking by forcing the cursor to be
shown, this results in immediate feedback for shape changes. This is in
line with the behavior of other editors I've tried.

Release Notes:

- Improved visual feedback when changing cursor shape
2024-07-11 12:47:10 -06:00
Kyle Kelley
e51d469025 Invalidate anchors when they get deleted (#14116)
Allows deleting the outputs directly within the editor. This also fixes
the overlap logic to make sure that the ends and the starts are
compared.


https://github.com/zed-industries/zed/assets/836375/84f5f582-95f3-4c6a-a3c9-54da6009e34d

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-07-11 11:21:41 -07:00
Conrad Irwin
018a2a29ea vim: Fix c when range ends in a multibyte character (#14139)
Release Notes:

- vim: Fixed `c <motion>` omitting trailing multibyte characters
([#13909](https://github.com/zed-industries/zed/issues/13909)).
2024-07-11 12:01:56 -06:00
Donough Liu
d49727ff10 terminal: Set TERM_PROGRAM and TERM_PROGRAM_VERSION environment variables in integrated terminal (#14213)
![image](https://github.com/zed-industries/zed/assets/31354274/9d1c5410-897b-40a1-8256-2d7e207f69ff)

These two environment variables are essential when people need to detect
terminal type and do something. Many popular terminals set them.

fixes https://github.com/zed-industries/zed/issues/4571

Release Notes:

- Set `TERM_PROGRAM` and `TERM_PROGRAM_VERSION` environment variables in
the integrated terminal
([#4571](https://github.com/zed-industries/zed/issues/4571)).
2024-07-11 20:48:46 +03:00
oliverpool
c195c4ddff docs: Document buffer_line_height (#14168)
`buffer_line_height` has been requested in #5590 and implemented in
#2718, however the documentation was still lacking.

Release Notes:

- N/A
2024-07-11 13:42:27 -04:00
Stanislav Alekseev
fd03454540 Fix reverse selections always being cleared (#14150)
When I implemented #13701, I kinda messed up with the reversed
selections, thinking that their anchors are flipped, so I flipped them
again. This caused the reverse selections to always be cleared

Release Notes:

- Fix reverse selections always being cleared, even if the right click
was performed inside
2024-07-11 11:35:18 -06:00
张小白
6eeec9b403 windows: Create window with correct size (#14218)
The `Bounds<DevicePixels>` we use to create a window represents the size
of the drawable area.

### Before:



https://github.com/zed-industries/zed/assets/14981363/52f0d196-b113-4b64-a0d1-407972674990

### After



https://github.com/zed-industries/zed/assets/14981363/83298b6c-5e5f-4a47-b051-35b4a02404ac



Release Notes:

- N/A
2024-07-11 09:54:59 -07:00
Marshall Bowers
b558e8da1e svelte: Bump to v0.0.2 (#14220)
This PR bumps the Svelte extension to v0.0.2.

Changes:

- https://github.com/zed-industries/zed/pull/12788

Release Notes:

- N/A
2024-07-11 11:47:44 -04:00
Peter Tripp
1d7b28c658 Add Upper/LowerCase binds to Linux Sublime Text keybinds (#14155) 2024-07-11 11:15:49 -04:00
Peter Tripp
de78eb44b1 Keymap changes for editor::JoinLines (#14136)
- Linux (default) add ctrl-shift-j
- Linux (default) remove ctrl-j
  - Conflicted with: `"ctrl-j": "workspace::ToggleBottomDock",`
- MacOS (sublime) add cmd-shift-j
2024-07-11 11:14:25 -04:00
Danilo Leal
c071e19899 docs: Add stray design tweaks (#14205)
- Mostly just tweaking some design (colors & spacing) stuff
- Some small accessibility things—e.g., underline decoration for links
and one h1 only per page
- Most of the other captured changes are really just Prettier indenting
stuff

Release Notes:

- N/A
2024-07-11 11:57:22 -03:00
Marshall Bowers
37fc4ce09d Allow Zed Nightly to use v0.0.7 of the Zed extension API (#14209)
This PR updates the Wasm API compatibility check to allow Nightly to
load extensions using v0.0.7 of the Zed extension API.

Release Notes:

- N/A
2024-07-11 10:54:15 -04:00
Danilo Leal
99f56252be docs: Tiny formatting tweaks on the Linux page (#14208)
Release Notes:

- N/A
2024-07-11 11:53:34 -03:00
Aleksei Gusev
f61abe0247 Pass hold: true to Alacritty for tasks (#13898)
It seems `hold: false` causes alacritty to close the channel earlier,
without waiting for the output from the child command to go to Zed.

Fixes [#13683](https://github.com/zed-industries/zed/issues/13683)

Release Notes:

- Fixed loosing output of a spawned task
([#13683](https://github.com/zed-industries/zed/issues/13683)).

[Screencast from 2024-07-06
18-28-56.webm](https://github.com/zed-industries/zed/assets/39293/4ebef8b5-7c0d-46be-9341-4ac0d809458d)
2024-07-11 17:50:00 +03:00
Marshall Bowers
45c54d189a assistant: Show a message when no docs providers are available (#14207)
This PR updates the `/docs` slash command to show a message to more
clearly indicate when there are no available docs providers.

<img width="379" alt="Screenshot 2024-07-11 at 10 31 53 AM"
src="https://github.com/zed-industries/zed/assets/1486634/d079f87c-4933-4da9-ad82-34dbfe6a284c">

Release Notes:

- N/A
2024-07-11 10:49:13 -04:00
Piotr Osiewicz
2727f55772 Add support for projects managed with Yarn (#13644)
TODO:
- [ ] File a PR with Yarn to add Zed to the list of supported IDEs.

Fixes: https://github.com/zed-industries/zed/issues/10107
Fixes: https://github.com/zed-industries/zed/issues/13706
Release Notes:

- Improved experience in projects using Yarn. Run `yarn dlx
@yarnpkg/sdks base` in the root of your project in order to elevate your
experience.

---------

Co-authored-by: Saurabh <79586784+m4saurabh@users.noreply.github.com>
2024-07-11 14:56:07 +02:00
tomoikey
291d64c803 lsp: Implement textDocument/signatureHelp for ProjectClientState::Local environment (#12909)
Closes https://github.com/zed-industries/zed/issues/5155
Closes https://github.com/zed-industries/zed/issues/4879


# Purpose
There was no way to know what to put in function signatures or struct
fields other than hovering at the moment. Therefore, it was necessary to
implement LSP's `textDocument/signatureHelp`.

I tried my best to match the surrounding coding style, but since this is
my first contribution, I believe there are various aspects that may be
lacking. I would greatly appreciate your code review.

# Description
When the window is displayed, the current argument or field at the
cursor's position is automatically bolded. If the cursor moves and there
is nothing to display, the window closes automatically.
To minimize changes and reduce the burden of review and debugging, the
SignatureHelp feature is implemented only when `is_local` is `true`.
Some `unimplemented!()` macros are embedded, but rest assured that they
are not called in this implementation.

# How to try it out
Press `cmd + i` (MacOS), `ctrl + i` (Linux).

# Enable auto signature help (2 ways)
### Add `"auto_signature_help": true` to `settings.json`
<img width="426" alt="image"
src="https://github.com/zed-industries/zed/assets/55743826/61310c39-47f9-4586-94b0-ae519dc3b37c">

Or

### Press `Auto Signature Help`. (Default `false`)
<img width="226" alt="image"
src="https://github.com/zed-industries/zed/assets/55743826/34155215-1eb5-4621-b09b-55df2f1ab6a8">

# Disable to show signature help after completion
### Add `"show_signature_help_after_completion": false` to
`settings.json`
<img width="438" alt="image"
src="https://github.com/zed-industries/zed/assets/55743826/5e5bacac-62e0-4921-9243-17e1e72d5eb6">

# Movie

https://github.com/zed-industries/zed/assets/55743826/77c12d51-b0a5-415d-8901-f93ef92098e7

# Screen Shot
<img width="628" alt="image"
src="https://github.com/zed-industries/zed/assets/55743826/3ebcf4b6-2b94-4dea-97f9-ac4f33e0291e">

<img width="637" alt="image"
src="https://github.com/zed-industries/zed/assets/55743826/6dc3eb4d-beee-460b-8dbe-d6eec6379b76">

Release Notes:

- Show function signature popovers
([4879](https://github.com/zed-industries/zed/issues/4879),
[5155](https://github.com/zed-industries/zed/issues/5155))

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-07-11 13:38:33 +03:00
Piotr Osiewicz
6a11184ea3 dart: Release 0.0.3 (#14176)
Includes: https://github.com/zed-industries/zed/pull/13686

Release Notes:

- Added Flutter tasks to Dart extension.
2024-07-11 11:48:04 +02:00
Thorsten Ball
ff1dcff2fb gpui example: Add reset button to Input example (#14163)
Extracted from #14051 which I don't want to merge in its current state.


Release Notes:

- N/A
2024-07-11 11:46:47 +02:00
张小白
bef2586eed windows: Fix rust tasks (#13413)
https://github.com/zed-industries/zed/assets/14981363/56c208da-132c-438a-92b3-e31505859262


Release Notes:

- N/A
2024-07-11 10:47:25 +02:00
Abdullah Alsigar
bdba8b23fa dart: Add Flutter runnables and tasks (#13686)
Release Notes:

- Added runnable tasks for Flutter
2024-07-11 10:46:53 +02:00
Piotr Osiewicz
22900554d5 Project panel: Prevent scrollbar size from scaling with rem size (#14167)
The underlying container had width of 0.75 rem, which was equal to 12px
at default ui_font_size. However, with larger values of ui_font_size the
scrollbar would drift towards the center of a project panel, as the
scrollbar itself has a fixed width of 12 pixels. This commit moves
towards using a fixed width of 12px for scrollbar container. The
alternative was to make the scrollbar scale with ui_font_size, but that
isn't what the Editor scrollbar does, so I decided against it.



Release Notes:

- Fixed position of scrollbar in project panel with non-default
`ui_font_size` values.
2024-07-11 10:34:15 +02:00
Denis Washington
6db0b6c5ad terminal: Prevent extra character on handled meta keystrokes (#14151)
On macOS, when `terminal.option_as_meta` is enabled, pressing key
combinations like `option+b` and `option+f` would lead to both an escape
sequence being sent to the terminal (the expected behavior with
`option_as_meta == true`) AND a character being inserted (the behavior
when `option_as_meta == false`). Prevent the latter by stopping
propagation of the key-down event if it corresponds to a terminal escape
sequence and `option_as_meta` is enabled.

Fixes #7728

Release Notes:

- Fixed insertion of extra characters for some keystrokes if
`terminal.option_as_meta` is enabled
([#7728](https://github.com/zed-industries/zed/issues/7728)).
2024-07-11 11:20:54 +03:00
Aaron Cunnington
ba11e9a9a8 Fix SystemUIFont typo in default settings (#14158)
Release Notes:

- N/A
2024-07-11 11:20:38 +03:00
Peter Tripp
de570133ff Docs: Fix theme.mode default settings (#14153)
- Updated docs to match the changes made in
https://github.com/zed-industries/zed/pull/13621
- Fixes: #14084.
2024-07-11 03:20:12 -04:00
Peter Tripp
f1b1a9fd5e Ignore whitespace commits (#13889)
This let's GitHub and the Git cli optionally "skip" certain revs when
generating `git blame`.

Co-authored-by: Gilles Peiffer <gilles.peiffer.yt@gmail.com>
2024-07-11 02:45:40 -04:00
Chris​‌​‮ ‬Hayes‌​​​
1b08f14c54 Document how to enable vim_mode in /docs/vim (#14138)
## Documents:

- **Added** instructions on how to enable "Vim mode" to the
["Settings"](https://zed.dev/docs/vim#settings) of
[/docs/vim](https://zed.dev/docs/vim).

While [/docs/configuring-zed](https://zed.dev/docs/configuring-zed)
_does_ mention the `vim_mode` setting,
[/docs/vim](https://zed.dev/docs/vim) does not.

This can be confusing for users like me who went straight to the vim
doc, and could not figure out how to enable vim.

## Release Notes:

- N/A
2024-07-10 21:59:03 -06:00
Sensational Code
36d3b16279 Add toggle hunk diff and expand all hunk diffs key bindings (#14130)
Noticed these were missing when I was reading through the docs.

Release Notes:

- Add toggle hunk diff and expand all hunk diffs key bindings
2024-07-10 21:22:52 -06:00
Ephram
945764e409 Selectable popover text (#12918)
Release Notes:

- Fixed #5236
- Added the ability to select and copy text from information popovers



https://github.com/zed-industries/zed/assets/50590465/d5c86623-342b-474b-913e-d07cc3f76de4

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio <ascii@zed.dev>
2024-07-10 21:14:34 -06:00
Jason Lee
f1281c14dd Revert Windows normal window title style to WS_EX_APPWINDOW (#14132)
Release Notes:

- N/A

@ConradIrwin we must revert this little change.

https://github.com/zed-industries/zed/pull/14063#issuecomment-2221867379
2024-07-10 20:49:55 -06:00
Hans
3b823d4a0b Add simple support for wrapscan (#13497)
For: #13417 

This is a simple version, I'm not sure if we just need to limit this
feature to vim mode, or maybe in normal editor mode, which involves
other logic like the location of the setting

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-10 20:42:37 -06:00
Xiaoguang Wang
46645b552f Remove unused variable query_editor_was_focused (#14128)
Fix
https://github.com/zed-industries/zed/pull/14128

Release Notes:

- N/A
2024-07-10 20:40:36 -06:00
Jason Lee
5bc3846d59 Fix #14106 Windows title bar broken (#14122)
Release Notes:

- N/A

----

Fix #14106 

Sorry, the previous change in #14063 I have made a mistake. I shouldn't
have changed the previous logic.

```diff
- if !state_ptr.hide_title_bar {
+ if state_ptr.hide_title_bar {
```

## Test


https://github.com/zed-industries/zed/assets/5518/e03fbcac-be6b-4a9d-8937-d3b5e236b564

And the popup window limit is still works.
2024-07-10 18:55:51 -06:00
jansol
e6d608fa05 linux: Add NewWorkspace to the Actions list in .desktop (#14097)
Fix
https://github.com/zed-industries/zed/pull/13807#issuecomment-2221324262


Release Notes:

- N/A
2024-07-10 16:16:21 -06:00
Peter Tripp
e106a39620 AlpineLinux: Fix install.sh and docs typo (#14105)
- AlpineLinux uses busybox `mktemp` which requires `mktemp -d` end with six XXXXXX (not five).
- Fixes #14082
2024-07-10 17:41:43 -04:00
Sören Meier
d32e9f759c svelte: Improve syntax highlighting (#12788)
This PR fixes `<script context="module">` not being highlighted.  
It also adds support for scss.

Release Notes:

- N/A
2024-07-10 15:42:50 -04:00
Jason Lee
15662f105e gpui: Fix TextStyle default font_family crash on Windows, use Segoe UI for Windows (#14040)
Release Notes:

- Fixed default font_family crash on Windows, use `Segoe UI`.

## Crash error message

```
thread 'main' panicked at crates\gpui\src\text_system.rs:150:9:
failed to resolve font 'Helvetica' or any of the fallbacks: 
Zed Plex Mono, Helvetica, Cantarell, Ubuntu, Noto Sans, DejaVu Sans
```
2024-07-10 13:40:42 -06:00
Jason Lee
1887a6db53 gpui: Fix popup kind window support on Windows (#14063)
Release Notes:

- N/A

----

Continue #14044 for Windows 

## The problem

The `cx.open_window` method has provided us a `window_kind` option to
allows creating a Popup kind. This behavior can work on macOS, the popup
kind window have no-border, no-shadow, no-resize, and followed the
`is_movable` if present true it can't move.

This PR to fix those supports on Windows.

The border and shadow still exist, I have tried to use WS_POPUP
window_style, but it will crash:

> This is looks like complex, it is out of my known.

```
    Blocking waiting for file lock on build directory
   Compiling gpui v0.1.0 (F:\work\zed\crates\gpui)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.96s
     Running `target\debug\examples\window_positioning.exe`
thread 'main' panicked at F:\Users\jason\.cargo\git\checkouts\blade-b2bcd1de1cf7ab6a\21a56f7\blade-graphics\src\vulkan\init.rs:864:18:
called `Result::unwrap()` on an `Err` value: ERROR_OUT_OF_DEVICE_MEMORY
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\examples\window_positioning.exe` (exit code: 0xc0000409, STATUS_STACK_BUFFER_OVERRUN)
```

So I just make a simple change, to use `WS_EX_TOOLWINDOW` this can
disable resize, and connect `is_movable` to `handle_hit_test_msg` to
disable move, and also no Status Bar icon.

## Before



https://github.com/zed-industries/zed/assets/5518/76740a71-e0ba-401f-958d-f4afdeb417c6

## After



https://github.com/zed-industries/zed/assets/5518/dca49f13-914c-425a-b8b6-b9fc15f8d208
2024-07-10 13:39:54 -06:00
张小白
fa9360f78d windows: Work around font rendering clipping issue (#14075)
Release Notes:

- N/A
2024-07-10 13:38:49 -06:00
Conrad Irwin
3ff738fa03 Fix panic clicking on multibyte chars (#14086)
Fixes: #12011

When hovering over a multibyte character in a debug build, Zed would
panic.
Follow up to #11296 

Release Notes:

- N/A
2024-07-10 13:38:26 -06:00
Nathan Sobo
74cf3d2d92 Include cached content in stream generate content request
This commit updates the GenerateContentRequest struct in the Google AI module
to include an optional cached_content field. This allows for the use of
previously cached content in subsequent requests, potentially improving
response times and maintaining context across multiple interactions.

The changes are propagated through the assistant and collab crates to ensure
that the cached content can be passed from the user interface down to the
API request level.

Changes:
- Added cached_content field to GenerateContentRequest in google_ai.rs
- Updated LanguageModelRequest struct in assistant.rs to include cached_contents
- Modified relevant functions in assistant_panel.rs, completion_provider/cloud.rs,
  inline_assistant.rs, and prompt_library.rs to accommodate the new field
- Updated language_model_request_to_google_ai function in collab/ai.rs to
  pass cached content to the Google AI request
- Added cached_contents field to CompleteWithLanguageModel message in zed.proto
2024-07-01 21:18:37 -06:00
Nathan Sobo
427491a24f Add caching support for language model content
This commit adds support for caching language model content using the Google AI API. The changes include:

1. Adding a new CacheLanguageModelContent request and response to the protocol.
2. Implementing the cache_language_model_content function in the RPC server.
3. Updating the Google AI client to support creating cached content.
4. Modifying the proto definitions to include the new messages.
2024-07-01 20:18:14 -06:00
Nathan Sobo
2781b1cce1 Add an API provider for creating cached content with Gemini.
Co-authored-by: mikayla <mikayla@zed.dev>
2024-07-01 15:31:20 -06:00
Nathan Sobo
ab69c05d99 Refactor token counting and add support for Gemini models
This commit refactors the token counting logic in the `CloudCompletionProvider`
struct to improve code organization and add support for Gemini models. Key changes include:

- Introduce a new `count_tokens_with_model` method to handle token counting
  for models that use the client's request mechanism.
- Update the `count_tokens` method to use the new helper method for Gemini
  and custom models.
- Replace the placeholder implementation for Gemini models with proper
  token counting support.

These changes provide a more consistent approach to token counting across
different model types and lay the groundwork for easier integration of
future models.

Co-authored-by: mikayla <mikayla@zed.dev>
2024-07-01 14:24:55 -06:00
Nathan Sobo
f06c3b5670 Add Gemini 1.5 Pro and Gemini 1.5 Flash as cloud model options
This commit adds support for two new Gemini models:
- Gemini 1.5 Pro
- Gemini 1.5 Flash

Changes include:
1. Adding new variants to the CloudModel enum
2. Updating the id() and display_name() methods for the new models
3. Setting max_token_count for the new models (128000 for Pro, 32000 for Flash)
4. Adding token counting logic for Gemini models (currently using OpenAI's tokenizer as an approximation)

Note: A proper tokenizer for Gemini models should be implemented in the future.

Co-authored-by: mikayla <mikayla@zed.dev>
2024-07-01 14:11:40 -06:00
156 changed files with 7520 additions and 3081 deletions

24
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,24 @@
# .git-blame-ignore-revs
#
# This file consists of a list of commits that should be ignored for
# `git blame` purposes. This is useful for ignoring commits that only
# changed whitespace / indentation / formatting, but did not change
# the underlying syntax tree.
#
# GitHub will pick this up automatically for blame views:
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
# To use this file locally, run:
# git blame --ignore-revs-file .git-blame-ignore-revs
# To always use this file by default, run:
# git config --local blame.ignoreRevsFile .git-blame-ignore-revs
# To disable this functionality, run:
# git config --local blame.ignoreRevsFile ""
# Comments are optional, but may provide helpful context.
# 2023-04-20 Set default tab_size for JSON to 2 and apply new formatting
# https://github.com/zed-industries/zed/pull/2394
eca93c124a488b4e538946cd2d313bd571aa2b86
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
# https://github.com/zed-industries/zed/pull/13887
813cc3f5e537372fc86720b5e71b6e1c815440ab

187
Cargo.lock generated
View File

@@ -87,7 +87,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
dependencies = [
"base64 0.22.0",
"bitflags 2.4.2",
"bitflags 2.6.0",
"home",
"libc",
"log",
@@ -110,7 +110,7 @@ version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
dependencies = [
"base64 0.22.0",
"bitflags 2.4.2",
"bitflags 2.6.0",
"home",
"libc",
"log",
@@ -1583,7 +1583,16 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
dependencies = [
"bit-vec 0.7.0",
]
[[package]]
@@ -1592,6 +1601,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
[[package]]
name = "bit_field"
version = "0.10.2"
@@ -1606,9 +1621,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.2"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
dependencies = [
"serde",
]
@@ -1634,11 +1649,11 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
dependencies = [
"ash",
"ash-window",
"bitflags 2.4.2",
"bitflags 2.6.0",
"block",
"bytemuck",
"codespan-reporting",
@@ -1664,7 +1679,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
dependencies = [
"proc-macro2",
"quote",
@@ -1674,7 +1689,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -1922,7 +1937,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"log",
"polling 3.3.2",
"rustix 0.38.32",
@@ -2391,16 +2406,6 @@ dependencies = [
"worktree",
]
[[package]]
name = "clipboard-win"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
dependencies = [
"lazy-bytes-cast",
"winapi",
]
[[package]]
name = "clock"
version = "0.1.0"
@@ -2859,7 +2864,7 @@ name = "cosmic-text"
version = "0.11.2"
source = "git+https://github.com/pop-os/cosmic-text?rev=542b20c#542b20ca4376a3b5de5fa629db1a4ace44e18e0c"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"fontdb",
"log",
"rangemap",
@@ -3605,6 +3610,7 @@ dependencies = [
"linkify",
"log",
"lsp",
"markdown",
"multi_buffer",
"ordered-float 2.10.0",
"parking_lot",
@@ -4012,7 +4018,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
dependencies = [
"bit-set",
"bit-set 0.5.3",
"regex",
]
@@ -4069,7 +4075,7 @@ name = "feedback"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"bitflags 2.6.0",
"client",
"db",
"editor",
@@ -4269,7 +4275,7 @@ checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
dependencies = [
"fontconfig-parser",
"log",
"memmap2 0.9.4",
"memmap2",
"slotmap",
"tinyvec",
"ttf-parser",
@@ -4403,7 +4409,7 @@ dependencies = [
name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"core-foundation",
"fsevent-sys 3.1.0",
"parking_lot",
@@ -4714,7 +4720,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"libc",
"libgit2-sys",
"log",
@@ -4813,6 +4819,7 @@ name = "google_ai"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"futures 0.3.28",
"http 0.1.0",
"serde",
@@ -4825,7 +4832,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"gpu-alloc-types",
]
@@ -4846,7 +4853,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
]
[[package]]
@@ -4867,7 +4874,6 @@ dependencies = [
"calloop",
"calloop-wayland-source",
"cbindgen",
"clipboard-win",
"cocoa",
"collections",
"core-foundation",
@@ -5103,7 +5109,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f7acb9683d7c7068aa46d47557bfa4e35a277964b350d9504a87b03610163fd"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"byteorder",
"heed-traits",
"heed-types",
@@ -6052,12 +6058,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "lazy-bytes-cast"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -6524,15 +6524,6 @@ dependencies = [
"rustix 0.38.32",
]
[[package]]
name = "memmap2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
dependencies = [
"libc",
]
[[package]]
name = "memmap2"
version = "0.9.4"
@@ -6666,17 +6657,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "naga"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e"
version = "0.20.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25"
dependencies = [
"bit-set",
"bitflags 2.4.2",
"arrayvec",
"bit-set 0.6.0",
"bitflags 2.6.0",
"cfg_aliases",
"codespan-reporting",
"hexf-parse",
"indexmap 2.2.6",
"log",
"num-traits",
"rustc-hash",
"spirv",
"termcolor",
@@ -6772,7 +6763,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"cfg-if",
"libc",
"memoffset",
@@ -6784,7 +6775,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -6853,7 +6844,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
@@ -7066,6 +7057,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "nvim-rs"
version = "0.6.0-pre"
@@ -7215,7 +7215,7 @@ version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"cfg-if",
"foreign-types 0.3.2",
"libc",
@@ -8183,6 +8183,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.28",
"prost",
"prost-build",
"serde",
@@ -8229,7 +8230,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"memchr",
"unicase",
]
@@ -8663,6 +8664,7 @@ dependencies = [
"image",
"language",
"log",
"multi_buffer",
"project",
"runtimelib",
"schemars",
@@ -9041,7 +9043,7 @@ version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"errno 0.3.8",
"itoa",
"libc",
@@ -9116,7 +9118,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"bytemuck",
"libm",
"smallvec",
@@ -9339,7 +9341,7 @@ version = "0.1.0"
dependencies = [
"any_vec",
"anyhow",
"bitflags 2.4.2",
"bitflags 2.6.0",
"client",
"collections",
"editor",
@@ -9767,13 +9769,13 @@ dependencies = [
[[package]]
name = "simplelog"
version = "0.9.0"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
dependencies = [
"chrono",
"log",
"termcolor",
"time",
]
[[package]]
@@ -9926,12 +9928,11 @@ dependencies = [
[[package]]
name = "spirv"
version = "0.2.0+1.5.4"
version = "0.3.0+sdk-1.3.268.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
dependencies = [
"bitflags 1.3.2",
"num-traits",
"bitflags 2.6.0",
]
[[package]]
@@ -10107,7 +10108,7 @@ dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.4.2",
"bitflags 2.6.0",
"byteorder",
"bytes 1.5.0",
"chrono",
@@ -10154,7 +10155,7 @@ dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.4.2",
"bitflags 2.6.0",
"byteorder",
"chrono",
"crc",
@@ -10576,7 +10577,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aef1f9d4c1dbdd1cb3a63be9efd2f04d8ddbc919d46112982c76818ffc2f1a7"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"cap-fs-ext",
"cap-std",
"fd-lock",
@@ -10717,9 +10718,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.1.3"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
@@ -10736,6 +10737,7 @@ dependencies = [
"gpui",
"libc",
"rand 0.8.5",
"release_channel",
"schemars",
"serde",
"serde_derive",
@@ -10879,18 +10881,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@@ -10941,7 +10943,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
@@ -11300,7 +11304,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"bytes 1.5.0",
"futures-core",
"futures-util",
@@ -12071,7 +12075,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"cursor-icon",
"log",
"serde",
@@ -12223,7 +12227,7 @@ version = "0.201.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"indexmap 2.2.6",
"semver",
]
@@ -12490,7 +12494,7 @@ checksum = "371d828b6849ea06d598ae7dd1c316e8dd9e99b76f77d93d5886cb25c7f8e188"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.4.2",
"bitflags 2.6.0",
"bytes 1.5.0",
"cap-fs-ext",
"cap-net-ext",
@@ -12577,7 +12581,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"rustix 0.38.32",
"wayland-backend",
"wayland-scanner",
@@ -12600,7 +12604,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -12612,7 +12616,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -12731,7 +12735,7 @@ checksum = "ae1136a209614ace00b0c11f04dc7cf42540773be3b22eff6ad165110aba29c1"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.4.2",
"bitflags 2.6.0",
"thiserror",
"tracing",
"wasmtime",
@@ -13161,7 +13165,7 @@ version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"windows-sys 0.52.0",
]
@@ -13180,7 +13184,7 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
"wit-bindgen-rt",
"wit-bindgen-rust-macro",
]
@@ -13236,7 +13240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.4.2",
"bitflags 2.6.0",
"indexmap 2.2.6",
"log",
"serde",
@@ -13442,18 +13446,17 @@ name = "xim-parser"
version = "0.2.1"
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
dependencies = [
"bitflags 2.4.2",
"bitflags 2.6.0",
]
[[package]]
name = "xkbcommon"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=2d4c4439160c7846ede0f0ece93bf73b1e613339#2d4c4439160c7846ede0f0ece93bf73b1e613339"
dependencies = [
"as-raw-xcb-connection",
"libc",
"memmap2 0.8.0",
"memmap2",
"xkeysym",
]
@@ -13714,7 +13717,7 @@ dependencies = [
[[package]]
name = "zed_dart"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -13864,7 +13867,7 @@ dependencies = [
[[package]]
name = "zed_svelte"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -284,10 +284,10 @@ async-trait = "0.1"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.13"
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -365,6 +365,7 @@ shellexpand = "2.1.0"
shlex = "1.3.0"
signal-hook = "0.3.17"
similar = "1.3"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -451,6 +452,7 @@ features = [
"Win32_System_Com_StructuredStorage",
"Win32_System_DataExchange",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_SystemInformation",
"Win32_System_SystemServices",

View File

@@ -100,6 +100,7 @@
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame"
}
},
@@ -397,7 +398,7 @@
"bindings": {
"ctrl-shift-k": "editor::DeleteLine",
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-j": "editor::JoinLines",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",

View File

@@ -126,7 +126,8 @@
"cmd-alt-z": "editor::RevertSelectedHunks",
"cmd-'": "editor::ToggleHunkDiff",
"cmd-\"": "editor::ExpandAllHunkDiffs",
"cmd-alt-g b": "editor::ToggleGitBlame"
"cmd-alt-g b": "editor::ToggleGitBlame",
"cmd-i": "editor::ShowSignatureHelp"
}
},
{

View File

@@ -24,6 +24,8 @@
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"

View File

@@ -27,6 +27,7 @@
"ctrl-,": "editor::GoToPrevHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"

View File

@@ -1,12 +1,6 @@
[
{
"context": "ProjectPanel || Editor",
"bindings": {
"ctrl-6": "pane::AlternateFile"
}
},
{
"context": "Editor && VimControl && !VimWaiting && !menu",
"context": "VimControl && !menu",
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -18,6 +12,8 @@
"down": "vim::Down",
"enter": "vim::NextLineStart",
"ctrl-m": "vim::NextLineStart",
"+": "vim::NextLineStart",
"-": "vim::PreviousLineStart",
"tab": "vim::Tab",
"shift-tab": "vim::Tab",
"k": "vim::Up",
@@ -198,20 +194,20 @@
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
"ctrl-w space": "editor::OpenExcerptsSplit",
"ctrl-w g space": "editor::OpenExcerptsSplit",
"-": "pane::RevealInProjectPanel"
"ctrl-6": "pane::AlternateFile"
}
},
{
// escape is in its own section so that it cancels a pending count.
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"context": "VimControl && VimCount",
"bindings": {
"0": ["vim::Number", 0]
}
},
{
"context": "vim_mode == normal",
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"ctrl-[": "editor::Cancel",
".": "vim::Repeat",
"c": ["vim::PushOperator", "Change"],
"shift-c": "vim::ChangeToEndOfLine",
@@ -255,127 +251,12 @@
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPrevHunk"
"[ c": "editor::GoToPrevHunk",
"g c c": "vim::ToggleComments"
}
},
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && VimCount && vim_mode != insert",
"bindings": {
"0": ["vim::Number", 0]
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename" // zed specific
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == c",
"bindings": {
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
}
},
{
"context": "Editor && vim_operator == d",
"bindings": {
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gu",
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gU",
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == g~",
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {
"s": ["vim::PushOperator", "DeleteSurrounds"]
}
},
{
"context": "Editor && vim_operator == y",
"bindings": {
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == y",
"bindings": {
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
}
},
{
"context": "Editor && vim_operator == ys",
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"b": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::Argument"
}
},
{
"context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
"context": "vim_mode == visual",
"bindings": {
"u": "vim::ConvertToLowerCase",
"U": "vim::ConvertToUpperCase",
@@ -410,23 +291,16 @@
">": "vim::Indent",
"<": "vim::Outdent",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }]
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && vim_mode == normal && !VimWaiting",
"bindings": {
"g c c": "vim::ToggleComments"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"g c": "vim::ToggleComments"
}
},
{
"context": "Editor && vim_mode == insert",
"context": "vim_mode == insert",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -445,30 +319,115 @@
}
},
{
"context": "Editor && vim_mode == replace",
"context": "vim_mode == replace",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"backspace": "vim::UndoReplace",
"tab": "vim::Tab",
"enter": "vim::Enter",
"backspace": "vim::UndoReplace"
"enter": "vim::Enter"
}
},
{
"context": "Editor && vim_mode != replace && VimWaiting",
"context": "vim_mode == waiting",
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"]
"enter": "vim::Enter"
}
},
{
"context": "Editor && vim_mode == insert && VimWaiting",
"context": "vim_mode == operator",
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore"
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators"
}
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",
"'": "vim::Quotes",
"`": "vim::BackQuotes",
"\"": "vim::DoubleQuotes",
"|": "vim::VerticalBars",
"(": "vim::Parentheses",
")": "vim::Parentheses",
"b": "vim::Parentheses",
"[": "vim::SquareBrackets",
"]": "vim::SquareBrackets",
"{": "vim::CurlyBrackets",
"}": "vim::CurlyBrackets",
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::Argument"
}
},
{
"context": "vim_operator == c",
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
}
},
{
"context": "vim_operator == d",
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"]
}
},
{
"context": "vim_operator == gu",
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gU",
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "vim_operator == g~",
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "vim_operator == y",
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
}
},
{
"context": "vim_operator == ys",
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
@@ -508,7 +467,8 @@
"x": "project_panel::RevealInFileManager",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent"
"-": "project_panel::SelectParent",
"ctrl-6": "pane::AlternateFile"
}
},
{

View File

@@ -47,7 +47,7 @@
// },
"buffer_line_height": "comfortable",
// The name of a font to use for rendering text in the UI
// (On macOS) You can set this to ".SysmtemUIFont" to use the system font
// (On macOS) You can set this to ".SystemUIFont" to use the system font
"ui_font_family": "Zed Plex Sans",
// The OpenType features to enable for text in the UI
"ui_font_features": {
@@ -116,6 +116,11 @@
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": true,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if softwrap is set to 'preferred_line_length', and will show any
@@ -128,14 +133,7 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -262,6 +260,8 @@
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
"relative_line_numbers": false,
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
"search_wrap": true,
// When to populate a new search's query based on the text under the cursor.
// This setting can take the following three values:
//
@@ -539,6 +539,14 @@
// "delay_ms": 600
}
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration through the shell hook, works for POSIX shells and fish.
// "load_direnv": "shell_hook"
// 2. Load direnv configuration using `direnv export json` directly.
// This can help with some shells that otherwise would not detect
// the direnv environment, such as nushell or elvish.
// "load_direnv": "direct"
"load_direnv": "shell_hook",
"inline_completions": {
// A list of globs representing files that inline completions should be disabled for.
"disabled_globs": [".env"]
@@ -714,10 +722,12 @@
}
},
"C": {
"format_on_save": "off"
"format_on_save": "off",
"use_on_type_format": false
},
"C++": {
"format_on_save": "off"
"format_on_save": "off",
"use_on_type_format": false
},
"CSS": {
"prettier": {
@@ -769,6 +779,7 @@
},
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
"prettier": {
"allowed": true
}

View File

@@ -38,6 +38,7 @@
"icon.accent": "#10a793ff",
"status_bar.background": "#262933ff",
"title_bar.background": "#262933ff",
"title_bar.inactive_background": "#21242bff",
"toolbar.background": "#1e2025ff",
"tab_bar.background": "#21242bff",
"tab.inactive_background": "#21242bff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#566ddaff",
"status_bar.background": "#3a353fff",
"title_bar.background": "#3a353fff",
"title_bar.inactive_background": "#221f26ff",
"toolbar.background": "#19171cff",
"tab_bar.background": "#221f26ff",
"tab.inactive_background": "#221f26ff",
@@ -422,6 +423,7 @@
"icon.accent": "#586cdaff",
"status_bar.background": "#bfbcc5ff",
"title_bar.background": "#bfbcc5ff",
"title_bar.inactive_background": "#e6e3ebff",
"toolbar.background": "#efecf4ff",
"tab_bar.background": "#e6e3ebff",
"tab.inactive_background": "#e6e3ebff",
@@ -806,6 +808,7 @@
"icon.accent": "#6684e0ff",
"status_bar.background": "#45433bff",
"title_bar.background": "#45433bff",
"title_bar.inactive_background": "#262622ff",
"toolbar.background": "#20201dff",
"tab_bar.background": "#262622ff",
"tab.inactive_background": "#262622ff",
@@ -1190,6 +1193,7 @@
"icon.accent": "#6684dfff",
"status_bar.background": "#cecab4ff",
"title_bar.background": "#cecab4ff",
"title_bar.inactive_background": "#eeebd7ff",
"toolbar.background": "#fefbecff",
"tab_bar.background": "#eeebd7ff",
"tab.inactive_background": "#eeebd7ff",
@@ -1574,6 +1578,7 @@
"icon.accent": "#36a165ff",
"status_bar.background": "#424136ff",
"title_bar.background": "#424136ff",
"title_bar.inactive_background": "#2c2b23ff",
"toolbar.background": "#22221bff",
"tab_bar.background": "#2c2b23ff",
"tab.inactive_background": "#2c2b23ff",
@@ -1958,6 +1963,7 @@
"icon.accent": "#37a165ff",
"status_bar.background": "#c5c4b9ff",
"title_bar.background": "#c5c4b9ff",
"title_bar.inactive_background": "#ebeae3ff",
"toolbar.background": "#f4f3ecff",
"tab_bar.background": "#ebeae3ff",
"tab.inactive_background": "#ebeae3ff",
@@ -2342,6 +2348,7 @@
"icon.accent": "#407ee6ff",
"status_bar.background": "#443c39ff",
"title_bar.background": "#443c39ff",
"title_bar.inactive_background": "#27211eff",
"toolbar.background": "#1b1918ff",
"tab_bar.background": "#27211eff",
"tab.inactive_background": "#27211eff",
@@ -2726,6 +2733,7 @@
"icon.accent": "#407ee6ff",
"status_bar.background": "#ccc7c5ff",
"title_bar.background": "#ccc7c5ff",
"title_bar.inactive_background": "#e9e6e4ff",
"toolbar.background": "#f0eeedff",
"tab_bar.background": "#e9e6e4ff",
"tab.inactive_background": "#e9e6e4ff",
@@ -3110,6 +3118,7 @@
"icon.accent": "#5169ebff",
"status_bar.background": "#433a43ff",
"title_bar.background": "#433a43ff",
"title_bar.inactive_background": "#252025ff",
"toolbar.background": "#1b181bff",
"tab_bar.background": "#252025ff",
"tab.inactive_background": "#252025ff",
@@ -3494,6 +3503,7 @@
"icon.accent": "#5169ebff",
"status_bar.background": "#c6b8c6ff",
"title_bar.background": "#c6b8c6ff",
"title_bar.inactive_background": "#e0d5e0ff",
"toolbar.background": "#f7f3f7ff",
"tab_bar.background": "#e0d5e0ff",
"tab.inactive_background": "#e0d5e0ff",
@@ -3878,6 +3888,7 @@
"icon.accent": "#267eadff",
"status_bar.background": "#33444dff",
"title_bar.background": "#33444dff",
"title_bar.inactive_background": "#1c2529ff",
"toolbar.background": "#161b1dff",
"tab_bar.background": "#1c2529ff",
"tab.inactive_background": "#1c2529ff",
@@ -4262,6 +4273,7 @@
"icon.accent": "#267eadff",
"status_bar.background": "#a6cadcff",
"title_bar.background": "#a6cadcff",
"title_bar.inactive_background": "#cdeaf9ff",
"toolbar.background": "#ebf8ffff",
"tab_bar.background": "#cdeaf9ff",
"tab.inactive_background": "#cdeaf9ff",
@@ -4646,6 +4658,7 @@
"icon.accent": "#7272caff",
"status_bar.background": "#3b3535ff",
"title_bar.background": "#3b3535ff",
"title_bar.inactive_background": "#252020ff",
"toolbar.background": "#1b1818ff",
"tab_bar.background": "#252020ff",
"tab.inactive_background": "#252020ff",
@@ -5030,6 +5043,7 @@
"icon.accent": "#7272caff",
"status_bar.background": "#c1bbbbff",
"title_bar.background": "#c1bbbbff",
"title_bar.inactive_background": "#ebe3e3ff",
"toolbar.background": "#f4ececff",
"tab_bar.background": "#ebe3e3ff",
"tab.inactive_background": "#ebe3e3ff",
@@ -5414,6 +5428,7 @@
"icon.accent": "#468b8fff",
"status_bar.background": "#353f39ff",
"title_bar.background": "#353f39ff",
"title_bar.inactive_background": "#1f2621ff",
"toolbar.background": "#171c19ff",
"tab_bar.background": "#1f2621ff",
"tab.inactive_background": "#1f2621ff",
@@ -5798,6 +5813,7 @@
"icon.accent": "#488b90ff",
"status_bar.background": "#bcc5bfff",
"title_bar.background": "#bcc5bfff",
"title_bar.inactive_background": "#e3ebe6ff",
"toolbar.background": "#ecf4eeff",
"tab_bar.background": "#e3ebe6ff",
"tab.inactive_background": "#e3ebe6ff",
@@ -6182,6 +6198,7 @@
"icon.accent": "#3e62f4ff",
"status_bar.background": "#3b453bff",
"title_bar.background": "#3b453bff",
"title_bar.inactive_background": "#1f231fff",
"toolbar.background": "#131513ff",
"tab_bar.background": "#1f231fff",
"tab.inactive_background": "#1f231fff",
@@ -6566,6 +6583,7 @@
"icon.accent": "#3e61f4ff",
"status_bar.background": "#b4ceb4ff",
"title_bar.background": "#b4ceb4ff",
"title_bar.inactive_background": "#daeedaff",
"toolbar.background": "#f3faf3ff",
"tab_bar.background": "#daeedaff",
"tab.inactive_background": "#daeedaff",
@@ -6950,6 +6968,7 @@
"icon.accent": "#3e8ed0ff",
"status_bar.background": "#3e4769ff",
"title_bar.background": "#3e4769ff",
"title_bar.inactive_background": "#262f51ff",
"toolbar.background": "#202646ff",
"tab_bar.background": "#262f51ff",
"tab.inactive_background": "#262f51ff",
@@ -7334,6 +7353,7 @@
"icon.accent": "#3e8fd0ff",
"status_bar.background": "#c1c5d8ff",
"title_bar.background": "#c1c5d8ff",
"title_bar.inactive_background": "#e5e8f5ff",
"toolbar.background": "#f5f7ffff",
"tab_bar.background": "#e5e8f5ff",
"tab.inactive_background": "#e5e8f5ff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#5ac1feff",
"status_bar.background": "#313337ff",
"title_bar.background": "#313337ff",
"title_bar.inactive_background": "#1f2127ff",
"toolbar.background": "#0d1016ff",
"tab_bar.background": "#1f2127ff",
"tab.inactive_background": "#1f2127ff",
@@ -407,6 +408,7 @@
"icon.accent": "#3b9ee5ff",
"status_bar.background": "#dcdddeff",
"title_bar.background": "#dcdddeff",
"title_bar.inactive_background": "#ececedff",
"toolbar.background": "#fcfcfcff",
"tab_bar.background": "#ececedff",
"tab.inactive_background": "#ececedff",
@@ -776,6 +778,7 @@
"icon.accent": "#72cffeff",
"status_bar.background": "#464a52ff",
"title_bar.background": "#464a52ff",
"title_bar.inactive_background": "#353944ff",
"toolbar.background": "#242835ff",
"tab_bar.background": "#353944ff",
"tab.inactive_background": "#353944ff",

View File

@@ -47,6 +47,7 @@
"icon.accent": "#83a598ff",
"status_bar.background": "#4c4642ff",
"title_bar.background": "#4c4642ff",
"title_bar.inactive_background": "#3a3735ff",
"toolbar.background": "#282828ff",
"tab_bar.background": "#3a3735ff",
"tab.inactive_background": "#3a3735ff",
@@ -430,6 +431,7 @@
"icon.accent": "#83a598ff",
"status_bar.background": "#4c4642ff",
"title_bar.background": "#4c4642ff",
"title_bar.inactive_background": "#393634ff",
"toolbar.background": "#1d2021ff",
"tab_bar.background": "#393634ff",
"tab.inactive_background": "#393634ff",
@@ -813,6 +815,7 @@
"icon.accent": "#83a598ff",
"status_bar.background": "#4c4642ff",
"title_bar.background": "#4c4642ff",
"title_bar.inactive_background": "#3b3735ff",
"toolbar.background": "#32302fff",
"tab_bar.background": "#3b3735ff",
"tab.inactive_background": "#3b3735ff",
@@ -1196,6 +1199,7 @@
"icon.accent": "#0b6678ff",
"status_bar.background": "#d9c8a4ff",
"title_bar.background": "#d9c8a4ff",
"title_bar.inactive_background": "#ecddb4ff",
"toolbar.background": "#fbf1c7ff",
"tab_bar.background": "#ecddb4ff",
"tab.inactive_background": "#ecddb4ff",
@@ -1579,6 +1583,7 @@
"icon.accent": "#0b6678ff",
"status_bar.background": "#d9c8a4ff",
"title_bar.background": "#d9c8a4ff",
"title_bar.inactive_background": "#ecddb5ff",
"toolbar.background": "#f9f5d7ff",
"tab_bar.background": "#ecddb5ff",
"tab.inactive_background": "#ecddb5ff",
@@ -1962,6 +1967,7 @@
"icon.accent": "#0b6678ff",
"status_bar.background": "#d9c8a4ff",
"title_bar.background": "#d9c8a4ff",
"title_bar.inactive_background": "#ecdcb3ff",
"toolbar.background": "#f2e5bcff",
"tab_bar.background": "#ecdcb3ff",
"tab.inactive_background": "#ecdcb3ff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#74ade8ff",
"status_bar.background": "#3b414dff",
"title_bar.background": "#3b414dff",
"title_bar.inactive_background": "#2e343eff",
"toolbar.background": "#282c33ff",
"tab_bar.background": "#2f343eff",
"tab.inactive_background": "#2f343eff",
@@ -412,6 +413,7 @@
"icon.accent": "#5c78e2ff",
"status_bar.background": "#dcdcddff",
"title_bar.background": "#dcdcddff",
"title_bar.inactive_background": "#ebebecff",
"toolbar.background": "#fafafaff",
"tab_bar.background": "#ebebecff",
"tab.inactive_background": "#ebebecff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#9bced6ff",
"status_bar.background": "#292738ff",
"title_bar.background": "#292738ff",
"title_bar.inactive_background": "#1c1b2aff",
"toolbar.background": "#191724ff",
"tab_bar.background": "#1c1b2aff",
"tab.inactive_background": "#1c1b2aff",
@@ -417,6 +418,7 @@
"icon.accent": "#57949fff",
"status_bar.background": "#dcd8d8ff",
"title_bar.background": "#dcd8d8ff",
"title_bar.inactive_background": "#fef9f2ff",
"toolbar.background": "#faf4edff",
"tab_bar.background": "#fef9f2ff",
"tab.inactive_background": "#fef9f2ff",
@@ -796,6 +798,7 @@
"icon.accent": "#9bced6ff",
"status_bar.background": "#38354eff",
"title_bar.background": "#38354eff",
"title_bar.inactive_background": "#28253cff",
"toolbar.background": "#232136ff",
"tab_bar.background": "#28253cff",
"tab.inactive_background": "#28253cff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#518b8bff",
"status_bar.background": "#333944ff",
"title_bar.background": "#333944ff",
"title_bar.inactive_background": "#2b3038ff",
"toolbar.background": "#282c33ff",
"tab_bar.background": "#2b3038ff",
"tab.inactive_background": "#2b3038ff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#278ad1ff",
"status_bar.background": "#073743ff",
"title_bar.background": "#073743ff",
"title_bar.inactive_background": "#04313bff",
"toolbar.background": "#002a35ff",
"tab_bar.background": "#04313bff",
"tab.inactive_background": "#04313bff",
@@ -407,6 +408,7 @@
"icon.accent": "#288bd1ff",
"status_bar.background": "#cfd0c4ff",
"title_bar.background": "#cfd0c4ff",
"title_bar.inactive_background": "#f3eddaff",
"toolbar.background": "#fdf6e3ff",
"tab_bar.background": "#f3eddaff",
"tab.inactive_background": "#f3eddaff",

View File

@@ -38,6 +38,7 @@
"icon.accent": "#499befff",
"status_bar.background": "#2a261cff",
"title_bar.background": "#2a261cff",
"title_bar.inactive_background": "#231f16ff",
"toolbar.background": "#1b1810ff",
"tab_bar.background": "#231f16ff",
"tab.inactive_background": "#231f16ff",

View File

@@ -189,8 +189,12 @@ pub struct LanguageModelRequest {
pub messages: Vec<LanguageModelRequestMessage>,
pub stop: Vec<String>,
pub temperature: f32,
pub cached_contents: Vec<CachedContentId>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CachedContentId(String);
impl LanguageModelRequest {
pub fn to_proto(&self) -> proto::CompleteWithLanguageModel {
proto::CompleteWithLanguageModel {
@@ -200,6 +204,7 @@ impl LanguageModelRequest {
temperature: self.temperature,
tool_choice: None,
tools: Vec::new(),
cached_contents: self.cached_contents.iter().map(|id| id.0.clone()).collect(),
}
}

View File

@@ -2445,19 +2445,43 @@ fn render_docs_slash_command_trailer(
return Empty.into_any();
};
if !store.is_indexing(&package) {
let mut children = Vec::new();
if store.is_indexing(&package) {
children.push(
div()
.id(("crates-being-indexed", row.0))
.child(Icon::new(IconName::ArrowCircle).with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
.tooltip({
let package = package.clone();
move |cx| Tooltip::text(format!("Indexing {package}"), cx)
})
.into_any_element(),
);
}
if let Some(latest_error) = store.latest_error_for_package(&package) {
children.push(
div()
.id(("latest-error", row.0))
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
.tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
.into_any_element(),
)
}
let is_indexing = store.is_indexing(&package);
let latest_error = store.latest_error_for_package(&package);
if !is_indexing && latest_error.is_none() {
return Empty.into_any();
}
div()
.id(("crates-being-indexed", row.0))
.child(Icon::new(IconName::ArrowCircle).with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
.tooltip(move |cx| Tooltip::text(format!("Indexing {package}"), cx))
.into_any_element()
h_flex().gap_2().children(children).into_any_element()
}
fn make_lsp_adapter_delegate(

View File

@@ -27,6 +27,8 @@ pub enum CloudModel {
Claude3Opus,
Claude3Sonnet,
Claude3Haiku,
Gemini15Pro,
Gemini15Flash,
Custom(String),
}
@@ -109,6 +111,8 @@ impl CloudModel {
Self::Claude3Opus => "claude-3-opus",
Self::Claude3Sonnet => "claude-3-sonnet",
Self::Claude3Haiku => "claude-3-haiku",
Self::Gemini15Pro => "gemini-1.5-pro",
Self::Gemini15Flash => "gemini-1.5-flash",
Self::Custom(id) => id,
}
}
@@ -123,6 +127,8 @@ impl CloudModel {
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3Haiku => "Claude 3 Haiku",
Self::Gemini15Pro => "Gemini 1.5 Pro",
Self::Gemini15Flash => "Gemini 1.5 Flash",
Self::Custom(id) => id.as_str(),
}
}
@@ -136,6 +142,8 @@ impl CloudModel {
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 200000,
Self::Gemini15Pro => 128000,
Self::Gemini15Flash => 32000,
Self::Custom(_) => 4096, // TODO: Make this configurable
}
}

View File

@@ -152,6 +152,11 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
temperature: request.temperature,
tools: Vec::new(),
tool_choice: None,
cached_contents: request
.cached_contents
.iter()
.map(|id| id.0.clone())
.collect(),
};
self.client

View File

@@ -1258,6 +1258,7 @@ impl Context {
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
cached_contents: Vec::new(), // todo!("support context caching")
}
}
@@ -1513,6 +1514,7 @@ impl Context {
messages: messages.collect(),
stop: vec![],
temperature: 1.0,
cached_contents: Vec::new(), // todo!
};
let stream = CompletionProvider::global(cx).complete(request, cx);

View File

@@ -851,6 +851,7 @@ impl InlineAssistant {
messages,
stop: vec!["|END|>".to_string()],
temperature,
cached_contents: Vec::new(),
})
})
}

View File

@@ -744,6 +744,7 @@ impl PromptLibrary {
}],
stop: Vec::new(),
temperature: 1.,
cached_contents: Vec::new(),
},
cx,
)

View File

@@ -8,7 +8,8 @@ use assistant_slash_command::{
};
use gpui::{AppContext, Model, Task, WeakView};
use indexed_docs::{
IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
ProviderId,
};
use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
@@ -34,22 +35,22 @@ impl DocsSlashCommand {
))
}
/// Ensures that the rustdoc provider is registered.
/// Ensures that the indexed doc providers for Rust are registered.
///
/// Ideally we would do this sooner, but we need to wait until we're able to
/// access the workspace so we can read the project.
fn ensure_rustdoc_provider_is_registered(
fn ensure_rust_doc_providers_are_registered(
&self,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) {
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
if indexed_docs_registry
.get_provider_store(ProviderId::rustdoc())
.get_provider_store(LocalRustdocProvider::id())
.is_none()
{
let index_provider_deps = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
@@ -63,9 +64,29 @@ impl DocsSlashCommand {
});
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
indexed_docs_registry.register_provider(Box::new(RustdocIndexer::new(Box::new(
LocalProvider::new(fs, cargo_workspace_root),
))));
indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new(
fs,
cargo_workspace_root,
)));
}
}
if indexed_docs_registry
.get_provider_store(DocsDotRsProvider::id())
.is_none()
{
let http_client = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
anyhow::Ok(project.read(cx).client().http_client().clone())
});
if let Some(http_client) = http_client.log_err() {
indexed_docs_registry
.register_provider(Box::new(DocsDotRsProvider::new(http_client)));
}
}
}
@@ -95,7 +116,7 @@ impl SlashCommand for DocsSlashCommand {
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rustdoc_provider_is_registered(workspace, cx);
self.ensure_rust_doc_providers_are_registered(workspace, cx);
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
let args = DocsSlashCommandArgs::parse(&query);
@@ -121,6 +142,14 @@ impl SlashCommand for DocsSlashCommand {
match args {
DocsSlashCommandArgs::NoProvider => {
let providers = indexed_docs_registry.list_providers();
if providers.is_empty() {
return Ok(vec![ArgumentCompletion {
label: "No available docs providers.".to_string(),
new_text: String::new(),
run_command: false,
}]);
}
Ok(providers
.into_iter()
.map(|provider| ArgumentCompletion {
@@ -171,46 +200,65 @@ impl SlashCommand for DocsSlashCommand {
};
let args = DocsSlashCommandArgs::parse(argument);
let text = cx.background_executor().spawn({
let task = cx.background_executor().spawn({
let store = args
.provider()
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
async move {
match args {
let (provider, key) = match args {
DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"),
DocsSlashCommandArgs::SearchPackageDocs {
provider, package, ..
} => {
let store = store?;
let item_docs = store.load(package.clone()).await?;
anyhow::Ok((provider, package, item_docs.to_string()))
}
} => (provider, package),
DocsSlashCommandArgs::SearchItemDocs {
provider,
item_path,
..
} => {
let store = store?;
let item_docs = store.load(item_path.clone()).await?;
} => (provider, item_path),
};
anyhow::Ok((provider, item_path, item_docs.to_string()))
let store = store?;
let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') {
let docs = store.load_many_by_prefix(prefix.to_string()).await?;
let mut text = String::new();
let mut ranges = Vec::new();
for (key, docs) in docs {
let prev_len = text.len();
text.push_str(&docs.0);
text.push_str("\n");
ranges.push((key, prev_len..text.len()));
text.push_str("\n");
}
}
(text, ranges)
} else {
let item_docs = store.load(key.clone()).await?;
let text = item_docs.to_string();
let range = 0..text.len();
(text, vec![(key, range)])
};
anyhow::Ok((provider, text, ranges))
}
});
cx.foreground_executor().spawn(async move {
let (provider, path, text) = text.await?;
let range = 0..text.len();
let (provider, text, ranges) = task.await?;
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::FileRust,
label: format!("docs ({provider}): {path}",).into(),
}],
sections: ranges
.into_iter()
.map(|(key, range)| SlashCommandOutputSection {
range,
icon: IconName::FileDoc,
label: format!("docs ({provider}): {key}",).into(),
})
.collect(),
run_commands_in_text: false,
})
})

View File

@@ -27,7 +27,7 @@ pub(crate) struct FetchSlashCommand;
impl FetchSlashCommand {
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") {
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}

View File

@@ -269,6 +269,7 @@ impl TerminalInlineAssistant {
messages,
stop: Vec::new(),
temperature: 1.0,
cached_contents: Vec::new(), // todo!
})
}

View File

@@ -13,8 +13,9 @@ use async_tungstenite::tungstenite::{
use clock::SystemClock;
use collections::HashMap;
use futures::{
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
TryFutureExt as _, TryStreamExt,
channel::oneshot,
future::{BoxFuture, LocalBoxFuture},
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
@@ -23,6 +24,7 @@ use http::{HttpClient, HttpClientWithUrl};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use proto::ProtoClient;
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
@@ -1408,6 +1410,11 @@ impl Client {
self.peer.send(self.connection_id()?, message)
}
fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
let connection_id = self.connection_id()?;
self.peer.send_dynamic(connection_id, envelope)
}
pub fn request<T: RequestMessage>(
&self,
request: T,
@@ -1606,6 +1613,20 @@ impl Client {
}
}
impl ProtoClient for Client {
fn request(
&self,
envelope: proto::Envelope,
request_type: &'static str,
) -> BoxFuture<'static, Result<proto::Envelope>> {
self.request_dynamic(envelope, request_type).boxed()
}
fn send(&self, envelope: proto::Envelope) -> Result<()> {
self.send_dynamic(envelope)
}
}
#[derive(Serialize, Deserialize)]
struct DevelopmentCredentials {
user_id: u64,

View File

@@ -101,6 +101,7 @@ pub fn language_model_request_to_google_ai(
.collect::<Result<Vec<_>>>()?,
generation_config: None,
safety_settings: None,
cached_content: request.cached_contents.into_iter().next(),
})
}
@@ -125,6 +126,60 @@ pub fn language_model_request_message_to_google_ai(
})
}
pub fn cache_language_model_content_request_to_google_ai(
request: proto::CacheLanguageModelContent,
) -> Result<google_ai::CreateCachedContentRequest> {
Ok(google_ai::CreateCachedContentRequest {
contents: request
.messages
.into_iter()
.map(language_model_request_message_to_google_ai)
.collect::<Result<Vec<_>>>()?,
tools: if request.tools.is_empty() {
None
} else {
Some(
request
.tools
.into_iter()
.try_fold(Vec::new(), |mut acc, tool| {
if let Some(variant) = tool.variant {
match variant {
proto::chat_completion_tool::Variant::Function(f) => {
let description = f.description.ok_or_else(|| {
anyhow!("Function tool is missing a description")
})?;
let parameters = f.parameters.ok_or_else(|| {
anyhow!("Function tool is missing parameters")
})?;
let parsed_parameters = serde_json::from_str(&parameters)
.map_err(|e| {
anyhow!("Failed to parse parameters: {}", e)
})?;
acc.push(google_ai::Tool {
function_declarations: vec![
google_ai::FunctionDeclaration {
name: f.name,
description,
parameters: parsed_parameters,
},
],
});
}
}
}
anyhow::Ok(acc)
})?,
)
},
ttl: request.ttl_seconds.map(|s| format!("{}s", s)),
display_name: None,
model: request.model,
system_instruction: None,
tool_config: None,
})
}
pub fn count_tokens_request_to_google_ai(
request: proto::CountTokensWithLanguageModel,
) -> Result<google_ai::CountTokensRequest> {

View File

@@ -616,6 +616,17 @@ impl Server {
)
}
})
.add_request_handler({
let app_state = app_state.clone();
user_handler(move |request, response, session| {
cache_language_model_content(
request,
response,
session,
app_state.config.google_ai_api_key.clone(),
)
})
})
.add_request_handler({
let app_state = app_state.clone();
user_handler(move |request, response, session| {
@@ -4723,6 +4734,43 @@ async fn complete_with_anthropic(
Ok(())
}
async fn cache_language_model_content(
request: proto::CacheLanguageModelContent,
response: Response<proto::CacheLanguageModelContent>,
session: UserSession,
google_ai_api_key: Option<Arc<str>>,
) -> Result<()> {
authorize_access_to_language_models(&session).await?;
if !request.model.starts_with("gemini") {
return Err(anyhow!(
"caching content for model: {:?} is not supported",
request.model
))?;
}
let api_key = google_ai_api_key
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
let cached_content = google_ai::create_cached_content(
session.http_client.as_ref(),
google_ai::API_URL,
&api_key,
crate::ai::cache_language_model_content_request_to_google_ai(request)?,
)
.await?;
response.send(proto::CacheLanguageModelContentResponse {
name: cached_content.name,
create_time: cached_content.create_time.timestamp() as u64,
update_time: cached_content.update_time.timestamp() as u64,
expire_time: cached_content.expire_time.map(|t| t.timestamp() as u64),
total_token_count: cached_content.usage_metadata.total_token_count,
})?;
Ok(())
}
struct CountTokensWithLanguageModelRateLimit;
impl RateLimit for CountTokensWithLanguageModelRateLimit {

View File

@@ -3327,7 +3327,7 @@ async fn test_local_settings(
let store = cx.global::<SettingsStore>();
assert_eq!(
store
.local_settings(worktree_b.read(cx).id().to_usize())
.local_settings(worktree_b.entity_id().as_u64() as _)
.collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
@@ -3346,7 +3346,7 @@ async fn test_local_settings(
let store = cx.global::<SettingsStore>();
assert_eq!(
store
.local_settings(worktree_b.read(cx).id().to_usize())
.local_settings(worktree_b.entity_id().as_u64() as _)
.collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{}"#.to_string()),
@@ -3375,7 +3375,7 @@ async fn test_local_settings(
let store = cx.global::<SettingsStore>();
assert_eq!(
store
.local_settings(worktree_b.read(cx).id().to_usize())
.local_settings(worktree_b.entity_id().as_u64() as _)
.collect::<Vec<_>>(),
&[
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
@@ -3407,7 +3407,7 @@ async fn test_local_settings(
let store = cx.global::<SettingsStore>();
assert_eq!(
store
.local_settings(worktree_b.read(cx).id().to_usize())
.local_settings(worktree_b.entity_id().as_u64() as _)
.collect::<Vec<_>>(),
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
)

View File

@@ -1237,7 +1237,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
}
for buffer in guest_project.opened_buffers() {
for buffer in guest_project.opened_buffers(cx) {
let buffer = buffer.read(cx);
assert_eq!(
buffer.deferred_ops_len(),
@@ -1287,8 +1287,8 @@ impl RandomizedTest for ProjectCollaborationTest {
for guest_buffer in guest_buffers {
let buffer_id =
guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
let host_buffer = host_project.read_with(host_cx, |project, _| {
project.buffer_for_id(buffer_id).unwrap_or_else(|| {
let host_buffer = host_project.read_with(host_cx, |project, cx| {
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
panic!(
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
client.username,

View File

@@ -49,6 +49,7 @@ lazy_static.workspace = true
linkify.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
multi_buffer.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true

View File

@@ -286,12 +286,14 @@ gpui::actions!(
SelectPageUp,
ShowCharacterPalette,
ShowInlineCompletion,
ShowSignatureHelp,
ShuffleLines,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
Tab,
TabPrev,
ToggleAutoSignatureHelp,
ToggleGitBlame,
ToggleGitBlameInline,
ToggleSelectionMenu,

View File

@@ -39,8 +39,10 @@ pub mod tasks;
#[cfg(test)]
mod editor_tests;
mod signature_help;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use ::git::diff::{DiffHunk, DiffHunkStatus};
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
pub(crate) use actions::*;
@@ -154,6 +156,7 @@ use workspace::{
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url;
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
pub const FILE_HEADER_HEIGHT: u8 = 1;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
@@ -501,6 +504,8 @@ pub struct Editor {
context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
signature_help_state: SignatureHelpState,
auto_signature_help: Option<bool>,
find_all_references_task_sources: Vec<Anchor>,
next_completion_id: CompletionId,
completion_documentation_pre_resolve_debounce: DebouncedDelay,
@@ -1819,6 +1824,8 @@ impl Editor {
context_menu: RwLock::new(None),
mouse_context_menu: None,
completion_tasks: Default::default(),
signature_help_state: SignatureHelpState::default(),
auto_signature_help: None,
find_all_references_task_sources: Vec::new(),
next_completion_id: 0,
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
@@ -2156,6 +2163,10 @@ impl Editor {
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
self.cursor_shape = cursor_shape;
// Disrupt blink for immediate user feedback that the cursor shape has changed
self.blink_manager.update(cx, BlinkManager::show_cursor);
cx.notify();
}
@@ -2411,6 +2422,15 @@ impl Editor {
self.request_autoscroll(autoscroll, cx);
}
self.selections_did_change(true, &old_cursor_position, request_completions, cx);
if self.should_open_signature_help_automatically(
&old_cursor_position,
self.signature_help_state.backspace_pressed(),
cx,
) {
self.show_signature_help(&ShowSignatureHelp, cx);
}
self.signature_help_state.set_backspace_pressed(false);
}
result
@@ -2866,6 +2886,10 @@ impl Editor {
return true;
}
if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) {
return true;
}
if self.hide_context_menu(cx).is_some() {
return true;
}
@@ -2942,7 +2966,7 @@ impl Editor {
}
let selections = self.selections.all_adjusted(cx);
let mut brace_inserted = false;
let mut bracket_inserted = false;
let mut edits = Vec::new();
let mut linked_edits = HashMap::<_, Vec<_>>::default();
let mut new_selections = Vec::with_capacity(selections.len());
@@ -3004,6 +3028,7 @@ impl Editor {
),
&bracket_pair.start[..prefix_len],
));
if autoclose
&& bracket_pair.close
&& following_text_allows_autoclose
@@ -3021,7 +3046,7 @@ impl Editor {
selection.range(),
format!("{}{}", text, bracket_pair.end).into(),
));
brace_inserted = true;
bracket_inserted = true;
continue;
}
}
@@ -3067,7 +3092,7 @@ impl Editor {
selection.end..selection.end,
bracket_pair.end.as_str().into(),
));
brace_inserted = true;
bracket_inserted = true;
new_selections.push((
Selection {
id: selection.id,
@@ -3224,7 +3249,7 @@ impl Editor {
s.select(new_selections)
});
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
if !bracket_inserted && EditorSettings::get_global(cx).use_on_type_format {
if let Some(on_type_format_task) =
this.trigger_on_type_formatting(text.to_string(), cx)
{
@@ -3232,6 +3257,14 @@ impl Editor {
}
}
let editor_settings = EditorSettings::get_global(cx);
if bracket_inserted
&& (editor_settings.auto_signature_help
|| editor_settings.show_signature_help_after_edits)
{
this.show_signature_help(&ShowSignatureHelp, cx);
}
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx);
@@ -4305,6 +4338,14 @@ impl Editor {
true,
cx,
);
let editor_settings = EditorSettings::get_global(cx);
if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help {
// After the code completion is finished, users often want to know what signatures are needed.
// so we should automatically call signature_help
self.show_signature_help(&ShowSignatureHelp, cx);
}
Some(cx.foreground_executor().spawn(async move {
apply_edits.await?;
Ok(())
@@ -5328,6 +5369,7 @@ impl Editor {
}
}
this.signature_help_state.set_backspace_pressed(true);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx);
let empty_str: Arc<str> = Arc::from("");
@@ -8487,7 +8529,7 @@ impl Editor {
) -> Vec<(TaskSourceKind, TaskTemplate)> {
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
let (worktree_id, file) = project
.buffer_for_id(runnable.buffer)
.buffer_for_id(runnable.buffer, cx)
.and_then(|buffer| buffer.read(cx).file())
.map(|file| (WorktreeId::from_usize(file.worktree_id()), file.clone()))
.unzip();
@@ -11604,8 +11646,11 @@ impl Editor {
if let Some(blame) = self.blame.as_ref() {
blame.update(cx, GitBlame::blur)
}
if !self.hover_state.focused(cx) {
hide_hover(self, cx);
}
self.hide_context_menu(cx);
hide_hover(self, cx);
cx.emit(EditorEvent::Blurred);
cx.notify();
}

View File

@@ -25,6 +25,9 @@ pub struct EditorSettings {
pub expand_excerpt_lines: u32,
#[serde(default)]
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
pub search_wrap: bool,
pub auto_signature_help: bool,
pub show_signature_help_after_edits: bool,
#[serde(default)]
pub jupyter: Jupyter,
}
@@ -228,6 +231,20 @@ pub struct EditorSettingsContent {
///
/// Default: select
pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
/// Whether the editor search results will loop
///
/// Default: true
pub search_wrap: Option<bool>,
/// Whether to automatically show a signature help pop-up or not.
///
/// Default: false
pub auto_signature_help: Option<bool>,
/// Whether to show the signature help pop-up after completions or bracket pairs inserted.
///
/// Default: true
pub show_signature_help_after_edits: Option<bool>,
/// Jupyter REPL settings.
pub jupyter: Option<Jupyter>,

View File

@@ -21,13 +21,16 @@ use language::{
BracketPairConfig,
Capability::ReadWrite,
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
Point,
ParsedMarkdown, Point,
};
use language_settings::IndentGuideSettings;
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
@@ -6831,6 +6834,626 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
);
}
#[gpui::test]
async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |_| {});
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.auto_signature_help = Some(true);
});
});
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
signature_help_provider: Some(lsp::SignatureHelpOptions {
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "/*".to_string(),
end: " */".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "[".to_string(),
end: "]".to_string(),
close: false,
surround: false,
newline: true,
},
BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
surround: true,
newline: false,
},
BracketPair {
start: "<".to_string(),
end: ">".to_string(),
close: false,
surround: true,
newline: true,
},
],
..Default::default()
},
autoclose_before: "})]".to_string(),
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let language = Arc::new(language);
cx.language_registry().add(language.clone());
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(language), cx);
});
cx.set_state(
&r#"
fn main() {
sampleˇ
}
"#
.unindent(),
);
cx.update_editor(|view, cx| {
view.handle_input("(", cx);
});
cx.assert_editor_state(
&"
fn main() {
sample(ˇ)
}
"
.unindent(),
);
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
handle_signature_help_request(&mut cx, mocked_response).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
let ParsedMarkdown {
text, highlights, ..
} = signature_help_state.unwrap().parsed_content;
assert_eq!(text, "param1: u8, param2: u8");
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
});
}
#[gpui::test]
async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.auto_signature_help = Some(false);
settings.show_signature_help_after_edits = Some(false);
});
});
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
signature_help_provider: Some(lsp::SignatureHelpOptions {
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "/*".to_string(),
end: " */".to_string(),
close: true,
surround: true,
newline: true,
},
BracketPair {
start: "[".to_string(),
end: "]".to_string(),
close: false,
surround: false,
newline: true,
},
BracketPair {
start: "\"".to_string(),
end: "\"".to_string(),
close: true,
surround: true,
newline: false,
},
BracketPair {
start: "<".to_string(),
end: ">".to_string(),
close: false,
surround: true,
newline: true,
},
],
..Default::default()
},
autoclose_before: "})]".to_string(),
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let language = Arc::new(language);
cx.language_registry().add(language.clone());
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(language), cx);
});
// Ensure that signature_help is not called when no signature help is enabled.
cx.set_state(
&r#"
fn main() {
sampleˇ
}
"#
.unindent(),
);
cx.update_editor(|view, cx| {
view.handle_input("(", cx);
});
cx.assert_editor_state(
&"
fn main() {
sample(ˇ)
}
"
.unindent(),
);
cx.editor(|editor, _| {
assert!(editor.signature_help_state.task().is_none());
});
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
// Ensure that signature_help is called when enabled afte edits
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.auto_signature_help = Some(false);
settings.show_signature_help_after_edits = Some(true);
});
});
});
cx.set_state(
&r#"
fn main() {
sampleˇ
}
"#
.unindent(),
);
cx.update_editor(|view, cx| {
view.handle_input("(", cx);
});
cx.assert_editor_state(
&"
fn main() {
sample(ˇ)
}
"
.unindent(),
);
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.update_editor(|editor, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
let ParsedMarkdown {
text, highlights, ..
} = signature_help_state.unwrap().parsed_content;
assert_eq!(text, "param1: u8, param2: u8");
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
editor.signature_help_state = SignatureHelpState::default();
});
// Ensure that signature_help is called when auto signature help override is enabled
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.auto_signature_help = Some(true);
settings.show_signature_help_after_edits = Some(false);
});
});
});
cx.set_state(
&r#"
fn main() {
sampleˇ
}
"#
.unindent(),
);
cx.update_editor(|view, cx| {
view.handle_input("(", cx);
});
cx.assert_editor_state(
&"
fn main() {
sample(ˇ)
}
"
.unindent(),
);
handle_signature_help_request(&mut cx, mocked_response).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
let ParsedMarkdown {
text, highlights, ..
} = signature_help_state.unwrap().parsed_content;
assert_eq!(text, "param1: u8, param2: u8");
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
});
}
#[gpui::test]
async fn test_signature_help(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.auto_signature_help = Some(true);
});
});
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
signature_help_provider: Some(lsp::SignatureHelpOptions {
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
// A test that directly calls `show_signature_help`
cx.update_editor(|editor, cx| {
editor.show_signature_help(&ShowSignatureHelp, cx);
});
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
handle_signature_help_request(&mut cx, mocked_response).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
let ParsedMarkdown {
text, highlights, ..
} = signature_help_state.unwrap().parsed_content;
assert_eq!(text, "param1: u8, param2: u8");
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
});
// When exiting outside from inside the brackets, `signature_help` is closed.
cx.set_state(indoc! {"
fn main() {
sample(ˇ);
}
fn sample(param1: u8, param2: u8) {}
"});
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
});
let mocked_response = lsp::SignatureHelp {
signatures: Vec::new(),
active_signature: None,
active_parameter: None,
};
handle_signature_help_request(&mut cx, mocked_response).await;
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
assert!(!editor.signature_help_state.is_shown());
});
// When entering inside the brackets from outside, `show_signature_help` is automatically called.
cx.set_state(indoc! {"
fn main() {
sample(ˇ);
}
fn sample(param1: u8, param2: u8) {}
"});
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
assert!(editor.signature_help_state.is_shown());
});
// Restore the popover with more parameter input
cx.set_state(indoc! {"
fn main() {
sample(param1, param2ˇ);
}
fn sample(param1: u8, param2: u8) {}
"});
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(1),
};
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
// When selecting a range, the popover is gone.
// Avoid using `cx.set_state` to not actually edit the document, just change its selections.
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
})
});
cx.assert_editor_state(indoc! {"
fn main() {
sample(param1, «ˇparam2»);
}
fn sample(param1: u8, param2: u8) {}
"});
cx.editor(|editor, _| {
assert!(!editor.signature_help_state.is_shown());
});
// When unselecting again, the popover is back if within the brackets.
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
})
});
cx.assert_editor_state(indoc! {"
fn main() {
sample(param1, ˇparam2);
}
fn sample(param1: u8, param2: u8) {}
"});
handle_signature_help_request(&mut cx, mocked_response).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.editor(|editor, _| {
assert!(editor.signature_help_state.is_shown());
});
// Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
})
});
cx.assert_editor_state(indoc! {"
fn main() {
sample(param1, ˇparam2);
}
fn sample(param1: u8, param2: u8) {}
"});
let mocked_response = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn sample(param1: u8, param2: u8)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(1),
};
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
cx.condition(|editor, _| editor.signature_help_state.is_shown())
.await;
cx.update_editor(|editor, cx| {
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
});
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
.await;
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
})
});
cx.assert_editor_state(indoc! {"
fn main() {
sample(param1, «ˇparam2»);
}
fn sample(param1: u8, param2: u8) {}
"});
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
})
});
cx.assert_editor_state(indoc! {"
fn main() {
sample(param1, ˇparam2);
}
fn sample(param1: u8, param2: u8) {}
"});
cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
.await;
}
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -12450,6 +13073,21 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
);
}
pub fn handle_signature_help_request(
cx: &mut EditorLspTestContext,
mocked_response: lsp::SignatureHelp,
) -> impl Future<Output = ()> {
let mut request =
cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
let mocked_response = mocked_response.clone();
async move { Ok(Some(mocked_response)) }
});
async move {
request.next().await;
}
}
/// Handle completion request passing a marked string specifying where the completion
/// should be triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range

View File

@@ -382,6 +382,7 @@ impl EditorElement {
cx.propagate();
}
});
register_action(view, cx, Editor::show_signature_help);
register_action(view, cx, Editor::next_inline_completion);
register_action(view, cx, Editor::previous_inline_completion);
register_action(view, cx, Editor::show_inline_completion);
@@ -709,18 +710,24 @@ impl EditorElement {
let Some(hub) = editor.collaboration_hub() else {
return;
};
let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1))
..DisplayPoint::new(
let start = snapshot.display_snapshot.clip_point(
DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
Bias::Left,
);
let end = snapshot.display_snapshot.clip_point(
DisplayPoint::new(
point.row(),
(point.column() + 1).min(snapshot.line_len(point.row())),
);
),
Bias::Right,
);
let range = snapshot
.buffer_snapshot
.anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left)
.anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
..snapshot
.buffer_snapshot
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
.anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
return;
@@ -2629,6 +2636,73 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_signature_help(
&self,
hitbox: &Hitbox,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
start_row: DisplayRow,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
em_width: Pixels,
cx: &mut WindowContext,
) {
let Some(newest_selection_head) = newest_selection_head else {
return;
};
let selection_row = newest_selection_head.row();
if selection_row < start_row {
return;
}
let Some(cursor_row_layout) = line_layouts.get(selection_row.minus(start_row) as usize)
else {
return;
};
let start_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
- scroll_pixel_position.x
+ content_origin.x;
let start_y =
selection_row.as_f32() * line_height + content_origin.y - scroll_pixel_position.y;
let max_size = size(
(120. * em_width) // Default size
.min(hitbox.size.width / 2.) // Shrink to half of the editor width
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
(16. * line_height) // Default size
.min(hitbox.size.height / 2.) // Shrink to half of the editor height
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
);
let maybe_element = self.editor.update(cx, |editor, cx| {
if let Some(popover) = editor.signature_help_state.popover_mut() {
let element = popover.render(
&self.style,
max_size,
editor.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
);
Some(element)
} else {
None
}
});
if let Some(mut element) = maybe_element {
let window_size = cx.viewport_size();
let size = element.layout_as_root(Size::<AvailableSpace>::default(), cx);
let mut point = point(start_x, start_y - size.height);
// Adjusting to ensure the popover does not overflow in the X-axis direction.
if point.x + size.width >= window_size.width {
point.x = window_size.width - size.width;
}
cx.defer_draw(element, point, 1)
}
}
fn paint_background(&self, layout: &EditorLayout, cx: &mut WindowContext) {
cx.paint_layer(layout.hitbox.bounds, |cx| {
let scroll_top = layout.position_map.snapshot.scroll_position().y;
@@ -3734,6 +3808,9 @@ impl EditorElement {
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
editor.update(cx, |editor, cx| {
if editor.hover_state.focused(cx) {
return;
}
if event.pressed_button == Some(MouseButton::Left)
|| event.pressed_button == Some(MouseButton::Middle)
{
@@ -5063,6 +5140,18 @@ impl Element for EditorElement {
vec![]
};
self.layout_signature_help(
&hitbox,
content_origin,
scroll_pixel_position,
newest_selection_head,
start_row,
&line_layouts,
line_height,
em_width,
cx,
);
if !cx.has_active_drag() {
self.layout_hover_popovers(
&snapshot,

View File

@@ -5,24 +5,26 @@ use crate::{
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
EditorStyle, Hover, RangeToAnchorExt,
};
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
Task, ViewContext, WeakView,
div, px, AnyElement, AsyncWindowContext, CursorStyle, FontWeight, Hsla, InteractiveElement,
IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View,
ViewContext, WeakView,
};
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use itertools::Itertools;
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use project::{HoverBlock, InlayHintLabelPart};
use settings::Settings;
use smol::stream::StreamExt;
use std::rc::Rc;
use std::{borrow::Cow, cell::RefCell};
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Tooltip};
use util::TryFutureExt;
use workspace::Workspace;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -40,6 +42,9 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
if EditorSettings::get_global(cx).hover_popover_enabled {
if show_keyboard_hover(editor, cx) {
return;
}
if let Some(anchor) = anchor {
show_hover(editor, anchor, false, cx);
} else {
@@ -48,6 +53,20 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
}
}
pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
let info_popovers = editor.hover_state.info_popovers.clone();
for p in info_popovers {
let keyboard_grace = p.keyboard_grace.borrow();
if *keyboard_grace {
if let Some(anchor) = p.anchor {
show_hover(editor, anchor, false, cx);
return true;
}
}
}
return false;
}
pub struct InlayHover {
pub range: InlayHighlight,
pub tooltip: HoverBlock,
@@ -113,12 +132,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
let blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None,
};
this.update(&mut cx, |this, cx| {
@@ -291,39 +312,40 @@ fn show_hover(
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
let mut info_popovers = Vec::with_capacity(hovers_response.len());
let mut info_popover_tasks = hovers_response
.into_iter()
.map(|hover_result| async {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = hover_result
.range
.and_then(|range| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)?;
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
Some(start..end)
})
.unwrap_or_else(|| anchor..anchor);
for hover_result in hovers_response {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = hover_result
.range
.and_then(|range| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)?;
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
Some(start..end)
})
.unwrap_or_else(|| anchor..anchor);
(
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
},
)
})
.collect::<FuturesUnordered<_>>();
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content =
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
info_popover_tasks.push((
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
},
));
}
for (highlight_range, info_popover) in info_popover_tasks {
hover_highlights.push(highlight_range);
info_popovers.push(info_popover);
}
@@ -357,72 +379,81 @@ async fn parse_blocks(
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> markdown::ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
cx: &mut AsyncWindowContext,
) -> Option<View<Markdown>> {
let fallback_language_name = if let Some(ref l) = language {
let l = Arc::clone(l);
Some(l.lsp_id().clone())
} else {
None
};
for block in blocks {
match &block.kind {
HoverBlockKind::PlainText => {
markdown::new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text.replace("\\n", "\n"));
let combined_text = blocks
.iter()
.map(|block| match &block.kind {
project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
Cow::Borrowed(block.text.trim())
}
HoverBlockKind::Markdown => {
markdown::parse_markdown_block(
&block.text.replace("\\n", "\n"),
language_registry,
language.clone(),
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await
project::HoverBlockKind::Code { language } => {
Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
}
})
.join("\n\n");
HoverBlockKind::Code { language } => {
if let Some(language) = language_registry
.language_for_name(language)
.now_or_never()
.and_then(Result::ok)
{
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
} else {
text.push_str(&block.text);
}
}
}
}
let rendered_block = cx
.new_view(|cx| {
let settings = ThemeSettings::get_global(cx);
let buffer_font_family = settings.buffer_font.family.clone();
let mut base_style = cx.text_style();
base_style.refine(&TextStyleRefinement {
font_family: Some(buffer_font_family.clone()),
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
});
let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
if leading_space > 0 {
highlights = highlights
.into_iter()
.map(|(range, style)| {
(
range.start.saturating_sub(leading_space)
..range.end.saturating_sub(leading_space),
style,
)
})
.collect();
region_ranges = region_ranges
.into_iter()
.map(|range| {
range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
})
.collect();
}
let markdown_style = MarkdownStyle {
base_text_style: base_style,
code_block: StyleRefinement::default().mt(rems(1.)).mb(rems(1.)),
inline_code: TextStyleRefinement {
background_color: Some(cx.theme().colors().background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: TextStyleRefinement {
color: Some(cx.theme().colors().editor_foreground),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(cx.theme().colors().editor_foreground),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: { cx.theme().players().local().selection },
break_style: Default::default(),
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
.text_base()
.mt(rems(1.))
.mb_0(),
};
ParsedMarkdown {
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
}
Markdown::new(
combined_text,
markdown_style.clone(),
Some(language_registry.clone()),
cx,
fallback_language_name,
)
})
.ok();
rendered_block
}
#[derive(Default, Debug)]
@@ -444,7 +475,7 @@ impl HoverState {
style: &EditorStyle,
visible_rows: Range<DisplayRow>,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
// If there is a diagnostic, position the popovers based on that.
@@ -482,29 +513,39 @@ impl HoverState {
elements.push(diagnostic_popover.render(style, max_size, cx));
}
for info_popover in &mut self.info_popovers {
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
elements.push(info_popover.render(max_size, cx));
}
Some((point, elements))
}
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
let mut hover_popover_is_focused = false;
for info_popover in &self.info_popovers {
for markdown_view in &info_popover.parsed_content {
if markdown_view.focus_handle(cx).is_focused(cx) {
hover_popover_is_focused = true;
}
}
}
return hover_popover_is_focused;
}
}
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: ParsedMarkdown,
pub parsed_content: Option<View<Markdown>>,
pub scroll_handle: ScrollHandle,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
div()
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
let keyboard_grace = Rc::clone(&self.keyboard_grace);
let mut d = div()
.id("info_popover")
.elevation_2(cx)
.overflow_y_scroll()
@@ -514,15 +555,17 @@ impl InfoPopover {
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(div().p_2().child(crate::render_parsed_markdown(
"content",
&self.parsed_content,
style,
workspace,
cx,
)))
.into_any_element()
.on_mouse_down(MouseButton::Left, move |_, cx| {
let mut keyboard_grace = keyboard_grace.borrow_mut();
*keyboard_grace = false;
cx.stop_propagation();
})
.p_2();
if let Some(markdown) = &self.parsed_content {
d = d.child(markdown.clone());
}
d.into_any_element()
}
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
@@ -593,8 +636,6 @@ impl DiagnosticPopover {
.when(window_is_transparent(cx), |this| {
this.bg(gpui::transparent_black())
})
.max_w(max_size.width)
.max_h(max_size.height)
.cursor(CursorStyle::PointingHand)
.tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
// Prevent a mouse move on the popover from being propagated to the editor,
@@ -608,6 +649,8 @@ impl DiagnosticPopover {
div()
.id("diagnostic-inner")
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.px_2()
.py_1()
.bg(diagnostic_colors.background)
@@ -642,17 +685,33 @@ mod tests {
InlayId, PointForPosition,
};
use collections::BTreeSet;
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use markdown::parser::MarkdownEvent;
use smol::stream::StreamExt;
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
use text::Bias;
use unindent::Unindent;
use util::test::marked_text_ranges;
impl InfoPopover {
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
let mut rendered_text = String::new();
if let Some(parsed_content) = self.parsed_content.clone() {
let markdown = parsed_content.read(cx);
let text = markdown.parsed_markdown().source().to_string();
let data = markdown.parsed_markdown().events();
let slice = data;
for (range, event) in slice.iter() {
if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
rendered_text.push_str(&text[range.clone()])
}
}
}
rendered_text
}
}
#[gpui::test]
async fn test_mouse_hover_info_popover_with_autocomplete_popover(
@@ -736,7 +795,7 @@ mod tests {
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -744,14 +803,13 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// check that the completion menu is still visible and that there still has only been 1 completion request
@@ -777,7 +835,7 @@ mod tests {
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
//verify the information popover is still visible and unchanged
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -785,14 +843,14 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// Mouse moved with no hover response dismisses
@@ -870,7 +928,7 @@ mod tests {
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -878,14 +936,14 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// Mouse moved with no hover response dismisses
@@ -931,34 +989,49 @@ mod tests {
let symbol_range = cx.lsp_range(indoc! {"
«fn» test() { println!(); }
"});
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some other basic docs".to_string(),
}),
range: Some(symbol_range),
}))
})
.next()
.await;
cx.editor(|editor, _cx| {
assert!(!editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
0,
"Expected no hovers but got but got: {:?}",
editor.hover_state.info_popovers
);
});
let mut requests =
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some other basic docs".to_string(),
}),
range: Some(symbol_range),
}))
});
requests.next().await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some other basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some other basic docs".to_string())
});
}
@@ -998,24 +1071,25 @@ mod tests {
})
.next()
.await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
.get_rendered_text(cx);
assert_eq!(
rendered.text,
rendered_text,
"regular text for hover to show".to_string(),
"No empty string hovers should be shown"
);
@@ -1063,24 +1137,25 @@ mod tests {
.next()
.await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
.get_rendered_text(cx);
assert_eq!(
rendered.text,
code_str.trim(),
rendered_text, code_str,
"Should not have extra line breaks at end of rendered hover"
);
});
@@ -1156,153 +1231,6 @@ mod tests {
});
}
#[gpui::test]
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
let editor = cx.add_window(|cx| Editor::single_line(cx));
editor
.update(cx, |editor, _cx| {
let style = editor.style.clone().unwrap();
struct Row {
blocks: Vec<HoverBlock>,
expected_marked_text: String,
expected_styles: Vec<HighlightStyle>,
}
let rows = &[
// Strong emphasis
Row {
blocks: vec![HoverBlock {
text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
}],
},
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](https://the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Lists
Row {
blocks: vec![HoverBlock {
text: "
lists:
* one
- a
- b
* two
- [c](https://the-url)
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
lists:
- one
- a
- b
- two
- «c»
- d"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Multi-paragraph list items
Row {
blocks: vec![HoverBlock {
text: "
* one two
three
* four five
* six seven
eight
nine
* ten
* six"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
- one two three
- four five
- six seven eight
nine
- ten
- six"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
];
for Row {
blocks,
expected_marked_text,
expected_styles,
} in &rows[0..]
{
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges
.into_iter()
.zip(expected_styles.iter().cloned())
.collect::<Vec<_>>();
assert_eq!(
rendered.text, expected_text,
"wrong text for input {blocks:?}"
);
let rendered_highlights: Vec<_> = rendered
.highlights
.iter()
.filter_map(|(range, highlight)| {
let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight))
})
.collect();
assert_eq!(
rendered_highlights, expected_highlights,
"wrong highlights for input {blocks:?}"
);
}
})
.unwrap();
}
#[gpui::test]
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
@@ -1546,9 +1474,8 @@ mod tests {
"Popover range should match the new type label part"
);
assert_eq!(
popover.parsed_content.text,
format!("A tooltip for `{new_type_label}`"),
"Rendered text should not anyhow alter backticks"
popover.get_rendered_text(cx),
format!("A tooltip for {new_type_label}"),
);
});
@@ -1602,7 +1529,7 @@ mod tests {
"Popover range should match the struct label part"
);
assert_eq!(
popover.parsed_content.text,
popover.get_rendered_text(cx),
format!("A tooltip for {struct_label}"),
"Rendered markdown element should remove backticks from text"
);

View File

@@ -371,7 +371,7 @@ async fn update_editor_from_message(
continue;
};
let buffer_id = BufferId::new(excerpt.buffer_id)?;
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
continue;
};

View File

@@ -49,13 +49,11 @@ fn display_ranges<'a>(
.pending
.as_ref()
.map(|pending| &pending.selection);
selections.disjoint.iter().chain(pending).map(move |s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
selections
.disjoint
.iter()
.chain(pending)
.map(move |s| s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map))
}
pub fn deploy_context_menu(

View File

@@ -0,0 +1,225 @@
mod popover;
mod state;
use crate::actions::ShowSignatureHelp;
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
use gpui::{AppContext, ViewContext};
use language::markdown::parse_markdown;
use multi_buffer::{Anchor, ToOffset};
use settings::Settings;
use std::ops::Range;
pub use popover::SignatureHelpPopover;
pub use state::SignatureHelpState;
// Language-specific settings may define quotes as "brackets", so filter them out separately.
const QUOTE_PAIRS: [(&'static str, &'static str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SignatureHelpHiddenBy {
AutoClose,
Escape,
Selection,
}
impl Editor {
pub fn toggle_auto_signature_help_menu(
&mut self,
_: &ToggleAutoSignatureHelp,
cx: &mut ViewContext<Self>,
) {
self.auto_signature_help = self
.auto_signature_help
.map(|auto_signature_help| !auto_signature_help)
.or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
match self.auto_signature_help {
Some(auto_signature_help) if auto_signature_help => {
self.show_signature_help(&ShowSignatureHelp, cx);
}
Some(_) => {
self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
}
None => {}
}
cx.notify();
}
pub(super) fn hide_signature_help(
&mut self,
cx: &mut ViewContext<Self>,
signature_help_hidden_by: SignatureHelpHiddenBy,
) -> bool {
if self.signature_help_state.is_shown() {
self.signature_help_state.kill_task();
self.signature_help_state.hide(signature_help_hidden_by);
cx.notify();
true
} else {
false
}
}
pub fn auto_signature_help_enabled(&self, cx: &AppContext) -> bool {
if let Some(auto_signature_help) = self.auto_signature_help {
auto_signature_help
} else {
EditorSettings::get_global(cx).auto_signature_help
}
}
pub(super) fn should_open_signature_help_automatically(
&mut self,
old_cursor_position: &Anchor,
backspace_pressed: bool,
cx: &mut ViewContext<Self>,
) -> bool {
if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
return false;
}
let newest_selection = self.selections.newest::<usize>(cx);
let head = newest_selection.head();
// There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace.
// If we dont exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this.
if !newest_selection.is_empty() && !backspace_pressed && head != newest_selection.tail() {
self.signature_help_state
.hide(SignatureHelpHiddenBy::Selection);
return false;
}
let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let bracket_range = |position: usize| match (position, position + 1) {
(0, b) if b <= buffer_snapshot.len() => 0..b,
(0, b) => 0..b - 1,
(a, b) if b <= buffer_snapshot.len() => a - 1..b,
(a, b) => a - 1..b - 1,
};
let not_quote_like_brackets = |start: Range<usize>, end: Range<usize>| {
let text = buffer_snapshot.text();
let (text_start, text_end) = (text.get(start), text.get(end));
QUOTE_PAIRS
.into_iter()
.all(|(start, end)| text_start != Some(start) && text_end != Some(end))
};
let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
let previous_brackets_range = bracket_range(previous_position);
let previous_brackets_surround = buffer_snapshot
.innermost_enclosing_bracket_ranges(
previous_brackets_range,
Some(&not_quote_like_brackets),
)
.filter(|(start_bracket_range, end_bracket_range)| {
start_bracket_range.start != previous_position
&& end_bracket_range.end != previous_position
});
let current_brackets_range = bracket_range(head);
let current_brackets_surround = buffer_snapshot
.innermost_enclosing_bracket_ranges(
current_brackets_range,
Some(&not_quote_like_brackets),
)
.filter(|(start_bracket_range, end_bracket_range)| {
start_bracket_range.start != head && end_bracket_range.end != head
});
match (previous_brackets_surround, current_brackets_surround) {
(None, None) => {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
false
}
(Some(_), None) => {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
false
}
(None, Some(_)) => true,
(Some(previous), Some(current)) => {
let condition = self.signature_help_state.hidden_by_selection()
|| previous != current
|| (previous == current && self.signature_help_state.is_shown());
if !condition {
self.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
}
condition
}
}
}
pub fn show_signature_help(&mut self, _: &ShowSignatureHelp, cx: &mut ViewContext<Self>) {
if self.pending_rename.is_some() {
return;
}
let position = self.selections.newest_anchor().head();
let Some((buffer, buffer_position)) =
self.buffer.read(cx).text_anchor_for_position(position, cx)
else {
return;
};
self.signature_help_state
.set_task(cx.spawn(move |editor, mut cx| async move {
let signature_help = editor
.update(&mut cx, |editor, cx| {
let language = editor.language_at(position, cx);
let project = editor.project.clone()?;
let (markdown, language_registry) = {
project.update(cx, |project, mut cx| {
let language_registry = project.languages().clone();
(
project.signature_help(&buffer, buffer_position, &mut cx),
language_registry,
)
})
};
Some((markdown, language_registry, language))
})
.ok()
.flatten();
let signature_help_popover = if let Some((
signature_help_task,
language_registry,
language,
)) = signature_help
{
// TODO allow multiple signature helps inside the same popover
if let Some(mut signature_help) = signature_help_task.await.into_iter().next() {
let mut parsed_content = parse_markdown(
signature_help.markdown.as_str(),
&language_registry,
language,
)
.await;
parsed_content
.highlights
.append(&mut signature_help.highlights);
Some(SignatureHelpPopover { parsed_content })
} else {
None
}
} else {
None
};
editor
.update(&mut cx, |editor, cx| {
let previous_popover = editor.signature_help_state.popover();
if previous_popover != signature_help_popover.as_ref() {
if let Some(signature_help_popover) = signature_help_popover {
editor
.signature_help_state
.set_popover(signature_help_popover);
} else {
editor
.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
}
cx.notify();
}
})
.ok();
}));
}
}

View File

@@ -0,0 +1,48 @@
use crate::{Editor, EditorStyle};
use gpui::{
div, AnyElement, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Size,
StatefulInteractiveElement, Styled, ViewContext, WeakView,
};
use language::ParsedMarkdown;
use ui::StyledExt;
use workspace::Workspace;
#[derive(Clone, Debug)]
pub struct SignatureHelpPopover {
pub parsed_content: ParsedMarkdown,
}
impl PartialEq for SignatureHelpPopover {
fn eq(&self, other: &Self) -> bool {
let str_equality = self.parsed_content.text.as_str() == other.parsed_content.text.as_str();
let highlight_equality = self.parsed_content.highlights == other.parsed_content.highlights;
str_equality && highlight_equality
}
}
impl SignatureHelpPopover {
pub fn render(
&mut self,
style: &EditorStyle,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
div()
.id("signature_help_popover")
.elevation_2(cx)
.overflow_y_scroll()
.max_w(max_size.width)
.max_h(max_size.height)
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(div().p_2().child(crate::render_parsed_markdown(
"signature_help_popover_content",
&self.parsed_content,
style,
workspace,
cx,
)))
.into_any_element()
}
}

View File

@@ -0,0 +1,65 @@
use crate::signature_help::popover::SignatureHelpPopover;
use crate::signature_help::SignatureHelpHiddenBy;
use gpui::Task;
#[derive(Default, Debug)]
pub struct SignatureHelpState {
task: Option<Task<()>>,
popover: Option<SignatureHelpPopover>,
hidden_by: Option<SignatureHelpHiddenBy>,
backspace_pressed: bool,
}
impl SignatureHelpState {
pub fn set_task(&mut self, task: Task<()>) {
self.task = Some(task);
self.hidden_by = None;
}
pub fn kill_task(&mut self) {
self.task = None;
}
pub fn popover(&self) -> Option<&SignatureHelpPopover> {
self.popover.as_ref()
}
pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
self.popover.as_mut()
}
pub fn backspace_pressed(&self) -> bool {
self.backspace_pressed
}
pub fn set_backspace_pressed(&mut self, backspace_pressed: bool) {
self.backspace_pressed = backspace_pressed;
}
pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
self.popover = Some(popover);
self.hidden_by = None;
}
pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
if self.hidden_by.is_none() {
self.popover = None;
self.hidden_by = Some(hidden_by);
}
}
pub fn hidden_by_selection(&self) -> bool {
self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
}
pub fn is_shown(&self) -> bool {
self.popover.is_some()
}
}
#[cfg(test)]
impl SignatureHelpState {
pub fn task(&self) -> Option<&Task<()>> {
self.task.as_ref()
}
}

View File

@@ -49,10 +49,9 @@ pub fn is_supported_wasm_api_version(
/// Returns the Wasm API version range that is supported by the Wasm host.
#[inline(always)]
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
let max_version = if release_channel == ReleaseChannel::Dev {
latest::MAX_VERSION
} else {
since_v0_0_6::MAX_VERSION
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_0_6::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version

View File

@@ -67,7 +67,10 @@ pub trait Fs: Send + Sync {
self.remove_file(path, options).await
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String>;
async fn load(&self, path: &Path) -> Result<String> {
Ok(String::from_utf8(self.load_bytes(path).await?)?)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
@@ -318,6 +321,11 @@ impl Fs for RealFs {
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
Ok(text)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
let path = path.to_path_buf();
let bytes = smol::unblock(|| std::fs::read(path)).await?;
Ok(bytes)
}
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
@@ -1433,6 +1441,10 @@ impl Fs for FakeFs {
Ok(String::from_utf8(content.clone())?)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
self.load_internal(path).await
}
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());

View File

@@ -14,3 +14,4 @@ futures.workspace = true
http.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true

View File

@@ -97,6 +97,7 @@ pub struct GenerateContentRequest {
pub contents: Vec<Content>,
pub generation_config: Option<GenerationConfig>,
pub safety_settings: Option<Vec<SafetySetting>>,
pub cached_content: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -267,3 +268,115 @@ pub struct CountTokensRequest {
pub struct CountTokensResponse {
pub total_tokens: usize,
}
pub async fn create_cached_content<T: HttpClient>(
client: &T,
api_url: &str,
api_key: &str,
request: CreateCachedContentRequest,
) -> Result<CreateCachedContentResponse> {
let uri = format!("{}/v1beta/cachedContents?key={}", api_url, api_key);
let request = serde_json::to_string(&request)?;
let mut response = client.post_json(&uri, request.into()).await?;
let mut text = String::new();
response.body_mut().read_to_string(&mut text).await?;
if response.status().is_success() {
Ok(serde_json::from_str::<CreateCachedContentResponse>(&text)?)
} else {
Err(anyhow!(
"error during createCachedContent, status code: {:?}, body: {}",
response.status(),
text
))
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateCachedContentRequest {
pub contents: Vec<Content>,
pub tools: Option<Vec<Tool>>,
pub ttl: Option<String>,
pub display_name: Option<String>,
pub model: String,
pub system_instruction: Option<Content>,
pub tool_config: Option<ToolConfig>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateCachedContentResponse {
pub name: String,
pub create_time: chrono::DateTime<chrono::Utc>,
pub update_time: chrono::DateTime<chrono::Utc>,
pub expire_time: Option<chrono::DateTime<chrono::Utc>>,
pub usage_metadata: UsageMetadata,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageMetadata {
pub total_token_count: u32,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub function_declarations: Vec<FunctionDeclaration>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FunctionDeclaration {
pub name: String,
pub description: String,
pub parameters: Schema,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
#[serde(rename = "type")]
pub schema_type: SchemaType,
pub format: Option<String>,
pub description: Option<String>,
pub nullable: Option<bool>,
pub enum_values: Option<Vec<String>>,
pub properties: Option<std::collections::HashMap<String, Box<Schema>>>,
pub required: Option<Vec<String>>,
pub items: Option<Box<Schema>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum SchemaType {
TypeUnspecified,
String,
Number,
Integer,
Boolean,
Array,
Object,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolConfig {
pub function_calling_config: Option<FunctionCallingConfig>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FunctionCallingConfig {
pub mode: Option<FunctionCallingMode>,
pub allowed_function_names: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FunctionCallingMode {
ModeUnspecified,
Auto,
Any,
None,
}

View File

@@ -134,18 +134,22 @@ x11rb = { version = "0.13.0", features = [
"resource_manager",
"sync",
] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "2d4c4439160c7846ede0f0ece93bf73b1e613339", features = [
"wayland",
"x11",
] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
"x11rb-xcb",
"x11rb-client",
] }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = [
"source-fontconfig-dlopen",
] }
x11-clipboard = "0.9.2"
[target.'cfg(windows)'.dependencies]
windows.workspace = true
windows-core = "0.57"
clipboard-win = "3.1.1"
[[example]]
name = "hello_world"

View File

@@ -193,6 +193,16 @@ impl TextInput {
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.content.len())
}
fn reset(&mut self) {
self.content = "".into();
self.selected_range = 0..0;
self.selection_reversed = false;
self.marked_range = None;
self.last_layout = None;
self.last_bounds = None;
self.is_selecting = false;
}
}
impl ViewInputHandler for TextInput {
@@ -494,13 +504,47 @@ struct InputExample {
recent_keystrokes: Vec<Keystroke>,
}
impl InputExample {
fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
self.recent_keystrokes.clear();
self.text_input
.update(cx, |text_input, _cx| text_input.reset());
cx.notify();
}
}
impl Render for InputExample {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let num_keystrokes = self.recent_keystrokes.len();
div()
.bg(rgb(0xaaaaaa))
.flex()
.flex_col()
.size_full()
.child(
div()
.bg(white())
.border_b_1()
.border_color(black())
.flex()
.flex_row()
.justify_between()
.child(format!("Keystrokes: {}", num_keystrokes))
.child(
div()
.border_1()
.border_color(black())
.px_2()
.bg(yellow())
.child("Reset")
.hover(|style| {
style
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
.cursor_pointer()
})
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
),
)
.child(self.text_input.clone())
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(

View File

@@ -406,6 +406,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
#[cfg(target_os = "windows")]
fn destroy(&self);
}
#[derive(PartialEq, Eq, Hash, Clone)]

View File

@@ -177,6 +177,9 @@ impl PlatformTextSystem for CosmicTextSystem {
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
self.0.write().layout_line(text, font_size, runs)
}
#[cfg(target_os = "windows")]
fn destroy(&self) {}
}
impl CosmicTextSystemState {

View File

@@ -24,7 +24,7 @@ use x11rb::xcb_ffi::XCBConnection;
use xim::{x11rb::X11rbClient, Client};
use xim::{AttributeName, InputStyle};
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb as xkbc;
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask};
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
@@ -94,6 +94,13 @@ impl From<xim::ClientError> for EventHandlerError {
}
}
#[derive(Debug, Default, Clone)]
struct XKBStateNotiy {
depressed_layout: LayoutIndex,
latched_layout: LayoutIndex,
locked_layout: LayoutIndex,
}
pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -113,6 +120,7 @@ pub struct X11ClientState {
pub(crate) mouse_focused_window: Option<xproto::Window>,
pub(crate) keyboard_focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State,
previous_xkb_state: XKBStateNotiy,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
@@ -350,6 +358,7 @@ impl X11Client {
mouse_focused_window: None,
keyboard_focused_window: None,
xkb: xkb_state,
previous_xkb_state: XKBStateNotiy::default(),
ximc,
xim_handler,
@@ -622,7 +631,11 @@ impl X11Client {
event.latched_group as u32,
event.locked_group.into(),
);
state.previous_xkb_state = XKBStateNotiy {
depressed_layout: event.base_group as u32,
latched_layout: event.latched_group as u32,
locked_layout: event.locked_group.into(),
};
let modifiers = Modifiers::from_xkb(&state.xkb);
if state.modifiers == modifiers {
drop(state);
@@ -644,11 +657,18 @@ impl X11Client {
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
state.pre_ime_key_down.take();
let keystroke = {
let code = event.detail.into();
let xkb_state = state.previous_xkb_state.clone();
state.xkb.update_mask(
event.state.bits() as ModMask,
0,
0,
xkb_state.depressed_layout,
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkbc::KeyDirection::Down);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -707,8 +727,16 @@ impl X11Client {
let keystroke = {
let code = event.detail.into();
let xkb_state = state.previous_xkb_state.clone();
state.xkb.update_mask(
event.state.bits() as ModMask,
0,
0,
xkb_state.depressed_layout,
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkbc::KeyDirection::Up);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -1169,10 +1197,13 @@ impl LinuxClient for X11Client {
let cursor = match state.cursor_cache.get(&style) {
Some(cursor) => *cursor,
None => {
let cursor = state
let Some(cursor) = state
.cursor_handle
.load_cursor(&state.xcb_connection, &style.to_icon_name())
.expect("failed to load cursor");
.log_err()
else {
return;
};
state.cursor_cache.insert(style, cursor);
cursor
}

View File

@@ -797,7 +797,7 @@ impl Platform for MacPlatform {
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, sync::Arc};
use std::{borrow::Cow, mem::ManuallyDrop, sync::Arc};
use ::util::ResultExt;
use anyhow::{anyhow, Result};
@@ -39,7 +39,7 @@ pub(crate) struct DirectWriteTextSystem(RwLock<DirectWriteState>);
struct DirectWriteComponent {
locale: String,
factory: IDWriteFactory5,
bitmap_factory: IWICImagingFactory2,
bitmap_factory: ManuallyDrop<IWICImagingFactory2>,
d2d1_factory: ID2D1Factory,
in_memory_loader: IDWriteInMemoryFontFileLoader,
builder: IDWriteFontSetBuilder1,
@@ -79,6 +79,7 @@ impl DirectWriteComponent {
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
let bitmap_factory: IWICImagingFactory2 =
CoCreateInstance(&CLSID_WICImagingFactory2, None, CLSCTX_INPROC_SERVER)?;
let bitmap_factory = ManuallyDrop::new(bitmap_factory);
let d2d1_factory: ID2D1Factory =
D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?;
// The `IDWriteInMemoryFontFileLoader` here is supported starting from
@@ -238,6 +239,11 @@ impl PlatformTextSystem for DirectWriteTextSystem {
..Default::default()
})
}
fn destroy(&self) {
let mut lock = self.0.write();
unsafe { ManuallyDrop::drop(&mut lock.components.bitmap_factory) };
}
}
impl DirectWriteState {
@@ -583,6 +589,18 @@ impl DirectWriteState {
DWRITE_MEASURING_MODE_NATURAL,
)?
};
// todo(windows)
// This is a walkaround, deleted when figured out.
let y_offset;
let extra_height;
if params.is_emoji {
y_offset = 0;
extra_height = 0;
} else {
// make some room for scaler.
y_offset = -1;
extra_height = 2;
}
if bounds.right < bounds.left {
Ok(Bounds {
@@ -593,11 +611,13 @@ impl DirectWriteState {
Ok(Bounds {
origin: point(
((bounds.left * params.scale_factor).ceil() as i32).into(),
((bounds.top * params.scale_factor).ceil() as i32).into(),
((bounds.top * params.scale_factor).ceil() as i32 + y_offset).into(),
),
size: size(
(((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(),
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32).into(),
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32
+ extra_height)
.into(),
),
})
}

View File

@@ -82,7 +82,7 @@ pub(crate) fn handle_msg(
WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
WM_SETCURSOR => handle_set_cursor(lparam, state_ptr),
WM_SETTINGCHANGE => handle_system_settings_changed(state_ptr),
WM_SETTINGCHANGE => handle_system_settings_changed(handle, state_ptr),
CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
_ => None,
};
@@ -732,7 +732,10 @@ fn handle_dpi_changed_msg(
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let new_dpi = wparam.loword() as f32;
state_ptr.state.borrow_mut().scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
let mut lock = state_ptr.state.borrow_mut();
lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
lock.border_offset.udpate(handle).log_err();
drop(lock);
let rect = unsafe { &*(lparam.0 as *const RECT) };
let width = rect.right - rect.left;
@@ -801,6 +804,9 @@ fn handle_hit_test_msg(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if !state_ptr.is_movable {
return None;
}
if !state_ptr.hide_title_bar {
return None;
}
@@ -1047,12 +1053,17 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
Some(1)
}
fn handle_system_settings_changed(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
fn handle_system_settings_changed(
handle: HWND,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
// mouse wheel
lock.system_settings.mouse_wheel_settings.update();
// mouse double click
lock.click_state.system_update();
// window border offset
lock.border_offset.udpate(handle).log_err();
Some(0)
}

View File

@@ -12,7 +12,6 @@ use std::{
use ::util::ResultExt;
use anyhow::{anyhow, Context, Result};
use clipboard_win::{get_clipboard_string, set_clipboard_string};
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
use parking_lot::RwLock;
@@ -22,9 +21,22 @@ use windows::{
core::*,
Win32::{
Foundation::*,
Globalization::u_memcpy,
Graphics::Gdi::*,
Security::Credentials::*,
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
System::{
Com::*,
DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard,
RegisterClipboardFormatW, SetClipboardData,
},
LibraryLoader::*,
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
Ole::*,
SystemInformation::*,
Threading::*,
Time::*,
},
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
},
UI::ViewManagement::UISettings,
@@ -40,6 +52,8 @@ pub(crate) struct WindowsPlatform {
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
clipboard_hash_format: u32,
clipboard_metadata_format: u32,
}
pub(crate) struct WindowsPlatformState {
@@ -88,6 +102,9 @@ impl WindowsPlatform {
let icon = load_icon().unwrap_or_default();
let state = RefCell::new(WindowsPlatformState::new());
let raw_window_handles = RwLock::new(SmallVec::new());
let clipboard_hash_format = register_clipboard_format(CLIPBOARD_HASH_FORMAT).unwrap();
let clipboard_metadata_format =
register_clipboard_format(CLIPBOARD_METADATA_FORMAT).unwrap();
Self {
state,
@@ -96,6 +113,8 @@ impl WindowsPlatform {
background_executor,
foreground_executor,
text_system,
clipboard_hash_format,
clipboard_metadata_format,
}
}
@@ -288,7 +307,7 @@ impl Platform for WindowsPlatform {
self.icon,
self.foreground_executor.clone(),
lock.current_cursor,
);
)?;
drop(lock);
let handle = window.get_raw_handle();
self.raw_window_handles.write().push(handle);
@@ -498,17 +517,15 @@ impl Platform for WindowsPlatform {
}
fn write_to_clipboard(&self, item: ClipboardItem) {
if item.text.len() > 0 {
set_clipboard_string(item.text()).unwrap();
}
write_to_clipboard(
item,
self.clipboard_hash_format,
self.clipboard_metadata_format,
);
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let text = get_clipboard_string().ok()?;
Some(ClipboardItem {
text,
metadata: None,
})
read_from_clipboard(self.clipboard_hash_format, self.clipboard_metadata_format)
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
@@ -586,9 +603,8 @@ impl Platform for WindowsPlatform {
impl Drop for WindowsPlatform {
fn drop(&mut self) {
unsafe {
OleUninitialize();
}
self.text_system.destroy();
unsafe { OleUninitialize() };
}
}
@@ -680,3 +696,133 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
let ui_settings = UISettings::new()?;
Ok(ui_settings.AutoHideScrollBars()?)
}
fn register_clipboard_format(format: PCWSTR) -> Result<u32> {
let ret = unsafe { RegisterClipboardFormatW(format) };
if ret == 0 {
Err(anyhow::anyhow!(
"Error when registering clipboard format: {}",
std::io::Error::last_os_error()
))
} else {
Ok(ret)
}
}
fn write_to_clipboard(item: ClipboardItem, hash_format: u32, metadata_format: u32) {
write_to_clipboard_inner(item, hash_format, metadata_format).log_err();
unsafe { CloseClipboard().log_err() };
}
fn write_to_clipboard_inner(
item: ClipboardItem,
hash_format: u32,
metadata_format: u32,
) -> Result<()> {
unsafe {
OpenClipboard(None)?;
EmptyClipboard()?;
let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
if let Some(ref metadata) = item.metadata {
let hash_result = {
let hash = ClipboardItem::text_hash(&item.text);
hash.to_ne_bytes()
};
let encode_wide = std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4);
set_data_to_clipboard(encode_wide, hash_format)?;
let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
set_data_to_clipboard(&metadata_wide, metadata_format)?;
}
}
Ok(())
}
fn set_data_to_clipboard(data: &[u16], format: u32) -> Result<()> {
unsafe {
let global = GlobalAlloc(GMEM_MOVEABLE, data.len() * 2)?;
let handle = GlobalLock(global);
u_memcpy(handle as _, data.as_ptr(), data.len() as _);
let _ = GlobalUnlock(global);
SetClipboardData(format, HANDLE(global.0 as isize))?;
}
Ok(())
}
fn read_from_clipboard(hash_format: u32, metadata_format: u32) -> Option<ClipboardItem> {
let result = read_from_clipboard_inner(hash_format, metadata_format).log_err();
unsafe { CloseClipboard().log_err() };
result
}
fn read_from_clipboard_inner(hash_format: u32, metadata_format: u32) -> Result<ClipboardItem> {
unsafe {
OpenClipboard(None)?;
let text = {
let handle = GetClipboardData(CF_UNICODETEXT.0 as u32)?;
let text = PCWSTR(handle.0 as *const u16);
String::from_utf16_lossy(text.as_wide())
};
let mut item = ClipboardItem {
text,
metadata: None,
};
let Some(hash) = read_hash_from_clipboard(hash_format) else {
return Ok(item);
};
let Some(metadata) = read_metadata_from_clipboard(metadata_format) else {
return Ok(item);
};
if hash == ClipboardItem::text_hash(&item.text) {
item.metadata = Some(metadata);
}
Ok(item)
}
}
fn read_hash_from_clipboard(hash_format: u32) -> Option<u64> {
unsafe {
let handle = GetClipboardData(hash_format).log_err()?;
let raw_ptr = handle.0 as *const u16;
let hash_bytes: [u8; 8] = std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
.to_vec()
.try_into()
.log_err()?;
Some(u64::from_ne_bytes(hash_bytes))
}
}
fn read_metadata_from_clipboard(metadata_format: u32) -> Option<String> {
unsafe {
let handle = GetClipboardData(metadata_format).log_err()?;
let text = PCWSTR(handle.0 as *const u16);
Some(String::from_utf16_lossy(text.as_wide()))
}
}
// clipboard
pub const CLIPBOARD_HASH_FORMAT: PCWSTR = windows::core::w!("zed-text-hash");
pub const CLIPBOARD_METADATA_FORMAT: PCWSTR = windows::core::w!("zed-metadata");
#[cfg(test)]
mod tests {
use crate::{ClipboardItem, Platform, WindowsPlatform};
#[test]
fn test_clipboard() {
let platform = WindowsPlatform::new();
let item = ClipboardItem::new("你好".to_string());
platform.write_to_clipboard(item.clone());
assert_eq!(platform.read_from_clipboard(), Some(item));
let item = ClipboardItem::new("12345".to_string());
platform.write_to_clipboard(item.clone());
assert_eq!(platform.read_from_clipboard(), Some(item));
let item = ClipboardItem::new("abcdef".to_string()).with_metadata(vec![3, 4]);
platform.write_to_clipboard(item.clone());
assert_eq!(platform.read_from_clipboard(), Some(item));
}
}

View File

@@ -11,7 +11,7 @@ use std::{
};
use ::util::ResultExt;
use anyhow::Context;
use anyhow::{Context, Result};
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
use raw_window_handle as rwh;
@@ -35,6 +35,7 @@ pub struct WindowsWindowState {
pub origin: Point<Pixels>,
pub logical_size: Size<Pixels>,
pub fullscreen_restore_bounds: Bounds<Pixels>,
pub border_offset: WindowBorderOffset,
pub scale_factor: f32,
pub callbacks: Callbacks,
@@ -57,6 +58,7 @@ pub(crate) struct WindowsWindowStatePtr {
pub(crate) state: RefCell<WindowsWindowState>,
pub(crate) handle: AnyWindowHandle,
pub(crate) hide_title_bar: bool,
pub(crate) is_movable: bool,
pub(crate) executor: ForegroundExecutor,
}
@@ -67,7 +69,7 @@ impl WindowsWindowState {
cs: &CREATESTRUCTW,
current_cursor: HCURSOR,
display: WindowsDisplay,
) -> Self {
) -> Result<Self> {
let scale_factor = {
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32
@@ -81,7 +83,8 @@ impl WindowsWindowState {
origin,
size: logical_size,
};
let renderer = windows_renderer::windows_renderer(hwnd, transparent);
let border_offset = WindowBorderOffset::default();
let renderer = windows_renderer::windows_renderer(hwnd, transparent)?;
let callbacks = Callbacks::default();
let input_handler = None;
let click_state = ClickState::new();
@@ -89,10 +92,11 @@ impl WindowsWindowState {
let nc_button_pressed = None;
let fullscreen = None;
Self {
Ok(Self {
origin,
logical_size,
fullscreen_restore_bounds,
border_offset,
scale_factor,
callbacks,
input_handler,
@@ -104,7 +108,7 @@ impl WindowsWindowState {
display,
fullscreen,
hwnd,
}
})
}
#[inline]
@@ -123,7 +127,8 @@ impl WindowsWindowState {
}
}
fn window_bounds(&self) -> WindowBounds {
// Calculate the bounds used for saving and whether the window is maximized.
fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) {
let placement = unsafe {
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
@@ -132,22 +137,22 @@ impl WindowsWindowState {
GetWindowPlacement(self.hwnd, &mut placement).log_err();
placement
};
let physical_size = size(
DevicePixels(placement.rcNormalPosition.right - placement.rcNormalPosition.left),
DevicePixels(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top),
);
let bounds = Bounds {
origin: logical_point(
placement.rcNormalPosition.left as f32,
placement.rcNormalPosition.top as f32,
(
calculate_client_rect(
placement.rcNormalPosition,
self.border_offset,
self.scale_factor,
),
size: physical_size.to_pixels(self.scale_factor),
};
placement.showCmd == SW_SHOWMAXIMIZED.0 as u32,
)
}
fn window_bounds(&self) -> WindowBounds {
let (bounds, maximized) = self.calculate_window_bounds();
if self.is_fullscreen() {
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
} else if placement.showCmd == SW_SHOWMAXIMIZED.0 as u32 {
} else if maximized {
WindowBounds::Maximized(bounds)
} else {
WindowBounds::Windowed(bounds)
@@ -198,22 +203,23 @@ impl WindowsWindowState {
}
impl WindowsWindowStatePtr {
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Rc<Self> {
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
let state = RefCell::new(WindowsWindowState::new(
hwnd,
context.transparent,
cs,
context.current_cursor,
context.display,
));
)?);
Rc::new(Self {
Ok(Rc::new(Self {
state,
hwnd,
handle: context.handle,
hide_title_bar: context.hide_title_bar,
is_movable: context.is_movable,
executor: context.executor.clone(),
})
}))
}
}
@@ -230,11 +236,12 @@ pub(crate) struct Callbacks {
}
struct WindowCreateContext {
inner: Option<Rc<WindowsWindowStatePtr>>,
inner: Option<Result<Rc<WindowsWindowStatePtr>>>,
handle: AnyWindowHandle,
hide_title_bar: bool,
display: WindowsDisplay,
transparent: bool,
is_movable: bool,
executor: ForegroundExecutor,
current_cursor: HCURSOR,
}
@@ -246,7 +253,7 @@ impl WindowsWindow {
icon: HICON,
executor: ForegroundExecutor,
current_cursor: HCURSOR,
) -> Self {
) -> Result<Self> {
let classname = register_wnd_class(icon);
let hide_title_bar = params
.titlebar
@@ -261,7 +268,14 @@ impl WindowsWindow {
.map(|title| title.as_ref())
.unwrap_or(""),
);
let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
let (dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
} else {
(
WS_EX_APPWINDOW,
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
)
};
let hinstance = get_module_handle();
let display = if let Some(display_id) = params.display_id {
// if we obtain a display_id, then this ID must be valid.
@@ -275,13 +289,14 @@ impl WindowsWindow {
hide_title_bar,
display,
transparent: true,
is_movable: params.is_movable,
executor,
current_cursor,
};
let lpparam = Some(&context as *const _ as *const _);
let raw_hwnd = unsafe {
CreateWindowExW(
WS_EX_APPWINDOW,
dwexstyle,
classname,
&windowname,
dwstyle,
@@ -295,32 +310,31 @@ impl WindowsWindow {
lpparam,
)
};
let state_ptr = Rc::clone(context.inner.as_ref().unwrap());
register_drag_drop(state_ptr.clone());
let wnd = Self(state_ptr);
let state_ptr = context.inner.take().unwrap()?;
register_drag_drop(state_ptr.clone())?;
unsafe {
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
..Default::default()
};
GetWindowPlacement(raw_hwnd, &mut placement).log_err();
GetWindowPlacement(raw_hwnd, &mut placement)?;
// the bounds may be not inside the display
let bounds = if display.check_given_bounds(params.bounds) {
params.bounds
} else {
display.default_bounds()
};
let bounds = bounds.to_device_pixels(wnd.0.state.borrow().scale_factor);
placement.rcNormalPosition.left = bounds.left().0;
placement.rcNormalPosition.right = bounds.right().0;
placement.rcNormalPosition.top = bounds.top().0;
placement.rcNormalPosition.bottom = bounds.bottom().0;
SetWindowPlacement(raw_hwnd, &placement).log_err();
let mut lock = state_ptr.state.borrow_mut();
let bounds = bounds.to_device_pixels(lock.scale_factor);
lock.border_offset.udpate(raw_hwnd)?;
placement.rcNormalPosition = calcualte_window_rect(bounds, lock.border_offset);
drop(lock);
SetWindowPlacement(raw_hwnd, &placement)?;
}
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok().log_err() };
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok()? };
wnd
Ok(Self(state_ptr))
}
}
@@ -536,10 +550,6 @@ impl PlatformWindow for WindowsWindow {
.executor
.spawn(async move {
let mut lock = state_ptr.state.borrow_mut();
lock.fullscreen_restore_bounds = Bounds {
origin: lock.origin,
size: lock.logical_size,
};
let StyleAndBounds {
style,
x,
@@ -549,6 +559,8 @@ impl PlatformWindow for WindowsWindow {
} = if let Some(state) = lock.fullscreen.take() {
state
} else {
let (window_bounds, _) = lock.calculate_window_bounds();
lock.fullscreen_restore_bounds = window_bounds;
let style =
WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _);
let mut rc = RECT::default();
@@ -853,6 +865,32 @@ struct StyleAndBounds {
cy: i32,
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct WindowBorderOffset {
width_offset: i32,
height_offset: i32,
}
impl WindowBorderOffset {
pub(crate) fn udpate(&mut self, hwnd: HWND) -> anyhow::Result<()> {
let window_rect = unsafe {
let mut rect = std::mem::zeroed();
GetWindowRect(hwnd, &mut rect)?;
rect
};
let client_rect = unsafe {
let mut rect = std::mem::zeroed();
GetClientRect(hwnd, &mut rect)?;
rect
};
self.width_offset =
(window_rect.right - window_rect.left) - (client_rect.right - client_rect.left);
self.height_offset =
(window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top);
Ok(())
}
}
fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
const CLASS_NAME: PCWSTR = w!("Zed::Window");
@@ -883,10 +921,14 @@ unsafe extern "system" fn wnd_proc(
let cs = unsafe { &*cs };
let ctx = cs.lpCreateParams as *mut WindowCreateContext;
let ctx = unsafe { &mut *ctx };
let state_ptr = WindowsWindowStatePtr::new(ctx, hwnd, cs);
let weak = Box::new(Rc::downgrade(&state_ptr));
let creation_result = WindowsWindowStatePtr::new(ctx, hwnd, cs);
if creation_result.is_err() {
ctx.inner = Some(creation_result);
return LRESULT(0);
}
let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap()));
unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
ctx.inner = Some(state_ptr);
ctx.inner = Some(creation_result);
return LRESULT(1);
}
let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowStatePtr>;
@@ -934,7 +976,7 @@ fn get_module_handle() -> HMODULE {
}
}
fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) {
fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) -> Result<()> {
let window_handle = state_ptr.hwnd;
let handler = WindowsDragDropHandler(state_ptr);
// The lifetime of `IDropTarget` is handled by Windows, it wont release untill
@@ -943,8 +985,54 @@ fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) {
let drag_drop_handler: IDropTarget = handler.into();
unsafe {
RegisterDragDrop(window_handle, &drag_drop_handler)
.expect("unable to register drag-drop event")
.context("unable to register drag-drop event")?;
}
Ok(())
}
fn calcualte_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBorderOffset) -> RECT {
// NOTE:
// The reason that not using `AdjustWindowRectEx()` here is
// that the size reported by this function is incorrect.
// You can test it, and there are similar discussions online.
// See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi
//
// So we manually calculate these values here.
let mut rect = RECT {
left: bounds.left().0,
top: bounds.top().0,
right: bounds.right().0,
bottom: bounds.bottom().0,
};
let left_offset = border_offset.width_offset / 2;
let top_offset = border_offset.height_offset / 2;
let right_offset = border_offset.width_offset - left_offset;
let bottom_offet = border_offset.height_offset - top_offset;
rect.left -= left_offset;
rect.top -= top_offset;
rect.right += right_offset;
rect.bottom += bottom_offet;
rect
}
fn calculate_client_rect(
rect: RECT,
border_offset: WindowBorderOffset,
scale_factor: f32,
) -> Bounds<Pixels> {
let left_offset = border_offset.width_offset / 2;
let top_offset = border_offset.height_offset / 2;
let right_offset = border_offset.width_offset - left_offset;
let bottom_offet = border_offset.height_offset - top_offset;
let left = rect.left + left_offset;
let top = rect.top + top_offset;
let right = rect.right - right_offset;
let bottom = rect.bottom - bottom_offet;
let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top));
Bounds {
origin: logical_point(left as f32, top as f32, scale_factor),
size: physical_size.to_pixels(scale_factor),
}
}
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
@@ -962,7 +1050,7 @@ mod windows_renderer {
platform::blade::{BladeRenderer, BladeSurfaceConfig},
};
pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> BladeRenderer {
pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> anyhow::Result<BladeRenderer> {
let raw = RawWindow { hwnd: hwnd.0 };
let gpu: Arc<gpu::Context> = Arc::new(
unsafe {
@@ -975,14 +1063,14 @@ mod windows_renderer {
},
)
}
.unwrap(),
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
);
let config = BladeSurfaceConfig {
size: gpu::Extent::default(),
transparent,
};
BladeRenderer::new(gpu, config)
Ok(BladeRenderer::new(gpu, config))
}
struct RawWindow {

View File

@@ -212,6 +212,8 @@ impl Default for TextStyle {
// todo(linux) make this configurable or choose better default
font_family: if cfg!(target_os = "linux") {
"FreeMono".into()
} else if cfg!(target_os = "windows") {
"Segoe UI".into()
} else {
"Helvetica".into()
},

View File

@@ -190,18 +190,10 @@ pub trait Styled: Sized {
self
}
/// Sets the element to justify flex items along the container's main axis
/// such that there is an equal amount of space between each item.
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
fn justify_between(mut self) -> Self {
self.style().justify_content = Some(JustifyContent::SpaceBetween);
self
}
/// Sets the element to justify flex items along the center of the container's main axis.
/// [Docs](https://tailwindcss.com/docs/justify-content#center)
fn justify_center(mut self) -> Self {
self.style().justify_content = Some(JustifyContent::Center);
/// Sets the element to align flex items along the baseline of the container's cross axis.
/// [Docs](https://tailwindcss.com/docs/align-items#baseline)
fn items_baseline(mut self) -> Self {
self.style().align_items = Some(AlignItems::Baseline);
self
}
@@ -219,6 +211,21 @@ pub trait Styled: Sized {
self
}
/// Sets the element to justify flex items along the center of the container's main axis.
/// [Docs](https://tailwindcss.com/docs/justify-content#center)
fn justify_center(mut self) -> Self {
self.style().justify_content = Some(JustifyContent::Center);
self
}
/// Sets the element to justify flex items along the container's main axis
/// such that there is an equal amount of space between each item.
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
fn justify_between(mut self) -> Self {
self.style().justify_content = Some(JustifyContent::SpaceBetween);
self
}
/// Sets the element to justify items along the container's main axis such
/// that there is an equal amount of space on each side of each item.
/// [Docs](https://tailwindcss.com/docs/justify-content#space-around)

View File

@@ -66,6 +66,7 @@ impl TextSystem {
// We should allow GPUI users to provide their own fallback font stack.
font("Zed Plex Mono"),
font("Helvetica"),
font("Segoe UI"), // Windows
font("Cantarell"), // Gnome
font("Ubuntu"), // Gnome (Ubuntu)
font("Noto Sans"), // KDE

View File

@@ -109,7 +109,13 @@ fn paint_line(
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext,
) -> Result<()> {
let line_bounds = Bounds::new(origin, size(layout.width, line_height));
let line_bounds = Bounds::new(
origin,
size(
layout.width,
line_height * (wrap_boundaries.len() as f32 + 1.),
),
);
cx.paint_layer(line_bounds, |cx| {
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
let baseline_offset = point(px(0.), padding_top + layout.ascent);

View File

@@ -1,6 +1,7 @@
mod item;
mod to_markdown;
use futures::future::BoxFuture;
pub use item::*;
pub use to_markdown::convert_rustdoc_to_markdown;
@@ -11,7 +12,7 @@ use anyhow::{bail, Context, Result};
use async_trait::async_trait;
use collections::{HashSet, VecDeque};
use fs::Fs;
use futures::AsyncReadExt;
use futures::{AsyncReadExt, FutureExt};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
@@ -23,124 +24,16 @@ struct RustdocItemWithHistory {
pub history: Vec<String>,
}
#[async_trait]
pub trait RustdocProvider {
async fn fetch_page(
&self,
package: &PackageName,
item: Option<&RustdocItem>,
) -> Result<Option<String>>;
}
pub struct RustdocIndexer {
provider: Box<dyn RustdocProvider + Send + Sync + 'static>,
}
impl RustdocIndexer {
pub fn new(provider: Box<dyn RustdocProvider + Send + Sync + 'static>) -> Self {
Self { provider }
}
}
#[async_trait]
impl IndexedDocsProvider for RustdocIndexer {
fn id(&self) -> ProviderId {
ProviderId::rustdoc()
}
fn database_path(&self) -> PathBuf {
paths::support_dir().join("docs/rust/rustdoc-db.1.mdb")
}
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
let Some(package_root_content) = self.provider.fetch_page(&package, None).await? else {
return Ok(());
};
let (crate_root_markdown, items) =
convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
database
.insert(package.to_string(), crate_root_markdown)
.await?;
let mut seen_items = HashSet::from_iter(items.clone());
let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
item,
#[cfg(debug_assertions)]
history: Vec::new(),
}));
while let Some(item_with_history) = items_to_visit.pop_front() {
let item = &item_with_history.item;
let Some(result) = self
.provider
.fetch_page(&package, Some(&item))
.await
.with_context(|| {
#[cfg(debug_assertions)]
{
format!(
"failed to fetch {item:?}: {history:?}",
history = item_with_history.history
)
}
#[cfg(not(debug_assertions))]
{
format!("failed to fetch {item:?}")
}
})?
else {
continue;
};
let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
database
.insert(format!("{package}::{}", item.display()), markdown)
.await?;
let parent_item = item;
for mut item in referenced_items {
if seen_items.contains(&item) {
continue;
}
seen_items.insert(item.clone());
item.path.extend(parent_item.path.clone());
match parent_item.kind {
RustdocItemKind::Mod => {
item.path.push(parent_item.name.clone());
}
_ => {}
}
items_to_visit.push_back(RustdocItemWithHistory {
#[cfg(debug_assertions)]
history: {
let mut history = item_with_history.history.clone();
history.push(item.url_path());
history
},
item,
});
}
}
Ok(())
}
}
pub struct LocalProvider {
pub struct LocalRustdocProvider {
fs: Arc<dyn Fs>,
cargo_workspace_root: PathBuf,
}
impl LocalProvider {
impl LocalRustdocProvider {
pub fn id() -> ProviderId {
ProviderId("rustdoc".into())
}
pub fn new(fs: Arc<dyn Fs>, cargo_workspace_root: PathBuf) -> Self {
Self {
fs,
@@ -150,25 +43,53 @@ impl LocalProvider {
}
#[async_trait]
impl RustdocProvider for LocalProvider {
async fn fetch_page(
&self,
crate_name: &PackageName,
item: Option<&RustdocItem>,
) -> Result<Option<String>> {
let mut local_cargo_doc_path = self.cargo_workspace_root.join("target/doc");
local_cargo_doc_path.push(crate_name.as_ref());
if let Some(item) = item {
local_cargo_doc_path.push(item.url_path());
} else {
local_cargo_doc_path.push("index.html");
}
impl IndexedDocsProvider for LocalRustdocProvider {
fn id(&self) -> ProviderId {
Self::id()
}
let Ok(contents) = self.fs.load(&local_cargo_doc_path).await else {
return Ok(None);
};
fn database_path(&self) -> PathBuf {
paths::support_dir().join("docs/rust/rustdoc-db.1.mdb")
}
Ok(Some(contents))
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
index_rustdoc(package, database, {
move |crate_name, item| {
let fs = self.fs.clone();
let cargo_workspace_root = self.cargo_workspace_root.clone();
let crate_name = crate_name.clone();
let item = item.cloned();
async move {
let target_doc_path = cargo_workspace_root.join("target/doc");
let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref());
if !fs.is_dir(&local_cargo_doc_path).await {
let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await;
if cargo_doc_exists_at_all {
bail!(
"no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`"
);
} else {
bail!("no cargo doc directory. run `cargo doc`");
}
}
if let Some(item) = item {
local_cargo_doc_path.push(item.url_path());
} else {
local_cargo_doc_path.push("index.html");
}
let Ok(contents) = fs.load(&local_cargo_doc_path).await else {
return Ok(None);
};
Ok(Some(contents))
}
.boxed()
}
})
.await
}
}
@@ -177,50 +98,152 @@ pub struct DocsDotRsProvider {
}
impl DocsDotRsProvider {
pub fn id() -> ProviderId {
ProviderId("docs-rs".into())
}
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
Self { http_client }
}
}
#[async_trait]
impl RustdocProvider for DocsDotRsProvider {
async fn fetch_page(
&self,
crate_name: &PackageName,
item: Option<&RustdocItem>,
) -> Result<Option<String>> {
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}{item_path}",
item_path = item
.map(|item| format!("/{}", item.url_path()))
.unwrap_or_default()
);
impl IndexedDocsProvider for DocsDotRsProvider {
fn id(&self) -> ProviderId {
Self::id()
}
let mut response = self
.http_client
.get(
&format!("https://docs.rs/{path}"),
AsyncBody::default(),
true,
)
.await?;
fn database_path(&self) -> PathBuf {
paths::support_dir().join("docs/rust/docs-rs-db.1.mdb")
}
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading docs.rs response body")?;
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
index_rustdoc(package, database, {
move |crate_name, item| {
let http_client = self.http_client.clone();
let crate_name = crate_name.clone();
let item = item.cloned();
async move {
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}{item_path}",
item_path = item
.map(|item| format!("/{}", item.url_path()))
.unwrap_or_default()
);
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let mut response = http_client
.get(
&format!("https://docs.rs/{path}"),
AsyncBody::default(),
true,
)
.await?;
Ok(Some(String::from_utf8(body)?))
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading docs.rs response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
Ok(Some(String::from_utf8(body)?))
}
.boxed()
}
})
.await
}
}
async fn index_rustdoc(
package: PackageName,
database: Arc<IndexedDocsDatabase>,
fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result<Option<String>>>
+ Send
+ Sync,
) -> Result<()> {
let Some(package_root_content) = fetch_page(&package, None).await? else {
return Ok(());
};
let (crate_root_markdown, items) =
convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
database
.insert(package.to_string(), crate_root_markdown)
.await?;
let mut seen_items = HashSet::from_iter(items.clone());
let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
item,
#[cfg(debug_assertions)]
history: Vec::new(),
}));
while let Some(item_with_history) = items_to_visit.pop_front() {
let item = &item_with_history.item;
let Some(result) = fetch_page(&package, Some(&item)).await.with_context(|| {
#[cfg(debug_assertions)]
{
format!(
"failed to fetch {item:?}: {history:?}",
history = item_with_history.history
)
}
#[cfg(not(debug_assertions))]
{
format!("failed to fetch {item:?}")
}
})?
else {
continue;
};
let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
database
.insert(format!("{package}::{}", item.display()), markdown)
.await?;
let parent_item = item;
for mut item in referenced_items {
if seen_items.contains(&item) {
continue;
}
seen_items.insert(item.clone());
item.path.extend(parent_item.path.clone());
match parent_item.kind {
RustdocItemKind::Mod => {
item.path.push(parent_item.name.clone());
}
_ => {}
}
items_to_visit.push_back(RustdocItemWithHistory {
#[cfg(debug_assertions)]
history: {
let mut history = item_with_history.history.clone();
history.push(item.url_path());
history
},
item,
});
}
}
Ok(())
}

View File

@@ -21,12 +21,6 @@ use crate::IndexedDocsRegistry;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
pub struct ProviderId(pub Arc<str>);
impl ProviderId {
pub fn rustdoc() -> Self {
Self("rustdoc".into())
}
}
/// The name of a package.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
pub struct PackageName(Arc<str>);
@@ -57,6 +51,7 @@ pub struct IndexedDocsStore {
provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
indexing_tasks_by_package:
RwLock<HashMap<PackageName, Shared<Task<Result<(), Arc<anyhow::Error>>>>>>,
latest_errors_by_package: RwLock<HashMap<PackageName, Arc<str>>>,
}
impl IndexedDocsStore {
@@ -86,9 +81,14 @@ impl IndexedDocsStore {
database_future,
provider,
indexing_tasks_by_package: RwLock::new(HashMap::default()),
latest_errors_by_package: RwLock::new(HashMap::default()),
}
}
pub fn latest_error_for_package(&self, package: &PackageName) -> Option<Arc<str>> {
self.latest_errors_by_package.read().get(package).cloned()
}
/// Returns whether the package with the given name is currently being indexed.
pub fn is_indexing(&self, package: &PackageName) -> bool {
self.indexing_tasks_by_package.read().contains_key(package)
@@ -103,6 +103,15 @@ impl IndexedDocsStore {
.await
}
pub async fn load_many_by_prefix(&self, prefix: String) -> Result<Vec<(String, MarkdownDocs)>> {
self.database_future
.clone()
.await
.map_err(|err| anyhow!(err))?
.load_many_by_prefix(prefix)
.await
}
pub fn index(
self: Arc<Self>,
package: PackageName,
@@ -125,16 +134,31 @@ impl IndexedDocsStore {
}
});
let index_task = async {
let database = this
.database_future
.clone()
.await
.map_err(|err| anyhow!(err))?;
this.provider.index(package, database).await
let index_task = {
let package = package.clone();
async {
let database = this
.database_future
.clone()
.await
.map_err(|err| anyhow!(err))?;
this.provider.index(package, database).await
}
};
index_task.await.map_err(Arc::new)
let result = index_task.await.map_err(Arc::new);
match &result {
Ok(_) => {
this.latest_errors_by_package.write().remove(&package);
}
Err(err) => {
this.latest_errors_by_package
.write()
.insert(package, err.to_string().into());
}
}
result
}
})
.shared();
@@ -242,6 +266,28 @@ impl IndexedDocsDatabase {
})
}
pub fn load_many_by_prefix(&self, prefix: String) -> Task<Result<Vec<(String, MarkdownDocs)>>> {
let env = self.env.clone();
let entries = self.entries;
self.executor.spawn(async move {
let txn = env.read_txn()?;
let results = entries
.iter(&txn)?
.filter_map(|entry| {
let (key, value) = entry.ok()?;
if key.starts_with(&prefix) {
Some((key, value))
} else {
None
}
})
.collect::<Vec<_>>();
Ok(results)
})
}
pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
let env = self.env.clone();
let entries = self.entries;

View File

@@ -122,7 +122,7 @@ lazy_static! {
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
soft_wrap: Some(SoftWrap::PreferredLineLength),
soft_wrap: Some(SoftWrap::EditorWidth),
..Default::default()
},
None,

View File

@@ -13,5 +13,4 @@ brackets = [
]
tab_size = 2
soft_wrap = "preferred_line_length"
prettier_parser_name = "markdown"

View File

@@ -1,9 +1,13 @@
use anyhow::Result;
use async_trait::async_trait;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use project::project_settings::ProjectSettings;
use serde_json::Value;
use settings::Settings;
use std::{
any::Any,
borrow::Cow,
@@ -25,6 +29,8 @@ pub struct PythonLspAdapter {
}
impl PythonLspAdapter {
const SERVER_NAME: &'static str = "pyright";
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
PythonLspAdapter { node }
}
@@ -33,14 +39,18 @@ impl PythonLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for PythonLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("pyright".into())
LanguageServerName(Self::SERVER_NAME.into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
Ok(Box::new(
self.node
.npm_package_latest_version(Self::SERVER_NAME)
.await?,
) as Box<_>)
}
async fn fetch_server_binary(
@@ -51,7 +61,7 @@ impl LspAdapter for PythonLspAdapter {
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<String>().unwrap();
let server_path = container_dir.join(SERVER_PATH);
let package_name = "pyright";
let package_name = Self::SERVER_NAME;
let should_install_language_server = self
.node
@@ -164,6 +174,20 @@ impl LspAdapter for PythonLspAdapter {
filter_range,
})
}
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.settings.clone())
.unwrap_or_default()
})
}
}
async fn get_cached_server_binary(

View File

@@ -575,12 +575,11 @@ fn retrieve_package_id_and_bin_name_from_metadata(
metadata: CargoMetadata,
abs_path: &Path,
) -> Option<(String, String)> {
let abs_path = abs_path.to_str()?;
for package in metadata.packages {
for target in package.targets {
let is_bin = target.kind.iter().any(|kind| kind == "bin");
if target.src_path == abs_path && is_bin {
let target_path = PathBuf::from(target.src_path);
if target_path == abs_path && is_bin {
return Some((package.id, target.name));
}
}

View File

@@ -68,10 +68,22 @@ pub struct TypeScriptLspAdapter {
impl TypeScriptLspAdapter {
const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
const SERVER_NAME: &'static str = "typescript-language-server";
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
TypeScriptLspAdapter { node }
}
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();
if is_yarn {
".yarn/sdks/typescript/lib"
} else {
"node_modules/typescript/lib"
}
}
}
struct TypeScriptVersions {
@@ -82,7 +94,7 @@ struct TypeScriptVersions {
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("typescript-language-server".into())
LanguageServerName(Self::SERVER_NAME.into())
}
async fn fetch_latest_server_version(
@@ -196,13 +208,14 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
adapter: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let tsdk_path = Self::tsdk_path(adapter).await;
Ok(Some(json!({
"provideFormatter": true,
"hostInfo": "zed",
"tsserver": {
"path": "node_modules/typescript/lib",
"path": tsdk_path,
},
"preferences": {
"includeInlayParameterNameHints": "all",
@@ -220,8 +233,17 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let override_options = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
})?;
if let Some(options) = override_options {
return Ok(options);
}
Ok(json!({
"completions": {
"completeFunctionCalls": true

View File

@@ -5,7 +5,9 @@ use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use project::project_settings::ProjectSettings;
use serde_json::{json, Value};
use settings::Settings;
use std::{
any::Any,
ffi::OsString,
@@ -28,6 +30,18 @@ impl VtslsLspAdapter {
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
VtslsLspAdapter { node }
}
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();
if is_yarn {
".yarn/sdks/typescript/lib"
} else {
"node_modules/typescript/lib"
}
}
}
struct TypeScriptVersions {
@@ -35,10 +49,11 @@ struct TypeScriptVersions {
server_version: String,
}
const SERVER_NAME: &'static str = "vtsls";
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vtsls".into())
LanguageServerName(SERVER_NAME.into())
}
async fn fetch_latest_server_version(
@@ -159,11 +174,12 @@ impl LspAdapter for VtslsLspAdapter {
async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
adapter: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let tsdk_path = Self::tsdk_path(&adapter).await;
Ok(Some(json!({
"typescript": {
"tsdk": "node_modules/typescript/lib",
"tsdk": tsdk_path,
"format": {
"enable": true
},
@@ -196,22 +212,33 @@ impl LspAdapter for VtslsLspAdapter {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
},
"autoUseWorkspaceTsdk": true
}
})))
}
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
adapter: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let override_options = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
})?;
if let Some(options) = override_options {
return Ok(options);
}
let tsdk_path = Self::tsdk_path(&adapter).await;
Ok(json!({
"typescript": {
"suggest": {
"completeFunctionCalls": true
},
"tsdk": "node_modules/typescript/lib",
"tsdk": tsdk_path,
"format": {
"enable": true
},
@@ -244,7 +271,8 @@ impl LspAdapter for VtslsLspAdapter {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
},
"autoUseWorkspaceTsdk": true
}
}))
}

View File

@@ -57,7 +57,7 @@ gpui = { workspace = true, features = ["test-support"] }
live_kit_server.workspace = true
nanoid.workspace = true
sha2.workspace = true
simplelog = "0.9"
simplelog.workspace = true
[build-dependencies]
serde.workspace = true

View File

@@ -645,7 +645,24 @@ impl LanguageServer {
on_type_formatting: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: None,
}),
..Default::default()
signature_help: Some(SignatureHelpClientCapabilities {
signature_information: Some(SignatureInformationSettings {
documentation_format: Some(vec![
MarkupKind::Markdown,
MarkupKind::PlainText,
]),
parameter_information: Some(ParameterInformationSettings {
label_offset_support: Some(true),
}),
active_parameter_support: Some(true),
}),
..SignatureHelpClientCapabilities::default()
}),
synchronization: Some(TextDocumentSyncClientCapabilities {
did_save: Some(true),
..TextDocumentSyncClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
}),
experimental: Some(json!({
"serverStatusNotification": true,

View File

@@ -1,5 +1,5 @@
use assets::Assets;
use gpui::{prelude::*, App, KeyBinding, Task, View, WindowOptions};
use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, Task, View, WindowOptions};
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime;
@@ -105,44 +105,49 @@ pub fn main() {
cx.activate(true);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| {
let markdown_style = MarkdownStyle {
base_text_style: gpui::TextStyle {
font_family: "Zed Plex Sans".into(),
color: cx.theme().colors().terminal_ansi_black,
..Default::default()
},
code_block: StyleRefinement::default()
.font_family("Zed Plex Mono")
.m(rems(1.))
.bg(rgb(0xAAAAAAA)),
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
..Default::default()
};
MarkdownExample::new(
MARKDOWN_EXAMPLE.to_string(),
MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
// @nate: Could we add inline-code specific styles to the theme?
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
},
markdown_style,
language_registry,
cx,
)
@@ -163,7 +168,8 @@ impl MarkdownExample {
language_registry: Arc<LanguageRegistry>,
cx: &mut WindowContext,
) -> Self {
let markdown = cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx));
let markdown =
cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None));
Self { markdown }
}
}

View File

@@ -0,0 +1,120 @@
use assets::Assets;
use gpui::*;
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime;
use settings::SettingsStore;
use std::sync::Arc;
use theme::LoadThemes;
use ui::div;
use ui::prelude::*;
const MARKDOWN_EXAMPLE: &'static str = r#"
this text should be selectable
wow so cool
## Heading 2
"#;
pub fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
language::init(cx);
SettingsStore::update(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
let node_runtime = FakeNodeRuntime::new();
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
languages::init(language_registry.clone(), node_runtime, cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
cx.activate(true);
let _ = cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| {
let markdown_style = MarkdownStyle {
base_text_style: gpui::TextStyle {
font_family: "Zed Mono".into(),
color: cx.theme().colors().text,
..Default::default()
},
code_block: StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
}),
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(rems(4.).into())),
left: Some(Length::Definite(rems(4.).into())),
right: Some(Length::Definite(rems(4.).into())),
bottom: Some(Length::Definite(rems(4.).into())),
},
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
break_style: Default::default(),
heading: Default::default(),
};
let markdown = cx.new_view(|cx| {
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None)
});
HelloWorld { markdown }
})
});
});
}
struct HelloWorld {
markdown: View<Markdown>,
}
impl Render for HelloWorld {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.size(Length::Definite(Pixels(700.0).into()))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(div().child(self.markdown.clone()).p_20())
}
}

View File

@@ -1,16 +1,17 @@
mod parser;
pub mod parser;
use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{
actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
TextStyleRefinement, View,
Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
Point, Render, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
TextStyle, TextStyleRefinement, View,
};
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::prelude::*;
@@ -18,7 +19,8 @@ use util::{ResultExt, TryFutureExt};
#[derive(Clone)]
pub struct MarkdownStyle {
pub code_block: TextStyleRefinement,
pub base_text_style: TextStyle,
pub code_block: StyleRefinement,
pub inline_code: TextStyleRefinement,
pub block_quote: TextStyleRefinement,
pub link: TextStyleRefinement,
@@ -26,8 +28,27 @@ pub struct MarkdownStyle {
pub block_quote_border_color: Hsla,
pub syntax: Arc<SyntaxTheme>,
pub selection_background_color: Hsla,
pub break_style: StyleRefinement,
pub heading: StyleRefinement,
}
impl Default for MarkdownStyle {
fn default() -> Self {
Self {
base_text_style: Default::default(),
code_block: Default::default(),
inline_code: Default::default(),
block_quote: Default::default(),
link: Default::default(),
rule_color: Default::default(),
block_quote_border_color: Default::default(),
syntax: Arc::new(SyntaxTheme::default()),
selection_background_color: Default::default(),
break_style: Default::default(),
heading: Default::default(),
}
}
}
pub struct Markdown {
source: String,
selection: Selection,
@@ -39,6 +60,7 @@ pub struct Markdown {
pending_parse: Option<Task<Option<()>>>,
focus_handle: FocusHandle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
actions!(markdown, [Copy]);
@@ -49,6 +71,7 @@ impl Markdown {
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>,
fallback_code_block_language: Option<String>,
) -> Self {
let focus_handle = cx.focus_handle();
let mut this = Self {
@@ -62,6 +85,7 @@ impl Markdown {
pending_parse: None,
focus_handle,
language_registry,
fallback_code_block_language,
};
this.parse(cx);
this
@@ -89,7 +113,14 @@ impl Markdown {
&self.source
}
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
&self.parsed_markdown
}
fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
if self.selection.end <= self.selection.start {
return;
}
let text = text.text_for_range(self.selection.start..self.selection.end);
cx.write_to_clipboard(ClipboardItem::new(text));
}
@@ -140,6 +171,7 @@ impl Render for Markdown {
cx.view().clone(),
self.style.clone(),
self.language_registry.clone(),
self.fallback_code_block_language.clone(),
)
}
}
@@ -185,11 +217,21 @@ impl Selection {
}
#[derive(Clone)]
struct ParsedMarkdown {
pub struct ParsedMarkdown {
source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>,
}
impl ParsedMarkdown {
pub fn source(&self) -> &SharedString {
&self.source
}
pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
return &self.events;
}
}
impl Default for ParsedMarkdown {
fn default() -> Self {
Self {
@@ -203,6 +245,7 @@ pub struct MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
impl MarkdownElement {
@@ -210,19 +253,31 @@ impl MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
) -> Self {
Self {
markdown,
style,
language_registry,
fallback_code_block_language,
}
}
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
let language_test = self.language_registry.as_ref()?.language_for_name(name);
let language_name = match language_test.now_or_never() {
Some(Ok(_)) => String::from(name),
Some(Err(_)) if !name.is_empty() && self.fallback_code_block_language.is_some() => {
self.fallback_code_block_language.clone().unwrap()
}
_ => String::new(),
};
let language = self
.language_registry
.as_ref()?
.language_for_name(name)
.language_for_name(language_name.as_str())
.map(|language| language.ok())
.shared();
@@ -417,7 +472,7 @@ impl MarkdownElement {
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
let text_style = cx.text_style();
let text_style = self.style.base_text_style.clone();
let font_id = cx.text_system().resolve_font(&text_style.font());
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let em_width = cx
@@ -462,14 +517,26 @@ impl Element for MarkdownElement {
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
let mut builder = MarkdownElementBuilder::new(
self.style.base_text_style.clone(),
self.style.syntax.clone(),
);
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
let markdown_end = if let Some(last) = parsed_markdown.events.last() {
last.0.end
} else {
0
};
for (range, event) in parsed_markdown.events.iter() {
match event {
MarkdownEvent::Start(tag) => {
match tag {
MarkdownTag::Paragraph => {
builder.push_div(div().mb_2().line_height(rems(1.3)));
builder.push_div(
div().mb_2().line_height(rems(1.3)),
range,
markdown_end,
);
}
MarkdownTag::Heading { level, .. } => {
let mut heading = div().mb_2();
@@ -480,7 +547,11 @@ impl Element for MarkdownElement {
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
_ => heading,
};
builder.push_div(heading);
heading.style().refine(&self.style.heading);
builder.push_text_style(
self.style.heading.text_style().clone().unwrap_or_default(),
);
builder.push_div(heading, range, markdown_end);
}
MarkdownTag::BlockQuote => {
builder.push_text_style(self.style.block_quote.clone());
@@ -490,6 +561,8 @@ impl Element for MarkdownElement {
.mb_2()
.border_l_4()
.border_color(self.style.block_quote_border_color),
range,
markdown_end,
);
}
MarkdownTag::CodeBlock(kind) => {
@@ -499,17 +572,18 @@ impl Element for MarkdownElement {
None
};
let mut d = div().w_full().rounded_lg();
d.style().refine(&self.style.code_block);
if let Some(code_block_text_style) = &self.style.code_block.text {
builder.push_text_style(code_block_text_style.to_owned());
}
builder.push_code_block(language);
builder.push_text_style(self.style.code_block.clone());
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
self.style.code_block.background_color,
|div, color| div.bg(color),
));
builder.push_div(d, range, markdown_end);
}
MarkdownTag::HtmlBlock => builder.push_div(div()),
MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
MarkdownTag::List(bullet_index) => {
builder.push_list(*bullet_index);
builder.push_div(div().pl_4());
builder.push_div(div().pl_4(), range, markdown_end);
}
MarkdownTag::Item => {
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
@@ -525,9 +599,11 @@ impl Element for MarkdownElement {
.items_start()
.gap_1()
.child(bullet),
range,
markdown_end,
);
// Without `w_0`, text doesn't wrap to the width of the container.
builder.push_div(div().flex_1().w_0());
builder.push_div(div().flex_1().w_0(), range, markdown_end);
}
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
font_style: Some(FontStyle::Italic),
@@ -552,6 +628,7 @@ impl Element for MarkdownElement {
builder.push_text_style(self.style.link.clone())
}
}
MarkdownTag::MetadataBlock(_) => {}
_ => log::error!("unsupported markdown tag {:?}", tag),
}
}
@@ -559,7 +636,10 @@ impl Element for MarkdownElement {
MarkdownTagEnd::Paragraph => {
builder.pop_div();
}
MarkdownTagEnd::Heading(_) => builder.pop_div(),
MarkdownTagEnd::Heading(_) => {
builder.pop_div();
builder.pop_text_style()
}
MarkdownTagEnd::BlockQuote => {
builder.pop_text_style();
builder.pop_div()
@@ -567,8 +647,10 @@ impl Element for MarkdownElement {
MarkdownTagEnd::CodeBlock => {
builder.trim_trailing_newline();
builder.pop_div();
builder.pop_text_style();
builder.pop_code_block();
if self.style.code_block.text.is_some() {
builder.pop_text_style();
}
}
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
MarkdownTagEnd::List(_) => {
@@ -609,18 +691,24 @@ impl Element for MarkdownElement {
.border_b_1()
.my_2()
.border_color(self.style.rule_color),
range,
markdown_end,
);
builder.pop_div()
}
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
MarkdownEvent::HardBreak => {
let mut d = div().py_3();
d.style().refine(&self.style.break_style);
builder.push_div(d, range, markdown_end);
builder.pop_div()
}
_ => log::error!("unsupported markdown event {:?}", event),
}
}
let mut rendered_markdown = builder.build();
let child_layout_id = rendered_markdown.element.request_layout(cx);
let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
let layout_id = cx.request_layout(gpui::Style::default(), [child_layout_id]);
(layout_id, rendered_markdown)
}
@@ -732,8 +820,32 @@ impl MarkdownElementBuilder {
self.text_style_stack.pop();
}
fn push_div(&mut self, div: Div) {
fn push_div(&mut self, mut div: Div, range: &Range<usize>, markdown_end: usize) {
self.flush_text();
if range.start == 0 {
//first element, remove top margin
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(px(0.).into())),
left: None,
right: None,
bottom: None,
},
..Default::default()
});
}
if range.end == markdown_end {
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
top: None,
left: None,
right: None,
bottom: Some(Length::Definite(rems(0.).into())),
},
..Default::default()
});
}
self.div_stack.push(div);
}

View File

@@ -7,11 +7,22 @@ use std::ops::Range;
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
let mut events = Vec::new();
let mut within_link = false;
let mut within_metadata = false;
for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
if within_metadata {
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) =
pulldown_event
{
within_metadata = false;
}
continue;
}
match pulldown_event {
pulldown_cmark::Event::Start(tag) => {
if let pulldown_cmark::Tag::Link { .. } = tag {
within_link = true;
match tag {
pulldown_cmark::Tag::Link { .. } => within_link = true,
pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = true,
_ => {}
}
events.push((range, MarkdownEvent::Start(tag.into())))
}

View File

@@ -131,11 +131,7 @@ impl AnchorRangeExt for Range<Anchor> {
}
fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
let start_cmp = self.start.cmp(&other.start, buffer);
let end_cmp = self.end.cmp(&other.end, buffer);
(start_cmp == Ordering::Less || start_cmp == Ordering::Equal)
&& (end_cmp == Ordering::Greater || end_cmp == Ordering::Equal)
self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
}
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {

View File

@@ -1129,7 +1129,7 @@ impl OutlinePanel {
EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
let project = self.project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id)
.buffer_for_id(buffer_id, cx)
.and_then(|buffer| buffer.read(cx).entry_id(cx));
project
.worktree_for_id(worktree_id, cx)
@@ -1147,7 +1147,7 @@ impl OutlinePanel {
.remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
let project = self.project.read(cx);
let entry_id = project
.buffer_for_id(buffer_id)
.buffer_for_id(buffer_id, cx)
.and_then(|buffer| buffer.read(cx).entry_id(cx));
entry_id.and_then(|entry_id| {
@@ -1622,11 +1622,7 @@ impl OutlinePanel {
ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
},
None => {
new_collapsed_entries
.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
ExcerptOutlines::NotFetched
}
None => ExcerptOutlines::NotFetched,
};
new_excerpts.entry(buffer_id).or_default().insert(
excerpt_id,
@@ -1674,11 +1670,6 @@ impl OutlinePanel {
.insert(CollapsedEntry::ExternalFile(buffer_id));
}
}
for excerpt_id in &excerpts {
new_collapsed_entries
.insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
}
}
if let Some(worktree) = worktree {

View File

@@ -84,7 +84,7 @@ impl Prettier {
path_to_check.pop();
}
let mut project_path_with_prettier_dependency = None;
let mut closest_package_json_path = None;
loop {
if installed_prettiers.contains(&path_to_check) {
log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
@@ -92,61 +92,44 @@ impl Prettier {
} else if let Some(package_json_contents) =
read_package_json(fs, &path_to_check).await?
{
if has_prettier_in_package_json(&package_json_contents) {
if has_prettier_in_node_modules(fs, &path_to_check).await? {
log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else if project_path_with_prettier_dependency.is_none() {
project_path_with_prettier_dependency = Some(path_to_check.clone());
}
if has_prettier_in_node_modules(fs, &path_to_check).await? {
log::debug!("Found prettier path {path_to_check:?} in the node_modules");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
match package_json_contents.get("workspaces") {
Some(serde_json::Value::Array(workspaces)) => {
match &project_path_with_prettier_dependency {
Some(project_path_with_prettier_dependency) => {
let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
if workspaces.iter().filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
}
}).any(|workspace_definition| {
if let Some(path_matcher) = PathMatcher::new(&[workspace_definition.clone()]).ok() {
path_matcher.is_match(subproject_path)
} else {
workspace_definition == subproject_path.to_string_lossy()
}
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
match &closest_package_json_path {
None => closest_package_json_path = Some(path_to_check.clone()),
Some(closest_package_json_path) => {
match package_json_contents.get("workspaces") {
Some(serde_json::Value::Array(workspaces)) => {
let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
if workspaces.iter().filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}");
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
}
}).any(|workspace_definition| {
workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
log::warn!("Skipping path {path_to_check:?} workspace root with workspaces {workspaces:?} that have no prettier installed");
}
None => {
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json");
}
}
},
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
},
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
}
}
}
}
}
if !path_to_check.pop() {
match project_path_with_prettier_dependency {
Some(closest_prettier_discovered) => {
anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")
}
None => {
log::debug!("Found no prettier in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
log::debug!("Found no prettier in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
}
@@ -448,22 +431,6 @@ async fn read_package_json(
Ok(None)
}
fn has_prettier_in_package_json(
package_json_contents: &HashMap<String, serde_json::Value>,
) -> bool {
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
false
}
enum Format {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -548,40 +515,36 @@ mod tests {
)
.await;
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/.config/zed/settings.json"),
)
.await,
Ok(ControlFlow::Continue(None))
),
"Should successfully find no prettier for path hierarchy without it"
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/.config/zed/settings.json"),
)
.await
.unwrap(),
ControlFlow::Continue(None),
"Should find no prettier for path hierarchy without it"
);
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/src/index.js")
)
.await,
Ok(ControlFlow::Continue(None))
),
"Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/src/index.js")
)
.await.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
"Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
);
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/node_modules/expect/build/print.js")
)
.await,
Ok(ControlFlow::Break(()))
),
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/node_modules/expect/build/print.js")
)
.await
.unwrap(),
ControlFlow::Break(()),
"Should not format files inside node_modules/"
);
}
@@ -691,18 +654,17 @@ mod tests {
)
.await;
match Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/web_blog/pages/[slug].tsx")
)
.await {
Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
Err(e) => {
let message = e.to_string();
assert!(message.contains("/root/work/web_blog"), "Error message should mention which project had prettier defined");
},
};
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/web_blog/pages/[slug].tsx")
)
.await
.unwrap(),
ControlFlow::Continue(None),
"Should find no prettier when node_modules don't have it"
);
assert_eq!(
Prettier::locate_prettier_installation(

View File

@@ -0,0 +1,928 @@
use crate::ProjectPath;
use anyhow::{anyhow, Context as _, Result};
use collections::{hash_map, HashMap};
use futures::{channel::oneshot, StreamExt as _};
use gpui::{
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::{
proto::{deserialize_version, serialize_version, split_operations},
Buffer, Capability, Language, Operation,
};
use rpc::{
proto::{self, AnyProtoClient, PeerId},
ErrorExt as _, TypedEnvelope,
};
use std::{io, path::Path, sync::Arc};
use text::BufferId;
use util::{debug_panic, maybe, ResultExt as _};
use worktree::{File, ProjectEntryId, RemoteWorktree, Worktree};
/// A set of open buffers.
pub struct BufferStore {
retain_buffers: bool,
opened_buffers: HashMap<BufferId, OpenBuffer>,
local_buffer_ids_by_path: HashMap<ProjectPath, BufferId>,
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, BufferId>,
#[allow(clippy::type_complexity)]
loading_buffers_by_path: HashMap<
ProjectPath,
postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
>,
loading_remote_buffers_by_id: HashMap<BufferId, Model<Buffer>>,
remote_buffer_listeners:
HashMap<BufferId, Vec<oneshot::Sender<Result<Model<Buffer>, anyhow::Error>>>>,
}
enum OpenBuffer {
Strong(Model<Buffer>),
Weak(WeakModel<Buffer>),
Operations(Vec<Operation>),
}
pub enum BufferStoreEvent {
BufferAdded(Model<Buffer>),
BufferChangedFilePath {
buffer: Model<Buffer>,
old_file: Option<Arc<File>>,
},
BufferSaved {
buffer: Model<Buffer>,
has_changed_file: bool,
saved_version: clock::Global,
},
}
impl EventEmitter<BufferStoreEvent> for BufferStore {}
impl BufferStore {
/// Creates a buffer store, optionally retaining its buffers.
///
/// If `retain_buffers` is `true`, then buffers are owned by the buffer store
/// and won't be released unless they are explicitly removed, or `retain_buffers`
/// is set to `false` via `set_retain_buffers`. Otherwise, buffers are stored as
/// weak handles.
pub fn new(retain_buffers: bool) -> Self {
Self {
retain_buffers,
opened_buffers: Default::default(),
remote_buffer_listeners: Default::default(),
loading_remote_buffers_by_id: Default::default(),
local_buffer_ids_by_path: Default::default(),
local_buffer_ids_by_entry_id: Default::default(),
loading_buffers_by_path: Default::default(),
}
}
pub fn open_buffer(
&mut self,
project_path: ProjectPath,
worktree: Model<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
let existing_buffer = self.get_by_path(&project_path, cx);
if let Some(existing_buffer) = existing_buffer {
return Task::ready(Ok(existing_buffer));
}
let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
// If the given path is already being loaded, then wait for that existing
// task to complete and return the same buffer.
hash_map::Entry::Occupied(e) => e.get().clone(),
// Otherwise, record the fact that this path is now being loaded.
hash_map::Entry::Vacant(entry) => {
let (mut tx, rx) = postage::watch::channel();
entry.insert(rx.clone());
let project_path = project_path.clone();
let load_buffer = match worktree.read(cx) {
Worktree::Local(_) => {
self.open_local_buffer_internal(project_path.path.clone(), worktree, cx)
}
Worktree::Remote(tree) => {
self.open_remote_buffer_internal(&project_path.path, tree, cx)
}
};
cx.spawn(move |this, mut cx| async move {
let load_result = load_buffer.await;
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
// Record the fact that the buffer is no longer loading.
this.loading_buffers_by_path.remove(&project_path);
let buffer = load_result.map_err(Arc::new)?;
Ok(buffer)
})?);
anyhow::Ok(())
})
.detach();
rx
}
};
cx.background_executor().spawn(async move {
Self::wait_for_loading_buffer(loading_watch)
.await
.map_err(|e| e.cloned())
})
}
fn open_local_buffer_internal(
&mut self,
path: Arc<Path>,
worktree: Model<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
let load_buffer = worktree.update(cx, |worktree, cx| {
let load_file = worktree.load_file(path.as_ref(), cx);
let reservation = cx.reserve_model();
let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
cx.spawn(move |_, mut cx| async move {
let loaded = load_file.await?;
let text_buffer = cx
.background_executor()
.spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) })
.await;
cx.insert_model(reservation, |_| {
Buffer::build(
text_buffer,
loaded.diff_base,
Some(loaded.file),
Capability::ReadWrite,
)
})
})
});
cx.spawn(move |this, mut cx| async move {
let buffer = match load_buffer.await {
Ok(buffer) => Ok(buffer),
Err(error) if is_not_found_error(&error) => cx.new_model(|cx| {
let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64());
let text_buffer = text::Buffer::new(0, buffer_id, "".into());
Buffer::build(
text_buffer,
None,
Some(Arc::new(File {
worktree,
path,
mtime: None,
entry_id: None,
is_local: true,
is_deleted: false,
is_private: false,
})),
Capability::ReadWrite,
)
}),
Err(e) => Err(e),
}?;
this.update(&mut cx, |this, cx| {
this.add_buffer(buffer.clone(), cx).log_err();
})?;
Ok(buffer)
})
}
fn open_remote_buffer_internal(
&self,
path: &Arc<Path>,
worktree: &RemoteWorktree,
cx: &ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
let worktree_id = worktree.id().to_proto();
let project_id = worktree.project_id();
let client = worktree.client();
let path_string = path.clone().to_string_lossy().to_string();
cx.spawn(move |this, mut cx| async move {
let response = client
.request(proto::OpenBufferByPath {
project_id,
worktree_id,
path: path_string,
})
.await?;
let buffer_id = BufferId::new(response.buffer_id)?;
this.update(&mut cx, |this, cx| {
this.wait_for_remote_buffer(buffer_id, cx)
})?
.await
})
}
pub fn create_buffer(
&mut self,
remote_client: Option<(AnyProtoClient, u64)>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
if let Some((remote_client, project_id)) = remote_client {
let create = remote_client.request(proto::OpenNewBuffer { project_id });
cx.spawn(|this, mut cx| async move {
let response = create.await?;
let buffer_id = BufferId::new(response.buffer_id)?;
this.update(&mut cx, |this, cx| {
this.wait_for_remote_buffer(buffer_id, cx)
})?
.await
})
} else {
Task::ready(Ok(self.create_local_buffer("", None, cx)))
}
}
pub fn create_local_buffer(
&mut self,
text: &str,
language: Option<Arc<Language>>,
cx: &mut ModelContext<Self>,
) -> Model<Buffer> {
let buffer = cx.new_model(|cx| {
Buffer::local(text, cx)
.with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx)
});
self.add_buffer(buffer.clone(), cx).log_err();
buffer
}
pub fn save_buffer(
&mut self,
buffer: Model<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
return Task::ready(Err(anyhow!("buffer doesn't have a file")));
};
match file.worktree.read(cx) {
Worktree::Local(_) => {
self.save_local_buffer(file.worktree.clone(), buffer, file.path.clone(), false, cx)
}
Worktree::Remote(tree) => self.save_remote_buffer(buffer, None, tree, cx),
}
}
pub fn save_buffer_as(
&mut self,
buffer: Model<Buffer>,
path: ProjectPath,
worktree: Model<Worktree>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let old_file = File::from_dyn(buffer.read(cx).file())
.cloned()
.map(Arc::new);
let task = match worktree.read(cx) {
Worktree::Local(_) => {
self.save_local_buffer(worktree, buffer.clone(), path.path, true, cx)
}
Worktree::Remote(tree) => {
self.save_remote_buffer(buffer.clone(), Some(path.to_proto()), tree, cx)
}
};
cx.spawn(|this, mut cx| async move {
task.await?;
this.update(&mut cx, |_, cx| {
cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file });
})
})
}
fn save_local_buffer(
&self,
worktree: Model<Worktree>,
buffer_handle: Model<Buffer>,
path: Arc<Path>,
mut has_changed_file: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let text = buffer.as_rope().clone();
let line_ending = buffer.line_ending();
let version = buffer.version();
if buffer.file().is_some_and(|file| !file.is_created()) {
has_changed_file = true;
}
let save = worktree.update(cx, |worktree, cx| {
worktree.write_file(path.as_ref(), text, line_ending, cx)
});
cx.spawn(move |this, mut cx| async move {
let new_file = save.await?;
let mtime = new_file.mtime;
buffer_handle.update(&mut cx, |buffer, cx| {
if has_changed_file {
buffer.file_updated(new_file, cx);
}
buffer.did_save(version.clone(), mtime, cx);
})?;
this.update(&mut cx, |_, cx| {
cx.emit(BufferStoreEvent::BufferSaved {
buffer: buffer_handle,
has_changed_file,
saved_version: version,
})
})?;
Ok(())
})
}
fn save_remote_buffer(
&self,
buffer_handle: Model<Buffer>,
new_path: Option<proto::ProjectPath>,
tree: &RemoteWorktree,
cx: &ModelContext<Self>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
let buffer_id = buffer.remote_id().into();
let version = buffer.version();
let rpc = tree.client();
let project_id = tree.project_id();
cx.spawn(move |_, mut cx| async move {
let response = rpc
.request(proto::SaveBuffer {
project_id,
buffer_id,
new_path,
version: serialize_version(&version),
})
.await?;
let version = deserialize_version(&response.version);
let mtime = response.mtime.map(|mtime| mtime.into());
buffer_handle.update(&mut cx, |buffer, cx| {
buffer.did_save(version.clone(), mtime, cx);
})?;
Ok(())
})
}
fn add_buffer(&mut self, buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> {
let remote_id = buffer.read(cx).remote_id();
let is_remote = buffer.read(cx).replica_id() != 0;
let open_buffer = if self.retain_buffers {
OpenBuffer::Strong(buffer.clone())
} else {
OpenBuffer::Weak(buffer.downgrade())
};
match self.opened_buffers.entry(remote_id) {
hash_map::Entry::Vacant(entry) => {
entry.insert(open_buffer);
}
hash_map::Entry::Occupied(mut entry) => {
if let OpenBuffer::Operations(operations) = entry.get_mut() {
buffer.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx))?;
} else if entry.get().upgrade().is_some() {
if is_remote {
return Ok(());
} else {
debug_panic!("buffer {} was already registered", remote_id);
Err(anyhow!("buffer {} was already registered", remote_id))?;
}
}
entry.insert(open_buffer);
}
}
if let Some(senders) = self.remote_buffer_listeners.remove(&remote_id) {
for sender in senders {
sender.send(Ok(buffer.clone())).ok();
}
}
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
if file.is_local {
self.local_buffer_ids_by_path.insert(
ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
remote_id,
);
if let Some(entry_id) = file.entry_id {
self.local_buffer_ids_by_entry_id
.insert(entry_id, remote_id);
}
}
}
cx.emit(BufferStoreEvent::BufferAdded(buffer));
Ok(())
}
pub fn buffers(&self) -> impl '_ + Iterator<Item = Model<Buffer>> {
self.opened_buffers
.values()
.filter_map(|buffer| buffer.upgrade())
}
pub fn loading_buffers(
&self,
) -> impl Iterator<
Item = (
&ProjectPath,
postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
),
> {
self.loading_buffers_by_path
.iter()
.map(|(path, rx)| (path, rx.clone()))
}
pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Model<Buffer>> {
self.buffers().find_map(|buffer| {
let file = File::from_dyn(buffer.read(cx).file())?;
if file.worktree_id(cx) == path.worktree_id && &file.path == &path.path {
Some(buffer)
} else {
None
}
})
}
pub fn get(&self, buffer_id: BufferId) -> Option<Model<Buffer>> {
self.opened_buffers
.get(&buffer_id)
.and_then(|buffer| buffer.upgrade())
}
pub fn get_existing(&self, buffer_id: BufferId) -> Result<Model<Buffer>> {
self.get(buffer_id)
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))
}
pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option<Model<Buffer>> {
self.get(buffer_id)
.or_else(|| self.loading_remote_buffers_by_id.get(&buffer_id).cloned())
}
fn get_or_remove_by_path(
&mut self,
entry_id: ProjectEntryId,
project_path: &ProjectPath,
) -> Option<(BufferId, Model<Buffer>)> {
let buffer_id = match self.local_buffer_ids_by_entry_id.get(&entry_id) {
Some(&buffer_id) => buffer_id,
None => match self.local_buffer_ids_by_path.get(project_path) {
Some(&buffer_id) => buffer_id,
None => {
return None;
}
},
};
let buffer = if let Some(buffer) = self.get(buffer_id) {
buffer
} else {
self.opened_buffers.remove(&buffer_id);
self.local_buffer_ids_by_path.remove(project_path);
self.local_buffer_ids_by_entry_id.remove(&entry_id);
return None;
};
Some((buffer_id, buffer))
}
pub fn wait_for_remote_buffer(
&mut self,
id: BufferId,
cx: &mut AppContext,
) -> Task<Result<Model<Buffer>>> {
let buffer = self.get(id);
if let Some(buffer) = buffer {
return Task::ready(Ok(buffer));
}
let (tx, rx) = oneshot::channel();
self.remote_buffer_listeners.entry(id).or_default().push(tx);
cx.background_executor().spawn(async move { rx.await? })
}
pub fn buffer_version_info(
&self,
cx: &AppContext,
) -> (Vec<proto::BufferVersion>, Vec<BufferId>) {
let buffers = self
.buffers()
.map(|buffer| {
let buffer = buffer.read(cx);
proto::BufferVersion {
id: buffer.remote_id().into(),
version: language::proto::serialize_version(&buffer.version),
}
})
.collect();
let incomplete_buffer_ids = self
.loading_remote_buffers_by_id
.keys()
.copied()
.collect::<Vec<_>>();
(buffers, incomplete_buffer_ids)
}
pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
self.set_retain_buffers(false, cx);
for buffer in self.buffers() {
buffer.update(cx, |buffer, cx| {
buffer.set_capability(Capability::ReadOnly, cx)
});
}
// Wake up all futures currently waiting on a buffer to get opened,
// to give them a chance to fail now that we've disconnected.
self.remote_buffer_listeners.clear();
}
pub fn set_retain_buffers(&mut self, retain_buffers: bool, cx: &mut AppContext) {
self.retain_buffers = retain_buffers;
for open_buffer in self.opened_buffers.values_mut() {
if retain_buffers {
if let OpenBuffer::Weak(buffer) = open_buffer {
if let Some(buffer) = buffer.upgrade() {
*open_buffer = OpenBuffer::Strong(buffer);
}
}
} else {
if let Some(buffer) = open_buffer.upgrade() {
buffer.update(cx, |buffer, _| buffer.give_up_waiting());
}
if let OpenBuffer::Strong(buffer) = open_buffer {
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
}
}
}
}
pub fn discard_incomplete(&mut self) {
self.opened_buffers
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
}
pub fn file_changed(
&mut self,
path: Arc<Path>,
entry_id: ProjectEntryId,
worktree_handle: &Model<worktree::Worktree>,
snapshot: &worktree::Snapshot,
cx: &mut ModelContext<Self>,
) -> Option<(Model<Buffer>, Arc<File>, Arc<File>)> {
let (buffer_id, buffer) = self.get_or_remove_by_path(
entry_id,
&ProjectPath {
worktree_id: snapshot.id(),
path,
},
)?;
let result = buffer.update(cx, |buffer, cx| {
let old_file = File::from_dyn(buffer.file())?;
if old_file.worktree != *worktree_handle {
return None;
}
let new_file = if let Some(entry) = old_file
.entry_id
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
{
File {
is_local: true,
entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
is_private: entry.is_private,
}
} else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
File {
is_local: true,
entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
is_deleted: false,
is_private: entry.is_private,
}
} else {
File {
is_local: true,
entry_id: old_file.entry_id,
path: old_file.path.clone(),
mtime: old_file.mtime,
worktree: worktree_handle.clone(),
is_deleted: true,
is_private: old_file.is_private,
}
};
if new_file == *old_file {
return None;
}
let old_file = Arc::new(old_file.clone());
let new_file = Arc::new(new_file);
buffer.file_updated(new_file.clone(), cx);
Some((cx.handle(), old_file, new_file))
});
if let Some((buffer, old_file, new_file)) = &result {
if new_file.path != old_file.path {
self.local_buffer_ids_by_path.remove(&ProjectPath {
path: old_file.path.clone(),
worktree_id: old_file.worktree_id(cx),
});
self.local_buffer_ids_by_path.insert(
ProjectPath {
worktree_id: new_file.worktree_id(cx),
path: new_file.path.clone(),
},
buffer_id,
);
cx.emit(BufferStoreEvent::BufferChangedFilePath {
buffer: buffer.clone(),
old_file: Some(old_file.clone()),
});
}
if new_file.entry_id != old_file.entry_id {
if let Some(entry_id) = old_file.entry_id {
self.local_buffer_ids_by_entry_id.remove(&entry_id);
}
if let Some(entry_id) = new_file.entry_id {
self.local_buffer_ids_by_entry_id
.insert(entry_id, buffer_id);
}
}
}
result
}
pub fn buffer_changed_file(
&mut self,
buffer: Model<Buffer>,
cx: &mut AppContext,
) -> Option<()> {
let file = File::from_dyn(buffer.read(cx).file())?;
let remote_id = buffer.read(cx).remote_id();
if let Some(entry_id) = file.entry_id {
match self.local_buffer_ids_by_entry_id.get(&entry_id) {
Some(_) => {
return None;
}
None => {
self.local_buffer_ids_by_entry_id
.insert(entry_id, remote_id);
}
}
};
self.local_buffer_ids_by_path.insert(
ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
},
remote_id,
);
Some(())
}
pub async fn create_buffer_for_peer(
this: Model<Self>,
peer_id: PeerId,
buffer_id: BufferId,
project_id: u64,
client: AnyProtoClient,
cx: &mut AsyncAppContext,
) -> Result<()> {
let Some(buffer) = this.update(cx, |this, _| this.get(buffer_id))? else {
return Ok(());
};
let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?;
let operations = operations.await;
let state = buffer.update(cx, |buffer, _| buffer.to_proto())?;
let initial_state = proto::CreateBufferForPeer {
project_id,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
};
if client.send(initial_state).log_err().is_some() {
let client = client.clone();
cx.background_executor()
.spawn(async move {
let mut chunks = split_operations(operations).peekable();
while let Some(chunk) = chunks.next() {
let is_last = chunks.peek().is_none();
client.send(proto::CreateBufferForPeer {
project_id,
peer_id: Some(peer_id),
variant: Some(proto::create_buffer_for_peer::Variant::Chunk(
proto::BufferChunk {
buffer_id: buffer_id.into(),
operations: chunk,
is_last,
},
)),
})?;
}
anyhow::Ok(())
})
.await
.log_err();
}
Ok(())
}
pub fn handle_update_buffer(
&mut self,
envelope: TypedEnvelope<proto::UpdateBuffer>,
is_remote: bool,
cx: &mut AppContext,
) -> Result<proto::Ack> {
let payload = envelope.payload.clone();
let buffer_id = BufferId::new(payload.buffer_id)?;
let ops = payload
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>, _>>()?;
match self.opened_buffers.entry(buffer_id) {
hash_map::Entry::Occupied(mut e) => match e.get_mut() {
OpenBuffer::Strong(buffer) => {
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?;
}
OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops),
OpenBuffer::Weak(_) => {}
},
hash_map::Entry::Vacant(e) => {
if !is_remote {
debug_panic!(
"received buffer update from {:?}",
envelope.original_sender_id
);
return Err(anyhow!("received buffer update for non-remote project"));
}
e.insert(OpenBuffer::Operations(ops));
}
}
Ok(proto::Ack {})
}
pub fn handle_create_buffer_for_peer(
&mut self,
envelope: TypedEnvelope<proto::CreateBufferForPeer>,
mut worktrees: impl Iterator<Item = Model<Worktree>>,
replica_id: u16,
capability: Capability,
cx: &mut ModelContext<Self>,
) -> Result<()> {
match envelope
.payload
.variant
.ok_or_else(|| anyhow!("missing variant"))?
{
proto::create_buffer_for_peer::Variant::State(mut state) => {
let buffer_id = BufferId::new(state.id)?;
let buffer_result = maybe!({
let mut buffer_file = None;
if let Some(file) = state.file.take() {
let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id);
let worktree = worktrees
.find(|worktree| worktree.read(cx).id() == worktree_id)
.ok_or_else(|| {
anyhow!("no worktree found for id {}", file.worktree_id)
})?;
buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
as Arc<dyn language::File>);
}
Buffer::from_proto(replica_id, capability, state, buffer_file)
});
match buffer_result {
Ok(buffer) => {
let buffer = cx.new_model(|_| buffer);
self.loading_remote_buffers_by_id.insert(buffer_id, buffer);
}
Err(error) => {
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
for listener in listeners {
listener.send(Err(anyhow!(error.cloned()))).ok();
}
}
}
}
}
proto::create_buffer_for_peer::Variant::Chunk(chunk) => {
let buffer_id = BufferId::new(chunk.buffer_id)?;
let buffer = self
.loading_remote_buffers_by_id
.get(&buffer_id)
.cloned()
.ok_or_else(|| {
anyhow!(
"received chunk for buffer {} without initial state",
chunk.buffer_id
)
})?;
let result = maybe!({
let operations = chunk
.operations
.into_iter()
.map(language::proto::deserialize_operation)
.collect::<Result<Vec<_>>>()?;
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))
});
if let Err(error) = result {
self.loading_remote_buffers_by_id.remove(&buffer_id);
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
for listener in listeners {
listener.send(Err(error.cloned())).ok();
}
}
} else if chunk.is_last {
self.loading_remote_buffers_by_id.remove(&buffer_id);
self.add_buffer(buffer, cx)?;
}
}
}
Ok(())
}
pub async fn handle_save_buffer(
this: Model<Self>,
project_id: u64,
worktree: Option<Model<Worktree>>,
envelope: TypedEnvelope<proto::SaveBuffer>,
mut cx: AsyncAppContext,
) -> Result<proto::BufferSaved> {
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let buffer = this.update(&mut cx, |this, _| this.get_existing(buffer_id))??;
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&envelope.payload.version))
})?
.await?;
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
if let Some(new_path) = envelope.payload.new_path {
let worktree = worktree.context("no such worktree")?;
let new_path = ProjectPath::from_proto(new_path);
this.update(&mut cx, |this, cx| {
this.save_buffer_as(buffer.clone(), new_path, worktree, cx)
})?
.await?;
} else {
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
.await?;
}
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
project_id,
buffer_id: buffer_id.into(),
version: serialize_version(buffer.saved_version()),
mtime: buffer.saved_mtime().map(|time| time.into()),
})
}
pub async fn wait_for_loading_buffer(
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
loop {
if let Some(result) = receiver.borrow().as_ref() {
match result {
Ok(buffer) => return Ok(buffer.to_owned()),
Err(e) => return Err(e.to_owned()),
}
}
receiver.next().await;
}
}
}
impl OpenBuffer {
fn upgrade(&self) -> Option<Model<Buffer>> {
match self {
OpenBuffer::Strong(handle) => Some(handle.clone()),
OpenBuffer::Weak(handle) => handle.upgrade(),
OpenBuffer::Operations(_) => None,
}
}
}
fn is_not_found_error(error: &anyhow::Error) -> bool {
error
.root_cause()
.downcast_ref::<io::Error>()
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}

View File

@@ -1,3 +1,5 @@
mod signature_help;
use crate::{
CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint,
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
@@ -6,6 +8,7 @@ use crate::{
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use clock::Global;
use futures::future;
use gpui::{AppContext, AsyncAppContext, Model};
use language::{
@@ -20,9 +23,14 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
OneOf, ServerCapabilities,
};
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use text::{BufferId, LineEnding};
pub use signature_help::{
SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
};
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
lsp::FormattingOptions {
tab_size,
@@ -121,6 +129,11 @@ pub(crate) struct GetDocumentHighlights {
pub position: PointUtf16,
}
#[derive(Clone)]
pub(crate) struct GetSignatureHelp {
pub position: PointUtf16,
}
#[derive(Clone)]
pub(crate) struct GetHover {
pub position: PointUtf16,
@@ -397,16 +410,18 @@ impl LspCommand for PerformRename {
message: proto::PerformRenameResponse,
project: Model<Project>,
_: Model<Buffer>,
mut cx: AsyncAppContext,
cx: AsyncAppContext,
) -> Result<ProjectTransaction> {
let message = message
.transaction
.ok_or_else(|| anyhow!("missing transaction"))?;
project
.update(&mut cx, |project, cx| {
project.deserialize_project_transaction(message, self.push_to_history, cx)
})?
.await
Project::deserialize_project_transaction(
project.downgrade(),
message,
self.push_to_history,
cx,
)
.await
}
fn buffer_id_from_proto(message: &proto::PerformRename) -> Result<BufferId> {
@@ -1225,6 +1240,116 @@ impl LspCommand for GetDocumentHighlights {
}
}
#[async_trait(?Send)]
impl LspCommand for GetSignatureHelp {
type Response = Option<SignatureHelp>;
type LspRequest = lsp::SignatureHelpRequest;
type ProtoRequest = proto::GetSignatureHelp;
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
capabilities.signature_help_provider.is_some()
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_cx: &AppContext,
) -> lsp::SignatureHelpParams {
let url_result = lsp::Url::from_file_path(path);
if url_result.is_err() {
log::error!("an invalid file path has been specified");
}
lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier {
uri: url_result.expect("invalid file path"),
},
position: point_to_lsp(self.position),
},
context: None,
work_done_progress_params: Default::default(),
}
}
async fn response_from_lsp(
self,
message: Option<lsp::SignatureHelp>,
_: Model<Project>,
buffer: Model<Buffer>,
_: LanguageServerId,
mut cx: AsyncAppContext,
) -> Result<Self::Response> {
let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
Ok(message.and_then(|message| SignatureHelp::new(message, language)))
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
let offset = buffer.point_utf16_to_offset(self.position);
proto::GetSignatureHelp {
project_id,
buffer_id: buffer.remote_id().to_proto(),
position: Some(serialize_anchor(&buffer.anchor_after(offset))),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
payload: Self::ProtoRequest,
_: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Self> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&payload.version))
})?
.await
.with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?;
let buffer_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
Ok(Self {
position: payload
.position
.and_then(deserialize_anchor)
.context("invalid position")?
.to_point_utf16(&buffer_snapshot),
})
}
fn response_to_proto(
response: Self::Response,
_: &mut Project,
_: PeerId,
_: &Global,
_: &mut AppContext,
) -> proto::GetSignatureHelpResponse {
proto::GetSignatureHelpResponse {
signature_help: response
.map(|signature_help| lsp_to_proto_signature(signature_help.original_data)),
}
}
async fn response_from_proto(
self,
response: proto::GetSignatureHelpResponse,
_: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Self::Response> {
let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
Ok(response
.signature_help
.map(|proto_help| proto_to_lsp_signature(proto_help))
.and_then(|lsp_help| SignatureHelp::new(lsp_help, language)))
}
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for GetHover {
type Response = Option<Hover>;

View File

@@ -0,0 +1,636 @@
use std::{ops::Range, sync::Arc};
use gpui::FontWeight;
use language::{
markdown::{MarkdownHighlight, MarkdownHighlightStyle},
Language,
};
use rpc::proto::{self, documentation};
pub const SIGNATURE_HELP_HIGHLIGHT_CURRENT: MarkdownHighlight =
MarkdownHighlight::Style(MarkdownHighlightStyle {
italic: false,
underline: false,
strikethrough: false,
weight: FontWeight::EXTRA_BOLD,
});
pub const SIGNATURE_HELP_HIGHLIGHT_OVERLOAD: MarkdownHighlight =
MarkdownHighlight::Style(MarkdownHighlightStyle {
italic: true,
underline: false,
strikethrough: false,
weight: FontWeight::NORMAL,
});
#[derive(Debug)]
pub struct SignatureHelp {
pub markdown: String,
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
pub(super) original_data: lsp::SignatureHelp,
}
impl SignatureHelp {
pub fn new(help: lsp::SignatureHelp, language: Option<Arc<Language>>) -> Option<Self> {
let function_options_count = help.signatures.len();
let signature_information = help
.active_signature
.and_then(|active_signature| help.signatures.get(active_signature as usize))
.or_else(|| help.signatures.first())?;
let str_for_join = ", ";
let parameter_length = signature_information
.parameters
.as_ref()
.map_or(0, |parameters| parameters.len());
let mut highlight_start = 0;
let (markdown, mut highlights): (Vec<_>, Vec<_>) = signature_information
.parameters
.as_ref()?
.iter()
.enumerate()
.map(|(i, parameter_information)| {
let label = match parameter_information.label.clone() {
lsp::ParameterLabel::Simple(string) => string,
lsp::ParameterLabel::LabelOffsets(offset) => signature_information
.label
.chars()
.skip(offset[0] as usize)
.take((offset[1] - offset[0]) as usize)
.collect::<String>(),
};
let label_length = label.len();
let highlights = help.active_parameter.and_then(|active_parameter| {
if i == active_parameter as usize {
Some((
highlight_start..(highlight_start + label_length),
SIGNATURE_HELP_HIGHLIGHT_CURRENT,
))
} else {
None
}
});
if i != parameter_length {
highlight_start += label_length + str_for_join.len();
}
(label, highlights)
})
.unzip();
if markdown.is_empty() {
None
} else {
let markdown = markdown.join(str_for_join);
let language_name = language
.map(|n| n.name().to_lowercase())
.unwrap_or_default();
let markdown = if function_options_count >= 2 {
let suffix = format!("(+{} overload)", function_options_count - 1);
let highlight_start = markdown.len() + 1;
highlights.push(Some((
highlight_start..(highlight_start + suffix.len()),
SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
)));
format!("```{language_name}\n{markdown} {suffix}")
} else {
format!("```{language_name}\n{markdown}")
};
Some(Self {
markdown,
highlights: highlights.into_iter().flatten().collect(),
original_data: help,
})
}
}
}
pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp {
proto::SignatureHelp {
signatures: lsp_help
.signatures
.into_iter()
.map(|signature| proto::SignatureInformation {
label: signature.label,
documentation: signature
.documentation
.map(|documentation| lsp_to_proto_documentation(documentation)),
parameters: signature
.parameters
.unwrap_or_default()
.into_iter()
.map(|parameter_info| proto::ParameterInformation {
label: Some(match parameter_info.label {
lsp::ParameterLabel::Simple(label) => {
proto::parameter_information::Label::Simple(label)
}
lsp::ParameterLabel::LabelOffsets(offsets) => {
proto::parameter_information::Label::LabelOffsets(
proto::LabelOffsets {
start: offsets[0],
end: offsets[1],
},
)
}
}),
documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
})
.collect(),
active_parameter: signature.active_parameter,
})
.collect(),
active_signature: lsp_help.active_signature,
active_parameter: lsp_help.active_parameter,
}
}
fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
proto::Documentation {
content: Some(match documentation {
lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
lsp::Documentation::MarkupContent(content) => {
proto::documentation::Content::MarkupContent(proto::MarkupContent {
is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
value: content.value,
})
}
}),
}
}
pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
lsp::SignatureHelp {
signatures: proto_help
.signatures
.into_iter()
.map(|signature| lsp::SignatureInformation {
label: signature.label,
documentation: signature.documentation.and_then(proto_to_lsp_documentation),
parameters: Some(
signature
.parameters
.into_iter()
.filter_map(|parameter_info| {
Some(lsp::ParameterInformation {
label: match parameter_info.label? {
proto::parameter_information::Label::Simple(string) => {
lsp::ParameterLabel::Simple(string)
}
proto::parameter_information::Label::LabelOffsets(offsets) => {
lsp::ParameterLabel::LabelOffsets([
offsets.start,
offsets.end,
])
}
},
documentation: parameter_info
.documentation
.and_then(proto_to_lsp_documentation),
})
})
.collect(),
),
active_parameter: signature.active_parameter,
})
.collect(),
active_signature: proto_help.active_signature,
active_parameter: proto_help.active_parameter,
}
}
fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
let documentation = {
Some(match documentation.content? {
documentation::Content::Value(string) => lsp::Documentation::String(string),
documentation::Content::MarkupContent(markup) => {
lsp::Documentation::MarkupContent(if markup.is_markdown {
lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: markup.value,
}
} else {
lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: markup.value,
}
})
}
})
};
documentation
}
#[cfg(test)]
mod tests {
use crate::lsp_command::signature_help::{
SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
};
#[test]
fn test_create_signature_help_markdown_string_1() {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nfoo: u8, bar: &str".to_string(),
vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_2() {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(1),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nfoo: u8, bar: &str".to_string(),
vec![(9..18, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_3() {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
label: "fn test1(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test2(hoge: String, fuga: bool)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
],
active_signature: Some(0),
active_parameter: Some(0),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nfoo: u8, bar: &str (+1 overload)".to_string(),
vec![
(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
(19..32, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_4() {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
label: "fn test1(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test2(hoge: String, fuga: bool)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
],
active_signature: Some(1),
active_parameter: Some(0),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
vec![
(0..12, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_5() {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
label: "fn test1(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test2(hoge: String, fuga: bool)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
],
active_signature: Some(1),
active_parameter: Some(1),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
vec![
(14..24, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_6() {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
label: "fn test1(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test2(hoge: String, fuga: bool)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
],
active_signature: Some(1),
active_parameter: None,
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
vec![(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_7() {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
label: "fn test1(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test2(hoge: String, fuga: bool)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
lsp::SignatureInformation {
label: "fn test3(one: usize, two: u32)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("one: usize".to_string()),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("two: u32".to_string()),
documentation: None,
},
]),
active_parameter: None,
},
],
active_signature: Some(2),
active_parameter: Some(1),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\none: usize, two: u32 (+2 overload)".to_string(),
vec![
(12..20, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
(21..34, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
]
)
);
}
#[test]
fn test_create_signature_help_markdown_string_8() {
let signature_help = lsp::SignatureHelp {
signatures: vec![],
active_signature: None,
active_parameter: None,
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_none());
}
#[test]
fn test_create_signature_help_markdown_string_9() {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
documentation: None,
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::LabelOffsets([8, 15]),
documentation: None,
},
lsp::ParameterInformation {
label: lsp::ParameterLabel::LabelOffsets([17, 26]),
documentation: None,
},
]),
active_parameter: None,
}],
active_signature: Some(0),
active_parameter: Some(0),
};
let maybe_markdown = SignatureHelp::new(signature_help, None);
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
let markdown = (markdown.markdown, markdown.highlights);
assert_eq!(
markdown,
(
"```\nfoo: u8, bar: &str".to_string(),
vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
)
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,23 @@ pub struct ProjectSettings {
/// Configuration for Git-related features
#[serde(default)]
pub git: GitSettings,
/// Configuration for how direnv configuration should be loaded
#[serde(default)]
pub load_direnv: DirenvSettings,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DirenvSettings {
/// Load direnv configuration through a shell hook
#[default]
ShellHook,
/// Load direnv configuration directly using `direnv export json`
///
/// Warning: This option is experimental and might cause some inconsistent behaviour compared to using the shell hook.
/// If it does, please report it to GitHub
Direct,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -322,6 +322,12 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
lsp::TextDocumentSyncOptions {
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
..Default::default()
},
)),
..Default::default()
},
..Default::default()
@@ -336,6 +342,12 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
lsp::TextDocumentSyncOptions {
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
..Default::default()
},
)),
..Default::default()
},
..Default::default()
@@ -3056,15 +3068,8 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
});
});
let remote = cx.update(|cx| {
Worktree::remote(
0,
1,
metadata,
Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
cx,
)
});
let remote =
cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
cx.executor().run_until_parked();

View File

@@ -180,6 +180,7 @@ impl Project {
settings.max_scroll_history_lines,
window,
completion_tx,
cx,
)
.map(|builder| {
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));

177
crates/project/src/yarn.rs Normal file
View File

@@ -0,0 +1,177 @@
//! This module deals with everything related to path handling for Yarn, the package manager for Web ecosystem.
//! Yarn is a bit peculiar, because it references paths within .zip files, which we obviously can't handle.
//! It also uses virtual paths for peer dependencies.
//!
//! Long story short, before we attempt to resolve a path as a "real" path, we try to treat is as a yarn path;
//! for .zip handling, we unpack the contents into the temp directory (yes, this is bad, against the spirit of Yarn and what-not)
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::Result;
use collections::HashMap;
use fs::Fs;
use gpui::{AppContext, Context, Model, ModelContext, Task};
use util::ResultExt;
pub(crate) struct YarnPathStore {
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
fs: Arc<dyn Fs>,
}
/// Returns `None` when passed path is a malformed virtual path or it's not a virtual path at all.
fn resolve_virtual(path: &Path) -> Option<Arc<Path>> {
let components: Vec<_> = path.components().collect();
let mut non_virtual_path = PathBuf::new();
let mut i = 0;
let mut is_virtual = false;
while i < components.len() {
if let Some(os_str) = components[i].as_os_str().to_str() {
// Detect the __virtual__ segment
if os_str == "__virtual__" {
let pop_count = components
.get(i + 2)?
.as_os_str()
.to_str()?
.parse::<usize>()
.ok()?;
// Apply dirname operation pop_count times
for _ in 0..pop_count {
non_virtual_path.pop();
}
i += 3; // Skip hash and pop_count components
is_virtual = true;
continue;
}
}
non_virtual_path.push(&components[i]);
i += 1;
}
is_virtual.then(|| Arc::from(non_virtual_path))
}
impl YarnPathStore {
pub(crate) fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Model<Self> {
cx.new_model(|_| Self {
temp_dirs: Default::default(),
fs,
})
}
pub(crate) fn process_path(
&mut self,
path: &Path,
protocol: &str,
cx: &ModelContext<Self>,
) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
let mut is_zip = protocol.eq("zip");
let path: &Path = if let Some(non_zip_part) = path
.as_os_str()
.as_encoded_bytes()
.strip_prefix("/zip:".as_bytes())
{
// typescript-language-server prepends the paths with zip:, which is messy.
is_zip = true;
Path::new(OsStr::new(
std::str::from_utf8(non_zip_part).expect("Invalid UTF-8"),
))
} else {
path
};
let as_virtual = resolve_virtual(&path);
let Some(path) = as_virtual.or_else(|| is_zip.then(|| Arc::from(path))) else {
return Task::ready(None);
};
if let Some(zip_file) = zip_path(&path) {
let zip_file: Arc<Path> = Arc::from(zip_file);
cx.spawn(|this, mut cx| async move {
let dir = this
.update(&mut cx, |this, _| {
this.temp_dirs
.get(&zip_file)
.map(|temp| temp.path().to_owned())
})
.ok()?;
let zip_root = if let Some(dir) = dir {
dir
} else {
let fs = this.update(&mut cx, |this, _| this.fs.clone()).ok()?;
let tempdir = dump_zip(zip_file.clone(), fs).await.log_err()?;
let new_path = tempdir.path().to_owned();
this.update(&mut cx, |this, _| {
this.temp_dirs.insert(zip_file.clone(), tempdir);
})
.ok()?;
new_path
};
// Rebase zip-path onto new temp path.
let as_relative = path.strip_prefix(zip_file).ok()?.into();
Some((zip_root.into(), as_relative))
})
} else {
Task::ready(None)
}
}
}
fn zip_path(path: &Path) -> Option<&Path> {
let path_str = path.to_str()?;
let zip_end = path_str.find(".zip/")?;
let zip_path = &path_str[..zip_end + 4]; // ".zip" is 4 characters long
Some(Path::new(zip_path))
}
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
let contents = fs.load_bytes(&path).await?;
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
Ok(dir)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_resolve_virtual() {
let test_cases = vec![
(
"/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
),
(
"/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
),
(
"/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
Some(Path::new("/path/to/some/subpath/to/file.dat")),
),
(
"/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
Some(Path::new("/path/subpath/to/file.dat")),
),
("/path/to/nonvirtual/", None),
("/path/to/malformed/__virtual__", None),
("/path/to/malformed/__virtual__/a0b1c2d3", None),
(
"/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
None,
),
];
for (input, expected) in test_cases {
let input_path = Path::new(input);
let resolved_path = resolve_virtual(input_path);
assert_eq!(resolved_path.as_deref(), expected);
}
}
}

View File

@@ -2293,7 +2293,7 @@ impl ProjectPanel {
.right_0()
.top_0()
.bottom_0()
.w_3()
.w(px(12.))
.cursor_default()
.child(ProjectPanelScrollbar::new(
percentage as f32..end_offset as f32,

View File

@@ -19,6 +19,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
prost.workspace = true
serde.workspace = true

View File

@@ -203,6 +203,10 @@ message Envelope {
CompleteWithLanguageModel complete_with_language_model = 166;
LanguageModelResponse language_model_response = 167;
CacheLanguageModelContent cache_language_model_content = 219;
CacheLanguageModelContentResponse cache_language_model_content_response = 220; // current max
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
CountTokensResponse count_tokens_response = 169;
GetCachedEmbeddings get_cached_embeddings = 189;
@@ -262,7 +266,10 @@ message Envelope {
OpenContextResponse open_context_response = 213;
UpdateContext update_context = 214;
SynchronizeContexts synchronize_contexts = 215;
SynchronizeContextsResponse synchronize_contexts_response = 216; // current max
SynchronizeContextsResponse synchronize_contexts_response = 216;
GetSignatureHelp get_signature_help = 217;
GetSignatureHelpResponse get_signature_help_response = 218;
}
reserved 158 to 161;
@@ -934,6 +941,55 @@ message GetCodeActionsResponse {
repeated VectorClockEntry version = 2;
}
message GetSignatureHelp {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetSignatureHelpResponse {
optional SignatureHelp signature_help = 1;
}
message SignatureHelp {
repeated SignatureInformation signatures = 1;
optional uint32 active_signature = 2;
optional uint32 active_parameter = 3;
}
message SignatureInformation {
string label = 1;
optional Documentation documentation = 2;
repeated ParameterInformation parameters = 3;
optional uint32 active_parameter = 4;
}
message Documentation {
oneof content {
string value = 1;
MarkupContent markup_content = 2;
}
}
enum MarkupKind {
PlainText = 0;
Markdown = 1;
}
message ParameterInformation {
oneof label {
string simple = 1;
LabelOffsets label_offsets = 2;
}
optional Documentation documentation = 3;
}
message LabelOffsets {
uint32 start = 1;
uint32 end = 2;
}
message GetHover {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -1973,6 +2029,25 @@ message CompleteWithLanguageModel {
float temperature = 4;
repeated ChatCompletionTool tools = 5;
optional string tool_choice = 6;
repeated string cached_contents = 7;
}
message CacheLanguageModelContent {
string model = 1;
repeated LanguageModelRequestMessage messages = 2;
repeated string stop = 3;
float temperature = 4;
repeated ChatCompletionTool tools = 5;
optional string tool_choice = 6;
optional uint64 ttl_seconds = 7;
}
message CacheLanguageModelContentResponse {
string name = 1;
uint64 create_time = 2;
uint64 update_time = 3;
optional uint64 expire_time = 4;
uint32 total_token_count = 5;
}
// A tool presented to the language model for its use
@@ -2135,6 +2210,7 @@ message MultiLspQuery {
oneof request {
GetHover get_hover = 5;
GetCodeActions get_code_actions = 6;
GetSignatureHelp get_signature_help = 7;
}
}
@@ -2153,6 +2229,7 @@ message LspResponse {
oneof response {
GetHoverResponse get_hover_response = 1;
GetCodeActionsResponse get_code_actions_response = 2;
GetSignatureHelpResponse get_signature_help_response = 3;
}
}

View File

@@ -7,18 +7,19 @@ mod typed_envelope;
pub use error::*;
pub use typed_envelope::*;
use anyhow::anyhow;
use collections::HashMap;
use futures::{future::BoxFuture, Future};
pub use prost::{DecodeError, Message};
use serde::Serialize;
use std::any::{Any, TypeId};
use std::time::Instant;
use std::{
any::{Any, TypeId},
cmp,
fmt::Debug,
iter,
time::{Duration, SystemTime, UNIX_EPOCH},
fmt::{self, Debug},
iter, mem,
sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use std::{fmt, mem};
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
@@ -59,6 +60,51 @@ pub enum MessagePriority {
Background,
}
pub trait ProtoClient: Send + Sync {
fn request(
&self,
envelope: Envelope,
request_type: &'static str,
) -> BoxFuture<'static, anyhow::Result<Envelope>>;
fn send(&self, envelope: Envelope) -> anyhow::Result<()>;
}
#[derive(Clone)]
pub struct AnyProtoClient(Arc<dyn ProtoClient>);
impl<T> From<Arc<T>> for AnyProtoClient
where
T: ProtoClient + 'static,
{
fn from(client: Arc<T>) -> Self {
Self(client)
}
}
impl AnyProtoClient {
pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
Self(client)
}
pub fn request<T: RequestMessage>(
&self,
request: T,
) -> impl Future<Output = anyhow::Result<T::Response>> {
let envelope = request.into_envelope(0, None, None);
let response = self.0.request(envelope, T::NAME);
async move {
T::Response::from_envelope(response.await?)
.ok_or_else(|| anyhow!("received response of the wrong type"))
}
}
pub fn send<T: EnvelopedMessage>(&self, request: T) -> anyhow::Result<()> {
let envelope = request.into_envelope(0, None, None);
self.0.send(envelope)
}
}
impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
fn payload_type_id(&self) -> TypeId {
TypeId::of::<T>()
@@ -148,6 +194,8 @@ messages!(
(ApplyCompletionAdditionalEditsResponse, Background),
(BufferReloaded, Foreground),
(BufferSaved, Foreground),
(CacheLanguageModelContent, Background),
(CacheLanguageModelContentResponse, Background),
(Call, Foreground),
(CallCanceled, Foreground),
(CancelCall, Foreground),
@@ -204,6 +252,8 @@ messages!(
(GetProjectSymbolsResponse, Background),
(GetReferences, Background),
(GetReferencesResponse, Background),
(GetSignatureHelp, Background),
(GetSignatureHelpResponse, Background),
(GetSupermavenApiKey, Background),
(GetSupermavenApiKeyResponse, Background),
(GetTypeDefinition, Background),
@@ -352,6 +402,7 @@ request_messages!(
ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse
),
(CacheLanguageModelContent, CacheLanguageModelContentResponse),
(Call, Ack),
(CancelCall, Ack),
(CopyProjectEntry, ProjectEntryResponse),
@@ -382,6 +433,7 @@ request_messages!(
(GetPrivateUserInfo, GetPrivateUserInfoResponse),
(GetProjectSymbols, GetProjectSymbolsResponse),
(GetReferences, GetReferencesResponse),
(GetSignatureHelp, GetSignatureHelpResponse),
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
(GetTypeDefinition, GetTypeDefinitionResponse),
(LinkedEditingRange, LinkedEditingRangeResponse),
@@ -482,6 +534,7 @@ entity_messages!(
GetHover,
GetProjectSymbols,
GetReferences,
GetSignatureHelp,
GetTypeDefinition,
InlayHints,
JoinProject,

View File

@@ -102,18 +102,21 @@ impl Render for QuickActionBar {
inlay_hints_enabled,
supports_inlay_hints,
git_blame_inline_enabled,
auto_signature_help_enabled,
) = {
let editor = editor.read(cx);
let selection_menu_enabled = editor.selection_menu_enabled(cx);
let inlay_hints_enabled = editor.inlay_hints_enabled();
let supports_inlay_hints = editor.supports_inlay_hints(cx);
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
(
selection_menu_enabled,
inlay_hints_enabled,
supports_inlay_hints,
git_blame_inline_enabled,
auto_signature_help_enabled,
)
};
@@ -265,6 +268,23 @@ impl Render for QuickActionBar {
},
);
menu = menu.toggleable_entry(
"Auto Signature Help",
auto_signature_help_enabled,
Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_auto_signature_help_menu(
&editor::actions::ToggleAutoSignatureHelp,
cx,
);
});
}
},
);
menu
});
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {

View File

@@ -114,25 +114,31 @@ impl DevServerProjects {
cx.notify();
});
let mut base_style = cx.text_style();
base_style.refine(&gpui::TextStyleRefinement {
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
});
let markdown_style = MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
base_text_style: base_style,
code_block: gpui::StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
..Default::default()
}),
..Default::default()
},
inline_code: Default::default(),
block_quote: Default::default(),
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
..Default::default()
},
rule_color: Default::default(),
block_quote_border_color: Default::default(),
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
};
let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
let markdown =
cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
Self {
mode: Mode::Default(None),

View File

@@ -24,6 +24,7 @@ futures.workspace = true
image.workspace = true
language.workspace = true
log.workspace = true
multi_buffer.workspace = true
project.workspace = true
runtimelib.workspace = true
schemars.workspace = true

View File

@@ -11,7 +11,8 @@ use gpui::{
actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
};
use language::Point;
use language::{Language, Point};
use multi_buffer::MultiBufferRow;
use project::Fs;
use settings::{Settings as _, SettingsStore};
use std::{ops::Range, sync::Arc};
@@ -174,34 +175,14 @@ impl RuntimePanel {
let range = if selection.is_empty() {
let cursor = selection.head();
let line_start = multi_buffer_snapshot.offset_to_point(cursor).row;
let mut start_offset = multi_buffer_snapshot.point_to_offset(Point::new(line_start, 0));
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
// Iterate backwards to find the start of the line
while start_offset > 0 {
let ch = multi_buffer_snapshot
.chars_at(start_offset - 1)
.next()
.unwrap_or('\0');
if ch == '\n' {
break;
}
start_offset -= 1;
}
let mut end_offset = cursor;
// Iterate forwards to find the end of the line
while end_offset < multi_buffer_snapshot.len() {
let ch = multi_buffer_snapshot
.chars_at(end_offset)
.next()
.unwrap_or('\0');
if ch == '\n' {
break;
}
end_offset += 1;
}
let end_point = Point::new(
cursor_row,
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
);
let end_offset = start_offset.saturating_add(end_point.column as usize);
// Create a range from the start to the end of the line
start_offset..end_offset
@@ -216,7 +197,7 @@ impl RuntimePanel {
&self,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Option<(String, Arc<str>, Range<Anchor>)> {
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
let editor = editor.upgrade()?;
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
@@ -226,30 +207,24 @@ impl RuntimePanel {
.text_for_range(anchor_range.clone())
.collect::<String>();
let start_language = buffer.language_at(anchor_range.start);
let end_language = buffer.language_at(anchor_range.end);
let language_name = if start_language == end_language {
start_language
.map(|language| language.code_fence_block_name())
.filter(|lang| **lang != *"markdown")?
} else {
// If the selection spans multiple languages, don't run it
let start_language = buffer.language_at(anchor_range.start)?;
let end_language = buffer.language_at(anchor_range.end)?;
if start_language != end_language {
return None;
};
}
Some((selected_text, language_name, anchor_range))
Some((selected_text, start_language.clone(), anchor_range))
}
pub fn language(
&self,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Option<Arc<str>> {
match self.snippet(editor, cx) {
Some((_, language, _)) => Some(language),
None => None,
}
) -> Option<Arc<Language>> {
let editor = editor.upgrade()?;
let selection = editor.read(cx).selections.newest::<usize>(cx);
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
buffer.language_at(selection.head()).cloned()
}
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
@@ -266,11 +241,12 @@ impl RuntimePanel {
pub fn kernelspec(
&self,
language_name: &str,
language: &Language,
cx: &mut ViewContext<Self>,
) -> Option<KernelSpecification> {
let settings = JupyterSettings::get_global(cx);
let selected_kernel = settings.kernel_selections.get(language_name);
let language_name = language.code_fence_block_name();
let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
self.kernel_specifications
.iter()
@@ -296,7 +272,7 @@ impl RuntimePanel {
return Ok(());
}
let (selected_text, language_name, anchor_range) = match self.snippet(editor.clone(), cx) {
let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
Some(snippet) => snippet,
None => return Ok(()),
};
@@ -304,8 +280,8 @@ impl RuntimePanel {
let entity_id = editor.entity_id();
let kernel_specification = self
.kernelspec(&language_name, cx)
.with_context(|| format!("No kernel found for language: {language_name}"))?;
.kernelspec(&language, cx)
.with_context(|| format!("No kernel found for language: {}", language.name()))?;
let session = self.sessions.entry(entity_id).or_insert_with(|| {
let view =
@@ -320,7 +296,6 @@ impl RuntimePanel {
panel.sessions.remove(&shutdown_event.entity_id());
}
}
//
},
);
@@ -350,7 +325,7 @@ impl RuntimePanel {
pub enum SessionSupport {
ActiveSession(View<Session>),
Inactive(KernelSpecification),
RequiresSetup(String),
RequiresSetup(Arc<str>),
Unsupported,
}
@@ -377,11 +352,12 @@ impl RuntimePanel {
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
None => {
let language: String = language.to_lowercase();
// If no kernelspec but language is one of typescript, python, r, or julia
// If no kernelspec but language is one of typescript or python
// then we return RequiresSetup
match language.as_str() {
"typescript" | "python" => SessionSupport::RequiresSetup(language),
match language.name().as_ref() {
"TypeScript" | "Python" => {
SessionSupport::RequiresSetup(language.name())
}
_ => SessionSupport::Unsupported,
}
}

View File

@@ -7,10 +7,13 @@ use editor::{
display_map::{
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
},
Anchor, AnchorRangeExt as _, Editor,
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
};
use futures::{FutureExt as _, StreamExt as _};
use gpui::{div, prelude::*, EventEmitter, Render, Task, View, ViewContext, WeakView};
use gpui::{
div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
};
use language::Point;
use project::Fs;
use runtimelib::{
ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest,
@@ -27,11 +30,13 @@ pub struct Session {
blocks: HashMap<String, EditorBlock>,
pub messaging_task: Task<()>,
pub kernel_specification: KernelSpecification,
_buffer_subscription: Subscription,
}
struct EditorBlock {
editor: WeakView<Editor>,
code_range: Range<Anchor>,
invalidation_anchor: Anchor,
block_id: BlockId,
execution_view: View<ExecutionView>,
}
@@ -45,7 +50,25 @@ impl EditorBlock {
) -> anyhow::Result<Self> {
let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
let block_id = editor.update(cx, |editor, cx| {
let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
let buffer = editor.buffer().clone();
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let end_point = code_range.end.to_point(&buffer_snapshot);
let next_row_start = end_point + Point::new(1, 0);
if next_row_start > buffer_snapshot.max_point() {
buffer.update(cx, |buffer, cx| {
buffer.edit(
[(
buffer_snapshot.max_point()..buffer_snapshot.max_point(),
"\n",
)],
None,
cx,
)
});
}
let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
let block = BlockProperties {
position: code_range.end,
height: execution_view.num_lines(cx).saturating_add(1),
@@ -54,12 +77,14 @@ impl EditorBlock {
disposition: BlockDisposition::Below,
};
editor.insert_blocks([block], None, cx)[0]
let block_id = editor.insert_blocks([block], None, cx)[0];
(block_id, invalidation_anchor)
})?;
anyhow::Ok(Self {
editor,
code_range,
invalidation_anchor,
block_id,
execution_view,
})
@@ -179,15 +204,55 @@ impl Session {
})
.shared();
let subscription = match editor.upgrade() {
Some(editor) => {
let buffer = editor.read(cx).buffer().clone();
cx.subscribe(&buffer, Self::on_buffer_event)
}
None => Subscription::new(|| {}),
};
return Self {
editor,
kernel: Kernel::StartingKernel(pending_kernel),
messaging_task: Task::ready(()),
blocks: HashMap::default(),
kernel_specification,
_buffer_subscription: subscription,
};
}
fn on_buffer_event(
&mut self,
buffer: Model<MultiBuffer>,
event: &multi_buffer::Event,
cx: &mut ViewContext<Self>,
) {
if let multi_buffer::Event::Edited { .. } = event {
let snapshot = buffer.read(cx).snapshot(cx);
let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
self.blocks.retain(|_id, block| {
if block.invalidation_anchor.is_valid(&snapshot) {
true
} else {
blocks_to_remove.insert(block.block_id);
false
}
});
if !blocks_to_remove.is_empty() {
self.editor
.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, None, cx);
})
.ok();
cx.notify();
}
}
}
fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
match &mut self.kernel {
Kernel::RunningKernel(kernel) => {

View File

@@ -557,6 +557,14 @@ impl Peer {
Ok(())
}
pub fn send_dynamic(&self, receiver_id: ConnectionId, message: proto::Envelope) -> Result<()> {
let connection = self.connection_state(receiver_id)?;
connection
.outgoing_tx
.unbounded_send(proto::Message::Envelope(message))?;
Ok(())
}
pub fn forward_send<T: EnvelopedMessage>(
&self,
sender_id: ConnectionId,

View File

@@ -9,7 +9,7 @@ use any_vec::AnyVec;
use collections::HashMap;
use editor::{
actions::{Tab, TabPrev},
DisplayPoint, Editor, EditorElement, EditorStyle,
DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
};
use futures::channel::oneshot;
use gpui::{
@@ -777,6 +777,15 @@ impl BufferSearchBar {
.get(&searchable_item.downgrade())
.filter(|matches| !matches.is_empty())
{
// If 'wrapscan' is disabled, searches do not wrap around the end of the file.
if !EditorSettings::get_global(cx).search_wrap {
if (direction == Direction::Next && index + count >= matches.len())
|| (direction == Direction::Prev && index < count)
{
crate::show_no_more_matches(cx);
return;
}
}
let new_match_index = searchable_item
.match_index_for_direction(matches, index, direction, count, cx);

View File

@@ -8,7 +8,8 @@ use editor::{
actions::SelectAll,
items::active_match_index,
scroll::{Autoscroll, Axis},
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN,
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer,
MAX_TAB_TITLE_LEN,
};
use gpui::{
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
@@ -143,7 +144,6 @@ pub struct ProjectSearchView {
panels_with_errors: HashSet<InputPanel>,
active_match_index: Option<usize>,
search_id: usize,
query_editor_was_focused: bool,
included_files_editor: View<Editor>,
excluded_files_editor: View<Editor>,
filters_enabled: bool,
@@ -714,7 +714,6 @@ impl ProjectSearchView {
search_options: options,
panels_with_errors: HashSet::default(),
active_match_index: None,
query_editor_was_focused: false,
included_files_editor,
excluded_files_editor,
filters_enabled,
@@ -970,6 +969,16 @@ impl ProjectSearchView {
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let match_ranges = self.model.read(cx).match_ranges.clone();
if !EditorSettings::get_global(cx).search_wrap {
if (direction == Direction::Next && index + 1 >= match_ranges.len())
|| (direction == Direction::Prev && index == 0)
{
crate::show_no_more_matches(cx);
return;
}
}
let new_index = self.results_editor.update(cx, |editor, cx| {
editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
});
@@ -989,7 +998,6 @@ impl ProjectSearchView {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&SelectAll, cx);
});
self.query_editor_was_focused = true;
let editor_handle = self.query_editor.focus_handle(cx);
cx.focus(&editor_handle);
}
@@ -1004,7 +1012,6 @@ impl ProjectSearchView {
let cursor = query_editor.selections.newest_anchor().head();
query_editor.change_selections(None, cx, |s| s.select_ranges([cursor..cursor]));
});
self.query_editor_was_focused = false;
let results_handle = self.results_editor.focus_handle(cx);
cx.focus(&results_handle);
}

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