Compare commits

...

84 Commits

Author SHA1 Message Date
Antonio Scandurra
3fbab23daa WIP 2024-02-05 11:09:10 -07:00
Antonio Scandurra
d742b3bfac Mark the window as dirty when first opening it (#7384)
Otherwise we won't display anything if the window never notifies.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-02-05 09:09:49 -07:00
Julia
3bf412feff Use window's screen rather than window itself to start display link
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-02-05 11:03:23 -05:00
Marshall Bowers
8030e8cf47 Improve the contrast of the default search_match_background colors (#7382)
This PR improves the contrast of the default `search_match_background`
colors.

Release Notes:

- Improved the contrast of the default `search.match_background` colors.
2024-02-05 10:45:41 -05:00
Kirill Bulatov
45429a4528 Fix inlay hints using stale editor data (#7376)
Refactor the hint query code to pass along an actual `cx` instead of its
potentially stale, cloned version.

Release Notes:

- Fixed occasional duplicate hints inserted and offset-related panics
when concurrently editing the buffer and querying multiple its ranges
for hints
2024-02-05 15:46:48 +02:00
Piotr Osiewicz
0755ce6486 picker: Outline Editor::new
That way crates that use Picker::new do not have to codegen constructor of Editor; tl;dr, 10% of LLVM shaved off of crates like vcs_menu or theme_selector in release mode.
2024-02-05 14:01:06 +01:00
Thorsten Ball
87d3f59515 Do not show completion documentation if disabled (#7372)
This fixes #7348 by not rendering completions if they are disabled in
the settings. Previously we only checked that setting when starting or
not starting background threads to fetch documentation. But in case we
already have documentation, this stops it from being rendered.

Release Notes:

- Fixed documentation in completions showing up even when disabled via
`show_completion_documentation`
([#7348](https://github.com/zed-industries/zed/issues/7348))
2024-02-05 11:43:16 +01:00
Andrey Kuzmin
ac74a72a9e Improve elm-language-server configuration (#7342)
Hi folks! @absynce and I paired a bit to improve the
`elm-language-server` configuration. We have realised that sometimes
`elm-language-server` settings were being reset to default. We had been
configuring `elm-language-server` like this:

```json
"lsp": {
  "elm-language-server": {
    "initialization_options": {
      "disableElmLSDiagnostics": true,
      "onlyUpdateDiagnosticsOnSave": true,
      "elmReviewDiagnostics": "warning"
    }
  }
}
```

And then we noticed that the following communication happened:

```
// Send:
{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{}}}
// Receive:
{"jsonrpc":"2.0","id":5,"method":"workspace/configuration","params":{"items":[{"section":"elmLS"}]}}
// Send:
{"jsonrpc":"2.0","id":5,"result":[null],"error":null}
```

In `elm-language-server` the settings from `didChangeConfiguration`
[replace the initial
settings](edd6813388/src/common/providers/diagnostics/diagnosticsProvider.ts (L188)).
Setting the value to `{}` effectively resets the configuration options
to defaults.

In Zed, `initialization_options` and `workspace_configuration` are two
different things, but in `elm-language-server` they are coupled.
Additionally, `elm-language-server` is requesting workspace
configuration for the `elmLS` section that doesn't exist.

This PR: 

1. Fixes settings reset on `didChangeConfiguration` by populating
`workspace_configuration` from `initialization_options`
2. Makes workspace configuration requests work by inserting an extra
copy of the settings under the `elmLS` key in `workspace_configuration`
— this is a bit ugly, but we're not sure how to make both kinds of
configuration messages work in the current setup.

This is how communication looks like after the proposed changes:

```
// Send:
{
  "jsonrpc": "2.0",
  "method": "workspace/didChangeConfiguration",
  "params": {
    "settings": {
      "disableElmLSDiagnostics": true,
      "onlyUpdateDiagnosticsOnSave": true,
      "elmReviewDiagnostics": "warning",
      "elmLS": {
        "disableElmLSDiagnostics": true,
        "onlyUpdateDiagnosticsOnSave": true,
        "elmReviewDiagnostics": "warning"
      }
    }
  }
}
// Receive:
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "workspace/configuration",
  "params": {
    "items": [
      {
        "section": "elmLS"
      }
    ]
  }
}
// Send:
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": [
    {
      "disableElmLSDiagnostics": true,
      "onlyUpdateDiagnosticsOnSave": true,
      "elmReviewDiagnostics": "warning"
    }
  ],
  "error": null
}
```

Things we have considered:

1. Extracting the `elm-language-server` settings into a separate
section: we haven't found this being widely used in Zed, seems that all
language server configuration should fall under the top level `lsp`
section
2. Changing the way `elm-language-server` configuration works:
`elm-language-server` has got integrations with multiple editors,
changing the configuration behaviour would mean updating all the
existing integrations. Plus we are not exactly sure if it's doing
anything wrong.

Release Notes:

- Improved elm-language-server configuration options

Co-authored-by: Jared M. Smith <absynce@gmail.com>
2024-02-04 01:57:24 +02:00
Antonio Scandurra
ae2c23bd8e Reintroduce ProMotion support (#7347)
This re-introduces the changes of #7305 but this time we create a
display link using the `NSScreen` associated with the window. We're
hoping we'll get these frame requests more reliably, and this seems
supported by the fact that awakening my laptop restores the frame
requests.

Release Notes:

- See #7305.

Co-authored-by: Nathan <nathan@zed.dev>
2024-02-03 16:33:08 -07:00
Andrew Lygin
8da6e62914 Editor toolbar configuration (#7338)
Adds settings for hiding breadcrumbs and quick action bar from
the editor toolbar. If both elements are hidden, the toolbar disappears
completely.

Example:

```json
"toolbar": {
  "breadcrumbs": true,
  "quick_actions": false
}
```

- It intentionally doesn't hide breadcrumbs in other views (for
instance, in the search result window) because their usage there differ
from the main editor.
- The editor controls how breadcrumbs are displayed in the toolbar, so
implementation differs a bit for breadcrumbs and quick actions bar.

Release Notes:

- Added support for configuring the editor toolbar ([4756](https://github.com/zed-industries/zed/issues/4756))
2024-02-03 22:40:54 +02:00
Rashid Almheiri
55185c159b Support documentation as a resolvable property (#7306)
Closes #7288 

<img width="1800" alt="Screenshot 2024-02-03 at 01 56 14"
src="https://github.com/zed-industries/zed/assets/69181766/ed97ef30-c958-45c8-812c-a59bbbd02b19">

Release Notes:
- Improved LSP `completionItem/resolve` request by supporting `documentation` as a resolvable property
2024-02-03 22:35:57 +02:00
Kirill Bulatov
c9a53b63a7 Fix the compilation error (#7328)
Follow-up of https://github.com/zed-industries/zed/pull/7326

Release Notes:

- N/A
2024-02-03 18:52:03 +02:00
Kirill Bulatov
1ab0af2fa3 Revert the commit that broke Zed display capabilities (#7326) 2024-02-03 18:32:56 +02:00
Robin Pfäffle
06674a21f9 Add support for relative terminal links (#7303)
Allow opening file paths relative to terminal's cwd


https://github.com/zed-industries/zed/assets/67913738/413a1107-541e-4c25-ae7c-cbe45469d452


Release Notes:

- Added support for opening file paths relative to terminal's cwd
([#7144](https://github.com/zed-industries/zed/issues/7144)).

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-02-03 17:04:27 +02:00
Antonio Scandurra
54aecd21ec Remove unnecessary focus_invalidated field (#7320)
I believe at some point this was used for tests but it doesn't seem
necessary anymore.

Release Notes:

- N/A
2024-02-03 05:41:58 -07:00
Antonio Scandurra
c906fd232d Reduce GPU memory usage (#7319)
This pull request decreases the size of each instance buffer and shares
instance buffers across windows.

Release Notes:

- Improved GPU memory usage.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-02-03 05:27:08 -07:00
Rashid Almheiri
d08d4174a5 Modify the default tab size of OCaml & OCaml Interface to 2 (#7315)
Thanks @pseudomata for the heads up.

Release Notes:
- N/A
2024-02-03 12:04:15 +02:00
Conrad Irwin
1a82470897 Open URLs with cmd-click (#7312)
Release Notes:

- Added ability to cmd-click on URLs in all buffers

---------

Co-authored-by: fdionisi <code@fdionisi.me>
2024-02-02 22:05:28 -07:00
Conrad Irwin
583273b6ee Bump alacritty to fix some panics (#7313)
Release Notes:

- Fixed some panics in the Terminal
([#6835](https://github.com/zed-industries/zed/issues/6835)).
2024-02-02 20:39:51 -07:00
Conrad Irwin
fcbc220408 Don't log errors on main (#7289)
Release Notes:

- N/A
2024-02-02 19:24:49 -07:00
Conrad Irwin
f09da1a1c8 vim hml (#7298)
- Add a setting for `vertical_scroll_offset`
- Fix H/M/L in vim with scrolloff



Release Notes:

- Added a settings for `vertical_scroll_offset`
- vim: Fix H/M/L with various values of vertical_scroll_offset

---------

Co-authored-by: Vbhavsar <vbhavsar@gmail.com>
Co-authored-by: fdionisi <code@fdionisi.me>
2024-02-02 19:24:36 -07:00
Tom Planche
e1efa7298e Update configuring_zed__key_bindings.md (#7310)
Added explanations for binding `null` to a keyboard binding.

Release Notes:

- N/A
2024-02-03 02:15:31 +02:00
WindSoilder
430f5d5d53 vim: Convert from visual mode to normal mode with a single click (#6985)
Release Notes:

- Fixed #4319

Here is a demo after the fix:

https://github.com/zed-industries/zed/assets/22256154/a690f146-73c9-4b0e-8527-e4faf690cca2

Actually I'm not really sure should I submit my idea to discussion,
since it's not a large change, and it's something like a bug fix. So I
directly create a pr here.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-02 16:44:47 -07:00
Antonio Scandurra
15edc46827 Maintain smooth frame rates when ProMotion and direct mode are enabled (#7305)
This is achieved by starting a `CADisplayLink` that will invoke the
`on_request_frame` callback at the refresh interval of the display.

We only actually draw frames when the window was dirty, or for 2 extra
seconds after the last input event to ensure ProMotion doesn't downclock
the refresh rate when the user is actively interacting with the window.

Release Notes:

- Improved performance when using a ProMotion display with fast key
repeat rates.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-02-02 16:42:46 -07:00
tomholford
f2ba969d5b Dismiss update notification when viewing releases notes (#7297)
After updating zed, a notification is shown in the bottom right with the
new version number, a link to the release notes, and an 'x' to dismiss
the dialog.

Before this PR, clicking the link to the release notes would not dismiss
the modal. So, a user returning to the IDE after viewing the notes in
the browser would still see the notification. With this change, clicking
'View release notes' also dismisses the notification.

Co-authored-by: tomholford <tomholford@users.noreply.github.com>

Release Notes:

- Made update notification to dismiss when viewing releases notes
2024-02-02 22:52:28 +02:00
Kirill Bulatov
2d58226a9b Avoid logging errors about missing themes dir (#7290) 2024-02-02 20:47:45 +02:00
Marshall Bowers
115f0672fb gpui: Add support for observing window appearance (#7294)
This PR adds support to GPUI for observing when the appearance of a
window changes.

Based on the initial work done in
https://github.com/zed-industries/zed/pull/6881.

Release Notes:

- N/A
2024-02-02 13:13:35 -05:00
Conrad Irwin
1f6bd6760f Fix main (#7293)
Release Notes:

- N/A
2024-02-02 12:21:45 -05:00
Antonio Scandurra
a0b52cc69a Scope line layout cache to each window (#7235)
This improves a performance problem we were observing when having
multiple windows updating at the same time, where each window would
invalidate the other window's layout cache.

Release Notes:

- Improved performance when having multiple Zed windows open.

Co-authored-by: Max Brunsfeld <max@zed.dev>
2024-02-02 09:11:20 -08:00
Marshall Bowers
5360c0ea28 theme_importer: Make VS Code theme parsing more lenient (#7292)
This PR updates the `theme_importer` to use `serde_json_lenient` to
parse VS Code themes.

This should allow us to parse themes that have trailing commas and such,
in addition to the comment support that we already had.

Release Notes:

- N/A
2024-02-02 12:09:05 -05:00
James Roberts
3995c22414 Use async-native-tls for websockets (#7254)
This change switches from using async_tungstenite::async_tls to
async_tungstenite::async_std with the async-native-tls feature.

The previous feature, async_tls, used async-tls which wraps rustls.
rustls bundles webpki-roots, which is a copy of Mozilla's root
certificates. These certificates are used by default, and manual
configuration is required to support custom certificates, such as those
required by web security gateways in enterprise environments.

Instead of introducing a new configuration option to Zed,
async-native-tls integrates with the platform-native certificate store
to support enterprise environments out-of-the-box. For MacOS, this adds
support for Security.framework TLS. This integration is provided through
openssl-sys, which is also the SSL certificate provider for isahc, the
library underlying Zed's HTTP client. Making websockets and HTTP
communications use the same SSL provider should keep Zed consistent
operations and make the project easier to maintain.



Release Notes:

- Fixed WebSocket communications using custom TLS certificates
([#4759](https://github.com/zed-industries/zed/issues/4759)).
2024-02-02 09:08:15 -08:00
Antonio Scandurra
659423a4a1 Use Mutex instead of a RefCell to acquire/release instance buffers (#7291)
This fixes a panic happening when releasing an instance buffer.
Releasing the buffer happens on a different thread but the borrow
checker was not catching it because the metal buffer completion handler
API doesn't have a `Send` marker on it.

Release Notes:

- N/A
2024-02-02 09:53:07 -07:00
Conrad Irwin
074acacdf7 Do more on channel join (#7268)
This change makes it so that if you are the first to join a channel,
your project is automatically shared.

It also makes it so that if you join a channel via a link and there are
no shared projects, you open the notes instead of an empty workspace
with nothing.

This is to try and address the discoverability of project sharing: we've
had
two reviews that have talked about channels, but not talked about
sharing
projects into them, which makes me suspect they didn't know about the
feature.

Release Notes:

- Added a setting `share_on_join` (defaulting to true). When set, and
you join an empty channel, your project is automatically shared.
2024-02-02 09:52:30 -07:00
Marshall Bowers
6f6cb53fad Tweak pull request template to (hopefully) make it clearer (#7287)
I keep seeing people leave the wrapping parentheses, like `(Added)` in
their release notes.

This PR tries making it clearer that we only want *one* of "Added",
"Fixed", or "Improved".

Release Notes:

- N/A
2024-02-02 11:41:47 -05:00
Thorsten Ball
3d76ed96f5 Use command_buffer.wait_until_scheduled in metal renderer (#7283)
This commit goes back to using `wait_until_scheduled` as opposed to
`wait_until_completed`. What this means, however, is that another draw
could take place before the previous one finished. When that happens we
don't want to reuse the same instance buffer because the GPU is actively
reading from it, so we use a pool instead.

Release Notes:

- Fixed a bug that caused inconsistent frame rate when scrolling on
certain hardware.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Antonio <antonio@zed.dev>
2024-02-02 09:09:58 -07:00
Bennet Bo Fenner
cf4f3ed79a Fix padding for notes label (#7280)
Fixes the padding of the "notes" button in collab-ui. I'm not sure why
the previous code used `div().h_7().w_full()`. Am I missing something
here?

### Before
![Screenshot 2024-02-02 at 15 26
44](https://github.com/zed-industries/zed/assets/53836821/0577b706-b433-4e02-802b-b6eccefb3acd)

### After
![Screenshot 2024-02-02 at 15 27
26](https://github.com/zed-industries/zed/assets/53836821/00fd81d8-82a1-4936-829c-022554259fd5)



Release Notes:
- Fixed padding of the "notes" button in the collaboration panel.

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-02-02 11:04:43 -05:00
Kirill Bulatov
bacb2a266a Add more data into LSP header parsing error contexts (#7282)
Follow-up of https://github.com/zed-industries/zed/pull/6929 

Release Notes:

- N/A
2024-02-02 18:04:23 +02:00
Marshall Bowers
33e5ba6278 Revert "Add YAML file type icon (#7185)" (#7286)
This PR reverts the addition of the YAML icon, as it doesn't look good
at the moment:

<img width="305" alt="Screenshot 2024-02-02 at 10 55 16 AM"
src="https://github.com/zed-industries/zed/assets/1486634/2fe2ddd5-5b73-4c52-a121-562d07352005">

This reverts commit a853a80634.

Release Notes:

- N/A
2024-02-02 11:02:10 -05:00
Thorsten Ball
11bd28870a editor: Add MoveUpByLines and MoveDownByLines actions (#7208)
This adds four new actions:

- `editor::MoveUpByLines`
- `editor::MoveDownByLines`
- `editor::SelectUpByLines`
- `editor::SelectDownByLines`

They all take a count by which to move the cursor up and down.
(Requested by Adam here:
https://twitter.com/adamwathan/status/1753017094248018302)


Example `keymap.json` entries:

```json
{
  "context": "Editor",
  "bindings": [
    "alt-up":         [ "editor::MoveUpByLines",     { "lines": 3 } ],
    "alt-down":       [ "editor::MoveDownByLines",   { "lines": 3 } ],
    "alt-shift-up":   [ "editor::SelectUpByLines",   { "lines": 3 } ],
    "alt-shift-down": [ "editor::SelectDownByLines", { "lines": 3 } ]
  ]
}
```

They are *not* bound by default, so as to not conflict with the
`alt-up/down` bindings that already exist.

Release Notes:

- Added four new actions: `editor::MoveUpByLines`,
`editor::MoveDownByLines`, `editor::SelectUpByLines`,
`editor::SelectDownByLines` that can take a line count configuration and
move the cursor up by the count.

### Demo



https://github.com/zed-industries/zed/assets/1185253/e78d4077-5bd5-4d72-a806-67695698af5d




https://github.com/zed-industries/zed/assets/1185253/0b086ec9-eb90-40a2-9009-844a215e6378
2024-02-02 08:48:04 -07:00
Thorsten Ball
01ddf840f5 completions: do not render empty multi-line documentation (#7279)
I ran into this a lot with Go code: the documentation would be empty so
we'd display a big box with nothing in it.

I think it's better if we only display the box if we have documentation.

Release Notes:

- Fixed documentation box in showing up when using auto-complete even if
documentation was empty.

## Before

![screenshot-2024-02-02-14 00
18@2x](https://github.com/zed-industries/zed/assets/1185253/e4915d51-a573-41f4-aa5d-21de6d1b0ff1)

## After

![screenshot-2024-02-02-14 01
58@2x](https://github.com/zed-industries/zed/assets/1185253/74b244af-3fc7-45e9-8cb3-7264e34b7ab7)
2024-02-02 16:42:43 +01:00
Thorsten Ball
ec9f44727e Fix Go tests failing on main (#7285)
Follow-up to #7276 in which I broke tests.

Release Notes:

- N/A
2024-02-02 16:35:58 +01:00
Rashid Almheiri
998f6cf80d Add OCaml support (#6929)
This pull request implements support for the [OCaml
Language](https://ocaml.org/).

### Additions
- [x]
[tree-sitter-ocaml](https://github.com/tree-sitter/tree-sitter-ocaml)
grammar
- [x] Highlight, Indents, Outline queries
- [x] A new file icon for .ml(i) files. Based on
[ocaml/ocaml-logo](https://github.com/ocaml/ocaml-logo/blob/master/Colour/SVG/colour-transparent-icon.svg)
- [x] LSP Integration with
[ocaml-language-server](https://github.com/ocaml/ocaml-lsp)
- [x] Completion Labels
- [x] Symbol Labels

### Bug Fixes
- [x] Improper parsing of LSP headers. 

### Missing [will file a separate issue]
- Documentation on completionItem, requires: `completionItem/resolve`
with support for `documentation` as a provider.

### Screenshots

<details><summary>Zed</summary>
<img width="1800" alt="Screenshot 2024-02-01 at 03 33 20"
src="https://github.com/zed-industries/zed/assets/69181766/e17c184e-203e-40c3-a08f-4de46226b79c">
</details>

Release Notes:
- Added OCaml Support
([#5316](https://github.com/zed-industries/zed/issues/5316)).

> [!NOTE]
> Partially completes #5316 
> To complete #5316:
> 1. addition of a reason tree-sitter grammar.
> 2. opam/esy integration, however it may be better as it's own plugin.
2024-02-02 16:58:07 +02:00
Robin Pfäffle
980d4f1003 Add inline code blocks in markdown preview (#7277)
Fixes #7236.

The reason why the code was not displayed correctly is due to missing
background color for the inline code block.

Previously to this PR:

<img width="1840" alt="SCR-20240202-mclv"
src="https://github.com/zed-industries/zed/assets/67913738/92f63e72-db86-4de9-bb42-40549679e159"
alt="Screenshot showing that inline code blocks are not highlighted in
Markdown preview">

After this PR:

<img width="1796" alt="SCR-20240202-mccs"
src="https://github.com/zed-industries/zed/assets/67913738/5cf039af-82d7-41b8-b461-f79dec5ddf6a"
alt="Screenshot showing that inline code blocks are now highlighted in
Markdown preview">



Release Notes:

- Fixed inline code block not shown in markdown preview
([#7236](https://github.com/zed-industries/zed/issues/7236)).
2024-02-02 16:51:05 +02:00
Thorsten Ball
79c1003b34 go: fix highlighting of brackets, variables, fields (#7276)
This changes the highlighting of Go code to make it more similar to how
we highlight Rust

* normal variables have the normal color, vs. being highlighted. This
really stuck out.
* brackets are properly highlighted

It also brings it closer to Neovim's tree-sitter highlighting by
changing how struct fields are highlighted.



Release Notes:

- Improved highlighting of Go code by tuning highlighting of variables,
brackets, and struct fields.

## Before & After

![screenshot-2024-02-02-11 38
08@2x](https://github.com/zed-industries/zed/assets/1185253/a754f166-89c1-40e8-a8da-b63155180896)
2024-02-02 11:44:28 +01:00
Piotr Osiewicz
d576cda789 project: Do not inline LSP related methods
The way Rust generics works, having a generic argument puts the burden of codegen on the crate that instantiates a generic function, which in our case is an editor.
2024-02-02 11:38:09 +01:00
Thorsten Ball
10cd978e93 go: improve outline queries to get rid of whitespace (#7273)
This changes the Go `outline.scm` queries a bit so that we don't have
these stray whitespaces floating around. I'm sure there's more
improvements possible, but for now I think this makes it look less
wrong.

Release Notes:

- Improved outline and breadcrumbs for Go code.

## Before

![before](https://github.com/zed-industries/zed/assets/1185253/d2b93c41-639b-4819-b87e-d8456960c5a7)

## After

![after](https://github.com/zed-industries/zed/assets/1185253/1ff6efb1-75a4-487b-8947-f070073887d4)
2024-02-02 11:24:04 +01:00
Bennet Bo Fenner
ce4c15dca6 Show diagnostics in scrollbar (#7175)
This PR implements support for displaying diagnostics in the scrollbar,
similar to what is already done for search results, symbols, git diff,
...

For example, changing a field name (`text`) without changing the
references looks like this in `buffer.rs` (note the red lines in the
scrollbar):

![image](https://github.com/zed-industries/zed/assets/53836821/c46f0d55-32e3-4334-8ad7-66d1578d5725)

As you can see, the errors, warnings, ... are displayed in the scroll
bar, which helps to identify possible problems with the current file.

Relevant issues: #4866, #6819

Release Notes:

- Added diagnostic indicators to the scrollbar
2024-02-02 12:10:42 +02:00
Thorsten Ball
2940a0ebd8 Revert "Avoid excessive blocking of main thread when rendering in direct mode (#7253)" (#7272)
This reverts commit 020c38a891 because it
leads to glitches when selecting text.



https://github.com/zed-industries/zed/assets/1185253/78c2c184-bc15-4b04-8c80-a23ca5c96afa



Release Notes:

- N/A
2024-02-02 10:33:08 +01:00
Alfred Kristal Ern
8fed9aaec2 Fix project panel selection related issues (#7245)
Fixes #7003 and #7005.

Selecting as a reaction to actually opening a new item doesn’t seem
robust in all cases, the PR improves that.

Release Notes:

- Fixed missing project panel file selection in certain cases
2024-02-02 11:17:46 +02:00
Ocean
5ed3b44686 Add rename to JetBrains keymaps (#7263)
Add rename actions to JetBrains keymaps.

Closes #7261.

Release Notes:

- Added rename keybindings to JetBrains keymap ([#7261](https://github.com/zed-industries/zed/issues/7261)).
2024-02-02 09:55:54 +02:00
Conrad Irwin
69e0ea92e4 Links to channel notes (#7262)
Release Notes:

- Added outline support for Markdown files
- Added the ability to link to channel notes:
https://zed.dev/channel/zed-283/notes#Roadmap
2024-02-01 22:22:02 -07:00
Max Brunsfeld
b35a7223b6 Add missing secret in release nightly workflow 2024-02-01 15:45:53 -08:00
Max Brunsfeld
020c38a891 Avoid excessive blocking of main thread when rendering in direct mode (#7253)
Release Notes:

- Fixed a bug that caused inconsistent frame rate when scrolling on
certain hardware.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-02-01 15:39:41 -08:00
Max Brunsfeld
21f4da6bf2 Correctly log LSP adapter name on LSP request error (#7232)
Previously, we were logging the language server's binary filename
instead.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
2024-02-01 14:17:09 -08:00
Kirill Bulatov
da44f637ed Order history items by open recency (#7248)
Closes https://github.com/zed-industries/zed/issues/7244

Follow-up of https://github.com/zed-industries/zed/pull/7210 that
returns back ordering of history items by open recency (last opened
history item should be on top)

Release Notes:

- N/A

---------

Co-authored-by: Andrew Lygin <alygin@gmail.com>
2024-02-01 23:45:03 +02:00
Conrad Irwin
0897c8eebd just kidding (#7241)
Release Notes:

- N/A
2024-02-01 11:57:09 -07:00
Conrad Irwin
7b9d51929d Deploy collab like nightly (#7174)
After this change we'll be able to push a tag to github to deploy to
collab.

The advantages of this are that there's no longer a separate step to
first
build the image, and then deploy it.

In the future I'd like to make this happen more automatically (maybe as
part of
bump nightly).

Release Notes:

- N/A
2024-02-01 11:54:49 -07:00
Antonio Scandurra
5424c8bfd5 Introduce a fast path for drawing quads with no borders / corner radii (#7231)
This will introduce an extra conditional but saves us from doing a bunch
of math in the simple case of drawing simple rectangles that aren't
rounded or don't have borders.


![Figure_1](https://github.com/zed-industries/zed/assets/482957/cba95ce2-2d9a-46ab-a142-35368334eb75)

Release Notes:

- Improved rendering performance.
2024-02-01 09:49:27 -08:00
Conrad Irwin
3521b50405 vim: Fix , and ; in visual mode (#7230)
Release Notes:

- vim: Fixed , and ; in visual mode
([#7182](https://github.com/zed-industries/zed/issues/7182)).
2024-02-01 10:13:30 -07:00
Mikayla Maki
d4264cbe4e Fix scrolling and wrapping in the markdown preview renderer (#7234)
Release Notes:

- N/A
2024-02-01 09:07:01 -08:00
Dairon M
97be0a930c Add syntax highlighting and LSP (erlang_lsp) for Erlang (#7093)
This pull request implements support for the [Erlang
Language](https://erlang.org/).

**It adds:**

* [tree-sitter-erlang](https://github.com/WhatsApp/tree-sitter-erlang)
grammar
highlights (Licensed under Apache-2 from WhatsApp which is compatible
with Zed licensing model), folds and indents
* Erlang file icon based on the [official
one](https://www.erlang.org/doc/erlang-logo.png)
* [erlang_ls](https://github.com/erlang-ls/erlang_ls) support

Fixes https://github.com/zed-industries/zed/issues/4939, possibly a
duplicate of https://github.com/zed-industries/zed/pull/7085 with more
features. Suppose @wingyplus wants to join efforts here.

**To complete (out of scope for this PR):**

* Support for the ELP language server from WhatsApp. CC @robertoaloi
* Better indentation handling, need something like
`indentNextLinePattern` in VS Code

**Screenshots:**

![Screenshot 2024-01-30 at 11 03 51
AM](https://github.com/zed-industries/zed/assets/168440/5289c245-9edd-46b8-b443-d7b3210f6510)
![Screenshot 2024-01-30 at 11 01 19
AM](https://github.com/zed-industries/zed/assets/168440/bd22b322-5344-44e6-b5f7-6e352fb3deef)
![Screenshot 2024-01-30 at 11 01 37
AM](https://github.com/zed-industries/zed/assets/168440/f28f6a15-383e-4719-8a87-fceae5062436)
![Screenshot 2024-01-30 at 11 02 03
AM](https://github.com/zed-industries/zed/assets/168440/980d5213-0367-4a08-86eb-5743dfa628eb)
![Screenshot 2024-01-30 at 11 02 19
AM](https://github.com/zed-industries/zed/assets/168440/ea998891-604d-48d6-929f-ae4c1bb3fae1)

Outline: 
![Screenshot 2024-01-31 at 9 09 36
AM](https://github.com/zed-industries/zed/assets/168440/46d56d94-21c3-414d-84fb-9251fa2506ab)



**Release Notes:**

* Added Erlang Support
([7093](https://github.com/zed-industries/zed/pull/7093)).

---------

Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
Co-authored-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
2024-02-01 18:54:26 +02:00
Thorsten Ball
3107ed847a lsp: if language server closes stdout/stderr, break loop (#7229)
Previously we would run these loops indefinitely when a language server
closed its stdout/stderr and the `read_until` returned `0` bytes read.

Easy to reproduce: start Zed with LSP attached, `kill -9` the LSP, see
logs accumulate.

Release Notes:

- Fix high CPU usage when a language server crashes (or closes its
stdout/stderr on purpose).

Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-02-01 17:49:53 +01:00
Kirill Bulatov
944a1f8fb0 Send lsp_types::InitializeParams with Zed version (#7216)
Based on the great work in
https://github.com/zed-industries/zed/pull/7130 , now sends this data

```
[crates/lsp/src/lsp.rs:588] ClientInfo { name: name.to_string(), version: Some(version.to_string()) } = ClientInfo {
    name: "Zed Dev",
    version: Some(
        "0.122.0",
    ),
}
```

with every LSP server initialization.

Release Notes:

- Added Zed name and version to LSP InitializeParams requests
2024-02-01 18:39:28 +02:00
Marshall Bowers
47a1ff7df9 markdown_preview: Sort dependencies in Cargo.toml (#7226)
This PR sorts the dependencies for the `markdown_preview` crate in
alphabetical order.

Release Notes:

- N/A
2024-02-01 11:26:50 -05:00
d1y
b9d5eb17a3 Fix typo (#7223)
Release Notes:

- N/A
2024-02-01 11:05:51 -05:00
Thorsten Ball
adc7cfb0d3 Fix moving focus to docks when navigating via keybinds (#7221)
This is a follow-up to #7141 and fixes the focus-switching to docks in
case they haven't been focused before.

We ran into issues when trying to focus a dock, that hasn't been focused
in the app's lifecycle: focus would only flip after the next re-render
(which could be triggered by moving the mouse, for example)

This changes the approach and uses the one we have for `toggle focus`
actions.

Release Notes:

- N/A

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: bennetbo <bennetbo@gmx.de>
2024-02-01 16:28:51 +01:00
thurain
a853a80634 Add YAML file type icon (#7185)
Add YAML file type icon from
[file-icons/icons](https://github.com/file-icons/icons)

https://github.com/file-icons/icons/blob/master/svg/YAML.svg
Release Notes:
- Added YAML file type icon.

---------

Co-authored-by: d1y <chenhonzhou@gmail.com>
2024-02-01 16:13:07 +02:00
Bennet Bo Fenner
2d41a119b3 markdown: Support alignment for table cells (#7201)
Just a small improvement as a follow up to @kierangilliam great work on
#6958

Rendering a table specified like this:
```markdown
| Left columns  | Center columns | Right columns |
| ------------- |:--------------:| -------------:|
| left foo      | center foo     | right foo     |
| left bar      | center bar     | right bar     |
| left baz      | center baz     | right baz     |
```
Does now look like this (notice the cell alignments):

![image](https://github.com/zed-industries/zed/assets/53836821/0f5b6a1e-a3c2-4fe9-bdcb-8654dbae7980)

Release Notes:
- N/A
2024-02-01 15:43:04 +02:00
Kirill Bulatov
0102ffbfca Refactor file_finder send element open code (#7210)
Follow-up of https://github.com/zed-industries/zed/pull/6947 (cc
@alygin) that fixes a few style nits and refactors the code around:
* use already stored `currently_opened_path` to decide what to do with
the history item sorting
* use the same method to set history items, encapsulate the bubbling up
logic there
* ensure history elements are properly sorted before populating

The main reason to change all that is the new comparator in the previous
version:
https://github.com/zed-industries/zed/pull/6947/files#diff-eac7c8c99856f77cee39117708cd1467fd5bbc8805da2564f851951638020842R234
that almost violated `util::extend_sorted` contract, requiring both
collections to be sorted the same way as the comparator would be: it did
work, because we bubbled currently open item up in the history items
list manually, and that we have only one such item.

Release Notes:
- N/A
2024-02-01 14:35:42 +02:00
Andrew Lygin
0edffd9248 Select the second item in the file finder by default (#6947)
This PR completes the first task of the Tabless editing feature (#6424).
It makes file finder select the previously opened file by default which
allows the user to quickly switch between two last opened files by
clicking `Cmd-P + Enter`.

This feature was also requested in #4663 comments.

Release Notes:
* Improved file finder selection: currently opened item is not selected now
2024-02-01 14:21:59 +02:00
Thorsten Ball
e65a76f0ec Add ability to navigate to/from docks via keybindings (#7141)
This adds the ability to navigate to/from docks (Terminal, Project,
Collaboration, Assistant) via keybindings.

When using the `ActivatePaneInDirection` keybinding from the
left/bottom/right dock, we check whether the movement is towards the
center panel. If it is, we focus the last active pane.

Fixes https://github.com/zed-industries/zed/issues/6833 and it came up
in a few other tickes/discussions.

Release Notes:

- Added ability to navigate to docks and back to the editor using the
`workspace::ActivatePaneInDirection` action (by default bound to `Ctrl-w
[hjkl]` in Vim mode).
([#6833](https://github.com/zed-industries/zed/issues/6833)).

## Drawback

There's this weird behavior: if you start Zed and no files are opened,
you focus terminal, go left (project panel), then back to right to
terminal, the terminal isn't focused. Even though we focus it in the
code.

Maybe this is a bug in the current focus handling code?

## Demo


https://github.com/zed-industries/zed/assets/1185253/5d56db40-36aa-4758-a3bc-7a0de20ce5d7

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 12:18:12 +01:00
Thorsten Ball
6c93c4bd35 assistant: render api key editor if no credentials are set (#7197)
This hopefully reduces confusion for new users. I updated the docs just
this morning, but I figured it's probably better to fix the issue
itself.

So what this does is to render the API key editor whenever the assistant
panel is opened/focused and no credentials can be found.

See: https://github.com/zed-industries/zed/discussions/6943

Release Notes:

- Fixed assistant panel not showing dialog to enter API key when opened
without saved credentials.

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 11:16:03 +01:00
Kieran Gill
8bafc61ef5 Add initial markdown preview to Zed (#6958)
Adds a "markdown: open preview" action to open a markdown preview.

https://github.com/zed-industries/zed/assets/18583882/6fd7f009-53f7-4f98-84ea-7dd3f0dd11bf


This PR extends the work done in `crates/rich_text` to render markdown
to also support:

- Variable heading sizes
- Markdown tables
- Code blocks
- Block quotes

## Release Notes

- Added `Markdown: Open preview` action to partially close
([#6789](https://github.com/zed-industries/zed/issues/6789)).

## Known issues that will not be included in this PR

- Images.
- Nested block quotes.
- Footnote Reference.
- Headers highlighting.
- Inline code highlighting (this will need to be implemented in
`rich_text`)
- Checkboxes (`- [ ]` and `- [x]`)
- Syntax highlighting in code blocks.
- Markdown table text alignment.
- Inner markdown URL clicks
2024-02-01 11:03:09 +02:00
Ares Andrew
3b882918f7 Filter LSP github releases that have no assets to properly download LSP servers (#7189)
Fixes https://github.com/zed-industries/zed/issues/7183

Release Notes:

- Filter lsp github releases that have no assets ([7189](https://github.com/zed-industries/zed/issues/7183))
2024-02-01 10:59:35 +02:00
Conrad Irwin
5e64d45194 Remove links to docs.zed.dev (#7187)
Release Notes:

- N/A
2024-01-31 22:26:15 -07:00
Conrad Irwin
3df7da236d Also add proxy to zed http client (#7184)
Follow up to #6765 because I couldn't figure out how to add to that PR.

Release Notes:

- N/A
2024-01-31 21:40:12 -07:00
lichuan6
5e81d780bd Read HTTP proxy from env (#6765)
This PR will use http proxy from env for downloading files.
2024-01-31 20:57:09 -07:00
d1y
cbc2746d70 docs: add gitcommit language and update go language (#7181)
Release Notes:

- N/A
2024-01-31 19:27:17 -07:00
Conrad Irwin
aaba98d8ec Debug build (#7176)
Release Notes:

- N/A
2024-01-31 19:22:58 -07:00
Conrad Irwin
2cc2a61c77 collab 0.44.0 2024-01-31 19:10:19 -07:00
Conrad Irwin
3025e5620d Tell the user when screen-sharing fails (#7171)
Release Notes:

- Added an alert when screen-sharing fails
2024-01-31 16:28:32 -07:00
Marshall Bowers
c4083c3cf6 Watch the themes directory for changes (#7173)
This PR makes Zed watch the themes directory for changes.

When theme files are added or modified, we reload the theme and apply
any changes to Zed.

Release Notes:

- Added live reloading for the themes directory.
2024-01-31 18:17:31 -05:00
Conrad Irwin
2187513026 app version to server (#7130)
- Send app version and release stage to collab on connect
- Read the new header on the server

Release Notes:

- Added the ability to collaborate with users on different releases of
Zed.
2024-01-31 15:46:24 -07:00
Conrad Irwin
5b7b5bfea5 Add a checksum telemetry request (#7168)
We're seeing a bit of nonsense on telemetry. Although the checksum seed
isn't secret per-se, it does make sending nonsense a little more effort.

Release Notes:

- N/A
2024-01-31 15:44:38 -07:00
165 changed files with 5290 additions and 1850 deletions

View File

@@ -2,4 +2,8 @@
Release Notes:
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
- Added/Fixed/Improved ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
**or**
- N/A

View File

@@ -81,6 +81,7 @@ jobs:
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@v3

107
.github/workflows/deploy_collab.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: Publish Collab Server Image
on:
push:
tags:
- collab-production
- collab-staging
env:
DOCKER_BUILDKIT: 1
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
jobs:
style:
name: Check formatting and Clippy lints
runs-on:
- self-hosted
- test
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run style checks
uses: ./.github/actions/check_style
tests:
name: Run tests
runs-on:
- self-hosted
- test
needs: style
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
fetch-depth: 0
- name: Run tests
uses: ./.github/actions/run_tests
publish:
name: Publish collab server image
needs:
- style
- tests
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sign into DigitalOcean docker registry
run: doctl registry login
- name: Prune Docker system
run: docker system prune --filter 'until=720h' -f
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Build docker image
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
deploy:
name: Deploy new server image
needs:
- publish
runs-on:
- self-hosted
- deploy
steps:
- name: Sign into Kubernetes
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }}
- name: Determine namespace
run: |
set -eu
if [[ $GITHUB_REF_NAME = "collab-production" ]]; then
echo "Deploying collab:$GITHUB_SHA to production"
echo "KUBE_NAMESPACE=production" >> $GITHUB_ENV
elif [[ $GITHUB_REF_NAME = "collab-staging" ]]; then
echo "Deploying collab:$GITHUB_SHA to staging"
echo "KUBE_NAMESPACE=staging" >> $GITHUB_ENV
else
echo "cowardly refusing to deploy from an unknown branch"
exit 1
fi
- name: Start rollout
run: kubectl -n "$KUBE_NAMESPACE" set image deployment/collab collab=registry.digitalocean.com/zed/collab:${GITHUB_SHA}
- name: Wait for rollout to finish
run: kubectl -n "$KUBE_NAMESPACE" rollout status deployment/collab

View File

@@ -1,49 +0,0 @@
name: Publish Collab Server Image
on:
push:
tags:
- collab-v*
env:
DOCKER_BUILDKIT: 1
DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
jobs:
publish:
name: Publish collab server image
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Sign into DigitalOcean docker registry
run: doctl registry login
- name: Prune Docker system
run: docker system prune
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: 'recursive'
- name: Determine version
run: |
set -eu
version=$(script/get-crate-version collab)
if [[ $GITHUB_REF_NAME != "collab-v${version}" ]]; then
echo "release tag ${GITHUB_REF_NAME} does not match version ${version}"
exit 1
fi
echo "Publishing collab version: ${version}"
echo "COLLAB_VERSION=${version}" >> $GITHUB_ENV
- name: Build docker image
run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}
- name: Publish docker image
run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION}

View File

@@ -59,6 +59,7 @@ jobs:
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Install Node
uses: actions/setup-node@v3

157
Cargo.lock generated
View File

@@ -103,9 +103,8 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
version = "0.20.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=2d2b894c3b869fadc78fce9d72cb5c8d2b764cac#2d2b894c3b869fadc78fce9d72cb5c8d2b764cac"
dependencies = [
"base64 0.21.4",
"bitflags 2.4.1",
@@ -441,6 +440,18 @@ dependencies = [
"event-listener",
]
[[package]]
name = "async-native-tls"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
dependencies = [
"async-std",
"native-tls",
"thiserror",
"url",
]
[[package]]
name = "async-net"
version = "1.7.0"
@@ -570,19 +581,6 @@ version = "4.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
[[package]]
name = "async-tls"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f23d769dbf1838d5df5156e7b1ad404f4c463d1ac2c6aeb6cd943630f8a8400"
dependencies = [
"futures-core",
"futures-io",
"rustls 0.19.1",
"webpki",
"webpki-roots 0.21.1",
]
[[package]]
name = "async-trait"
version = "0.1.73"
@@ -600,7 +598,8 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5682ea0913e5c20780fe5785abacb85a411e7437bf52a1bedb93ddb3972cb8dd"
dependencies = [
"async-tls",
"async-native-tls",
"async-std",
"futures-io",
"futures-util",
"log",
@@ -1367,6 +1366,7 @@ dependencies = [
"image",
"lazy_static",
"log",
"once_cell",
"parking_lot 0.11.2",
"postage",
"rand 0.8.5",
@@ -1377,6 +1377,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"sha2 0.10.7",
"smol",
"sum_tree",
"sysinfo",
@@ -1438,7 +1439,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.43.0"
version = "0.44.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1484,6 +1485,7 @@ dependencies = [
"prometheus",
"prost 0.8.0",
"rand 0.8.5",
"release_channel",
"reqwest",
"rpc",
"scrypt",
@@ -2414,6 +2416,7 @@ dependencies = [
"itertools 0.10.5",
"language",
"lazy_static",
"linkify",
"log",
"lsp",
"multi_buffer",
@@ -2422,6 +2425,7 @@ dependencies = [
"postage",
"project",
"rand 0.8.5",
"release_channel",
"rich_text",
"rpc",
"schemars",
@@ -2658,6 +2662,7 @@ dependencies = [
"env_logger",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
"menu",
"picker",
@@ -3862,12 +3867,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json_comments"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dbbfed4e59ba9750e15ba154fdfd9329cee16ff3df539c2666b70f58cc32105"
[[package]]
name = "jwt"
version = "0.16.0"
@@ -4017,6 +4016,7 @@ dependencies = [
"language",
"lsp",
"project",
"release_channel",
"serde",
"serde_json",
"settings",
@@ -4135,6 +4135,15 @@ dependencies = [
"safemem",
]
[[package]]
name = "linkify"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
dependencies = [
"memchr",
]
[[package]]
name = "linkme"
version = "0.3.17"
@@ -4261,6 +4270,7 @@ dependencies = [
"lsp-types",
"parking_lot 0.11.2",
"postage",
"release_channel",
"serde",
"serde_derive",
"serde_json",
@@ -4314,6 +4324,26 @@ dependencies = [
"libc",
]
[[package]]
name = "markdown_preview"
version = "0.1.0"
dependencies = [
"anyhow",
"editor",
"gpui",
"language",
"lazy_static",
"log",
"menu",
"project",
"pulldown-cmark",
"rich_text",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -5795,6 +5825,7 @@ dependencies = [
"pretty_assertions",
"rand 0.8.5",
"regex",
"release_channel",
"rpc",
"schemars",
"serde",
@@ -5859,6 +5890,7 @@ dependencies = [
"picker",
"postage",
"project",
"release_channel",
"serde_json",
"settings",
"smol",
@@ -6032,6 +6064,7 @@ dependencies = [
"editor",
"gpui",
"search",
"settings",
"ui",
"workspace",
]
@@ -6702,19 +6735,6 @@ dependencies = [
"rustix 0.38.30",
]
[[package]]
name = "rustls"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
dependencies = [
"base64 0.13.1",
"log",
"ring",
"sct 0.6.1",
"webpki",
]
[[package]]
name = "rustls"
version = "0.21.7"
@@ -6723,7 +6743,7 @@ checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"ring",
"rustls-webpki",
"sct 0.7.0",
"sct",
]
[[package]]
@@ -6866,16 +6886,6 @@ dependencies = [
"sha2 0.9.9",
]
[[package]]
name = "sct"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sct"
version = "0.7.0"
@@ -7597,7 +7607,7 @@ dependencies = [
"paste",
"percent-encoding",
"rust_decimal",
"rustls 0.21.7",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
@@ -7611,7 +7621,7 @@ dependencies = [
"tracing",
"url",
"uuid 1.4.1",
"webpki-roots 0.24.0",
"webpki-roots",
]
[[package]]
@@ -8124,6 +8134,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"db",
"dirs 4.0.0",
"editor",
@@ -8222,7 +8233,6 @@ dependencies = [
"gpui",
"indexmap 1.9.3",
"indoc",
"json_comments",
"log",
"palette",
"pathfinder_color",
@@ -8230,6 +8240,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"serde_json_lenient",
"simplelog",
"strum",
"theme",
@@ -8783,6 +8794,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-erlang"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ced5145ebb17f83243bf055b74e108da7cc129e12faab4166df03f59b287f4"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-gitcommit"
version = "0.3.3"
@@ -8921,6 +8942,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-ocaml"
version = "0.20.4"
source = "git+https://github.com/tree-sitter/tree-sitter-ocaml?rev=4abfdc1c7af2c6c77a370aee974627be1c285b3b#4abfdc1c7af2c6c77a370aee974627be1c285b3b"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-php"
version = "0.21.1"
@@ -9090,6 +9120,7 @@ dependencies = [
"http",
"httparse",
"log",
"native-tls",
"rand 0.8.5",
"sha-1 0.9.8",
"thiserror",
@@ -9419,6 +9450,7 @@ dependencies = [
"parking_lot 0.11.2",
"project",
"regex",
"release_channel",
"search",
"serde",
"serde_derive",
@@ -9796,25 +9828,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940"
dependencies = [
"webpki",
]
[[package]]
name = "webpki-roots"
version = "0.24.0"
@@ -10304,6 +10317,7 @@ dependencies = [
"indexmap 1.9.3",
"install_cli",
"isahc",
"itertools 0.11.0",
"journal",
"language",
"language_selector",
@@ -10312,6 +10326,7 @@ dependencies = [
"libc",
"log",
"lsp",
"markdown_preview",
"menu",
"mimalloc",
"node_runtime",
@@ -10361,6 +10376,7 @@ dependencies = [
"tree-sitter-elixir",
"tree-sitter-elm",
"tree-sitter-embedded-template",
"tree-sitter-erlang",
"tree-sitter-gitcommit",
"tree-sitter-gleam",
"tree-sitter-glsl",
@@ -10375,6 +10391,7 @@ dependencies = [
"tree-sitter-markdown",
"tree-sitter-nix",
"tree-sitter-nu",
"tree-sitter-ocaml",
"tree-sitter-php",
"tree-sitter-proto",
"tree-sitter-purescript",

View File

@@ -42,6 +42,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/multi_buffer",
@@ -99,11 +100,14 @@ ctor = "0.2.6"
derive_more = "0.99.17"
env_logger = "0.9"
futures = "0.3"
git2 = { version = "0.15", default-features = false}
git2 = { version = "0.15", default-features = false }
globset = "0.4"
indoc = "1"
# We explicitly disable a http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
isahc = { version = "1.7.2", default-features = false, features = [
"static-curl",
"text-decoding",
] }
lazy_static = "1.4.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
@@ -111,6 +115,7 @@ parking_lot = "0.11.1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
prost = "0.8"
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
@@ -120,7 +125,10 @@ schemars = "0.8"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.1", features = [
"preserve_order",
"raw_value",
] }
serde_repr = "0.1"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
@@ -135,11 +143,12 @@ tree-sitter = { version = "0.20", features = ["wasm"] }
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
tree-sitter-c = "0.20.1"
tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", rev = "dd5e59721a5f8dae34604060833902b882023aaf" }
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev="f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-erlang = "0.4.0"
tree-sitter-gitcommit = { git = "https://github.com/gbprod/tree-sitter-gitcommit" }
tree-sitter-gleam = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "58b7cac8fc14c92b0677c542610d8738c373fa81" }
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
@@ -154,8 +163,9 @@ tree-sitter-lua = "0.0.14"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" }
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
tree-sitter-php = "0.21.1"
tree-sitter-proto = {git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
tree-sitter-python = "0.20.2"
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }

View File

@@ -6,6 +6,9 @@ COPY . .
# Compile collab server
ARG CARGO_PROFILE_RELEASE_PANIC=abort
ARG GITHUB_SHA
ENV GITHUB_SHA=$GITHUB_SHA
RUN --mount=type=cache,target=./script/node_modules \
--mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \

View File

@@ -0,0 +1 @@
<svg height="64" viewBox="0 0 128 128" width="64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="0" x2="128" y1="128" y2="0"><stop offset="0" stop-color="#333"/><stop offset="1" stop-color="#5d5d5d"/></linearGradient><path d="m12.239265 30.664279h14.960911c-5.59432 5.460938-7.654216 10.692785-10.342106 18.023379-3.200764 8.729348-.549141 29.987457 3.815534 37.55289 2.943384 5.101853 6.282685 8.994876 8.233522 11.095173h-16.667861zm89.614855 0h13.90661v66.671442h-13.55518c1.31391-1.750328 3.43934-4.534454 5.12085-6.426163 2.32782-2.618784 4.97023-6.978412 4.97023-6.978412l-16.015202-8.133112s-5.48977 11.600331-15.964999 15.964998c-10.475214 4.364666-19.784679-.838179-25.604243-7.530659-5.819578-6.692502-5.82371-22.14014-5.82371-22.14014h60.797524c1.16391-14.839892-2.63216-21.249816-4.66901-25.90547-.91799-2.098266-1.89261-3.810819-3.16287-5.522484zm-38.356164 1.757154c.35429-.01632.731685-.0092 1.104497 0 11.930114.290977 13.053143 12.802122 13.053143 12.802122h-27.311192s2.170772-12.298638 13.153552-12.802122z" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,7 +1,9 @@
{
"suffixes": {
"Emakefile": "erlang",
"aac": "audio",
"accdb": "storage",
"app.src": "erlang",
"avif": "image",
"bak": "backup",
"bash": "terminal",
@@ -23,6 +25,8 @@
"doc": "document",
"docx": "document",
"eex": "elixir",
"erl": "erlang",
"escript": "erlang",
"eslintrc": "eslint",
"eslintrc.js": "eslint",
"eslintrc.json": "eslint",
@@ -37,17 +41,18 @@
"gif": "image",
"gitattributes": "vcs",
"gitignore": "vcs",
"gitmodules": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"go": "go",
"h": "code",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
"heif": "image",
"hrl": "erlang",
"hs": "haskell",
"htm": "template",
"html": "template",
"hs": "haskell",
"ib": "storage",
"ico": "image",
"ini": "settings",
@@ -64,6 +69,8 @@
"mdb": "storage",
"mdf": "storage",
"mdx": "document",
"ml": "ocaml",
"mli": "ocaml",
"mp3": "audio",
"mp4": "video",
"myd": "storage",
@@ -85,6 +92,7 @@
"psd": "image",
"py": "python",
"rb": "ruby",
"rebar.config": "erlang",
"rkt": "code",
"rs": "rust",
"rtf": "document",
@@ -104,13 +112,15 @@
"txt": "document",
"vue": "vue",
"wav": "audio",
"webp": "image",
"webm": "video",
"webp": "image",
"xls": "document",
"xlsx": "document",
"xml": "template",
"xrl": "erlang",
"yaml": "settings",
"yml": "settings",
"yrl": "erlang",
"zlogin": "terminal",
"zsh": "terminal",
"zsh_aliases": "terminal",
@@ -133,7 +143,7 @@
"icon": "icons/file_icons/folder.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
"icon": "icons/file_icons/css.svg"
},
"default": {
"icon": "icons/file_icons/file.svg"
@@ -144,6 +154,9 @@
"elixir": {
"icon": "icons/file_icons/elixir.svg"
},
"erlang": {
"icon": "icons/file_icons/erlang.svg"
},
"eslint": {
"icon": "icons/file_icons/eslint.svg"
},
@@ -168,6 +181,9 @@
"log": {
"icon": "icons/file_icons/info.svg"
},
"ocaml": {
"icon": "icons/file_icons/ocaml.svg"
},
"phoenix": {
"icon": "icons/file_icons/phoenix.svg"
},

View File

@@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.73843 11.709C6.70584 11.6334 6.60683 11.4367 6.55712 11.3736C6.44917 11.2362 6.42396 11.2258 6.39221 11.0523C6.33703 10.7501 6.19094 10.202 6.01879 9.82381C5.92987 9.62863 5.78201 9.46467 5.64665 9.32312C5.52847 9.19895 5.26214 8.99002 5.2157 9.00037C4.7807 9.09487 4.64576 9.55902 4.44115 9.92673C4.32795 10.1301 4.208 10.3031 4.1188 10.5195C4.03641 10.7184 4.04373 10.9387 3.90268 11.1095C3.75801 11.2849 3.66398 11.4715 3.59311 11.6981C3.57968 11.7412 3.54148 12.1939 3.5 12.3006L4.14649 12.2511C4.74896 12.2958 4.57496 12.547 5.51526 12.4922L7 12.4423C6.95398 12.2942 6.89056 12.1228 6.86613 12.067C6.82472 11.9732 6.77267 11.7897 6.73843 11.709Z" fill="black"/>
<path d="M8.72454 8.42043C8.61775 8.50889 8.40904 8.72165 7.95506 8.8021C7.75133 8.83825 7.56076 8.84122 7.35158 8.82925C7.24918 8.8236 7.15263 8.81758 7.04997 8.81605C6.9895 8.81552 6.78663 8.80812 6.79667 8.83039L6.77408 8.89506C6.7776 8.91633 6.78497 8.96949 6.78703 8.98237C6.79534 9.03461 6.79766 9.07617 6.79939 9.12421C6.80252 9.22297 6.79228 9.32592 6.79667 9.42559C6.80577 9.63232 6.87262 9.82076 6.88106 10.0293C6.89029 10.2615 6.99037 10.5072 7.08718 10.6969C7.12393 10.7691 7.17988 10.7773 7.20426 10.8663C7.23284 10.9681 7.20579 11.0762 7.21968 11.1848C7.27417 11.6058 7.37982 12.0459 7.54501 12.4259C7.54621 12.4291 7.54747 12.4325 7.54893 12.4354C7.75293 12.3961 7.95732 12.3119 8.22239 12.2669C8.70839 12.1841 9.3843 12.2268 9.81848 12.1801C10.9171 12.0616 11.5133 12.6972 12.5 12.4367V3.09052C12.5 2.21217 11.8798 1.5 11.1142 1.5H2.88578C2.1205 1.5 1.5 2.21217 1.5 3.09052V6.5608C1.69828 6.47851 1.98348 5.99435 2.07285 5.87661C2.2292 5.67071 2.25758 5.40808 2.33546 5.24267C2.51281 4.86596 2.54331 4.60691 2.94645 4.60691C3.13436 4.60691 3.20899 4.65663 3.3361 4.85238C3.42454 4.98851 3.57731 5.24 3.64881 5.40815C3.73134 5.60215 3.86583 5.86464 3.92497 5.91763C3.96876 5.95698 4.01221 5.9865 4.05275 6.00396C4.11813 6.0321 4.17222 5.98047 4.21595 5.94051C4.27176 5.8895 4.29582 5.78556 4.34751 5.64692C4.422 5.44689 4.5032 5.20721 4.54938 5.12356C4.62932 4.97897 4.65656 4.80739 4.74288 4.72427C4.8702 4.60172 5.03632 4.59311 5.08203 4.58274C5.33779 4.52478 5.45408 4.72419 5.58006 4.85315C5.66253 4.93764 5.77522 5.10785 5.85523 5.33594C5.91776 5.51408 5.99736 5.67887 6.03065 5.78174C6.06281 5.88103 6.14222 6.04018 6.18926 6.23098C6.23199 6.40424 6.34635 6.537 6.3898 6.61936C6.3898 6.61936 6.45632 6.83319 6.86079 7.02864C6.9485 7.07104 7.12579 7.13998 7.23157 7.18413C7.40733 7.25741 7.57757 7.24788 7.79432 7.21807C7.94888 7.21807 8.03261 6.96123 8.10284 6.75556C8.14437 6.634 8.18418 6.28566 8.21129 6.18675C8.23754 6.09051 8.17614 6.01608 8.22843 5.93174C8.28956 5.83329 8.32591 5.82796 8.3612 5.69961C8.43701 5.42478 8.87524 5.4109 9.12156 5.4109C9.32689 5.4109 9.30078 5.63967 9.6491 5.56143C9.84858 5.51652 10.0408 5.59094 10.2526 5.65531C10.4309 5.7096 10.5986 5.77145 10.699 5.90642C10.764 5.99382 10.9254 6.43169 10.761 6.45037C10.7768 6.47257 10.7884 6.5126 10.8179 6.53456C10.7813 6.69974 10.622 6.58207 10.5335 6.56087C10.4142 6.5325 10.33 6.56514 10.2133 6.6244C10.0138 6.72635 9.72206 6.71446 9.5483 6.88055C9.40085 7.02132 9.40111 7.33558 9.33234 7.51166C9.33234 7.51166 9.14137 8.07551 8.72454 8.42043Z" fill="black"/>
<path d="M3.6514 8.80413C3.57333 8.79137 3.50083 8.77702 3.425 8.75238C3.28339 8.70644 3.12867 8.66165 2.9892 8.60788C2.90451 8.57488 2.62239 8.41409 2.5611 8.36877C2.41737 8.26211 2.32191 7.97231 2.20955 8.00214C2.13782 8.02098 2.06795 8.06058 2.02333 8.17701C1.98692 8.27196 1.97457 8.43521 1.94936 8.54469C1.92011 8.67177 1.86959 8.7904 1.82536 8.91149C1.74401 9.13362 1.59759 9.33453 1.5345 9.55093C1.52181 9.59546 1.51055 9.64527 1.5 9.6972V10.5274V11.9637V12.1713C1.57359 12.1915 1.65057 12.2164 1.73674 12.2535C2.37264 12.5265 2.52781 12.5497 3.15152 12.4348L3.21002 12.4223C3.25775 12.2625 3.2946 11.718 3.32555 11.5494C3.34966 11.4202 3.38279 11.3173 3.39537 11.1853C3.40723 11.06 3.39427 10.9406 3.3876 10.8267C3.37011 10.5414 3.51669 10.4395 3.58667 10.1945C3.64976 9.97283 3.68617 9.7206 3.73839 9.49399C3.78847 9.27653 3.86665 8.96922 4 8.85975C3.98382 8.82938 3.72144 8.81539 3.6514 8.80413Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -42,6 +42,7 @@
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
"shift-f6": "editor::Rename",
"cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
@@ -83,7 +84,8 @@
{
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open"
"enter": "project_panel::Open",
"shift-f6": "project_panel::Rename"
}
}
]

View File

@@ -96,6 +96,8 @@
}
}
],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
@@ -333,8 +335,6 @@
],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -502,5 +502,18 @@
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
}
},
{
"context": "Dock",
"bindings": {
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"]
}
}
]

View File

@@ -104,8 +104,17 @@
"show_whitespaces": "selection",
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone muted by default
"mute_on_join": false
// Join calls with the microphone live by default
"mute_on_join": false,
// Share your project when you are the first to join a channel
"share_on_join": true
},
// Toolbar related settings
"toolbar": {
// Whether to show breadcrumbs.
"breadcrumbs": true,
// Whether to show quick action buttons.
"quick_actions": true
},
// Scrollbar related settings
"scrollbar": {
@@ -127,8 +136,12 @@
// Whether to show selections in the scrollbar.
"selections": true,
// Whether to show symbols selections in the scrollbar.
"symbols_selections": true
"symbols_selections": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
"relative_line_numbers": false,
// When to populate a new search's query based on the text under the cursor.
// This setting can take the following three values:
@@ -496,6 +509,12 @@
},
"JSON": {
"tab_size": 2
},
"OCaml": {
"tab_size": 2
},
"OCaml Interface": {
"tab_size": 2
}
},
// Zed's Prettier integration settings.

View File

@@ -1,5 +1,5 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
// see the documentation: https://zed.dev/docs/configuring-zed#folder-specific-settings
{}

View File

@@ -199,9 +199,13 @@ impl AssistantPanel {
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
cx.notify();
if self.focus_handle.is_focused(cx) {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
if self.has_credentials() {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
}
}
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
cx.focus_view(api_key_editor);
}
}
@@ -777,6 +781,10 @@ impl AssistantPanel {
});
}
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
self.api_key_editor = Some(build_api_key_editor(cx));
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
@@ -870,7 +878,7 @@ impl AssistantPanel {
cx.update(|cx| completion_provider.delete_credentials(cx))?
.await;
this.update(&mut cx, |this, cx| {
this.api_key_editor = Some(build_api_key_editor(cx));
this.build_api_key_editor(cx);
this.focus_handle.focus(cx);
cx.notify();
})
@@ -1136,7 +1144,7 @@ impl AssistantPanel {
}
}
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
@@ -1147,9 +1155,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(api_key_editor) = self.api_key_editor.clone() {
const INSTRUCTIONS: [&'static str; 5] = [
const INSTRUCTIONS: [&'static str; 6] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
" - You can create an API key at: platform.openai.com/api-keys",
" - Make sure your OpenAI account has credits",
" - Having a subscription for another service like GitHub Copilot won't work.",
" ",
"Paste your OpenAI API key and press Enter to use the assistant:"
@@ -1342,7 +1351,9 @@ impl Panel for AssistantPanel {
cx.spawn(|this, mut cx| async move {
load_credentials.await;
this.update(&mut cx, |this, cx| {
if this.editors.is_empty() {
if !this.has_credentials() {
this.build_api_key_editor(cx);
} else if this.editors.is_empty() {
this.new_conversation(cx);
}
})

View File

@@ -1,7 +1,7 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION};
use client::{Client, TelemetrySettings, ZED_APP_PATH};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
@@ -108,29 +108,28 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
})
.detach();
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client);
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client);
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
})
.detach();
} else {
update_subscription.take();
}
})
.detach();
updater
});
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
updater
});
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
pub fn check(_: &Check, cx: &mut WindowContext) {

View File

@@ -40,10 +40,11 @@ impl Render for UpdateNotification {
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(|_, cx| {
.on_click(cx.listener(|this, _, cx| {
crate::view_release_notes(&Default::default(), cx);
}),
)
this.dismiss(&menu::Cancel, cx)
})),
);
}
}

View File

@@ -7,6 +7,7 @@ use settings::Settings;
#[derive(Deserialize, Debug)]
pub struct CallSettings {
pub mute_on_join: bool,
pub share_on_join: bool,
}
/// Configuration of voice calls in Zed.
@@ -16,6 +17,11 @@ pub struct CallSettingsContent {
///
/// Default: false
pub mute_on_join: Option<bool>,
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: true
pub share_on_join: Option<bool>,
}
impl Settings for CallSettings {

View File

@@ -617,6 +617,10 @@ impl Room {
self.local_participant.role == proto::ChannelRole::Admin
}
pub fn local_participant_is_guest(&self) -> bool {
self.local_participant.role == proto::ChannelRole::Guest
}
pub fn set_participant_role(
&mut self,
user_id: u64,
@@ -1202,7 +1206,7 @@ impl Room {
})
}
pub(crate) fn share_project(
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,

View File

@@ -74,11 +74,19 @@ impl Channel {
pub fn link(&self) -> String {
RELEASE_CHANNEL.link_prefix().to_owned()
+ "channel/"
+ &self.slug()
+ &Self::slug(&self.name)
+ "-"
+ &self.id.to_string()
}
pub fn notes_link(&self, heading: Option<String>) -> String {
self.link()
+ "/notes"
+ &heading
.map(|h| format!("#{}", Self::slug(&h)))
.unwrap_or_default()
}
pub fn is_root_channel(&self) -> bool {
self.parent_path.is_empty()
}
@@ -90,9 +98,8 @@ impl Channel {
.unwrap_or(self.id)
}
pub fn slug(&self) -> String {
let slug: String = self
.name
pub fn slug(str: &str) -> String {
let slug: String = str
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();

View File

@@ -329,6 +329,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
let http = FakeHttpClient::with_404_response();

View File

@@ -27,11 +27,12 @@ sum_tree = { path = "../sum_tree" }
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-tls"] }
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
futures.workspace = true
image = "0.23"
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
@@ -39,6 +40,7 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha2 = "0.10"
smol.workspace = true
sysinfo.workspace = true
tempfile.workspace = true

View File

@@ -15,14 +15,13 @@ use futures::{
TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, SemanticVersion,
Task, WeakModel,
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use postage::watch;
use rand::prelude::*;
use release_channel::ReleaseChannel;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -58,9 +57,6 @@ lazy_static! {
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
.and_then(|s| if s.is_empty() { None } else { Some(s) });
pub static ref ZED_APP_VERSION: Option<SemanticVersion> = std::env::var("ZED_APP_VERSION")
.ok()
.and_then(|v| v.parse().ok());
pub static ref ZED_APP_PATH: Option<PathBuf> =
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
pub static ref ZED_ALWAYS_ACTIVE: bool =
@@ -1011,13 +1007,22 @@ impl Client {
.update(|cx| ReleaseChannel::try_global(cx))
.ok()
.flatten();
let app_version = cx
.update(|cx| AppVersion::global(cx).to_string())
.ok()
.unwrap_or_default();
let request = Request::builder()
.header(
"Authorization",
format!("{} {}", credentials.user_id, credentials.access_token),
)
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION);
.header("x-zed-protocol-version", rpc::PROTOCOL_VERSION)
.header("x-zed-app-version", app_version)
.header(
"x-zed-release-channel",
release_channel.map(|r| r.dev_name()).unwrap_or("unknown"),
);
let http = self.http.clone();
cx.background_executor().spawn(async move {
@@ -1035,7 +1040,7 @@ impl Client {
rpc_url.set_scheme("wss").unwrap();
let request = request.uri(rpc_url.as_str()).body(())?;
let (stream, _) =
async_tungstenite::async_tls::client_async_tls(request, stream).await?;
async_tungstenite::async_std::client_async_tls(request, stream).await?;
Ok(Connection::new(
stream
.map_err(|error| anyhow!(error))

View File

@@ -4,16 +4,19 @@ use crate::TelemetrySettings;
use chrono::{DateTime, Utc};
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use serde::Serialize;
use settings::{Settings, SettingsStore};
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sha2::{Digest, Sha256};
use std::io::Write;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use tempfile::NamedTempFile;
use util::http::{HttpClient, ZedHttpClient};
use util::http::{self, HttpClient, Method, ZedHttpClient};
#[cfg(not(debug_assertions))]
use util::ResultExt;
use util::TryFutureExt;
@@ -142,6 +145,16 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
.or_else(|| {
env::var("ZED_CLIENT_CHECKSUM_SEED")
.ok()
.map(|s| s.as_bytes().into())
})
});
impl Telemetry {
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel =
@@ -500,6 +513,10 @@ impl Telemetry {
return;
}
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
return;
};
let this = self.clone();
self.executor
.spawn(
@@ -540,9 +557,27 @@ impl Telemetry {
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
this.http_client
.post_json(&this.http_client.zed_url("/api/events"), json_bytes.into())
.await?;
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&json_bytes);
summer.update(checksum_seed);
let mut checksum = String::new();
for byte in summer.finalize().as_slice() {
use std::fmt::Write;
write!(&mut checksum, "{:02x}", byte).unwrap();
}
let request = http::Request::builder()
.method(Method::POST)
.uri(&this.http_client.zed_url("/api/events"))
.header("Content-Type", "text/plain")
.header("x-zed-checksum", checksum)
.body(json_bytes.into());
let response = this.http_client.send(request?).await?;
if response.status() != 200 {
log::error!("Failed to send events: HTTP {:?}", response.status());
}
anyhow::Ok(())
}
.log_err(),

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.43.0"
version = "0.44.0"
publish = false
license = "AGPL-3.0-or-later"
@@ -61,6 +61,7 @@ util = { path = "../util" }
uuid.workspace = true
[dev-dependencies]
release_channel = { path = "../release_channel" }
async-trait.workspace = true
audio = { path = "../audio" }
call = { path = "../call", features = ["test-support"] }

View File

@@ -3,3 +3,35 @@
This crate is what we run at https://collab.zed.dev.
It contains our back-end logic for collaboration, to which we connect from the Zed client via a websocket after authenticating via https://zed.dev, which is a separate repo running on Vercel.
# Local Development
Detailed instructions on getting started are [here](https://zed.dev/docs/local-collaboration).
# Deployment
We run two instances of collab:
* Staging (https://staging-collab.zed.dev)
* Production (https://collab.zed.dev)
Both of these run on the Kubernetes cluster hosted in Digital Ocean.
Deployment is triggered by pushing to the `collab-staging` (or `collab-production`) tag in Github. The best way to do this is:
* `./script/deploy-collab staging`
* `./script/deploy-collab production`
You can tell what is currently deployed with `./script/what-is-deployed`.
# Database Migrations
To create a new migration:
```
./script/sqlx migrate add <name>
```
Migrations are run automatically on service start, so run `foreman start` again. The service will crash if the migrations fail.
When you create a new migration, you also need to update the [SQLite schema](./migrations.sqlite/20221109000000_test_schema.sql) that is used for testing.

View File

@@ -14,6 +14,7 @@ use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
#[tokio::main]
async fn main() -> Result<()> {
@@ -26,7 +27,7 @@ async fn main() -> Result<()> {
match args().skip(1).next().as_deref() {
Some("version") => {
println!("collab v{VERSION}");
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
}
Some("migrate") => {
run_migrations().await?;
@@ -105,7 +106,7 @@ async fn run_migrations() -> Result<()> {
}
async fn handle_root() -> String {
format!("collab v{VERSION}")
format!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"))
}
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {

View File

@@ -64,6 +64,7 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder;
use tracing::{field, info_span, instrument, Instrument};
use util::SemanticVersion;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@@ -795,6 +796,7 @@ fn broadcast<F>(
lazy_static! {
static ref ZED_PROTOCOL_VERSION: HeaderName = HeaderName::from_static("x-zed-protocol-version");
static ref ZED_APP_VERSION: HeaderName = HeaderName::from_static("x-zed-app-version");
}
pub struct ProtocolVersion(u32);
@@ -824,6 +826,32 @@ impl Header for ProtocolVersion {
}
}
pub struct AppVersionHeader(SemanticVersion);
impl Header for AppVersionHeader {
fn name() -> &'static HeaderName {
&ZED_APP_VERSION
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let version = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?
.parse()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(version))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, values: &mut E) {
values.extend([self.0.to_string().parse().unwrap()]);
}
}
pub fn routes(server: Arc<Server>) -> Router<Body> {
Router::new()
.route("/rpc", get(handle_websocket_request))
@@ -838,6 +866,7 @@ pub fn routes(server: Arc<Server>) -> Router<Body> {
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
_app_version_header: Option<TypedHeader<AppVersionHeader>>,
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
Extension(server): Extension<Arc<Server>>,
Extension(user): Extension<User>,
@@ -851,6 +880,7 @@ pub async fn handle_websocket_request(
)
.into_response();
}
let socket_address = socket_address.to_string();
ws.on_upgrade(move |socket| {
use util::ResultExt;

View File

@@ -161,15 +161,15 @@ async fn test_channel_notes_participant_indices(
// Clients A, B, and C open the channel notes
let channel_view_a = cx_a
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_a.clone(), cx))
.await
.unwrap();
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.await
.unwrap();
let channel_view_c = cx_c
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_c.clone(), cx))
.await
.unwrap();
@@ -644,7 +644,7 @@ async fn test_channel_buffer_changes(
let project_b = client_b.build_empty_local_project(cx_b);
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
let channel_view_b = cx_b
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
.update(|cx| ChannelView::open(channel_id, None, workspace_b.clone(), cx))
.await
.unwrap();
deterministic.run_until_parked();

View File

@@ -1905,7 +1905,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 1.
let channel_notes_1_a = cx_a
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_1_id, None, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_1_a.update(cx_a, |notes, cx| {
@@ -1951,7 +1951,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
// Client A opens the notes for channel 2.
let channel_notes_2_a = cx_a
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
.update(|cx| ChannelView::open(channel_2_id, None, workspace_a.clone(), cx))
.await
.unwrap();
channel_notes_2_a.update(cx_a, |notes, cx| {

View File

@@ -153,6 +153,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
});

View File

@@ -6,11 +6,14 @@ use client::{
Collaborator, ParticipantIndex,
};
use collections::HashMap;
use editor::{CollaborationHub, Editor, EditorEvent};
use editor::{
display_map::ToDisplayPoint, scroll::Autoscroll, CollaborationHub, DisplayPoint, Editor,
EditorEvent,
};
use gpui::{
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
VisualContext as _, WindowContext,
actions, AnyElement, AnyView, AppContext, ClipboardItem, Entity as _, EventEmitter,
FocusableView, IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View,
ViewContext, VisualContext as _, WeakView, WindowContext,
};
use project::Project;
use std::{
@@ -23,10 +26,10 @@ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
};
actions!(collab, [Deploy]);
actions!(collab, [CopyLink]);
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
@@ -34,21 +37,30 @@ pub fn init(cx: &mut AppContext) {
pub struct ChannelView {
pub editor: View<Editor>,
workspace: WeakView<Workspace>,
project: Model<Project>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
remote_id: Option<ViewId>,
_editor_event_subscription: Subscription,
_reparse_subscription: Option<Subscription>,
}
impl ChannelView {
pub fn open(
channel_id: ChannelId,
link_position: Option<String>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let pane = workspace.read(cx).active_pane().clone();
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
let channel_view = Self::open_in_pane(
channel_id,
link_position,
pane.clone(),
workspace.clone(),
cx,
);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
@@ -66,10 +78,12 @@ impl ChannelView {
pub fn open_in_pane(
channel_id: ChannelId,
link_position: Option<String>,
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
let project = workspace.project().to_owned();
let channel_store = ChannelStore::global(cx);
@@ -82,12 +96,13 @@ impl ChannelView {
let channel_buffer = channel_buffer.await?;
let markdown = markdown.await.log_err();
channel_buffer.update(&mut cx, |buffer, cx| {
buffer.buffer().update(cx, |buffer, cx| {
channel_buffer.update(&mut cx, |channel_buffer, cx| {
channel_buffer.buffer().update(cx, |buffer, cx| {
buffer.set_language_registry(language_registry);
if let Some(markdown) = markdown {
buffer.set_language(Some(markdown), cx);
}
let Some(markdown) = markdown else {
return;
};
buffer.set_language(Some(markdown), cx);
})
})?;
@@ -101,12 +116,18 @@ impl ChannelView {
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
let view = cx.new_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
@@ -121,6 +142,12 @@ impl ChannelView {
}
}
if let Some(link_position) = link_position {
view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
view
})
})
@@ -128,16 +155,29 @@ impl ChannelView {
pub fn new(
project: Model<Project>,
workspace: WeakView<Workspace>,
channel_store: Model<ChannelStore>,
channel_buffer: Model<ChannelBuffer>,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = channel_buffer.read(cx).buffer();
let this = cx.view().downgrade();
let editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
channel_buffer.clone(),
)));
editor.set_custom_context_menu(move |_, position, cx| {
let this = this.clone();
Some(ui::ContextMenu::build(cx, move |menu, _| {
menu.entry("Copy link to section", None, move |cx| {
this.update(cx, |this, cx| {
this.copy_link_for_position(position.clone(), cx)
})
.ok();
})
}))
});
editor
});
let _editor_event_subscription =
@@ -148,14 +188,94 @@ impl ChannelView {
Self {
editor,
workspace,
project,
channel_store,
channel_buffer,
remote_id: None,
_editor_event_subscription,
_reparse_subscription: None,
}
}
fn focus_position_from_link(
&mut self,
position: String,
first_attempt: bool,
cx: &mut ViewContext<Self>,
) {
let position = Channel::slug(&position).to_lowercase();
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
if let Some(item) = outline
.items
.iter()
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(&map)])
})
});
return;
}
}
if !first_attempt {
return;
}
self._reparse_subscription = Some(cx.subscribe(
&self.editor,
move |this, _, e: &EditorEvent, cx| {
match e {
EditorEvent::Reparsed => {
this.focus_position_from_link(position.clone(), false, cx);
this._reparse_subscription.take();
}
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
this._reparse_subscription.take();
}
_ => {}
};
},
));
}
fn copy_link(&mut self, _: &CopyLink, cx: &mut ViewContext<Self>) {
let position = self
.editor
.update(cx, |editor, cx| editor.selections.newest_display(cx).start);
self.copy_link_for_position(position, cx)
}
fn copy_link_for_position(&self, position: DisplayPoint, cx: &mut ViewContext<Self>) {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
let mut closest_heading = None;
if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
for item in outline.items {
if item.range.start.to_display_point(&snapshot) > position {
break;
}
closest_heading = Some(item);
}
}
let Some(channel) = self.channel(cx) else {
return;
};
let link = channel.notes_link(closest_heading.map(|heading| heading.text));
cx.write_to_clipboard(ClipboardItem::new(link));
self.workspace
.update(cx, |workspace, cx| {
workspace.show_toast(Toast::new(0, "Link copied to clipboard"), cx);
})
.ok();
}
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
self.channel_buffer.read(cx).channel(cx)
}
@@ -215,8 +335,11 @@ impl ChannelView {
impl EventEmitter<EditorEvent> for ChannelView {}
impl Render for ChannelView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.clone()
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.size_full()
.on_action(cx.listener(Self::copy_link))
.child(self.editor.clone())
}
}
@@ -274,6 +397,7 @@ impl Item for ChannelView {
Some(cx.new_view(|cx| {
Self::new(
self.project.clone(),
self.workspace.clone(),
self.channel_store.clone(),
self.channel_buffer.clone(),
cx,
@@ -356,7 +480,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
let open = ChannelView::open_in_pane(state.channel_id, None, pane, workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;

View File

@@ -453,7 +453,7 @@ impl ChatPanel {
})
.collect::<Vec<_>>();
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
rich_text::render_rich_text(message.body.clone(), &mentions, language_registry, None)
}
fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {

View File

@@ -40,7 +40,7 @@ use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
Workspace,
OpenChannelNotes, Workspace,
};
actions!(
@@ -69,6 +69,19 @@ pub fn init(cx: &mut AppContext) {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<CollabPanel>(cx);
});
workspace.register_action(|_, _: &OpenChannelNotes, cx| {
let channel_id = ActiveCall::global(cx)
.read(cx)
.room()
.and_then(|room| room.read(cx).channel_id());
if let Some(channel_id) = channel_id {
let workspace = cx.view().clone();
cx.window_context().defer(move |cx| {
ChannelView::open(channel_id, None, workspace, cx).detach_and_log_err(cx)
});
}
});
})
.detach();
}
@@ -957,7 +970,7 @@ impl CollabPanel {
.child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, IconName::File)),
)
.child(div().h_7().w_full().child(Label::new("notes")))
.child(Label::new("notes"))
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
}
@@ -1678,7 +1691,7 @@ impl CollabPanel {
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
ChannelView::open(channel_id, workspace, cx).detach();
ChannelView::open(channel_id, None, workspace, cx).detach();
}
}

View File

@@ -14,13 +14,13 @@ pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions,
WindowContext, WindowKind, WindowOptions,
};
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use settings::Settings;
use workspace::AppState;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
collab,
@@ -41,7 +41,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
notifications::init(&app_state, cx);
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut WindowContext) {
let call = ActiveCall::global(cx).read(cx);
if let Some(room) = call.room().cloned() {
let client = call.client();
@@ -64,7 +64,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", cx, |e, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e)));
}
}

View File

@@ -445,7 +445,7 @@ impl Copilot {
)
.detach();
let server = server.initialize(Default::default()).await?;
let server = cx.update(|cx| server.initialize(None, cx))?.await?;
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {

View File

@@ -20,7 +20,7 @@ test-support = [
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
"tree-sitter-typescript"
"tree-sitter-typescript",
]
[dependencies]
@@ -33,13 +33,14 @@ convert_case = "0.6.0"
copilot = { path = "../copilot" }
db = { path = "../db" }
futures.workspace = true
fuzzy = { path = "../fuzzy" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
indoc = "1.0.4"
itertools = "0.10"
language = { path = "../language" }
lazy_static.workspace = true
linkify = "0.10.0"
log.workspace = true
lsp = { path = "../lsp" }
multi_buffer = { path = "../multi_buffer" }
@@ -79,6 +80,7 @@ language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
release_channel = { path = "../release_channel" }
rand.workspace = true
settings = { path = "../settings", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }

View File

@@ -70,6 +70,30 @@ pub struct FoldAt {
pub struct UnfoldAt {
pub buffer_row: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MoveUpByLines {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MoveDownByLines {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectUpByLines {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectDownByLines {
#[serde(default)]
pub(super) lines: u32,
}
impl_actions!(
editor,
[
@@ -84,7 +108,11 @@ impl_actions!(
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt
UnfoldAt,
MoveUpByLines,
MoveDownByLines,
SelectUpByLines,
SelectDownByLines,
]
);

View File

@@ -25,8 +25,8 @@ mod wrap_map;
use crate::EditorStyle;
use crate::{
link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
hover_links::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt, InlayId,
MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
pub use block_map::{BlockMap, BlockPoint};
use collections::{BTreeMap, HashMap, HashSet};
@@ -586,8 +586,9 @@ impl DisplaySnapshot {
text_system,
editor_style,
rem_size,
anchor: _,
scroll_anchor: _,
visible_rows: _,
vertical_scroll_margin: _,
}: &TextLayoutDetails,
) -> Arc<LineLayout> {
let mut runs = Vec::new();

View File

@@ -1168,7 +1168,7 @@ mod tests {
use super::*;
use crate::{
display_map::{InlayHighlights, TextHighlights},
link_go_to_definition::InlayHighlight,
hover_links::InlayHighlight,
InlayId, MultiBuffer,
};
use gpui::AppContext;

View File

@@ -22,9 +22,9 @@ mod inlay_hint_cache;
mod debounced_delay;
mod git;
mod highlight_matching_bracket;
mod hover_links;
mod hover_popover;
pub mod items;
mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod persistence;
@@ -74,10 +74,10 @@ use language::{
language_settings::{self, all_language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
};
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
@@ -374,6 +374,7 @@ pub struct Editor {
hovered_cursors: HashMap<HoveredCursor, Task<()>>,
pub show_local_selections: bool,
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
show_wrap_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
@@ -402,7 +403,7 @@ pub struct Editor {
remote_id: Option<ViewId>,
hover_state: HoverState,
gutter_hovered: bool,
link_go_to_definition_state: LinkGoToDefinitionState,
hovered_link_state: Option<HoveredLinkState>,
copilot_state: CopilotState,
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
@@ -413,6 +414,12 @@ pub struct Editor {
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool,
use_autoclose: bool,
custom_context_menu: Option<
Box<
dyn 'static
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
}
pub struct EditorSnapshot {
@@ -845,15 +852,21 @@ impl CompletionsMenu {
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = {
let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item];
let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child(
render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx),
)),
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
_ => None,
};
multiline_docs.map(|div| {
@@ -870,6 +883,8 @@ impl CompletionsMenu {
// because that would move the cursor.
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
})
} else {
None
};
let list = uniform_list(
@@ -1418,7 +1433,7 @@ impl Editor {
buffer: buffer.clone(),
display_map: display_map.clone(),
selections,
scroll_manager: ScrollManager::new(),
scroll_manager: ScrollManager::new(cx),
columnar_selection_tail: None,
add_selections_state: None,
select_next_state: None,
@@ -1436,6 +1451,7 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
show_wrap_guides: None,
placeholder_text: None,
@@ -1465,7 +1481,7 @@ impl Editor {
leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
link_go_to_definition_state: Default::default(),
hovered_link_state: Default::default(),
copilot_state: Default::default(),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -1476,6 +1492,7 @@ impl Editor {
hovered_cursors: Default::default(),
editor_actions: Default::default(),
show_copilot_suggestions: mode == EditorMode::Full,
custom_context_menu: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1665,6 +1682,14 @@ impl Editor {
self.collaboration_hub = Some(hub);
}
pub fn set_custom_context_menu(
&mut self,
f: impl 'static
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
) {
self.custom_context_menu = Some(Box::new(f))
}
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
self.completion_provider = Some(hub);
}
@@ -3065,8 +3090,9 @@ impl Editor {
text_system: cx.text_system().clone(),
editor_style: self.style.clone().unwrap(),
rem_size: cx.rem_size(),
anchor: self.scroll_manager.anchor().anchor,
scroll_anchor: self.scroll_manager.anchor(),
visible_rows: self.visible_line_count(),
vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin,
}
}
@@ -5358,6 +5384,86 @@ impl Editor {
})
}
pub fn move_up_by_lines(&mut self, action: &MoveUpByLines, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate();
return;
}
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) = movement::up_by_rows(
map,
selection.start,
action.lines,
selection.goal,
false,
&text_layout_details,
);
selection.collapse_to(cursor, goal);
});
})
}
pub fn move_down_by_lines(&mut self, action: &MoveDownByLines, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate();
return;
}
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let line_mode = s.line_mode;
s.move_with(|map, selection| {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) = movement::down_by_rows(
map,
selection.start,
action.lines,
selection.goal,
false,
&text_layout_details,
);
selection.collapse_to(cursor, goal);
});
})
}
pub fn select_down_by_lines(&mut self, action: &SelectDownByLines, cx: &mut ViewContext<Self>) {
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::down_by_rows(map, head, action.lines, goal, false, &text_layout_details)
})
})
}
pub fn select_up_by_lines(&mut self, action: &SelectUpByLines, cx: &mut ViewContext<Self>) {
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up_by_rows(map, head, action.lines, goal, false, &text_layout_details)
})
})
}
pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
if self.take_rename(true, cx).is_some() {
return;
@@ -7141,11 +7247,8 @@ impl Editor {
cx.spawn(|editor, mut cx| async move {
let definitions = definitions.await?;
editor.update(&mut cx, |editor, cx| {
editor.navigate_to_definitions(
definitions
.into_iter()
.map(GoToDefinitionLink::Text)
.collect(),
editor.navigate_to_hover_links(
definitions.into_iter().map(HoverLink::Text).collect(),
split,
cx,
);
@@ -7155,9 +7258,9 @@ impl Editor {
.detach_and_log_err(cx);
}
pub fn navigate_to_definitions(
pub fn navigate_to_hover_links(
&mut self,
mut definitions: Vec<GoToDefinitionLink>,
mut definitions: Vec<HoverLink>,
split: bool,
cx: &mut ViewContext<Editor>,
) {
@@ -7169,10 +7272,14 @@ impl Editor {
if definitions.len() == 1 {
let definition = definitions.pop().unwrap();
let target_task = match definition {
GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
HoverLink::InlayHint(lsp_location, server_id) => {
self.compute_target_location(lsp_location, server_id, cx)
}
HoverLink::Url(url) => {
cx.open_url(&url);
Task::ready(Ok(None))
}
};
cx.spawn(|editor, mut cx| async move {
let target = target_task.await.context("target resolution task")?;
@@ -7223,29 +7330,27 @@ impl Editor {
let title = definitions
.iter()
.find_map(|definition| match definition {
GoToDefinitionLink::Text(link) => {
link.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
})
}
GoToDefinitionLink::InlayHint(_, _) => None,
HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
)
}),
HoverLink::InlayHint(_, _) => None,
HoverLink::Url(_) => None,
})
.unwrap_or("Definitions".to_string());
let location_tasks = definitions
.into_iter()
.map(|definition| match definition {
GoToDefinitionLink::Text(link) => {
Task::Ready(Some(Ok(Some(link.target))))
}
GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
HoverLink::InlayHint(lsp_location, server_id) => {
editor.compute_target_location(lsp_location, server_id, cx)
}
HoverLink::Url(_) => Task::ready(Ok(None)),
})
.collect::<Vec<_>>();
(title, location_tasks)
@@ -7289,9 +7394,7 @@ impl Editor {
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
project
.language_server_for_buffer(buffer.read(cx), server_id, cx)
.map(|(_, lsp_adapter)| {
LanguageServerName(Arc::from(lsp_adapter.name()))
})
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
});
language_server_name.map(|language_server_name| {
project.open_local_buffer_via_lsp(
@@ -8663,6 +8766,9 @@ impl Editor {
)),
cx,
);
let editor_settings = EditorSettings::get_global(cx);
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
cx.notify();
}

View File

@@ -10,7 +10,9 @@ pub struct EditorSettings {
pub show_completion_documentation: bool,
pub completion_documentation_secondary_query_debounce: u64,
pub use_on_type_format: bool,
pub toolbar: Toolbar,
pub scrollbar: Scrollbar,
pub vertical_scroll_margin: f32,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
pub redact_private_values: bool,
@@ -28,12 +30,19 @@ pub enum SeedQuerySetting {
Never,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Toolbar {
pub breadcrumbs: bool,
pub quick_actions: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
pub symbols_selections: bool,
pub diagnostics: bool,
}
/// When to show the scrollbar in the editor.
@@ -84,8 +93,15 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub use_on_type_format: Option<bool>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings
pub scrollbar: Option<ScrollbarContent>,
/// The number of lines to keep above/below the cursor when auto-scrolling.
///
/// Default: 3.
pub vertical_scroll_margin: Option<f32>,
/// Whether the line numbers on editors gutter are relative or not.
///
/// Default: false
@@ -103,6 +119,19 @@ pub struct EditorSettingsContent {
pub redact_private_values: Option<bool>,
}
// Toolbar related settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ToolbarContent {
/// Whether to display breadcrumbs in the editor toolbar.
///
/// Default: true
pub breadcrumbs: Option<bool>,
/// Whether to display quik action buttons in the editor toolbar.
///
/// Default: true
pub quick_actions: Option<bool>,
}
/// Scrollbar related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarContent {
@@ -122,6 +151,10 @@ pub struct ScrollbarContent {
///
/// Default: true
pub symbols_selections: Option<bool>,
/// Whether to show diagnostic indicators in the scrollbar.
///
/// Default: true
pub diagnostics: Option<bool>,
}
impl Settings for EditorSettings {

View File

@@ -8392,6 +8392,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -9,11 +9,6 @@ use crate::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
items::BufferSearchHighlights,
link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
LinkGoToDefinitionState,
},
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
@@ -35,6 +30,7 @@ use gpui::{
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
use lsp::DiagnosticSeverity;
use multi_buffer::Anchor;
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
@@ -146,7 +142,11 @@ impl EditorElement {
register_action(view, cx, Editor::move_left);
register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down);
register_action(view, cx, Editor::move_down_by_lines);
register_action(view, cx, Editor::select_down_by_lines);
register_action(view, cx, Editor::move_up);
register_action(view, cx, Editor::move_up_by_lines);
register_action(view, cx, Editor::select_up_by_lines);
register_action(view, cx, Editor::cancel);
register_action(view, cx, Editor::newline);
register_action(view, cx, Editor::newline_above);
@@ -332,7 +332,14 @@ impl EditorElement {
register_action(view, cx, Editor::display_cursor_names);
}
fn register_key_listeners(&self, cx: &mut ElementContext) {
fn register_key_listeners(
&self,
cx: &mut ElementContext,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
) {
let position_map = layout.position_map.clone();
let stacking_order = cx.stacking_order().clone();
cx.on_key_event({
let editor = self.editor.clone();
move |event: &ModifiersChangedEvent, phase, cx| {
@@ -340,46 +347,41 @@ impl EditorElement {
return;
}
if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
cx.stop_propagation();
}
editor.update(cx, |editor, cx| {
Self::modifiers_changed(
editor,
event,
&position_map,
text_bounds,
&stacking_order,
cx,
)
})
}
});
}
pub(crate) fn modifiers_changed(
fn modifiers_changed(
editor: &mut Editor,
event: &ModifiersChangedEvent,
position_map: &PositionMap,
text_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>,
) -> bool {
let pending_selection = editor.has_pending_selection();
if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
if event.command && !pending_selection {
let point = point.clone();
let snapshot = editor.snapshot(cx);
let kind = point.definition_kind(event.shift);
show_link_definition(kind, editor, point, snapshot, cx);
return false;
}
}
) {
let mouse_position = cx.mouse_position();
if !text_bounds.contains(&mouse_position)
|| !cx.was_top_layer(&mouse_position, stacking_order)
{
if editor.link_go_to_definition_state.symbol_range.is_some()
|| !editor.link_go_to_definition_state.definitions.is_empty()
{
editor.link_go_to_definition_state.symbol_range.take();
editor.link_go_to_definition_state.definitions.clear();
cx.notify();
}
editor.link_go_to_definition_state.task = None;
editor.clear_highlights::<LinkGoToDefinitionState>(cx);
return;
}
false
editor.update_hovered_link(
position_map.point_for_position(text_bounds, mouse_position),
&position_map.snapshot,
event.modifiers,
cx,
)
}
fn mouse_left_down(
@@ -480,13 +482,7 @@ impl EditorElement {
&& cx.was_top_layer(&event.position, stacking_order)
{
let point = position_map.point_for_position(text_bounds, event.position);
let could_be_inlay = point.as_valid().is_none();
let split = event.modifiers.alt;
if event.modifiers.shift || could_be_inlay {
go_to_fetched_type_definition(editor, point, split, cx);
} else {
go_to_fetched_definition(editor, point, split, cx);
}
editor.handle_click_hovered_link(point, event.modifiers, cx);
cx.stop_propagation();
} else if end_selection {
@@ -559,31 +555,14 @@ impl EditorElement {
if text_hovered && was_top {
let point_for_position = position_map.point_for_position(text_bounds, event.position);
match point_for_position.as_valid() {
Some(point) => {
update_go_to_definition_link(
editor,
Some(GoToDefinitionTrigger::Text(point)),
modifiers.command,
modifiers.shift,
cx,
);
hover_at(editor, Some(point), cx);
Self::update_visible_cursor(editor, point, position_map, cx);
}
None => {
update_inlay_link_and_hover_points(
&position_map.snapshot,
point_for_position,
editor,
modifiers.command,
modifiers.shift,
cx,
);
}
editor.update_hovered_link(point_for_position, &position_map.snapshot, modifiers, cx);
if let Some(point) = point_for_position.as_valid() {
hover_at(editor, Some(point), cx);
Self::update_visible_cursor(editor, point, position_map, cx);
}
} else {
update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
editor.hide_hovered_link(cx);
hover_at(editor, None, cx);
if gutter_hovered && was_top {
cx.stop_propagation();
@@ -925,13 +904,13 @@ impl EditorElement {
if self
.editor
.read(cx)
.link_go_to_definition_state
.definitions
.is_empty()
.hovered_link_state
.as_ref()
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
{
cx.set_cursor_style(CursorStyle::IBeam);
} else {
cx.set_cursor_style(CursorStyle::PointingHand);
} else {
cx.set_cursor_style(CursorStyle::IBeam);
}
}
@@ -1477,6 +1456,64 @@ impl EditorElement {
}
}
if layout.is_singleton && scrollbar_settings.diagnostics {
let max_point = layout
.position_map
.snapshot
.display_snapshot
.buffer_snapshot
.max_point();
let diagnostics = layout
.position_map
.snapshot
.buffer_snapshot
.diagnostics_in_range::<_, Point>(Point::zero()..max_point, false)
// We want to sort by severity, in order to paint the most severe diagnostics last.
.sorted_by_key(|diagnostic| std::cmp::Reverse(diagnostic.diagnostic.severity));
for diagnostic in diagnostics {
let start_display = diagnostic
.range
.start
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = diagnostic
.range
.end
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if diagnostic.range.start == diagnostic.range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
let color = match diagnostic.diagnostic.severity {
DiagnosticSeverity::ERROR => cx.theme().status().error,
DiagnosticSeverity::WARNING => cx.theme().status().warning,
DiagnosticSeverity::INFORMATION => cx.theme().status().info,
_ => cx.theme().status().hint,
};
cx.paint_quad(quad(
bounds,
Corners::default(),
color,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
cx.paint_quad(quad(
thumb_bounds,
Corners::default(),
@@ -2106,6 +2143,9 @@ impl EditorElement {
// Symbols Selections
(is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
||
// Diagnostics
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
||
// Scrollmanager
editor.scroll_manager.scrollbars_visible()
}
@@ -3039,9 +3079,9 @@ impl Element for EditorElement {
let key_context = self.editor.read(cx).key_context(cx);
cx.with_key_dispatch(Some(key_context), Some(focus_handle.clone()), |_, cx| {
self.register_actions(cx);
self.register_key_listeners(cx);
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
self.register_key_listeners(cx, text_bounds, &layout);
cx.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.editor.clone()),
@@ -3158,16 +3198,6 @@ pub struct PointForPosition {
}
impl PointForPosition {
#[cfg(test)]
pub fn valid(valid: DisplayPoint) -> Self {
Self {
previous_valid: valid,
next_valid: valid,
exact_unclipped: valid,
column_overshoot_after_line_end: 0,
}
}
pub fn as_valid(&self) -> Option<DisplayPoint> {
if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
Some(self.previous_valid)

View File

@@ -1,6 +1,6 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
link_go_to_definition::{InlayHighlight, RangeInEditor},
hover_links::{InlayHighlight, RangeInEditor},
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
ExcerptId, Hover, RangeToAnchorExt,
};
@@ -605,8 +605,8 @@ mod tests {
use crate::{
editor_tests::init_test,
element::PointForPosition,
hover_links::update_inlay_link_and_hover_points,
inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
link_go_to_definition::update_inlay_link_and_hover_points,
test::editor_lsp_test_context::EditorLspTestContext,
InlayId,
};

View File

@@ -19,7 +19,7 @@ use crate::{
use anyhow::Context;
use clock::Global;
use futures::future;
use gpui::{Model, ModelContext, Task, ViewContext};
use gpui::{AsyncWindowContext, Model, ModelContext, Task, ViewContext};
use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
use parking_lot::RwLock;
use project::{InlayHint, ResolveState};
@@ -29,7 +29,7 @@ use language::language_settings::InlayHintSettings;
use smol::lock::Semaphore;
use sum_tree::Bias;
use text::{BufferId, ToOffset, ToPoint};
use util::post_inc;
use util::{post_inc, ResultExt};
pub struct InlayHintCache {
hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
@@ -78,6 +78,7 @@ pub(super) enum InvalidationStrategy {
/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes.
/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead.
/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen.
#[derive(Debug)]
pub(super) struct InlaySplice {
pub to_remove: Vec<InlayId>,
pub to_insert: Vec<Inlay>,
@@ -619,7 +620,6 @@ fn spawn_new_update_tasks(
update_cache_version: usize,
cx: &mut ViewContext<'_, Editor>,
) {
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
excerpts_to_query
{
@@ -636,8 +636,7 @@ fn spawn_new_update_tasks(
continue;
}
let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) {
let cached_excerpt_hints = cached_excerpt_hints.read();
let cached_buffer_version = &cached_excerpt_hints.buffer_version;
if cached_excerpt_hints.version > update_cache_version
@@ -647,20 +646,15 @@ fn spawn_new_update_tasks(
}
};
let (multi_buffer_snapshot, Some(query_ranges)) =
editor.buffer.update(cx, |multi_buffer, cx| {
(
multi_buffer.snapshot(cx),
determine_query_ranges(
multi_buffer,
excerpt_id,
&excerpt_buffer,
excerpt_visible_range,
cx,
),
)
})
else {
let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| {
determine_query_ranges(
multi_buffer,
excerpt_id,
&excerpt_buffer,
excerpt_visible_range,
cx,
)
}) else {
return;
};
let query = ExcerptQuery {
@@ -671,18 +665,8 @@ fn spawn_new_update_tasks(
reason,
};
let new_update_task = |query_ranges| {
new_update_task(
query,
query_ranges,
multi_buffer_snapshot,
buffer_snapshot.clone(),
Arc::clone(&visible_hints),
cached_excerpt_hints,
Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
cx,
)
};
let mut new_update_task =
|query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx);
match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
hash_map::Entry::Occupied(mut o) => {
@@ -790,62 +774,55 @@ const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
fn new_update_task(
query: ExcerptQuery,
query_ranges: QueryRanges,
multi_buffer_snapshot: MultiBufferSnapshot,
buffer_snapshot: BufferSnapshot,
visible_hints: Arc<Vec<Inlay>>,
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
lsp_request_limiter: Arc<Semaphore>,
excerpt_buffer: Model<Buffer>,
cx: &mut ViewContext<'_, Editor>,
) -> Task<()> {
cx.spawn(|editor, mut cx| async move {
let closure_cx = cx.clone();
let fetch_and_update_hints = |invalidate, range| {
fetch_and_update_hints(
editor.clone(),
multi_buffer_snapshot.clone(),
buffer_snapshot.clone(),
Arc::clone(&visible_hints),
cached_excerpt_hints.as_ref().map(Arc::clone),
query,
invalidate,
range,
Arc::clone(&lsp_request_limiter),
closure_cx.clone(),
)
};
let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
|visible_range| async move {
(
visible_range.clone(),
fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
.await,
)
},
))
cx.spawn(move |editor, mut cx| async move {
let visible_range_update_results = future::join_all(
query_ranges
.visible
.into_iter()
.filter_map(|visible_range| {
let fetch_task = editor
.update(&mut cx, |_, cx| {
fetch_and_update_hints(
excerpt_buffer.clone(),
query,
visible_range.clone(),
query.invalidate.should_invalidate(),
cx,
)
})
.log_err()?;
Some(async move { (visible_range, fetch_task.await) })
}),
)
.await;
let hint_delay = cx.background_executor().timer(Duration::from_millis(
INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
));
let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
editor
.update(&mut cx, |editor, _| {
if let Some(task_ranges) = editor
.inlay_hint_cache
.update_tasks
.get_mut(&query.excerpt_id)
{
task_ranges.invalidate_range(&buffer_snapshot, &range);
}
})
.ok()
};
let query_range_failed =
|range: &Range<language::Anchor>, e: anyhow::Error, cx: &mut AsyncWindowContext| {
log::error!("inlay hint update task for range {range:?} failed: {e:#}");
editor
.update(cx, |editor, cx| {
if let Some(task_ranges) = editor
.inlay_hint_cache
.update_tasks
.get_mut(&query.excerpt_id)
{
let buffer_snapshot = excerpt_buffer.read(cx).snapshot();
task_ranges.invalidate_range(&buffer_snapshot, &range);
}
})
.ok()
};
for (range, result) in visible_range_update_results {
if let Err(e) = result {
query_range_failed(&range, e);
query_range_failed(&range, e, &mut cx);
}
}
@@ -855,149 +832,171 @@ fn new_update_task(
.before_visible
.into_iter()
.chain(query_ranges.after_visible.into_iter())
.map(|invisible_range| async move {
(
invisible_range.clone(),
fetch_and_update_hints(false, invisible_range).await,
)
.filter_map(|invisible_range| {
let fetch_task = editor
.update(&mut cx, |_, cx| {
fetch_and_update_hints(
excerpt_buffer.clone(),
query,
invisible_range.clone(),
false, // visible screen request already invalidated the entries
cx,
)
})
.log_err()?;
Some(async move { (invisible_range, fetch_task.await) })
}),
)
.await;
for (range, result) in invisible_range_update_results {
if let Err(e) = result {
query_range_failed(&range, e);
query_range_failed(&range, e, &mut cx);
}
}
})
}
async fn fetch_and_update_hints(
editor: gpui::WeakView<Editor>,
multi_buffer_snapshot: MultiBufferSnapshot,
buffer_snapshot: BufferSnapshot,
visible_hints: Arc<Vec<Inlay>>,
cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
fn fetch_and_update_hints(
excerpt_buffer: Model<Buffer>,
query: ExcerptQuery,
invalidate: bool,
fetch_range: Range<language::Anchor>,
lsp_request_limiter: Arc<Semaphore>,
mut cx: gpui::AsyncWindowContext,
) -> anyhow::Result<()> {
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
(None, false)
} else {
match lsp_request_limiter.try_acquire() {
Some(guard) => (Some(guard), false),
None => (Some(lsp_request_limiter.acquire().await), true),
}
};
let fetch_range_to_log =
fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
let inlay_hints_fetch_task = editor
.update(&mut cx, |editor, cx| {
if got_throttled {
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
Some((_, _, current_visible_range)) => {
let visible_offset_length = current_visible_range.len();
let double_visible_range = current_visible_range
.start
.saturating_sub(visible_offset_length)
..current_visible_range
.end
.saturating_add(visible_offset_length)
.min(buffer_snapshot.len());
!double_visible_range
.contains(&fetch_range.start.to_offset(&buffer_snapshot))
&& !double_visible_range
.contains(&fetch_range.end.to_offset(&buffer_snapshot))
},
None => true,
};
if query_not_around_visible_range {
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
if let Some(task_ranges) = editor
.inlay_hint_cache
.update_tasks
.get_mut(&query.excerpt_id)
{
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
}
return None;
}
}
editor
.buffer()
.read(cx)
.buffer(query.buffer_id)
.and_then(|buffer| {
let project = editor.project.as_ref()?;
Some(project.update(cx, |project, cx| {
project.inlay_hints(buffer, fetch_range.clone(), cx)
}))
})
})
.ok()
.flatten();
let new_hints = match inlay_hints_fetch_task {
Some(fetch_task) => {
log::debug!(
"Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
query_reason = query.reason,
);
log::trace!(
"Currently visible hints: {visible_hints:?}, cached hints present: {}",
cached_excerpt_hints.is_some(),
);
fetch_task.await.context("inlay hint fetch task")?
}
None => return Ok(()),
};
drop(lsp_request_guard);
log::debug!(
"Fetched {} hints for range {fetch_range_to_log:?}",
new_hints.len()
);
log::trace!("Fetched hints: {new_hints:?}");
invalidate: bool,
cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<()>> {
cx.spawn(|editor, mut cx| async move {
let buffer_snapshot = excerpt_buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let (lsp_request_limiter, multi_buffer_snapshot) = editor.update(&mut cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter);
(lsp_request_limiter, multi_buffer_snapshot)
})?;
let background_task_buffer_snapshot = buffer_snapshot.clone();
let background_fetch_range = fetch_range.clone();
let new_update = cx
.background_executor()
.spawn(async move {
calculate_hint_updates(
query.excerpt_id,
invalidate,
background_fetch_range,
new_hints,
&background_task_buffer_snapshot,
cached_excerpt_hints,
&visible_hints,
)
})
.await;
if let Some(new_update) = new_update {
log::debug!(
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
new_update.remove_from_visible.len(),
new_update.remove_from_cache.len(),
new_update.add_to_cache.len()
);
log::trace!("New update: {new_update:?}");
editor
let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
(None, false)
} else {
match lsp_request_limiter.try_acquire() {
Some(guard) => (Some(guard), false),
None => (Some(lsp_request_limiter.acquire().await), true),
}
};
let fetch_range_to_log =
fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
let inlay_hints_fetch_task = editor
.update(&mut cx, |editor, cx| {
apply_hint_update(
editor,
new_update,
query,
invalidate,
buffer_snapshot,
multi_buffer_snapshot,
cx,
);
if got_throttled {
let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) {
Some((_, _, current_visible_range)) => {
let visible_offset_length = current_visible_range.len();
let double_visible_range = current_visible_range
.start
.saturating_sub(visible_offset_length)
..current_visible_range
.end
.saturating_add(visible_offset_length)
.min(buffer_snapshot.len());
!double_visible_range
.contains(&fetch_range.start.to_offset(&buffer_snapshot))
&& !double_visible_range
.contains(&fetch_range.end.to_offset(&buffer_snapshot))
},
None => true,
};
if query_not_around_visible_range {
log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
if let Some(task_ranges) = editor
.inlay_hint_cache
.update_tasks
.get_mut(&query.excerpt_id)
{
task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
}
return None;
}
}
editor
.buffer()
.read(cx)
.buffer(query.buffer_id)
.and_then(|buffer| {
let project = editor.project.as_ref()?;
Some(project.update(cx, |project, cx| {
project.inlay_hints(buffer, fetch_range.clone(), cx)
}))
})
})
.ok();
}
Ok(())
.ok()
.flatten();
let cached_excerpt_hints = editor.update(&mut cx, |editor, _| {
editor
.inlay_hint_cache
.hints
.get(&query.excerpt_id)
.cloned()
})?;
let visible_hints = editor.update(&mut cx, |editor, cx| editor.visible_inlay_hints(cx))?;
let new_hints = match inlay_hints_fetch_task {
Some(fetch_task) => {
log::debug!(
"Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
query_reason = query.reason,
);
log::trace!(
"Currently visible hints: {visible_hints:?}, cached hints present: {}",
cached_excerpt_hints.is_some(),
);
fetch_task.await.context("inlay hint fetch task")?
}
None => return Ok(()),
};
drop(lsp_request_guard);
log::debug!(
"Fetched {} hints for range {fetch_range_to_log:?}",
new_hints.len()
);
log::trace!("Fetched hints: {new_hints:?}");
let background_task_buffer_snapshot = buffer_snapshot.clone();
let background_fetch_range = fetch_range.clone();
let new_update = cx
.background_executor()
.spawn(async move {
calculate_hint_updates(
query.excerpt_id,
invalidate,
background_fetch_range,
new_hints,
&background_task_buffer_snapshot,
cached_excerpt_hints,
&visible_hints,
)
})
.await;
if let Some(new_update) = new_update {
log::debug!(
"Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
new_update.remove_from_visible.len(),
new_update.remove_from_cache.len(),
new_update.add_to_cache.len()
);
log::trace!("New update: {new_update:?}");
editor
.update(&mut cx, |editor, cx| {
apply_hint_update(
editor,
new_update,
query,
invalidate,
buffer_snapshot,
multi_buffer_snapshot,
cx,
);
})
.ok();
}
anyhow::Ok(())
})
}
fn calculate_hint_updates(
@@ -2436,7 +2435,7 @@ pub mod tests {
});
}
#[gpui::test(iterations = 10)]
#[gpui::test(iterations = 30)]
async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
@@ -3216,6 +3215,7 @@ pub mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -1,7 +1,7 @@
use crate::{
editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition,
persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, EditorEvent, EditorSettings,
ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
NavigationData, ToPoint as _,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashSet;
@@ -682,8 +682,7 @@ impl Item for Editor {
}
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
hide_link_definition(self, cx);
self.link_go_to_definition_state.last_trigger_point = None;
self.hide_hovered_link(cx);
}
fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -801,7 +800,11 @@ impl Item for Editor {
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
if self.show_breadcrumbs {
ToolbarItemLocation::PrimaryLeft
} else {
ToolbarItemLocation::Hidden
}
}
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {

View File

@@ -25,31 +25,40 @@ pub fn deploy_context_menu(
return;
}
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
let menu = custom(editor, point, cx);
editor.custom_context_menu = Some(custom);
if menu.is_none() {
return;
}
menu.unwrap()
} else {
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
let context_menu = ui::ContextMenu::build(cx, |menu, _cx| {
menu.action("Rename Symbol", Box::new(Rename))
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
.action("Find All References", Box::new(FindAllReferences))
.action(
"Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: false,
}),
)
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
});
ui::ContextMenu::build(cx, |menu, _cx| {
menu.action("Rename Symbol", Box::new(Rename))
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
.action("Find All References", Box::new(FindAllReferences))
.action(
"Code Actions",
Box::new(ToggleCodeActions {
deployed_from_indicator: false,
}),
)
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
})
};
let context_menu_focus = context_menu.focus_handle(cx);
cx.focus(&context_menu_focus);

View File

@@ -2,14 +2,12 @@
//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate.
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, TextSystem};
use crate::{char_kind, scroll::ScrollAnchor, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, WindowTextSystem};
use language::Point;
use std::{ops::Range, sync::Arc};
use multi_buffer::Anchor;
/// Defines search strategy for items in `movement` module.
/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas
/// `FindRange::MultiLine` keeps going until the end of a string.
@@ -22,11 +20,12 @@ pub enum FindRange {
/// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters.
pub struct TextLayoutDetails {
pub(crate) text_system: Arc<TextSystem>,
pub(crate) text_system: Arc<WindowTextSystem>,
pub(crate) editor_style: EditorStyle,
pub(crate) rem_size: Pixels,
pub anchor: Anchor,
pub scroll_anchor: ScrollAnchor,
pub visible_rows: Option<f32>,
pub vertical_scroll_margin: f32,
}
/// Returns a column to the left of the current point, wrapping

View File

@@ -6,13 +6,14 @@ use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason,
Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, InlayHintRefreshReason,
MultiBufferSnapshot, ToPoint,
};
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext};
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
use settings::Settings;
use std::{
cmp::Ordering,
time::{Duration, Instant},
@@ -21,7 +22,6 @@ use util::ResultExt;
use workspace::{ItemId, WorkspaceId};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Default)]
@@ -128,7 +128,7 @@ impl OngoingScroll {
}
pub struct ScrollManager {
vertical_scroll_margin: f32,
pub(crate) vertical_scroll_margin: f32,
anchor: ScrollAnchor,
ongoing: OngoingScroll,
autoscroll_request: Option<(Autoscroll, bool)>,
@@ -140,9 +140,9 @@ pub struct ScrollManager {
}
impl ScrollManager {
pub fn new() -> Self {
pub fn new(cx: &mut WindowContext) -> Self {
ScrollManager {
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
anchor: ScrollAnchor::new(),
ongoing: OngoingScroll::new(),
autoscroll_request: None,

View File

@@ -12,17 +12,26 @@ pub enum Autoscroll {
}
impl Autoscroll {
/// scrolls the minimal amount to (try) and fit all cursors onscreen
pub fn fit() -> Self {
Self::Strategy(AutoscrollStrategy::Fit)
}
/// scrolls the minimal amount to fit the newest cursor
pub fn newest() -> Self {
Self::Strategy(AutoscrollStrategy::Newest)
}
/// scrolls so the newest cursor is vertically centered
pub fn center() -> Self {
Self::Strategy(AutoscrollStrategy::Center)
}
/// scrolls so the neweset cursor is near the top
/// (offset by vertical_scroll_margin)
pub fn focused() -> Self {
Self::Strategy(AutoscrollStrategy::Focused)
}
}
#[derive(PartialEq, Eq, Default, Clone, Copy)]
@@ -31,6 +40,7 @@ pub enum AutoscrollStrategy {
Newest,
#[default]
Center,
Focused,
Top,
Bottom,
}
@@ -155,6 +165,11 @@ impl Editor {
scroll_position.y = (target_top - margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Focused => {
scroll_position.y =
(target_top - self.scroll_manager.vertical_scroll_margin).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.y = (target_top).max(0.0);
self.set_scroll_position_internal(scroll_position, local, true, cx);

View File

@@ -4,7 +4,8 @@ use crate::{
use collections::BTreeMap;
use futures::Future;
use gpui::{
AnyWindowHandle, AppContext, Keystroke, ModelContext, View, ViewContext, VisualTestContext,
AnyWindowHandle, AppContext, Keystroke, ModelContext, Pixels, Point, View, ViewContext,
VisualTestContext,
};
use indoc::indoc;
use itertools::Itertools;
@@ -187,6 +188,31 @@ impl EditorTestContext {
ranges[0].start.to_display_point(&snapshot)
}
pub fn pixel_position(&mut self, marked_text: &str) -> Point<Pixels> {
let display_point = self.display_point(marked_text);
self.pixel_position_for(display_point)
}
pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point<Pixels> {
self.update_editor(|editor, cx| {
let newest_point = editor.selections.newest_display(cx).head();
let pixel_position = editor.pixel_position_of_newest_cursor.unwrap();
let line_height = editor
.style()
.unwrap()
.text
.line_height_in_pixels(cx.rem_size());
let snapshot = editor.snapshot(cx);
let details = editor.text_layout_details(cx);
let y = pixel_position.y
+ line_height * (display_point.row() as f32 - newest_point.row() as f32);
let x = pixel_position.x + snapshot.x_for_display_point(display_point, &details)
- snapshot.x_for_display_point(newest_point, &details);
Point::new(x, y)
})
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
@@ -343,7 +369,7 @@ impl EditorTestContext {
}
impl Deref for EditorTestContext {
type Target = gpui::TestAppContext;
type Target = gpui::VisualTestContext;
fn deref(&self) -> &Self::Target {
&self.cx

View File

@@ -1,14 +1,13 @@
use client::ZED_APP_VERSION;
use gpui::AppContext;
use human_bytes::human_bytes;
use release_channel::ReleaseChannel;
use release_channel::{AppVersion, ReleaseChannel};
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{RefreshKind, System, SystemExt};
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
app_version: Option<String>,
app_version: String,
release_channel: &'static str,
os_name: &'static str,
os_version: Option<String>,
@@ -18,9 +17,7 @@ pub struct SystemSpecs {
impl SystemSpecs {
pub fn new(cx: &AppContext) -> Self {
let app_version = ZED_APP_VERSION
.or_else(|| cx.app_metadata().app_version)
.map(|v| v.to_string());
let app_version = AppVersion::global(cx).to_string();
let release_channel = ReleaseChannel::global(cx).display_name();
let os_name = cx.app_metadata().os_name;
let system = System::new_with_specifics(RefreshKind::new().with_memory());
@@ -48,18 +45,15 @@ impl Display for SystemSpecs {
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
None => format!("OS: {}", self.os_name),
};
let app_version_information = self
.app_version
.as_ref()
.map(|app_version| format!("Zed: v{} ({})", app_version, self.release_channel));
let app_version_information =
format!("Zed: v{} ({})", self.app_version, self.release_channel);
let system_specs = [
app_version_information,
Some(os_information),
Some(format!("Memory: {}", human_bytes(self.memory as f64))),
Some(format!("Architecture: {}", self.architecture)),
os_information,
format!("Memory: {}", human_bytes(self.memory as f64)),
format!("Architecture: {}", self.architecture),
]
.into_iter()
.flatten()
.collect::<Vec<String>>()
.join("\n");

View File

@@ -15,6 +15,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" }
itertools = "0.11"
menu = { path = "../menu" }
picker = { path = "../picker" }
postage.workspace = true

View File

@@ -1,13 +1,14 @@
#[cfg(test)]
mod file_finder_tests;
use collections::HashMap;
use collections::{HashMap, HashSet};
use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
@@ -64,33 +65,15 @@ impl FileFinder {
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
let history_items = workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.chain(
workspace
.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
.into_iter()
.filter(|(history_path, _)| {
Some(history_path)
!= currently_opened_path
.as_ref()
.map(|found_path| &found_path.project)
})
.filter(|(_, history_abs_path)| {
history_abs_path.as_ref()
!= currently_opened_path
.as_ref()
.and_then(|found_path| found_path.absolute.as_ref())
})
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
.filter(|(_, history_abs_path)| match history_abs_path {
Some(abs_path) => history_file_exists(abs_path),
None => true,
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path))
.collect::<Vec<_>>();
let project = workspace.project().clone();
let weak_workspace = cx.view().downgrade();
@@ -139,7 +122,7 @@ pub struct FileFinderDelegate {
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
currently_opened_path: Option<FoundPath>,
matches: Matches,
selected_index: Option<usize>,
selected_index: usize,
cancel_flag: Arc<AtomicBool>,
history_items: Vec<FoundPath>,
}
@@ -209,31 +192,21 @@ impl Matches {
fn push_new_matches(
&mut self,
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
new_search_matches: impl Iterator<Item = ProjectPanelOrdMatch>,
extend_old_matches: bool,
) {
let matching_history_paths = matching_history_item_paths(history_items, query);
let matching_history_paths =
matching_history_item_paths(history_items, currently_opened, query);
let new_search_matches = new_search_matches
.filter(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
let history_items_to_show = history_items.iter().filter_map(|history_item| {
Some((
history_item.clone(),
Some(
matching_history_paths
.get(&history_item.project.path)?
.clone(),
),
))
});
self.history.clear();
util::extend_sorted(
&mut self.history,
history_items_to_show,
100,
|(_, a), (_, b)| b.cmp(a),
);
self.set_new_history(
currently_opened,
Some(&matching_history_paths),
history_items,
);
if extend_old_matches {
self.search
.retain(|path_match| !matching_history_paths.contains_key(&path_match.0.path));
@@ -242,14 +215,52 @@ impl Matches {
}
util::extend_sorted(&mut self.search, new_search_matches, 100, |a, b| b.cmp(a));
}
fn set_new_history<'a>(
&mut self,
currently_opened: Option<&'a FoundPath>,
query_matches: Option<&'a HashMap<Arc<Path>, ProjectPanelOrdMatch>>,
history_items: impl IntoIterator<Item = &'a FoundPath> + 'a,
) {
let mut processed_paths = HashSet::default();
self.history = history_items
.into_iter()
.chain(currently_opened)
.filter(|&path| processed_paths.insert(path))
.filter_map(|history_item| match &query_matches {
Some(query_matches) => Some((
history_item.clone(),
Some(query_matches.get(&history_item.project.path)?.clone()),
)),
None => Some((history_item.clone(), None)),
})
.enumerate()
.sorted_by(
|(index_a, (path_a, match_a)), (index_b, (path_b, match_b))| match (
Some(path_a) == currently_opened,
Some(path_b) == currently_opened,
) {
// bubble currently opened files to the top
(true, false) => cmp::Ordering::Less,
(false, true) => cmp::Ordering::Greater,
// arrange the files by their score (best score on top) and by their occurrence in the history
// (history items visited later are on the top)
_ => match_b.cmp(match_a).then(index_a.cmp(index_b)),
},
)
.map(|(_, paths)| paths)
.collect();
}
}
fn matching_history_item_paths(
history_items: &Vec<FoundPath>,
currently_opened: Option<&FoundPath>,
query: &PathLikeWithPosition<FileSearchQuery>,
) -> HashMap<Arc<Path>, ProjectPanelOrdMatch> {
let history_items_by_worktrees = history_items
.iter()
.chain(currently_opened)
.filter_map(|found_path| {
let candidate = PathMatchCandidate {
path: &found_path.project.path,
@@ -301,7 +312,7 @@ fn matching_history_item_paths(
matching_history_paths
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct FoundPath {
project: ProjectPath,
absolute: Option<PathBuf>,
@@ -372,7 +383,7 @@ impl FileFinderDelegate {
latest_search_query: None,
currently_opened_path,
matches: Matches::default(),
selected_index: None,
selected_index: 0,
cancel_flag: Arc::new(AtomicBool::new(false)),
history_items,
}
@@ -427,7 +438,6 @@ impl FileFinderDelegate {
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
picker
.update(&mut cx, |picker, cx| {
picker.delegate.selected_index.take();
picker
.delegate
.set_search_matches(search_id, did_cancel, query, matches, cx)
@@ -454,12 +464,14 @@ impl FileFinderDelegate {
.map(|query| query.path_like.path_query());
self.matches.push_new_matches(
&self.history_items,
self.currently_opened_path.as_ref(),
&query,
matches.into_iter(),
extend_old_matches,
);
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
self.selected_index = self.calculate_selected_index();
cx.notify();
}
}
@@ -630,6 +642,19 @@ impl FileFinderDelegate {
.log_err();
})
}
/// Skips first history match (that is displayed topmost) if it's currently opened.
fn calculate_selected_index(&self) -> usize {
if let Some(Match::History(path, _)) = self.matches.get(0) {
if Some(path) == self.currently_opened_path.as_ref() {
let elements_after_first = self.matches.len() - 1;
if elements_after_first > 0 {
return 1;
}
}
}
0
}
}
impl PickerDelegate for FileFinderDelegate {
@@ -644,11 +669,11 @@ impl PickerDelegate for FileFinderDelegate {
}
fn selected_index(&self) -> usize {
self.selected_index.unwrap_or(0)
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = Some(ix);
self.selected_index = ix;
cx.notify();
}
@@ -671,22 +696,22 @@ impl PickerDelegate for FileFinderDelegate {
if raw_query.is_empty() {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count);
self.selected_index.take();
self.matches = Matches {
history: self
.history_items
.iter()
.filter(|history_item| {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local() && history_item.absolute.is_some())
})
.cloned()
.map(|p| (p, None))
.collect(),
history: Vec::new(),
search: Vec::new(),
};
self.matches.set_new_history(
self.currently_opened_path.as_ref(),
None,
self.history_items.iter().filter(|history_item| {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
|| (project.is_local() && history_item.absolute.is_some())
}),
);
self.selected_index = self.calculate_selected_index();
cx.notify();
Task::ready(())
} else {

View File

@@ -1062,6 +1062,177 @@ async fn test_search_sorts_history_items(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/root",
json!({
"test": {
"1_qw": "",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
// Open new buffer
open_queried_buffer("1", 1, "1_qw", &workspace, cx).await;
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_match_selection(&finder, 0, "1_qw");
});
}
#[gpui::test]
async fn test_keep_opened_file_on_top_of_search_results_and_select_next_one(
cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"bar.rs": "// Bar file",
"lib.rs": "// Lib file",
"maaa.rs": "// Maaaaaaa",
"main.rs": "// Main file",
"moo.rs": "// Moooooo",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
open_close_queried_buffer("bar", 1, "bar.rs", &workspace, cx).await;
open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
open_queried_buffer("main", 1, "main.rs", &workspace, cx).await;
// main.rs is on top, previously used is selected
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "lib.rs");
assert_match_at_position(finder, 2, "bar.rs");
});
// all files match, main.rs is still on top
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches(".rs".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 5);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "bar.rs");
});
// main.rs is not among matches, select top item
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("b".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 2);
assert_match_at_position(finder, 0, "bar.rs");
});
// main.rs is back, put it on top and select next item
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("m".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "moo.rs");
});
// get back to the initial state
picker
.update(cx, |finder, cx| {
finder.delegate.update_matches("".to_string(), cx)
})
.await;
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "main.rs");
assert_match_selection(finder, 1, "lib.rs");
});
}
#[gpui::test]
async fn test_history_items_shown_in_order_of_open(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/test",
json!({
"test": {
"1.txt": "// One",
"2.txt": "// Two",
"3.txt": "// Three",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
open_queried_buffer("1", 1, "1.txt", &workspace, cx).await;
open_queried_buffer("2", 1, "2.txt", &workspace, cx).await;
open_queried_buffer("3", 1, "3.txt", &workspace, cx).await;
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "3.txt");
assert_match_selection(finder, 1, "2.txt");
assert_match_at_position(finder, 2, "1.txt");
});
cx.dispatch_action(Confirm); // Open 2.txt
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "2.txt");
assert_match_selection(finder, 1, "3.txt");
assert_match_at_position(finder, 2, "1.txt");
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm); // Open 1.txt
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
assert_eq!(finder.delegate.matches.len(), 3);
assert_match_at_position(finder, 0, "1.txt");
assert_match_selection(finder, 1, "2.txt");
assert_match_at_position(finder, 2, "3.txt");
});
}
#[gpui::test]
async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
let app_state = init_test(cx);
@@ -1172,6 +1343,27 @@ async fn open_close_queried_buffer(
expected_editor_title: &str,
workspace: &View<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
let history_items = open_queried_buffer(
input,
expected_matches,
expected_editor_title,
workspace,
cx,
)
.await;
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
history_items
}
async fn open_queried_buffer(
input: &str,
expected_matches: usize,
expected_editor_title: &str,
workspace: &View<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
let picker = open_file_picker(&workspace, cx);
cx.simulate_input(input);
@@ -1186,7 +1378,6 @@ async fn open_close_queried_buffer(
finder.delegate.history_items.clone()
});
cx.dispatch_action(SelectNext);
cx.dispatch_action(Confirm);
cx.read(|cx| {
@@ -1198,8 +1389,6 @@ async fn open_close_queried_buffer(
);
});
cx.dispatch_action(workspace::CloseActiveItem { save_intent: None });
history_items
}
@@ -1313,3 +1502,37 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
.collect(),
}
}
#[track_caller]
fn assert_match_selection(
finder: &Picker<FileFinderDelegate>,
expected_selection_index: usize,
expected_file_name: &str,
) {
assert_eq!(
finder.delegate.selected_index(),
expected_selection_index,
"Match is not selected"
);
assert_match_at_position(finder, expected_selection_index, expected_file_name);
}
#[track_caller]
fn assert_match_at_position(
finder: &Picker<FileFinderDelegate>,
match_index: usize,
expected_file_name: &str,
) {
let match_item = finder
.delegate
.matches
.get(match_index)
.unwrap_or_else(|| panic!("Finder has no match for index {match_index}"));
let match_file_name = match match_item {
Match::History(found_path, _) => found_path.absolute.as_deref().unwrap().file_name(),
Match::Search(path_match) => path_match.0.path.file_name(),
}
.unwrap()
.to_string_lossy();
assert_eq!(match_file_name, expected_file_name);
}

View File

@@ -652,27 +652,20 @@ impl AppContext {
}
}
} else {
for window in self.windows.values() {
if let Some(window) = window.as_ref() {
if window.dirty {
window.platform_window.invalidate();
}
}
}
#[cfg(any(test, feature = "test-support"))]
for window in self
.windows
.values()
.filter_map(|window| {
let window = window.as_ref()?;
(window.dirty || window.focus_invalidated).then_some(window.handle)
window.dirty.get().then_some(window.handle)
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
}
#[allow(clippy::collapsible_else_if)]
if self.pending_effects.is_empty() {
break;
}
@@ -749,7 +742,7 @@ impl AppContext {
fn apply_refresh_effect(&mut self) {
for window in self.windows.values_mut() {
if let Some(window) = window.as_mut() {
window.dirty = true;
window.dirty.set(true);
}
}
}

View File

@@ -1,9 +1,10 @@
use crate::{
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter,
ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform,
Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View,
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@@ -236,6 +237,11 @@ impl TestAppContext {
self.test_platform.has_pending_prompt()
}
/// All the urls that have been opened with cx.open_url() during this test.
pub fn opened_url(&self) -> Option<String> {
self.test_platform.opened_url.borrow().clone()
}
/// Simulates the user resizing the window to the new size.
pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
self.test_window(window_handle).simulate_resize(size);
@@ -625,6 +631,36 @@ impl<'a> VisualTestContext {
self.cx.simulate_input(self.window, input)
}
/// Simulate a mouse move event to the given point
pub fn simulate_mouse_move(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
self.simulate_event(MouseMoveEvent {
position,
modifiers,
pressed_button: None,
})
}
/// Simulate a primary mouse click at the given point
pub fn simulate_click(&mut self, position: Point<Pixels>, modifiers: Modifiers) {
self.simulate_event(MouseDownEvent {
position,
modifiers,
button: MouseButton::Left,
click_count: 1,
});
self.simulate_event(MouseUpEvent {
position,
modifiers,
button: MouseButton::Left,
click_count: 1,
});
}
/// Simulate a modifiers changed event
pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
self.simulate_event(ModifiersChangedEvent { modifiers })
}
/// Simulates the user resizing the window to the new size.
pub fn simulate_resize(&self, size: Size<Pixels>) {
self.simulate_window_resize(self.window, size)

View File

@@ -11,7 +11,7 @@ use crate::{
Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
SharedString, Size, Task, TaskLabel, WindowContext,
};
use anyhow::{anyhow, Result};
use anyhow::Result;
use async_task::Runnable;
use futures::channel::oneshot;
use parking::Unparker;
@@ -23,11 +23,10 @@ use std::hash::{Hash, Hasher};
use std::time::Duration;
use std::{
any::Any,
fmt::{self, Debug, Display},
fmt::{self, Debug},
ops::Range,
path::{Path, PathBuf},
rc::Rc,
str::FromStr,
sync::Arc,
};
use uuid::Uuid;
@@ -39,6 +38,7 @@ pub(crate) use mac::*;
#[cfg(any(test, feature = "test-support"))]
pub(crate) use test::*;
use time::UtcOffset;
pub use util::SemanticVersion;
#[cfg(target_os = "macos")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
@@ -175,7 +175,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn invalidate(&self);
fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
@@ -697,45 +696,6 @@ impl Default for CursorStyle {
}
}
/// A datastructure representing a semantic version number
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct SemanticVersion {
major: usize,
minor: usize,
patch: usize,
}
impl FromStr for SemanticVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut components = s.trim().split('.');
let major = components
.next()
.ok_or_else(|| anyhow!("missing major version number"))?
.parse()?;
let minor = components
.next()
.ok_or_else(|| anyhow!("missing minor version number"))?
.parse()?;
let patch = components
.next()
.ok_or_else(|| anyhow!("missing patch version number"))?
.parse()?;
Ok(Self {
major,
minor,
patch,
})
}
}
impl Display for SemanticVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
/// A clipboard item that should be copied to the clipboard
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ClipboardItem {

View File

@@ -170,4 +170,34 @@ impl Modifiers {
pub fn modified(&self) -> bool {
self.control || self.alt || self.shift || self.command || self.function
}
/// helper method for Modifiers with no modifiers
pub fn none() -> Modifiers {
Default::default()
}
/// helper method for Modifiers with just command
pub fn command() -> Modifiers {
Modifiers {
command: true,
..Default::default()
}
}
/// helper method for Modifiers with just shift
pub fn shift() -> Modifiers {
Modifiers {
shift: true,
..Default::default()
}
}
/// helper method for Modifiers with command + shift
pub fn command_shift() -> Modifiers {
Modifiers {
shift: true,
command: true,
..Default::default()
}
}
}

View File

@@ -3,6 +3,7 @@ use crate::{
Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
};
use block::ConcreteBlock;
use cocoa::{
base::{NO, YES},
foundation::NSUInteger,
@@ -14,17 +15,19 @@ use foreign_types::ForeignType;
use media::core_video::CVMetalTextureCache;
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{ffi::c_void, mem, ptr, sync::Arc};
use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
#[cfg(not(feature = "runtime_shaders"))]
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &'static str =
include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
pub(crate) struct MetalRenderer {
device: metal::Device,
layer: metal::MetalLayer,
command_queue: CommandQueue,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
@@ -36,13 +39,14 @@ pub(crate) struct MetalRenderer {
polychrome_sprites_pipeline_state: metal::RenderPipelineState,
surfaces_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
#[allow(clippy::arc_with_non_send_sync)]
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: CVMetalTextureCache,
}
impl MetalRenderer {
pub fn new(is_opaque: bool) -> Self {
pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
device
} else {
@@ -54,7 +58,7 @@ impl MetalRenderer {
layer.set_device(&device);
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
layer.set_presents_with_transaction(true);
layer.set_opaque(is_opaque);
layer.set_opaque(true);
unsafe {
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
@@ -93,10 +97,6 @@ impl MetalRenderer {
mem::size_of_val(&unit_vertices) as u64,
MTLResourceOptions::StorageModeManaged,
);
let instances = device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
);
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
&device,
@@ -165,8 +165,11 @@ impl MetalRenderer {
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
let core_video_texture_cache =
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
Self {
device,
layer,
command_queue,
paths_rasterization_pipeline_state,
@@ -178,9 +181,9 @@ impl MetalRenderer {
polychrome_sprites_pipeline_state,
surfaces_pipeline_state,
unit_vertices,
instances,
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache: unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() },
core_video_texture_cache,
}
}
@@ -208,14 +211,24 @@ impl MetalRenderer {
);
return;
};
let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| {
self.device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
)
});
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let mut instance_offset = 0;
let Some(path_tiles) =
self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
else {
panic!("failed to rasterize {} paths", scene.paths().len());
let Some(path_tiles) = self.rasterize_paths(
scene.paths(),
&mut instance_buffer,
&mut instance_offset,
command_buffer,
) else {
log::error!("failed to rasterize {} paths", scene.paths().len());
return;
};
let render_pass_descriptor = metal::RenderPassDescriptor::new();
@@ -243,22 +256,29 @@ impl MetalRenderer {
let ok = match batch {
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
shadows,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => self.draw_quads(
quads,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => {
self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
}
PrimitiveBatch::Paths(paths) => self.draw_paths(
paths,
&path_tiles,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
underlines,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -269,6 +289,7 @@ impl MetalRenderer {
} => self.draw_monochrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -279,12 +300,14 @@ impl MetalRenderer {
} => self.draw_polychrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
surfaces,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -292,7 +315,7 @@ impl MetalRenderer {
};
if !ok {
panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
scene.paths.len(),
scene.shadows.len(),
scene.quads.len(),
@@ -300,28 +323,39 @@ impl MetalRenderer {
scene.monochrome_sprites.len(),
scene.polychrome_sprites.len(),
scene.surfaces.len(),
)
);
break;
}
}
command_encoder.end_encoding();
self.instances.did_modify_range(NSRange {
instance_buffer.did_modify_range(NSRange {
location: 0,
length: instance_offset as NSUInteger,
});
let instance_buffer_pool = self.instance_buffer_pool.clone();
let instance_buffer = Cell::new(Some(instance_buffer));
let block = ConcreteBlock::new(move |_| {
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().push(instance_buffer);
}
});
let block = block.copy();
command_buffer.add_completed_handler(&block);
command_buffer.commit();
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
command_buffer.wait_until_completed();
command_buffer.wait_until_scheduled();
drawable.present();
}
fn rasterize_paths(
&mut self,
paths: &[Path<ScaledPixels>],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
) -> Option<HashMap<PathId, AtlasTile>> {
let mut tiles = HashMap::default();
@@ -347,9 +381,9 @@ impl MetalRenderer {
}
for (texture_id, vertices) in vertices_by_texture_id {
align_offset(offset);
align_offset(instance_offset);
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
let next_offset = *offset + vertices_bytes_len;
let next_offset = *instance_offset + vertices_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return None;
}
@@ -369,8 +403,8 @@ impl MetalRenderer {
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
let texture_size = Size {
width: DevicePixels::from(texture.width()),
@@ -382,7 +416,8 @@ impl MetalRenderer {
&texture_size as *const Size<DevicePixels> as *const _,
);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
vertices.as_ptr() as *const u8,
@@ -397,7 +432,7 @@ impl MetalRenderer {
vertices.len() as u64,
);
command_encoder.end_encoding();
*offset = next_offset;
*instance_offset = next_offset;
}
Some(tiles)
@@ -406,14 +441,15 @@ impl MetalRenderer {
fn draw_shadows(
&mut self,
shadows: &[Shadow],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if shadows.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -423,13 +459,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -439,9 +475,10 @@ impl MetalRenderer {
);
let shadow_bytes_len = mem::size_of_val(shadows);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + shadow_bytes_len;
let next_offset = *instance_offset + shadow_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -460,21 +497,22 @@ impl MetalRenderer {
6,
shadows.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_quads(
&mut self,
quads: &[Quad],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if quads.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -484,13 +522,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -500,9 +538,10 @@ impl MetalRenderer {
);
let quad_bytes_len = mem::size_of_val(quads);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + quad_bytes_len;
let next_offset = *instance_offset + quad_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -517,7 +556,7 @@ impl MetalRenderer {
6,
quads.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -525,7 +564,8 @@ impl MetalRenderer {
&mut self,
paths: &[Path<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -573,7 +613,7 @@ impl MetalRenderer {
if sprites.is_empty() {
break;
} else {
align_offset(offset);
align_offset(instance_offset);
let texture_id = prev_texture_id.take().unwrap();
let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -583,8 +623,8 @@ impl MetalRenderer {
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
@@ -593,20 +633,20 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
let buffer_contents =
unsafe { (self.instances.contents() as *mut u8).add(*offset) };
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
@@ -622,7 +662,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
sprites.clear();
}
}
@@ -632,14 +672,15 @@ impl MetalRenderer {
fn draw_underlines(
&mut self,
underlines: &[Underline],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if underlines.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -649,13 +690,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -665,9 +706,10 @@ impl MetalRenderer {
);
let underline_bytes_len = mem::size_of_val(underlines);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + underline_bytes_len;
let next_offset = *instance_offset + underline_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -686,7 +728,7 @@ impl MetalRenderer {
6,
underlines.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -694,14 +736,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[MonochromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -716,8 +759,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -731,15 +774,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -758,7 +802,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -766,14 +810,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[PolychromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -788,8 +833,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -803,15 +848,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -830,14 +876,15 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_surfaces(
&mut self,
surfaces: &[Surface],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -889,16 +936,16 @@ impl MetalRenderer {
.unwrap()
};
align_offset(offset);
let next_offset = *offset + mem::size_of::<Surface>();
align_offset(instance_offset);
let next_offset = *instance_offset + mem::size_of::<Surface>();
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
command_encoder.set_vertex_buffer(
SurfaceInputIndex::Surfaces as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SurfaceInputIndex::TextureSize as u64,
@@ -915,8 +962,8 @@ impl MetalRenderer {
);
unsafe {
let buffer_contents =
(self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
as *mut SurfaceBounds;
ptr::write(
buffer_contents,
SurfaceBounds {
@@ -927,7 +974,7 @@ impl MetalRenderer {
}
command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
*offset = next_offset;
*instance_offset = next_offset;
}
true
}

View File

@@ -146,6 +146,7 @@ pub(crate) struct MacPlatformState {
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
display_linker: MacDisplayLinker,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
@@ -176,6 +177,7 @@ impl MacPlatform {
foreground_executor: ForegroundExecutor::new(dispatcher),
text_system: Arc::new(MacTextSystem::new()),
display_linker: MacDisplayLinker::new(),
instance_buffer_pool: Arc::default(),
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
@@ -494,7 +496,13 @@ impl Platform for MacPlatform {
handle: AnyWindowHandle,
options: WindowOptions,
) -> Box<dyn PlatformWindow> {
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
let instance_buffer_pool = self.0.lock().instance_buffer_pool.clone();
Box::new(MacWindow::open(
handle,
options,
self.foreground_executor(),
instance_buffer_pool,
))
}
fn set_display_link_output_callback(

View File

@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
quad.corner_radii.top_right == 0. &&
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
quad.border_widths.bottom == 0.) {
return input.background_color;
}
float2 half_size =
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
float2 center =

View File

@@ -16,8 +16,8 @@ use cocoa::{
},
base::{id, nil},
foundation::{
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
NSSize, NSString, NSUInteger,
NSArray, NSAutoreleasePool, NSDefaultRunLoopMode, NSDictionary, NSFastEnumeration,
NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
};
use core_graphics::display::CGRect;
@@ -168,6 +168,7 @@ unsafe fn build_classes() {
sel!(displayLayer:),
display_layer as extern "C" fn(&Object, Sel, id),
);
decl.add_method(sel!(step:), step as extern "C" fn(&Object, Sel, id));
decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
decl.add_method(
@@ -260,6 +261,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeScreen:),
window_did_change_screen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -320,7 +325,8 @@ struct MacWindowState {
handle: AnyWindowHandle,
executor: ForegroundExecutor,
native_window: id,
native_view: NonNull<id>,
native_view: NonNull<Object>,
display_link: id,
renderer: MetalRenderer,
kind: WindowKind,
request_frame_callback: Option<Box<dyn FnMut()>>,
@@ -458,6 +464,7 @@ impl MacWindow {
handle: AnyWindowHandle,
options: WindowOptions,
executor: ForegroundExecutor,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
) -> Self {
unsafe {
let pool = NSAutoreleasePool::new(nil);
@@ -521,15 +528,17 @@ impl MacWindow {
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let display_link = start_display_link(native_window.screen(), native_view);
let window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
executor,
native_window,
native_view: NonNull::new_unchecked(native_view as *mut _),
renderer: MetalRenderer::new(true),
native_view: NonNull::new_unchecked(native_view),
display_link,
renderer: MetalRenderer::new(instance_buffer_pool),
kind: options.kind,
request_frame_callback: None,
event_callback: None,
@@ -663,6 +672,7 @@ impl MacWindow {
}
window.0.lock().move_traffic_light();
pool.drain();
window
@@ -683,10 +693,19 @@ impl MacWindow {
}
}
unsafe fn start_display_link(native_screen: id, native_view: id) -> id {
let display_link: id =
msg_send![native_screen, displayLinkWithTarget: native_view selector: sel!(step:)];
let main_run_loop: id = msg_send![class!(NSRunLoop), mainRunLoop];
let _: () = msg_send![display_link, addToRunLoop: main_run_loop forMode: NSDefaultRunLoopMode];
display_link
}
impl Drop for MacWindow {
fn drop(&mut self) {
let this = self.0.lock();
let mut this = self.0.lock();
let window = this.native_window;
this.display_link = nil;
this.executor
.spawn(async move {
unsafe {
@@ -1000,13 +1019,6 @@ impl PlatformWindow for MacWindow {
}
}
fn invalidate(&self) {
let this = self.0.lock();
unsafe {
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
}
}
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
@@ -1353,6 +1365,19 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
}
}
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
unsafe {
let screen = lock.native_window.screen();
if screen == nil {
lock.display_link = nil;
} else {
lock.display_link = start_display_link(screen, lock.native_view.as_ptr());
}
}
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = window_state.lock();
@@ -1502,6 +1527,22 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
}
}
extern "C" fn step(this: &Object, _: Sel, display_link: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if lock.display_link == display_link {
if let Some(mut callback) = lock.request_frame_callback.take() {
drop(lock);
callback();
window_state.lock().request_frame_callback = Some(callback);
}
} else {
unsafe {
let _: () = msg_send![display_link, invalidate];
}
}
}
extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
unsafe { msg_send![class!(NSArray), array] }
}

View File

@@ -25,6 +25,7 @@ pub(crate) struct TestPlatform {
active_cursor: Mutex<CursorStyle>,
current_clipboard_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
pub opened_url: RefCell<Option<String>>,
weak: Weak<Self>,
}
@@ -45,6 +46,7 @@ impl TestPlatform {
active_window: Default::default(),
current_clipboard_item: Mutex::new(None),
weak: weak.clone(),
opened_url: Default::default(),
})
}
@@ -188,8 +190,8 @@ impl Platform for TestPlatform {
fn stop_display_link(&self, _display_id: DisplayId) {}
fn open_url(&self, _url: &str) {
unimplemented!()
fn open_url(&self, url: &str) {
*self.opened_url.borrow_mut() = Some(url.to_string())
}
fn on_open_urls(&self, _callback: Box<dyn FnMut(Vec<String>)>) {

View File

@@ -171,7 +171,7 @@ impl PlatformWindow for TestWindow {
}
fn appearance(&self) -> WindowAppearance {
unimplemented!()
WindowAppearance::Light
}
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
@@ -276,16 +276,12 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {
unimplemented!()
}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
unimplemented!()
}
fn invalidate(&self) {}
fn draw(&self, _scene: &crate::Scene) {}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {

View File

@@ -15,6 +15,7 @@ use crate::{
use anyhow::anyhow;
use collections::{BTreeSet, FxHashMap, FxHashSet};
use core::fmt;
use derive_more::Deref;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::{smallvec, SmallVec};
@@ -38,9 +39,8 @@ pub struct FontFamilyId(pub usize);
pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
/// The GPUI text layout and rendering sub system.
/// The GPUI text rendering sub system.
pub struct TextSystem {
line_layout_cache: Arc<LineLayoutCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
@@ -53,7 +53,6 @@ pub struct TextSystem {
impl TextSystem {
pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
TextSystem {
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
platform_text_system,
font_metrics: RwLock::default(),
raster_bounds: RwLock::default(),
@@ -234,43 +233,66 @@ impl TextSystem {
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
pub(crate) fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
}
}
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
/// The GPUI text layout subsystem.
#[derive(Deref)]
pub struct WindowTextSystem {
line_layout_cache: Arc<LineLayoutCache>,
#[deref]
text_system: Arc<TextSystem>,
}
Ok(layout)
impl WindowTextSystem {
pub(crate) fn new(text_system: Arc<TextSystem>) -> Self {
Self {
line_layout_cache: Arc::new(LineLayoutCache::new(
text_system.platform_text_system.clone(),
)),
text_system,
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
}
/// Shape the given line, at the given font_size, for painting to the screen.
@@ -429,43 +451,39 @@ impl TextSystem {
self.line_layout_cache.finish_frame(reused_views)
}
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
pub(crate) fn rasterize_glyph(
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
Ok(layout)
}
}

View File

@@ -143,7 +143,7 @@ impl Boundary {
#[cfg(test)]
mod tests {
use super::*;
use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
#[test]
@@ -218,7 +218,7 @@ mod tests {
#[crate::test]
fn test_wrap_shaped_line(cx: &mut TestAppContext) {
cx.update(|cx| {
let text_system = cx.text_system().clone();
let text_system = WindowTextSystem::new(cx.text_system().clone());
let normal = TextRun {
len: 0,

View File

@@ -6,8 +6,8 @@ use crate::{
KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds,
WindowOptions,
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowAppearance,
WindowBounds, WindowOptions, WindowTextSystem,
};
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
@@ -22,7 +22,7 @@ use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
cell::RefCell,
cell::{Cell, RefCell},
collections::hash_map::Entry,
fmt::{Debug, Display},
future::Future,
@@ -34,7 +34,7 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Duration,
time::{Duration, Instant},
};
use util::{measure, ResultExt};
@@ -95,8 +95,8 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
struct FocusEvent {
previous_focus_path: SmallVec<[FocusId; 8]>,
current_focus_path: SmallVec<[FocusId; 8]>,
old_focus_path: SmallVec<[FocusId; 8]>,
new_focus_path: SmallVec<[FocusId; 8]>,
}
slotmap::new_key_type! {
@@ -251,6 +251,7 @@ pub struct Window {
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
pub(crate) rem_size: Pixels,
pub(crate) viewport_size: Size<Pixels>,
layout_engine: Option<TaffyLayoutEngine>,
@@ -268,17 +269,17 @@ pub struct Window {
scale_factor: f32,
bounds: WindowBounds,
bounds_observers: SubscriberSet<(), AnyObserver>,
appearance: WindowAppearance,
appearance_observers: SubscriberSet<(), AnyObserver>,
active: bool,
pub(crate) dirty: bool,
pub(crate) dirty: Rc<Cell<bool>>,
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
pub(crate) refreshing: bool,
pub(crate) drawing: bool,
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) focus_invalidated: bool,
}
#[derive(Default, Debug)]
@@ -337,13 +338,61 @@ impl Window {
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
let bounds = platform_window.bounds();
let appearance = platform_window.appearance();
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
let dirty = Rc::new(Cell::new(true));
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
platform_window.on_request_frame(Box::new({
let mut cx = cx.to_async();
let mut cx = AsyncWindowContext::new(cx.to_async(), handle);
let dirty = dirty.clone();
let last_input_timestamp = last_input_timestamp.clone();
move || {
measure("frame duration", || {
handle.update(&mut cx, |_, cx| cx.draw()).log_err();
})
fn handle_frame_request(
dirty: &Rc<Cell<bool>>,
last_input_timestamp: &Rc<Cell<Instant>>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
if dirty.get() {
measure("frame duration", || {
let original_focus_path =
cx.update(|cx| cx.window.rendered_frame.focus_path())?;
let mut draw_count = 0;
while dirty.get() {
cx.update(|cx| {
let old_window_active = cx.window.rendered_frame.focus_path();
let old_focus_path = cx.window.rendered_frame.focus_path();
cx.draw();
cx.notify_focus_listeners(
&original_focus_path,
old_focus_path,
old_window_active,
);
})?;
draw_count += 1;
if draw_count > 2 {
log::warn!(
"redrew frame {} times due to focus changes",
draw_count
);
}
}
cx.update(|cx| cx.present())?;
Ok(())
})?;
} else if last_input_timestamp.get().elapsed() < Duration::from_secs(1) {
// Keep presenting the current scene for 1 extra second since the
// last input to prevent the display from underclocking the refresh rate.
cx.update(|cx| cx.present())?;
}
Ok(())
}
handle_frame_request(&dirty, &last_input_timestamp, &mut cx).log_err();
}
}));
platform_window.on_resize(Box::new({
@@ -362,6 +411,14 @@ impl Window {
.log_err();
}
}));
platform_window.on_appearance_changed(Box::new({
let mut cx = cx.to_async();
move || {
handle
.update(&mut cx, |_, cx| cx.appearance_changed())
.log_err();
}
}));
platform_window.on_active_status_change(Box::new({
let mut cx = cx.to_async();
move |active| {
@@ -393,6 +450,7 @@ impl Window {
platform_window,
display_id,
sprite_atlas,
text_system,
rem_size: px(16.),
viewport_size: content_size,
layout_engine: Some(TaffyLayoutEngine::new()),
@@ -410,17 +468,17 @@ impl Window {
scale_factor,
bounds,
bounds_observers: SubscriberSet::new(),
appearance,
appearance_observers: SubscriberSet::new(),
active: false,
dirty: false,
dirty,
last_input_timestamp,
refreshing: false,
drawing: false,
activation_observers: SubscriberSet::new(),
focus: None,
focus_enabled: true,
pending_input: None,
#[cfg(any(test, feature = "test-support"))]
focus_invalidated: false,
}
}
}
@@ -472,7 +530,7 @@ impl<'a> WindowContext<'a> {
pub fn refresh(&mut self) {
if !self.window.drawing {
self.window.refreshing = true;
self.window.dirty = true;
self.window.dirty.set(true);
}
}
@@ -505,12 +563,6 @@ impl<'a> WindowContext<'a> {
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = true;
}
self.refresh();
}
@@ -530,6 +582,11 @@ impl<'a> WindowContext<'a> {
self.window.focus_enabled = false;
}
/// Accessor for the text system.
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
&self.window.text_system
}
/// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused();
@@ -734,6 +791,20 @@ impl<'a> WindowContext<'a> {
self.window.bounds
}
fn appearance_changed(&mut self) {
self.window.appearance = self.window.platform_window.appearance();
self.window
.appearance_observers
.clone()
.retain(&(), |callback| callback(self));
}
/// Returns the appearance of the current window.
pub fn appearance(&self) -> WindowAppearance {
self.window.appearance
}
/// Returns the size of the drawable area within the window.
pub fn viewport_size(&self) -> Size<Pixels> {
self.window.viewport_size
@@ -927,16 +998,12 @@ impl<'a> WindowContext<'a> {
&self.window.next_frame.z_index_stack
}
/// Draw pixels to the display for this window based on the contents of its scene.
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
/// the contents of the new [Scene], use [present].
pub(crate) fn draw(&mut self) {
self.window.dirty = false;
self.window.dirty.set(false);
self.window.drawing = true;
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = false;
}
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
{
let input_handler = self.window.platform_window.take_input_handler();
@@ -1035,18 +1102,28 @@ impl<'a> WindowContext<'a> {
}
element_arena.clear();
});
let previous_focus_path = self.window.rendered_frame.focus_path();
let previous_window_active = self.window.rendered_frame.window_active;
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
self.window.next_frame.clear();
let current_focus_path = self.window.rendered_frame.focus_path();
let current_window_active = self.window.rendered_frame.window_active;
self.window.refreshing = false;
self.window.drawing = false;
}
if previous_focus_path != current_focus_path
|| previous_window_active != current_window_active
{
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
fn notify_focus_listeners(
&mut self,
original_focus_path: &SmallVec<[FocusId; 8]>,
old_focus_path: SmallVec<[FocusId; 8]>,
old_window_active: bool,
) {
let new_window_active = self.window.rendered_frame.window_active;
let new_focus_path = self.window.rendered_frame.focus_path();
if new_window_active == old_window_active && new_focus_path == *original_focus_path {
log::warn!("skipping focus notifications due to restoring focus to the original handle in a single frame");
return;
}
if new_focus_path != old_focus_path || new_window_active != old_window_active {
if !old_focus_path.is_empty() && new_focus_path.is_empty() {
self.window
.focus_lost_listeners
.clone()
@@ -1054,15 +1131,15 @@ impl<'a> WindowContext<'a> {
}
let event = FocusEvent {
previous_focus_path: if previous_window_active {
previous_focus_path
old_focus_path: if old_window_active {
old_focus_path
} else {
Default::default()
SmallVec::new()
},
current_focus_path: if current_window_active {
current_focus_path
new_focus_path: if new_window_active {
new_focus_path
} else {
Default::default()
SmallVec::new()
},
};
self.window
@@ -1070,16 +1147,17 @@ impl<'a> WindowContext<'a> {
.clone()
.retain(&(), |listener| listener(&event, self));
}
}
fn present(&self) {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.refreshing = false;
self.window.drawing = false;
}
/// Dispatch a mouse or keyboard event on the window.
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`.
self.app.propagate_event = true;
// Handlers may set this to true by calling `prevent_default`.
@@ -2023,7 +2101,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
if !self.window.drawing {
self.window_cx.window.dirty = true;
self.window_cx.window.dirty.set(true);
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
@@ -2058,6 +2136,20 @@ impl<'a, V: 'static> ViewContext<'a, V> {
subscription
}
/// Registers a callback to be invoked when the window appearance changes.
pub fn observe_window_appearance(
&mut self,
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
let (subscription, activate) = self.window.appearance_observers.insert(
(),
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
);
activate();
subscription
}
/// Register a listener to be called when the given focus handle receives focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus(
@@ -2071,8 +2163,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if event.previous_focus_path.last() != Some(&focus_id)
&& event.current_focus_path.last() == Some(&focus_id)
if event.old_focus_path.last() != Some(&focus_id)
&& event.new_focus_path.last() == Some(&focus_id)
{
listener(view, cx)
}
@@ -2097,8 +2189,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if !event.previous_focus_path.contains(&focus_id)
&& event.current_focus_path.contains(&focus_id)
if !event.old_focus_path.contains(&focus_id)
&& event.new_focus_path.contains(&focus_id)
{
listener(view, cx)
}
@@ -2123,8 +2215,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if event.previous_focus_path.last() == Some(&focus_id)
&& event.current_focus_path.last() != Some(&focus_id)
if event.old_focus_path.last() == Some(&focus_id)
&& event.new_focus_path.last() != Some(&focus_id)
{
listener(view, cx)
}
@@ -2166,8 +2258,8 @@ impl<'a, V: 'static> ViewContext<'a, V> {
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if event.previous_focus_path.contains(&focus_id)
&& !event.current_focus_path.contains(&focus_id)
if event.old_focus_path.contains(&focus_id)
&& !event.new_focus_path.contains(&focus_id)
{
listener(view, cx)
}

View File

@@ -29,7 +29,7 @@ async-trait.workspace = true
clock = { path = "../clock" }
collections = { path = "../collections" }
futures.workspace = true
fuzzy = { path = "../fuzzy" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
globset.workspace = true
gpui = { path = "../gpui" }
@@ -38,7 +38,6 @@ log.workspace = true
lsp = { path = "../lsp" }
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true }
regex.workspace = true
rpc = { path = "../rpc" }
@@ -55,6 +54,7 @@ text = { path = "../text" }
theme = { path = "../theme" }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
pulldown-cmark.workspace = true
tree-sitter.workspace = true
unicase = "2.6"
util = { path = "../util" }

View File

@@ -2993,6 +2993,11 @@ impl BufferSnapshot {
self.git_diff.hunks_intersecting_range_rev(range, self)
}
/// Returns if the buffer contains any diagnostics.
pub fn has_diagnostics(&self) -> bool {
!self.diagnostics.is_empty()
}
/// Returns all the diagnostics intersecting the given range.
pub fn diagnostics_in_range<'a, T, O>(
&'a self,

View File

@@ -30,6 +30,7 @@ workspace = { path = "../workspace" }
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
release_channel = { path = "../release_channel" }
env_logger.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }
unindent.workspace = true

View File

@@ -100,6 +100,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
language::init(cx);
client::init_settings(cx);
Project::init_settings(cx);

View File

@@ -27,6 +27,7 @@ serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
util = { path = "../util" }
release_channel = { path = "../release_channel" }
[dev-dependencies]
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }

View File

@@ -5,7 +5,7 @@ pub use lsp_types::*;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
use gpui::{AsyncAppContext, BackgroundExecutor, Task};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::{barrier, prelude::Stream};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -322,20 +322,54 @@ impl LanguageServer {
let mut buffer = Vec::new();
loop {
buffer.clear();
stdout.read_until(b'\n', &mut buffer).await?;
stdout.read_until(b'\n', &mut buffer).await?;
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
break;
};
if stdout.read_until(b'\n', &mut buffer).await? == 0 {
break;
};
let header = std::str::from_utf8(&buffer)?;
let message_len: usize = header
let mut segments = header.lines();
let message_len: usize = segments
.next()
.with_context(|| {
format!("unable to find the first line of the LSP message header `{header}`")
})?
.strip_prefix(CONTENT_LEN_HEADER)
.ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
.trim_end()
.parse()?;
.with_context(|| format!("invalid LSP message header `{header}`"))?
.parse()
.with_context(|| {
format!("failed to parse Content-Length of LSP message header: `{header}`")
})?;
if let Some(second_segment) = segments.next() {
match second_segment {
"" => (), // Header end
header_field => {
if header_field.starts_with("Content-Type:") {
stdout.read_until(b'\n', &mut buffer).await?;
} else {
anyhow::bail!(
"inside `{header}`, expected a Content-Type header field or a header ending CRLF, got `{second_segment:?}`"
)
}
}
}
} else {
anyhow::bail!(
"unable to find the second line of the LSP message header `{header}`"
);
}
buffer.resize(message_len, 0);
stdout.read_exact(&mut buffer).await?;
if let Ok(message) = str::from_utf8(&buffer) {
log::trace!("incoming message: {}", message);
log::trace!("incoming message: {message}");
for handler in io_handlers.lock().values_mut() {
handler(IoKind::StdOut, message);
}
@@ -378,6 +412,8 @@ impl LanguageServer {
// Don't starve the main thread when receiving lots of messages at once.
smol::future::yield_now().await;
}
Ok(())
}
async fn handle_stderr<Stderr>(
@@ -393,7 +429,12 @@ impl LanguageServer {
loop {
buffer.clear();
stderr.read_until(b'\n', &mut buffer).await?;
let bytes_read = stderr.read_until(b'\n', &mut buffer).await?;
if bytes_read == 0 {
return Ok(());
}
if let Ok(message) = str::from_utf8(&buffer) {
log::trace!("incoming stderr message:{message}");
for handler in io_handlers.lock().values_mut() {
@@ -450,7 +491,11 @@ impl LanguageServer {
/// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)
pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
pub fn initialize(
mut self,
options: Option<Value>,
cx: &AppContext,
) -> Task<Result<Arc<Self>>> {
let root_uri = Url::from_file_path(&self.root_path).unwrap();
#[allow(deprecated)]
let params = InitializeParams {
@@ -515,7 +560,10 @@ impl LanguageServer {
completion_item: Some(CompletionItemCapability {
snippet_support: Some(true),
resolve_support: Some(CompletionItemCapabilityResolveSupport {
properties: vec!["additionalTextEdits".to_string()],
properties: vec![
"documentation".to_string(),
"additionalTextEdits".to_string(),
],
}),
..Default::default()
}),
@@ -579,18 +627,25 @@ impl LanguageServer {
uri: root_uri,
name: Default::default(),
}]),
client_info: None,
client_info: Some(ClientInfo {
name: release_channel::ReleaseChannel::global(cx)
.display_name()
.to_string(),
version: Some(release_channel::AppVersion::global(cx).to_string()),
}),
locale: None,
};
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
self.name = info.name;
}
self.capabilities = response.capabilities;
cx.spawn(|_| async move {
let response = self.request::<request::Initialize>(params).await?;
if let Some(info) = response.server_info {
self.name = info.name;
}
self.capabilities = response.capabilities;
self.notify::<notification::Initialized>(InitializedParams {})?;
Ok(Arc::new(self))
self.notify::<notification::Initialized>(InitializedParams {})?;
Ok(Arc::new(self))
})
}
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
@@ -1213,6 +1268,9 @@ mod tests {
#[gpui::test]
async fn test_fake(cx: &mut TestAppContext) {
cx.update(|cx| {
release_channel::init("0.0.0", cx);
});
let (server, mut fake) =
FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async());
@@ -1229,7 +1287,7 @@ mod tests {
})
.detach();
let server = server.initialize(None).await.unwrap();
let server = cx.update(|cx| server.initialize(None, cx)).await.unwrap();
server
.notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(

View File

@@ -0,0 +1,31 @@
[package]
name = "markdown_preview"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/markdown_preview.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lazy_static.workspace = true
log.workspace = true
menu = { path = "../menu" }
project = { path = "../project" }
pulldown-cmark.workspace = true
rich_text = { path = "../rich_text" }
theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,14 @@
use gpui::{actions, AppContext};
use workspace::Workspace;
pub mod markdown_preview_view;
pub mod markdown_renderer;
actions!(markdown, [OpenPreview]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, cx| {
markdown_preview_view::MarkdownPreviewView::register(workspace, cx);
})
.detach();
}

View File

@@ -0,0 +1,137 @@
use editor::{Editor, EditorEvent};
use gpui::{
canvas, AnyElement, AppContext, AvailableSpace, EventEmitter, FocusHandle, FocusableView,
InteractiveElement, IntoElement, ParentElement, Render, Styled, View, ViewContext,
};
use language::LanguageRegistry;
use std::sync::Arc;
use ui::prelude::*;
use workspace::item::Item;
use workspace::Workspace;
use crate::{markdown_renderer::render_markdown, OpenPreview};
pub struct MarkdownPreviewView {
focus_handle: FocusHandle,
languages: Arc<LanguageRegistry>,
contents: String,
}
impl MarkdownPreviewView {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
let languages = workspace.app_state().languages.clone();
workspace.register_action(move |workspace, _: &OpenPreview, cx| {
if workspace.has_active_modal(cx) {
cx.propagate();
return;
}
let languages = languages.clone();
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
let view: View<MarkdownPreviewView> =
cx.new_view(|cx| MarkdownPreviewView::new(editor, languages, cx));
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
cx.notify();
}
});
}
pub fn new(
active_editor: View<Editor>,
languages: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
cx.subscribe(&active_editor, |this, editor, event: &EditorEvent, cx| {
if *event == EditorEvent::Edited {
let editor = editor.read(cx);
let contents = editor.buffer().read(cx).snapshot(cx).text();
this.contents = contents;
cx.notify();
}
})
.detach();
let editor = active_editor.read(cx);
let contents = editor.buffer().read(cx).snapshot(cx).text();
Self {
focus_handle,
languages,
contents,
}
}
}
impl FocusableView for MarkdownPreviewView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PreviewEvent {}
impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
impl Item for MarkdownPreviewView {
type Event = PreviewEvent;
fn tab_content(
&self,
_detail: Option<usize>,
selected: bool,
_cx: &WindowContext,
) -> AnyElement {
h_flex()
.gap_2()
.child(Icon::new(IconName::FileDoc).color(if selected {
Color::Default
} else {
Color::Muted
}))
.child(Label::new("Markdown preview").color(if selected {
Color::Default
} else {
Color::Muted
}))
.into_any()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("markdown preview")
}
fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
}
impl Render for MarkdownPreviewView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let rendered_markdown = v_flex()
.items_start()
.justify_start()
.key_context("MarkdownPreview")
.track_focus(&self.focus_handle)
.id("MarkdownPreview")
.overflow_y_scroll()
.overflow_x_hidden()
.size_full()
.bg(cx.theme().colors().editor_background)
.p_4()
.children(render_markdown(&self.contents, &self.languages, cx));
div().flex_1().child(
// FIXME: This shouldn't be necessary
// but the overflow_scroll above doesn't seem to work without it
canvas(move |bounds, cx| {
rendered_markdown.into_any().draw(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
)
})
.size_full(),
)
}
}

View File

@@ -0,0 +1,346 @@
use std::{ops::Range, sync::Arc};
use gpui::{
div, px, rems, AnyElement, DefiniteLength, Div, ElementId, Hsla, ParentElement, SharedString,
Styled, StyledText, WindowContext,
};
use language::LanguageRegistry;
use pulldown_cmark::{Alignment, CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use rich_text::render_rich_text;
use theme::{ActiveTheme, Theme};
use ui::{h_flex, v_flex};
enum TableState {
Header,
Body,
}
struct MarkdownTable {
column_alignments: Vec<Alignment>,
header: Vec<Div>,
body: Vec<Vec<Div>>,
current_row: Vec<Div>,
state: TableState,
border_color: Hsla,
}
impl MarkdownTable {
fn new(border_color: Hsla, column_alignments: Vec<Alignment>) -> Self {
Self {
column_alignments,
header: Vec::new(),
body: Vec::new(),
current_row: Vec::new(),
state: TableState::Header,
border_color,
}
}
fn finish_row(&mut self) {
match self.state {
TableState::Header => {
self.header.extend(self.current_row.drain(..));
self.state = TableState::Body;
}
TableState::Body => {
self.body.push(self.current_row.drain(..).collect());
}
}
}
fn add_cell(&mut self, contents: AnyElement) {
let container = match self.alignment_for_next_cell() {
Alignment::Left | Alignment::None => div(),
Alignment::Center => v_flex().items_center(),
Alignment::Right => v_flex().items_end(),
};
let cell = container
.w_full()
.child(contents)
.px_2()
.py_1()
.border_color(self.border_color);
let cell = match self.state {
TableState::Header => cell.border_2(),
TableState::Body => cell.border_1(),
};
self.current_row.push(cell);
}
fn finish(self) -> Div {
let mut table = v_flex().w_full();
let mut header = h_flex();
for cell in self.header {
header = header.child(cell);
}
table = table.child(header);
for row in self.body {
let mut row_div = h_flex();
for cell in row {
row_div = row_div.child(cell);
}
table = table.child(row_div);
}
table
}
fn alignment_for_next_cell(&self) -> Alignment {
self.column_alignments
.get(self.current_row.len())
.copied()
.unwrap_or(Alignment::None)
}
}
struct Renderer<I> {
source_contents: String,
iter: I,
theme: Arc<Theme>,
finished: Vec<Div>,
language_registry: Arc<LanguageRegistry>,
table: Option<MarkdownTable>,
list_depth: usize,
block_quote_depth: usize,
}
impl<'a, I> Renderer<I>
where
I: Iterator<Item = (Event<'a>, Range<usize>)>,
{
fn new(
iter: I,
source_contents: String,
language_registry: &Arc<LanguageRegistry>,
theme: Arc<Theme>,
) -> Self {
Self {
iter,
source_contents,
theme,
table: None,
finished: vec![],
language_registry: language_registry.clone(),
list_depth: 0,
block_quote_depth: 0,
}
}
fn run(mut self, cx: &WindowContext) -> Self {
while let Some((event, source_range)) = self.iter.next() {
match event {
Event::Start(tag) => {
self.start_tag(tag);
}
Event::End(tag) => {
self.end_tag(tag, source_range, cx);
}
Event::Rule => {
let rule = div().w_full().h(px(2.)).bg(self.theme.colors().border);
self.finished.push(div().mb_4().child(rule));
}
_ => {}
}
}
self
}
fn start_tag(&mut self, tag: Tag<'a>) {
match tag {
Tag::List(_) => {
self.list_depth += 1;
}
Tag::BlockQuote => {
self.block_quote_depth += 1;
}
Tag::Table(column_alignments) => {
self.table = Some(MarkdownTable::new(
self.theme.colors().border,
column_alignments,
));
}
_ => {}
}
}
fn end_tag(&mut self, tag: Tag, source_range: Range<usize>, cx: &WindowContext) {
match tag {
Tag::Paragraph => {
if self.list_depth > 0 || self.block_quote_depth > 0 {
return;
}
let element = self.render_md_from_range(source_range.clone(), cx);
let paragraph = div().mb_3().child(element);
self.finished.push(paragraph);
}
Tag::Heading(level, _, _) => {
let mut headline = self.headline(level);
if source_range.start > 0 {
headline = headline.mt_4();
}
let element = self.render_md_from_range(source_range.clone(), cx);
let headline = headline.child(element);
self.finished.push(headline);
}
Tag::List(_) => {
if self.list_depth == 1 {
let element = self.render_md_from_range(source_range.clone(), cx);
let list = div().mb_3().child(element);
self.finished.push(list);
}
self.list_depth -= 1;
}
Tag::BlockQuote => {
let element = self.render_md_from_range(source_range.clone(), cx);
let block_quote = h_flex()
.mb_3()
.child(
div()
.w(px(4.))
.bg(self.theme.colors().border)
.h_full()
.mr_2()
.mt_1(),
)
.text_color(self.theme.colors().text_muted)
.child(element);
self.finished.push(block_quote);
self.block_quote_depth -= 1;
}
Tag::CodeBlock(kind) => {
let contents = self.source_contents[source_range.clone()].trim();
let contents = contents.trim_start_matches("```");
let contents = contents.trim_end_matches("```");
let contents = match kind {
CodeBlockKind::Fenced(language) => {
contents.trim_start_matches(&language.to_string())
}
CodeBlockKind::Indented => contents,
};
let contents: String = contents.into();
let contents = SharedString::from(contents);
let code_block = div()
.mb_3()
.px_4()
.py_0()
.bg(self.theme.colors().surface_background)
.child(StyledText::new(contents));
self.finished.push(code_block);
}
Tag::Table(_alignment) => {
if self.table.is_none() {
log::error!("Table end without table ({:?})", source_range);
return;
}
let table = self.table.take().unwrap();
let table = table.finish().mb_4();
self.finished.push(table);
}
Tag::TableHead => {
if self.table.is_none() {
log::error!("Table head without table ({:?})", source_range);
return;
}
self.table.as_mut().unwrap().finish_row();
}
Tag::TableRow => {
if self.table.is_none() {
log::error!("Table row without table ({:?})", source_range);
return;
}
self.table.as_mut().unwrap().finish_row();
}
Tag::TableCell => {
if self.table.is_none() {
log::error!("Table cell without table ({:?})", source_range);
return;
}
let contents = self.render_md_from_range(source_range.clone(), cx);
self.table.as_mut().unwrap().add_cell(contents);
}
_ => {}
}
}
fn render_md_from_range(
&self,
source_range: Range<usize>,
cx: &WindowContext,
) -> gpui::AnyElement {
let mentions = &[];
let language = None;
let paragraph = &self.source_contents[source_range.clone()];
let rich_text = render_rich_text(
paragraph.into(),
mentions,
&self.language_registry,
language,
);
let id: ElementId = source_range.start.into();
rich_text.element(id, cx)
}
fn headline(&self, level: HeadingLevel) -> Div {
let size = match level {
HeadingLevel::H1 => rems(2.),
HeadingLevel::H2 => rems(1.5),
HeadingLevel::H3 => rems(1.25),
HeadingLevel::H4 => rems(1.),
HeadingLevel::H5 => rems(0.875),
HeadingLevel::H6 => rems(0.85),
};
let color = match level {
HeadingLevel::H6 => self.theme.colors().text_muted,
_ => self.theme.colors().text,
};
let line_height = DefiniteLength::from(rems(1.25));
let headline = h_flex()
.w_full()
.line_height(line_height)
.text_size(size)
.text_color(color)
.mb_4()
.pb(rems(0.15));
headline
}
}
pub fn render_markdown(
markdown_input: &str,
language_registry: &Arc<LanguageRegistry>,
cx: &WindowContext,
) -> Vec<Div> {
let theme = cx.theme().clone();
let options = Options::all();
let parser = Parser::new_ext(markdown_input, options);
let renderer = Renderer::new(
parser.into_offset_iter(),
markdown_input.to_owned(),
language_registry,
theme,
);
let renderer = renderer.run(cx);
return renderer.finished;
}

View File

@@ -39,7 +39,7 @@ lsp = { path = "../lsp" }
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
pulldown-cmark.workspace = true
rand.workspace = true
rich_text = { path = "../rich_text" }
schemars.workspace = true

View File

@@ -3052,6 +3052,12 @@ impl MultiBufferSnapshot {
self.has_conflict
}
pub fn has_diagnostics(&self) -> bool {
self.excerpts
.iter()
.any(|excerpt| excerpt.buffer.has_diagnostics())
}
pub fn diagnostic_group<'a, O>(
&'a self,
group_id: usize,

View File

@@ -58,13 +58,16 @@ impl<D: PickerDelegate> FocusableView for Picker<D> {
}
}
fn create_editor(placeholder: Arc<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(placeholder, cx);
editor
})
}
impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(delegate.placeholder_text(), cx);
editor
});
let editor = create_editor(delegate.placeholder_text(), cx);
cx.subscribe(&editor, Self::on_input_editor_event).detach();
let mut this = Self {
delegate,

View File

@@ -195,11 +195,11 @@ impl Prettier {
},
Path::new("/"),
None,
cx,
cx.clone(),
)
.context("prettier server creation")?;
let server = executor
.spawn(server.initialize(None))
let server = cx
.update(|cx| executor.spawn(server.initialize(None, cx)))?
.await
.context("prettier server initialization")?;
Ok(Self::Real(RealPrettier {

View File

@@ -75,6 +75,7 @@ fs = { path = "../fs", features = ["test-support"] }
git2.workspace = true
gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
release_channel = { path = "../release_channel" }
lsp = { path = "../lsp", features = ["test-support"] }
prettier = { path = "../prettier", features = ["test-support"] }
pretty_assertions.workspace = true

View File

@@ -3130,7 +3130,9 @@ impl Project {
(None, override_options) => initialization_options = override_options,
_ => {}
}
let language_server = language_server.initialize(initialization_options).await?;
let language_server = cx
.update(|cx| language_server.initialize(initialization_options, cx))?
.await?;
language_server
.notify::<lsp::notification::DidChangeConfiguration>(
@@ -4412,13 +4414,13 @@ impl Project {
}
}
pub fn definition<T: ToPointUtf16>(
#[inline(never)]
fn definition_impl(
&self,
buffer: &Model<Buffer>,
position: T,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
@@ -4426,14 +4428,22 @@ impl Project {
cx,
)
}
pub fn type_definition<T: ToPointUtf16>(
pub fn definition<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.definition_impl(buffer, position, cx)
}
fn type_definition_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
@@ -4441,7 +4451,30 @@ impl Project {
cx,
)
}
pub fn type_definition<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.type_definition_impl(buffer, position, cx)
}
fn references_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Location>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetReferences { position },
cx,
)
}
pub fn references<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
@@ -4449,10 +4482,19 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Location>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.references_impl(buffer, position, cx)
}
fn document_highlights_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<DocumentHighlight>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetReferences { position },
GetDocumentHighlights { position },
cx,
)
}
@@ -4464,12 +4506,7 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<DocumentHighlight>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetDocumentHighlights { position },
cx,
)
self.document_highlights_impl(buffer, position, cx)
}
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
@@ -4692,13 +4729,12 @@ impl Project {
}
}
pub fn hover<T: ToPointUtf16>(
fn hover_impl(
&self,
buffer: &Model<Buffer>,
position: T,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Hover>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
@@ -4706,14 +4742,23 @@ impl Project {
cx,
)
}
pub fn completions<T: ToOffset + ToPointUtf16>(
pub fn hover<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
) -> Task<Result<Option<Hover>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.hover_impl(buffer, position, cx)
}
#[inline(never)]
fn completions_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
if self.is_local() {
let snapshot = buffer.read(cx).snapshot();
let offset = position.to_offset(&snapshot);
@@ -4760,6 +4805,15 @@ impl Project {
Task::ready(Ok(Default::default()))
}
}
pub fn completions<T: ToOffset + ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.completions_impl(buffer, position, cx)
}
pub fn resolve_completions(
&self,
@@ -5036,6 +5090,20 @@ impl Project {
}
}
fn code_actions_impl(
&self,
buffer_handle: &Model<Buffer>,
range: Range<Anchor>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<CodeAction>>> {
self.request_lsp(
buffer_handle.clone(),
LanguageServerToQuery::Primary,
GetCodeActions { range },
cx,
)
}
pub fn code_actions<T: Clone + ToOffset>(
&self,
buffer_handle: &Model<Buffer>,
@@ -5044,12 +5112,7 @@ impl Project {
) -> Task<Result<Vec<CodeAction>>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
self.request_lsp(
buffer_handle.clone(),
LanguageServerToQuery::Primary,
GetCodeActions { range },
cx,
)
self.code_actions_impl(buffer_handle, range, cx)
}
pub fn apply_code_action(
@@ -5418,13 +5481,12 @@ impl Project {
Ok(project_transaction)
}
pub fn prepare_rename<T: ToPointUtf16>(
fn prepare_rename_impl(
&self,
buffer: Model<Buffer>,
position: T,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Range<Anchor>>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer,
LanguageServerToQuery::Primary,
@@ -5432,11 +5494,20 @@ impl Project {
cx,
)
}
pub fn perform_rename<T: ToPointUtf16>(
pub fn prepare_rename<T: ToPointUtf16>(
&self,
buffer: Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Range<Anchor>>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.prepare_rename_impl(buffer, position, cx)
}
fn perform_rename_impl(
&self,
buffer: Model<Buffer>,
position: PointUtf16,
new_name: String,
push_to_history: bool,
cx: &mut ModelContext<Self>,
@@ -5453,22 +5524,28 @@ impl Project {
cx,
)
}
pub fn on_type_format<T: ToPointUtf16>(
pub fn perform_rename<T: ToPointUtf16>(
&self,
buffer: Model<Buffer>,
position: T,
new_name: String,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<ProjectTransaction>> {
let position = position.to_point_utf16(buffer.read(cx));
self.perform_rename_impl(buffer, position, new_name, push_to_history, cx)
}
pub fn on_type_format_impl(
&self,
buffer: Model<Buffer>,
position: PointUtf16,
trigger: String,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
let (position, tab_size) = buffer.update(cx, |buffer, cx| {
let position = position.to_point_utf16(buffer);
(
position,
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
.tab_size,
)
let tab_size = buffer.update(cx, |buffer, cx| {
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx).tab_size
});
self.request_lsp(
buffer.clone(),
@@ -5483,6 +5560,18 @@ impl Project {
)
}
pub fn on_type_format<T: ToPointUtf16>(
&self,
buffer: Model<Buffer>,
position: T,
trigger: String,
push_to_history: bool,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.on_type_format_impl(buffer, position, trigger, push_to_history, cx)
}
pub fn inlay_hints<T: ToOffset>(
&self,
buffer_handle: Model<Buffer>,
@@ -5491,6 +5580,15 @@ impl Project {
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
self.inlay_hints_impl(buffer_handle, range, cx)
}
fn inlay_hints_impl(
&self,
buffer_handle: Model<Buffer>,
range: Range<Anchor>,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<Vec<InlayHint>>> {
let buffer = buffer_handle.read(cx);
let range_start = range.start;
let range_end = range.end;
let buffer_id = buffer.remote_id().into();

View File

@@ -4380,6 +4380,7 @@ fn init_test(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
language::init(cx);
Project::init_settings(cx);
});

View File

@@ -262,11 +262,14 @@ impl ProjectPanel {
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
let file_path = entry.path.clone();
let worktree_id = worktree.read(cx).id();
let entry_id = entry.id;
workspace
.open_path(
ProjectPath {
worktree_id: worktree.read(cx).id(),
path: entry.path.clone(),
worktree_id,
path: file_path.clone(),
},
None,
focus_opened_item,
@@ -281,8 +284,16 @@ impl ProjectPanel {
_ => None,
}
});
if !focus_opened_item {
if let Some(project_panel) = project_panel.upgrade() {
if let Some(project_panel) = project_panel.upgrade() {
// Always select the entry, regardless of whether it is opened or not.
project_panel.update(cx, |project_panel, _| {
project_panel.selection = Some(Selection {
worktree_id,
entry_id
});
});
if !focus_opened_item {
let focus_handle = project_panel.read(cx).focus_handle.clone();
cx.focus(&focus_handle);
}

View File

@@ -33,6 +33,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
release_channel = { path = "../release_channel" }
settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }

View File

@@ -392,6 +392,7 @@ mod tests {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);

View File

@@ -14,6 +14,7 @@ assistant = { path = "../assistant" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
search = { path = "../search" }
settings = { path = "../settings" }
ui = { path = "../ui" }
workspace = { path = "../workspace" }

View File

@@ -1,11 +1,12 @@
use assistant::{AssistantPanel, InlineAssist};
use editor::Editor;
use editor::{Editor, EditorSettings};
use gpui::{
Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
Subscription, View, ViewContext, WeakView,
};
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -16,16 +17,26 @@ pub struct QuickActionBar {
active_item: Option<Box<dyn ItemHandle>>,
_inlay_hints_enabled_subscription: Option<Subscription>,
workspace: WeakView<Workspace>,
show: bool,
}
impl QuickActionBar {
pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
Self {
pub fn new(
buffer_search_bar: View<BufferSearchBar>,
workspace: &Workspace,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
buffer_search_bar,
active_item: None,
_inlay_hints_enabled_subscription: None,
workspace: workspace.weak_handle(),
}
show: true,
};
this.apply_settings(cx);
cx.observe_global::<SettingsStore>(|this, cx| this.apply_settings(cx))
.detach();
this
}
fn active_editor(&self) -> Option<View<Editor>> {
@@ -33,6 +44,24 @@ impl QuickActionBar {
.as_ref()
.and_then(|item| item.downcast::<Editor>())
}
fn apply_settings(&mut self, cx: &mut ViewContext<Self>) {
let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
if new_show != self.show {
self.show = new_show;
cx.emit(ToolbarItemEvent::ChangeLocation(
self.get_toolbar_item_location(),
));
}
}
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
if self.show && self.active_editor().is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
}
impl Render for QuickActionBar {
@@ -40,7 +69,6 @@ impl Render for QuickActionBar {
let Some(editor) = self.active_editor() else {
return div().id("empty quick action bar");
};
let inlay_hints_button = Some(QuickActionBarButton::new(
"toggle inlay hints",
IconName::InlayHint,
@@ -155,36 +183,28 @@ impl ToolbarItemView for QuickActionBar {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
match active_pane_item {
Some(active_item) => {
self.active_item = Some(active_item.boxed_clone());
self._inlay_hints_enabled_subscription.take();
self.active_item = active_pane_item.map(ItemHandle::boxed_clone);
if let Some(active_item) = active_pane_item {
self._inlay_hints_enabled_subscription.take();
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let editor = editor.read(cx);
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|| supports_inlay_hints != new_supports_inlay_hints;
inlay_hints_enabled = new_inlay_hints_enabled;
supports_inlay_hints = new_supports_inlay_hints;
if should_notify {
cx.notify()
}
}));
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
None => {
self.active_item = None;
ToolbarItemLocation::Hidden
if let Some(editor) = active_item.downcast::<Editor>() {
let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
self._inlay_hints_enabled_subscription =
Some(cx.observe(&editor, move |_, editor, cx| {
let editor = editor.read(cx);
let new_inlay_hints_enabled = editor.inlay_hints_enabled();
let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
|| supports_inlay_hints != new_supports_inlay_hints;
inlay_hints_enabled = new_inlay_hints_enabled;
supports_inlay_hints = new_supports_inlay_hints;
if should_notify {
cx.notify()
}
}));
}
}
self.get_toolbar_item_location()
}
}

View File

@@ -1,9 +1,9 @@
use gpui::{AppContext, Global};
use gpui::{AppContext, Global, SemanticVersion};
use once_cell::sync::Lazy;
use std::env;
#[doc(hidden)]
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
Lazy::new(|| {
env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
@@ -11,6 +11,7 @@ pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
} else {
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())
};
#[doc(hidden)]
pub static RELEASE_CHANNEL: Lazy<ReleaseChannel> =
Lazy::new(|| match RELEASE_CHANNEL_NAME.as_str() {
@@ -39,6 +40,29 @@ impl AppCommitSha {
}
}
struct GlobalAppVersion(SemanticVersion);
impl Global for GlobalAppVersion {}
pub struct AppVersion;
impl AppVersion {
pub fn init(pkg_version: &str, cx: &mut AppContext) {
let version = if let Some(from_env) = env::var("ZED_APP_VERSION").ok() {
from_env.parse().expect("invalid ZED_APP_VERSION")
} else {
cx.app_metadata()
.app_version
.unwrap_or_else(|| pkg_version.parse().expect("invalid version in Cargo.toml"))
};
cx.set_global(GlobalAppVersion(version))
}
pub fn global(cx: &AppContext) -> SemanticVersion {
cx.global::<GlobalAppVersion>().0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel {
#[default]
@@ -52,11 +76,12 @@ struct GlobalReleaseChannel(ReleaseChannel);
impl Global for GlobalReleaseChannel {}
impl ReleaseChannel {
pub fn init(cx: &mut AppContext) {
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
}
pub fn init(pkg_version: &str, cx: &mut AppContext) {
AppVersion::init(pkg_version, cx);
cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL))
}
impl ReleaseChannel {
pub fn global(cx: &AppContext) -> Self {
cx.global::<GlobalReleaseChannel>().0
}

View File

@@ -22,7 +22,7 @@ futures.workspace = true
gpui = { path = "../gpui" }
language = { path = "../language" }
lazy_static.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
pulldown-cmark.workspace = true
smallvec.workspace = true
smol.workspace = true
sum_tree = { path = "../sum_tree" }

View File

@@ -13,6 +13,7 @@ use util::RangeExt;
pub enum Highlight {
Code,
Id(HighlightId),
InlineCode(bool),
Highlight(HighlightStyle),
Mention,
SelfMention,
@@ -47,7 +48,7 @@ pub struct Mention {
}
impl RichText {
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
let theme = cx.theme();
let code_background = theme.colors().surface_background;
@@ -67,6 +68,23 @@ impl RichText {
background_color: Some(code_background),
..id.style(theme.syntax()).unwrap_or_default()
},
Highlight::InlineCode(link) => {
if !*link {
HighlightStyle {
background_color: Some(code_background),
..Default::default()
}
} else {
HighlightStyle {
background_color: Some(code_background),
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}
}
}
Highlight::Highlight(highlight) => *highlight,
Highlight::Mention => HighlightStyle {
font_weight: Some(FontWeight::BOLD),
@@ -83,7 +101,12 @@ impl RichText {
)
.on_click(self.link_ranges.clone(), {
let link_urls = self.link_urls.clone();
move |ix, cx| cx.open_url(&link_urls[ix])
move |ix, cx| {
let url = &link_urls[ix];
if url.starts_with("http") {
cx.open_url(url);
}
}
})
.tooltip({
let link_ranges = self.link_ranges.clone();
@@ -179,22 +202,14 @@ pub fn render_markdown_mut(
}
Event::Code(t) => {
text.push_str(t.as_ref());
if link_url.is_some() {
highlights.push((
prev_len..text.len(),
Highlight::Highlight(HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}),
));
}
let is_link = link_url.is_some();
if let Some(link_url) = link_url.clone() {
link_ranges.push(prev_len..text.len());
link_urls.push(link_url);
}
highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
@@ -256,7 +271,7 @@ pub fn render_markdown_mut(
}
}
pub fn render_markdown(
pub fn render_rich_text(
block: String,
mentions: &[Mention],
language_registry: &Arc<LanguageRegistry>,

View File

@@ -210,7 +210,7 @@ impl SettingsStore {
if let Some(release_settings) = &self
.raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME)
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
{
if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings)
@@ -543,7 +543,7 @@ impl SettingsStore {
if let Some(release_settings) = &self
.raw_user_settings
.get(&*release_channel::RELEASE_CHANNEL_NAME)
.get(&*release_channel::RELEASE_CHANNEL.dev_name())
{
if let Some(release_settings) = setting_value
.deserialize_setting(&release_settings)

View File

@@ -11,7 +11,8 @@ doctest = false
[dependencies]
alacritty_terminal = "0.21"
# needed for "a few weeks" until alacritty 0.13.2 is out
alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "2d2b894c3b869fadc78fce9d72cb5c8d2b764cac" }
anyhow.workspace = true
db = { path = "../db" }
dirs = "4.0.0"

View File

@@ -86,6 +86,15 @@ pub enum Event {
Open(MaybeNavigationTarget),
}
#[derive(Clone, Debug)]
pub struct PathLikeTarget {
/// File system path, absolute or relative, existing or not.
/// Might have line and column number(s) attached as `file.rs:1:23`
pub maybe_path: String,
/// Current working directory of the terminal
pub terminal_dir: Option<PathBuf>,
}
/// A string inside terminal, potentially useful as a URI that can be opened.
#[derive(Clone, Debug)]
pub enum MaybeNavigationTarget {
@@ -93,7 +102,7 @@ pub enum MaybeNavigationTarget {
Url(String),
/// File system path, absolute or relative, existing or not.
/// Might have line and column number(s) attached as `file.rs:1:23`
PathLike(String),
PathLike(PathLikeTarget),
}
#[derive(Clone)]
@@ -364,7 +373,7 @@ impl TerminalBuilder {
pty,
pty_options.hold,
false,
);
)?;
//Kick things off
let pty_tx = event_loop.channel();
@@ -626,6 +635,12 @@ impl Terminal {
}
}
fn get_cwd(&self) -> Option<PathBuf> {
self.foreground_process_info
.as_ref()
.map(|info| info.cwd.clone())
}
///Takes events from Alacritty and translates them to behavior on this view
fn process_terminal_event(
&mut self,
@@ -800,7 +815,10 @@ impl Terminal {
let target = if is_url {
MaybeNavigationTarget::Url(maybe_url_or_path)
} else {
MaybeNavigationTarget::PathLike(maybe_url_or_path)
MaybeNavigationTarget::PathLike(PathLikeTarget {
maybe_path: maybe_url_or_path,
terminal_dir: self.get_cwd(),
})
};
cx.emit(Event::Open(target));
} else {
@@ -852,7 +870,10 @@ impl Terminal {
let navigation_target = if is_url {
MaybeNavigationTarget::Url(word)
} else {
MaybeNavigationTarget::PathLike(word)
MaybeNavigationTarget::PathLike(PathLikeTarget {
maybe_path: word,
terminal_dir: self.get_cwd(),
})
};
cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
}

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