Compare commits

...

445 Commits

Author SHA1 Message Date
Max Brunsfeld
a26744a96c zed 0.92.1 2023-06-27 15:19:23 -07:00
Max Brunsfeld
02a426b657 Fix bugs in handling combined injections exposed by HEEx (#2652)
Fixes
https://linear.app/zed-industries/issue/Z-2481/heex-this-snippet-triggers-a-hard-crash

Release Notes:

- Fixed a crash that would sometimes occur when editing a HEEx file
([#1703](https://github.com/zed-industries/community/issues/1703)).
2023-06-27 15:18:25 -07:00
Joseph T. Lyons
c1bb4e7808 v0.92.x preview 2023-06-21 13:58:53 -04:00
Nate Butler
af8671cf7f Update rose pine theme family (#2624)
Update the Rose Pine theme family, including new syntax overrides
matching the official theme docs:
https://rosepinetheme.com/palette/ingredients/

Also adds a doc to our internal docs starting to outline how our syntax
highlighting works.

Before:
<img width="626" alt="CleanShot 2023-06-21 at 12 19 17@2x"
src="https://github.com/zed-industries/zed/assets/1714999/5ebe1cd0-1707-483b-a50f-bb53dbf89ba2">

After:
<img width="718" alt="CleanShot 2023-06-21 at 12 18 48@2x"
src="https://github.com/zed-industries/zed/assets/1714999/46b588aa-31a0-4402-934b-e3de7bc2f6b5">

From their site:

![image](https://github.com/zed-industries/zed/assets/1714999/83d4f671-e15f-4423-a01e-b382145e79fb)


Release Notes:

- Updated Rose Pine theme family to better match original theme.
2023-06-21 12:46:18 -04:00
Nate Butler
a3e65528ba Update syntax colors 2023-06-21 12:21:01 -04:00
Nate Butler
eeb155a951 Remove unused light variable 2023-06-21 12:20:47 -04:00
Nate Butler
f0138a7a87 Add initial syntax highlighting doc 2023-06-21 12:15:24 -04:00
Mikayla Maki
5860b7b143 Add copy-on-click to diagnostic messages (#2634)
I finally got fed up with being unable to copy error messages. This adds
a click target and tooltip to f8-style diagnostics that copies their
text on click.

Release Notes:

- Added the ability to copy under-line diagnostic errors on click
2023-06-20 17:05:20 -07:00
Mikayla Maki
11125a62c7 Add copy-on-click to diagnostic messages 2023-06-20 16:48:30 -07:00
Antonio Scandurra
6ed86781b2 Instruct the assistant to reply to a specific message (#2631)
Closes
https://linear.app/zed-industries/issue/Z-2384/hitting-cmd-enter-in-a-user-or-system-message-should-generate-a

Release Notes:

- Introduced the ability to generate assistant messages for any
user/system message, as well as generating multiple assists at the same
time, one for each cursor. (preview-only)
2023-06-20 18:16:23 +02:00
Nathan Sobo
03993ea719 Rename SumTree::push_tree to ::append (#2626)
This is something I've been meaning to rename in SumTree. Was looking at
the code and figure why not now.
2023-06-20 08:37:55 -06:00
Julia
adccfdfbbf Avoid assigning NSCursor style when it already is that style (#2632)
This avoids a high cost which appears to be the system rasterizing the
cursor every time we call this, fixes a slowdown when scrolling rapidly
while mouse motion continually attempted to assign the style

Fixes
https://linear.app/zed-industries/issue/Z-2200/editor-scroll-slowdown

Release Notes:

- Improved the performance of scrolling by avoiding unnecessary cursor
changes
([#1597](https://github.com/zed-industries/community/issues/1597)).
2023-06-20 10:02:04 -04:00
Antonio Scandurra
1d84da1d33 Improve prompt 2023-06-20 15:32:51 +02:00
Julia
dc07b60e40 Avoid assigning NSCursor style when it already is that style
This avoids a high cost which appears to be the system rasterizing the
cursor every time we call this, fixes a slowdown when scrolling rapidly
while mouse motion continually attempted to assign the style

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
2023-06-20 09:31:30 -04:00
Antonio Scandurra
8673b0b75b Avoid including pending or errored messages on assist 2023-06-20 11:59:51 +02:00
Max Brunsfeld
ebf0c96130 Show a notification when a dependency for a language server is missing (#2630)
Closes
https://linear.app/zed-industries/issue/Z-2402/elixir-ls-requires-a-system-elixir-runtime-to-be-installed
Closes
https://linear.app/zed-industries/issue/Z-666/notify-users-that-they-need-go-installed-to-use-go-language-server

Release Notes:

- Added a notification when unable to download the Go language server
because Go is not installed.
- Added a notification when unable to run the Elixir language server
because Elixir is not installed.
2023-06-19 20:03:30 -07:00
Max Brunsfeld
050c22312c Update plugin runtime LspAdapter impl 2023-06-19 19:38:31 -07:00
Nathan Sobo
8c298a9da5 Rename SumTree::push_tree to ::append 2023-06-19 19:49:33 -06:00
Max Brunsfeld
1215de0c42 Show a notification when unable to start elixir-ls 2023-06-19 18:05:30 -07:00
Max Brunsfeld
57c2d85c66 Show a notification that gopls can't be installed without go 2023-06-19 18:05:12 -07:00
Max Brunsfeld
360bbebbd9 Introduce LspAdapterDelegate trait, passed to LspDelegates 2023-06-19 16:12:25 -07:00
Max Brunsfeld
05d69c804c Fix issues with syntax highlighting in elixir and heex (#2629)
Fixes
https://linear.app/zed-industries/issue/Z-2391/text-within-h-isnt-highlighted-the-same-as-in-heex-files
Fixes
https://linear.app/zed-industries/issue/Z-2415/syntax-highlighting-bug-in-strings

Release Notes:

- Fixed some issues with syntax highlighting in Elixir and Heex.

If the release notes are only intended for a specific release channel
only, add `(<release_channel>-only)` to the end of the release note
line.
These will be removed by the person making the release.
2023-06-19 12:56:09 -07:00
Max Brunsfeld
04430fdbd6 Fix issues with syntax highlighting in elixir and heex 2023-06-19 12:34:46 -07:00
Piotr Osiewicz
2a3c660d1f settings: accept trailing commas (#2606)
Z-2357

I've found a crate that handles both comments and trailing commas in
JSON. It is a fork of `serde_json`, but it is maintained & up-to-date.
Sadly RawValue seems to not play nicely with it; I've ran into
deserialisation issues around use of RawValue. For this PR I've migrated
to `Value` API.

Obviously this is just a point of discussion, not something I'd merge
straight away. There may be better solutions to this particular problem.

I've also noticed that `serde_json_lenient` does not handle trailing
commas after bindings array. I'm not sure how big of an issue that is.

Release Notes:
- Improved handling of trailing commas in settings files.
[#1322](https://github.com/zed-industries/community/issues/1322)
2023-06-19 18:29:03 +02:00
Antonio Scandurra
cb55356106 WIP 2023-06-19 17:53:05 +02:00
Antonio Scandurra
75e2329028 Allow for multi-cursor assist and cycle_role actions
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Kyle Caverly <kyle@zed.dev>
2023-06-19 17:23:40 +02:00
Antonio Scandurra
9191a82447 Remove Assistant::open_ai_request_messages 2023-06-19 14:35:33 +02:00
Antonio Scandurra
dc9231d5a3 Merge remote-tracking branch 'origin/main' into replies 2023-06-19 14:28:21 +02:00
Antonio Scandurra
70ccbbafc1 Allow message splitting with multiple cursors (#2627)
Closes
https://linear.app/zed-industries/issue/Z-2408/make-block-splitting-work-with-multiple-cursors
2023-06-19 12:07:16 +02:00
Antonio Scandurra
c9df963142 Allow message splitting with multiple cursors 2023-06-19 12:00:45 +02:00
Joseph T. Lyons
c858ab263c Add pane activation bindings for Atom keymap (#2625)
Release Notes:

- N/A
2023-06-18 02:09:26 -04:00
Joseph T. Lyons
bb04d65b8e Add pane activation bindings for Atom keymap 2023-06-18 02:05:25 -04:00
Nate Butler
ffd1190cca Update rose pine theme family 2023-06-16 23:02:32 -04:00
Nathan Sobo
c3b2b4c4e3 Cycle message roles on ctrl-r (#2619)
I'd like to follow up to allow roles to be cycled for the selected range
and support multi-cursors, but this is a start and contains a
refactoring, so going to merge.

Release Notes:

- Added the ability to cycle roles in the assistant with `ctrl-r`
2023-06-16 14:11:01 -06:00
Kirill Bulatov
7ff194f21f React on message-less LSP requests properly (#2620) 2023-06-16 23:00:14 +03:00
Kirill Bulatov
77f5b5a80d React on message-less LSP requests properly
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-16 22:02:29 +03:00
Nathan Sobo
54c71c1a35 Insert reply after the currently selected user message 2023-06-16 12:41:07 -06:00
Nathan Sobo
11f318566e Allow splitting of messages using shift-enter (#2613)
Closes
https://linear.app/zed-industries/issue/Z-2364/allow-splitting-blocks

Release Notes:

- Added support for splitting messages in the assistant panel using
`shift-enter`. (preview-only)
2023-06-16 11:50:50 -06:00
Nathan Sobo
75b5ac8488 Cycle message roles on ctrl-r 2023-06-16 11:50:18 -06:00
Nathan Sobo
c179dd9990 Remove redundant tests 2023-06-16 11:43:16 -06:00
Nathan Sobo
6c0f65cfe0 Avoid inserting redundant newlines
Co-Authored-By: Piotr Osiewicz <piotr@zed.dev>
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-06-16 10:36:42 -06:00
Piotr Osiewicz
ef6cb11d5c Emit editor event whether we insert a newline or not. 2023-06-16 13:29:12 +02:00
Mikayla Maki
8235d78afe Voice support (#2602)
This PR adds APIs for voice chat to our live kit integration

Release Notes:

- N/A
2023-06-15 19:40:01 -07:00
Mikayla Maki
d7dbc3b5d3 Implement test stubs 2023-06-15 19:28:59 -07:00
Max Brunsfeld
8592208d3a Fix failure to upload panics when multiple panics happen at the same time (#2616)
When multiple panics occur at the same time (usually because one thread
panics, and another thread joins it), multiple panic JSON objects can
get written to the same panic file. The resulting file won't be valid
JSON.

This PR addresses that problem via two changes:
* Format panic files as single-line JSON objects
* When a panic file  isn't valid JSON, try taking the first line

In the future, we could try combining all of the backtraces, but for
now, I just want to avoid a problem of not reporting a panic at all.

Release Notes:

- Fixed a problem with Zed's internal crash reporting.
2023-06-15 15:31:49 -07:00
Mikayla Maki
b9270925dd Focus on toggle dock (#2612)
This PR updates some dock behaviors. Now the toggle-dock commands
(cmd-j/b/r) also toggle focus. This also adds zoom serialization to the
docks.

Release Notes:
- Bug fix: Toggle dock commands (cmd-j/b/r) now move focus 
- Bug fix: Dock zoom is now restored with the rest of the workspace
2023-06-15 15:20:20 -07:00
Max Brunsfeld
e525706581 Fix failure to upload panics when multiple panics happen at the same time 2023-06-15 15:16:26 -07:00
Mikayla Maki
b156644daf fixed tests to match new behavior 2023-06-15 15:09:37 -07:00
Mikayla Maki
b7e3ac5bf4 Fix bug on workspace deserialization 2023-06-15 14:56:37 -07:00
Mikayla Maki
b9cb594626 Fix fmt 2023-06-15 14:51:38 -07:00
Max Brunsfeld
5f93a4effd Don't rely on debug symbols for panic reporting (#2615)
This fixes a regression introduced in
https://github.com/zed-industries/zed/pull/2560, where panic reports did
not include backtraces. The problem was that in that PR, I assumed we
could retrieve file paths for symbols in our backtraces. But actually,
that functionality only works when the app is built locally, and a
`.dSYM` file can be magically found by the OS. We don't ship those dSYM
files with Zed, so panic symbols do not have file paths available.

Panic backtraces will still be more useful and less noisy than before
though: we will strip out frames for which we don't have symbol names,
and remove leading panic-handling stack frames from the backtraces.

Release Notes:

- N/A
2023-06-15 14:09:42 -07:00
Max Brunsfeld
e7e0f2183f Don't rely on debug symbols for panic reporting 2023-06-15 12:27:39 -07:00
Antonio Scandurra
0a8d67c4ca Activate screen-sharing when leader activates a panel (#2614)
Fixes
https://linear.app/zed-industries/issue/Z-1875/screen-sharing-tab-is-not-activated-when-leader-is-on-a-panel

Release Notes:

- Fixed a bug that caused followers to not see the leader's screen when
they activated a panel.
2023-06-15 18:13:07 +02:00
Antonio Scandurra
f068712a0a Activate screen-sharing when leader activates a panel 2023-06-15 18:01:30 +02:00
Nathan Sobo
8c6ba13fef Never insert an empty prefix when splitting a message with a non-empty range
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Piotr Osiewicz <piotr@zed.dev>
2023-06-15 09:02:15 -06:00
Nate Butler
0863486803 Add internal markdown docs (#2598)
This PR adds internal docs directly to the codebase.

There are a few goals here:

- Make it easier to document our internal processes
- Put the documentation where people already are
- Allow changes to be in sync with PRs
- Make it easier for people coming in to discover our docs and onboard
faster.

With 2 more people joining us in the next few weeks it would be great to
get these up.

Release Notes:

- N/A (No public facing changes)
2023-06-15 10:55:55 -04:00
Nate Butler
87c3216236 Fix design tools link 2023-06-15 10:38:15 -04:00
Nate Butler
5149db6acd Update docs/index.md
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2023-06-15 10:36:53 -04:00
Antonio Scandurra
21e8e8763e Allow splitting of messages using shift-enter 2023-06-15 14:05:26 +02:00
Mikayla Maki
6662066821 Add zoom serialization to the workspace 2023-06-14 23:43:45 -07:00
Mikayla Maki
790d995381 Add focusing into the toggle dock commands 2023-06-14 23:04:41 -07:00
Mikayla Maki
a4b009a497 Update test-app sound 2023-06-14 18:40:45 -07:00
Mikayla Maki
ce3847c9fc WIP: Use audio support to send live data 2023-06-14 18:40:45 -07:00
Mikayla Maki
bbf05c8eac Wire in audio APIs from swift
co-authored-by: nathan <nathan@zed.dev>
2023-06-14 18:40:45 -07:00
Mikayla Maki
8ccb75029f Add entitlements file to bundle step (#2611)
This completes the bundle changes that will be needed to access voice,
as well as adds permissions for accessing other MacOS services, the
camera, and the necessary permissions for plugins. This was developed by
combining the entitlements of iTerm and VSCode, cross-referenced with
the entitlements of Firefox. 

Release Notes:

- Fixed a bug in enabling authorization for macOS services (preview
only)
2023-06-14 18:06:55 -07:00
Mikayla Maki
b481e383ce Update bundle 2023-06-14 17:37:47 -07:00
Mikayla Maki
493e489c51 Add entitlements file to bundle step 2023-06-14 17:34:48 -07:00
Joseph Lyons
2e723cac62 v0.92.x dev 2023-06-14 13:27:36 -04:00
Mikayla Maki
75b1f60126 Add UsageDescriptions to Info.plist (#2609)
This updates our dependency on cargo-bundle to point at a fork I've
created that adjusts the latest version's behavior to work for our use
case. This PR also updates the Zed Info.plist to include the following
keys:

- NSSystemAdministrationUsageDescription
- NSAppleEventsUsageDescription
- NSBluetoothAlwaysUsageDescription
- NSCalendarsUsageDescription
- NSCameraUsageDescription
- NSContactsUsageDescription
- NSLocationAlwaysUsageDescription
- NSLocationUsageDescription
- NSLocationWhenInUseUsageDescription
- NSMicrophoneUsageDescription
- NSSpeechRecognitionUsageDescription
- NSRemindersUsageDescription

Release Notes:

- Bug fix: Allow applications run via the Zed terminal to request
authorization for macOS services (e.g. microphone and camera)
2023-06-14 10:04:54 -07:00
Antonio Scandurra
2b8b954c3e Overhaul assistant panel (#2610)
Closes
https://linear.app/zed-industries/issue/Z-2368/use-a-different-icon-for-the-assistant-panel
Closes
https://linear.app/zed-industries/issue/Z-2363/ship-the-assistant-only-on-preview
Closes
https://linear.app/zed-industries/issue/Z-2331/scrolling-makes-it-hard-to-read
Closes
https://linear.app/zed-industries/issue/Z-2306/allow-undo-and-collaboration-in-assistant

This pull request is a significant overhaul of the assistant panel,
which now uses a simple `Buffer` as opposed to a `MultiBuffer` to show
messages. Specifically, we track the start of each message with an
anchor located right after the newline (or `Anchor::MIN` for the first
message). When the anchor becomes invalid (that is, the newline is
deleted), we merge the message with the preceding ones. Crucially,
messages don't actually get deleted so that, if the newline anchor
becomes valid again (such as when undoing/redoing), we can restore the
messages as well.

As part of this overhaul, we are also improving the scrolling behavior
to maintain the viewport stable only when editing or moving the cursor,
but otherwise leave the scroll position unchanged when manually
scrolling up or down.

Note that with these changes, we are limiting access to the assistant to
users on preview (and dev), as we want to polish the behavior a little
more before shipping to the general public. Users on stable will still
be able to see the default settings/keybindings of the assistant, but I
think that's okay, as they won't be able to do anything with them.

Release Notes:

- Added support for undo/redo in the assistant (preview-only)
- Improved the scrolling behavior of the assistant when it was
generating responses. Now Zed will keep the viewport stable only when
editing or moving the cursor, but otherwise leave the scroll position
unchanged when manually scrolling up or down (preview-only)
- Changed the icon of the assistant panel (preview-only)

**Note for @JosephTLyons: given that we're feature flagging this, let's
make sure things on stable look reasonable and work correctly. Things to
look out for: ensure a stock installation works, changing the settings
on stable works, changing the keybinding on stable works.**
2023-06-14 14:09:09 +02:00
Antonio Scandurra
4efe62b3e5 Use robot icon for assistant to prevent confusion with conversations 2023-06-14 12:14:43 +02:00
Antonio Scandurra
049c987310 Avoid loading the assistant panel on stable 2023-06-14 12:08:49 +02:00
Antonio Scandurra
56b0bf8601 Save cursor scroll position when the editor is auto-scrolled 2023-06-14 12:00:27 +02:00
Antonio Scandurra
1aa1774688 Avoid auto-scrolling the editor when inserting/removing headers 2023-06-14 10:49:23 +02:00
Antonio Scandurra
f8b9417406 Keep cursor stable as autocompletions are being streamed 2023-06-14 10:41:18 +02:00
Antonio Scandurra
75ad76bfb2 💄 2023-06-14 09:47:30 +02:00
Antonio Scandurra
7dab17e233 Re-enable copy support in the assistant 2023-06-14 09:40:43 +02:00
Antonio Scandurra
27c83ca3f7 Remove unnecessary set_render_excerpt_header method 2023-06-14 09:38:49 +02:00
Antonio Scandurra
9b7617403d Parse buffer as Markdown 2023-06-14 09:31:41 +02:00
Antonio Scandurra
ea5d677ef8 Merge remote-tracking branch 'origin/main' into assistant-buffer 2023-06-14 09:16:45 +02:00
Mikayla Maki
adc8337ad4 Restore misconfigured Cargo.toml 2023-06-13 18:08:33 -07:00
Mikayla Maki
8ad7eb7598 Switch to using new version of cargo bundle with more capabilities 2023-06-13 18:01:51 -07:00
Kevin Hovsäter
b365e48ff0 Toggle comments for empty single line selections (#2594) 2023-06-13 20:15:11 +02:00
Max Brunsfeld
77367bf2e4 Move worktree tests into their own file (#2608)
The worktree has a lot of tests. Let's reduce their indentation, and
make it easier to navigate to them. We deserve it.

Just doing this in its own PR, to minimize conflicts.

Release Notes:

- N/A
2023-06-13 10:50:49 -07:00
Max Brunsfeld
5b6d1a27ff Flatten worktree tests module structure 2023-06-13 10:31:29 -07:00
Max Brunsfeld
c17dbab6f1 Move worktree tests to their own file 2023-06-13 10:22:14 -07:00
Piotr Osiewicz
b272db9e21 editor: Add language setting for comment continuations (#2605)
Per @JosephTLyons request I've added a language setting for comment
continuations.

Release Notes:

- Added a language setting for comment continuations.
2023-06-13 18:59:46 +02:00
Max Brunsfeld
aedef7bc58 Remove panic when programatically updating an invalid setting (#2607)
Fixes
https://linear.app/zed-industries/issue/Z-2170/thread-main-panicked-at-could-not-deserialize-setting-type-welcomebase
Fixes
https://linear.app/zed-industries/issue/Z-2228/thread-main-panicked-at-could-not-deserialize-setting-type
2023-06-13 09:50:54 -07:00
Max Brunsfeld
1cd11bfe66 Remove panic when programatically updating an invalid setting 2023-06-13 09:43:57 -07:00
Antonio Scandurra
0db0a1ccef Skip merged messages when inserting new ones
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-13 16:06:50 +02:00
Antonio Scandurra
6e5de2fbbb Update blocks when cycling the message role
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-13 16:00:13 +02:00
Antonio Scandurra
00cede63a8 Show message headers again
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-13 15:55:52 +02:00
Antonio Scandurra
2842fc2b1d Merge messages whose header has been invalidated 2023-06-13 15:16:04 +02:00
Antonio Scandurra
2ae8b558b9 Get back to a compiling state with Buffer backing the assistant 2023-06-13 13:43:06 +02:00
Joseph T. Lyons
5e68dc5c92 Fix comments in settings files (#2604)
I accidentally added the information about folder-specific settings to
the user settings default file - just fixing that.

Release Notes:

- N/A
2023-06-13 04:06:53 -04:00
Joseph Lyons
5821bc4161 Fix comments in settings files 2023-06-13 04:03:51 -04:00
Max Brunsfeld
d8a2e176e6 Add support for HEEx templates in Elixir, fix a bug in handling nested language injections (#2603)
Closes https://linear.app/zed-industries/issue/Z-2211/heex-support

Release Notes:

- Added support for Elixir HEEx templates.
- Fixed a bug that caused incorrect syntax highlighting in ERB templates
([#1619](https://github.com/zed-industries/community/issues/1619)).
2023-06-12 17:49:07 -07:00
Max Brunsfeld
df76ab98cf Add Heex language 2023-06-12 17:32:20 -07:00
Max Brunsfeld
01bd5c30fc Fix syntax map bug with combined injections inside of other injections 2023-06-12 17:32:20 -07:00
Max Brunsfeld
ac1882b99b Move SyntaxMap tests to their own file 2023-06-12 16:24:17 -07:00
Max Brunsfeld
04e43899c0 Change action namespace to 'debug' for LSP log and syntax tree view 2023-06-12 15:54:36 -07:00
Max Brunsfeld
8542911eec Add a syntax tree view, for developing and debugging language support (#2601)
This PR adds a syntax tree view, which lets you view the syntax tree of
any layer in the active editor's `SyntaxMap`.

This view uses some new APIs that I added to Tree-sitter, which allow us
to efficiently render the syntax tree using a `UniformList`. Tree-sitter
PR: https://github.com/tree-sitter/tree-sitter/pull/2316

![Screen Shot 2023-06-12 at 3 33 36
PM](https://github.com/zed-industries/zed/assets/326587/2a27ee7b-bf29-4b3b-bfa8-fb47f97a2785)

Release Notes:

- Added a *syntax tree view* that shows Zed's internal syntax tree(s)
for the active editor. You can open it running the `debug: open syntax
tree view` command from the command palette.
2023-06-12 15:50:39 -07:00
Max Brunsfeld
018466171b Include field names in the syntax tree 2023-06-12 15:32:16 -07:00
Max Brunsfeld
594b9def20 Upgrade Tree-sitter 2023-06-12 15:31:59 -07:00
Max Brunsfeld
b4f3a88b38 Close the menu when selecting a different layer in the syntax tree view 2023-06-12 15:14:57 -07:00
Max Brunsfeld
56b749788f Improve the look of the syntax tree view UI 2023-06-12 15:14:57 -07:00
Max Brunsfeld
e969e3b028 Sync selection between syntax tree view and editor 2023-06-12 15:14:57 -07:00
Max Brunsfeld
086cfe57c5 Start work on a syntax tree view 2023-06-12 15:14:56 -07:00
Piotr Osiewicz
3d02f7ce5f Move plugins to feature-gate (#2599)
This PR:
- Reduces our dependency count for a release build by 20%
- Reduces our release compile time by about 30s (out of ~5 minutes)
2023-06-12 21:04:09 +02:00
Antonio Scandurra
7db690b713 WIP 2023-06-12 17:50:13 +02:00
Nate Butler
7f71ee0870 Expand tools doc further 2023-06-12 09:45:42 -04:00
Nate Butler
f142449014 Add back links 2023-06-12 09:02:49 -04:00
Nate Butler
57ae64d44a Use simple markdown 2023-06-12 09:00:59 -04:00
Joseph T. Lyons
db5bb4ec03 Do not rely on user's system time for telemetry events (#2597)
Some user's don't have their system clocks configured right and we are
seeing events 10 years into the future. I'm stripping out the code that
adds time via the client and am adding it in on zed.dev. We will lose a
little accuracy, as the time will be when the batch hits the server, but
I think its negligible (currently, batches send up every 30 seconds, at
the max) and worth it to protect our data from user's who wonkily dont
set care about their system time.

- https://github.com/zed-industries/zed.dev/pull/332

Release Notes:

- N/A
2023-06-09 18:08:11 -04:00
Joseph Lyons
0b3b732310 Do not rely on user's system time for telemetry events 2023-06-09 18:02:20 -04:00
Max Brunsfeld
56a4c2afae Bump tree-sitter-yaml to avoid submodule error during build 2023-06-09 14:58:31 -07:00
Joseph T. Lyons
d8c1ab9c68 Fix formatter comment in default settings (#2593)
Fix comment related to the `formatter` configuration option in Zed's
default settings.

Release Notes:

- N/A
2023-06-09 15:14:04 -04:00
Max Brunsfeld
c1f1ee6b05 collab 0.14.2 2023-06-09 11:50:35 -07:00
Max Brunsfeld
b2bdca4779 Bump protocol version 2023-06-09 11:50:15 -07:00
Max Brunsfeld
097632467d collab 0.14.1 2023-06-09 11:37:47 -07:00
Max Brunsfeld
87efd25d42 Avoid dropping the worktree_repository_statuses table for now 2023-06-09 11:37:39 -07:00
Max Brunsfeld
bb65d75798 Enable rust backtraces on kubernetes 2023-06-09 11:15:21 -07:00
Max Brunsfeld
9cbb63d374 collab 0.14.0 2023-06-09 11:12:45 -07:00
Piotr Osiewicz
5bef2f1778 Z-2303 editor: fix comment extension on the start of line (#2595)
Release Notes:

- Improved comment extension for cases where cursor was placed before
start of comment marker
2023-06-09 20:02:51 +02:00
Max Brunsfeld
fb83ab8e9f Add outline support for Ruby singleton methods (#2592)
This pull request add support for Ruby singleton methods in the document
outline.

**Before**

<img width="1490" alt="outline-before"
src="https://github.com/zed-industries/zed/assets/503025/e90afd4c-bcdd-477b-92cf-fe34cec54980">

**After**

<img width="1493" alt="outline-after"
src="https://github.com/zed-industries/zed/assets/503025/067cc80b-7718-4395-9079-4677a689b9be">

Release Notes:

- Added support for singleton methods in Ruby outline view. Fixed
[#1442](https://github.com/zed-industries/community/issues/1442).
2023-06-09 08:35:34 -07:00
Max Brunsfeld
738b06a778 Update JavaScript outline queries (#2596)
Add support for exported const declarations in the outline view. Did not
include more context (e.g., whether or not the it's exported or not)
because we're not doing that for TypeScript right now. We could do that
in a follow up pull request, addressing both JavaScript and TypeScript
at once.


**Before**

<img width="1186" alt="outline-before"
src="https://github.com/zed-industries/zed/assets/503025/e72b0ed4-52b0-4a7c-af70-b2f4758e3554">

**After**

<img width="1186" alt="outline-after"
src="https://github.com/zed-industries/zed/assets/503025/cd91bc7f-8ce3-485f-9b33-d487ad6c718d">


Release Notes:

- Added support for exported const declarations in the JavaScript
outline view. Fixed
[#1399](https://github.com/zed-industries/community/issues/1399).
2023-06-09 08:34:14 -07:00
Kevin Hovsäter
4213cc013c Update JavaScript outline queries
Add support for exported const declarations in the outline view.
2023-06-09 15:33:27 +02:00
Kevin Hovsäter
6ce3f3bf27 Refactor toggle comment tests 2023-06-09 13:22:04 +02:00
Kevin Hovsäter
a8d43c6d71 Toggle comments for empty single line selections 2023-06-09 10:10:49 +02:00
Kevin Hovsäter
7deddd1149 Fix formatter comment in default settings 2023-06-09 08:08:07 +02:00
Kevin Hovsäter
57ff173e29 Add outline support for Ruby singleton methods 2023-06-09 07:31:09 +02:00
Nate Butler
85b049f250 Build tokens for each theme (#2590)
This PR adds the ability to export tokens for each theme. 

You can export tokens by:

1. `cd styles`
2. `npm run build-tokens`
3. Tokens will be output in the target folder (`styles/target`)

The tokens match the ColorScheme object. In the future we may also
export tokens for our styleTrees.

Release Notes:

- N/A (No public facing changes)

---
TODO:

- [x] Generate Token Studio theme index file
- [x] ColorScheme
    - [x] name:
    - [x] isLight
    - [x] lowest
    - [x] middle
    - [x] highest
    - [x] popoverShadow
    - [x] modalShadow
    - [x] players
    - [x] syntax
2023-06-08 16:40:46 -04:00
Nate Butler
ddcbc774ab $tokens.json => $themes.json 2023-06-08 16:36:15 -04:00
Nate Butler
e4cbc29f98 Update buildTokens to export $metadata.json and $themes.json 2023-06-08 16:19:28 -04:00
Max Brunsfeld
6304897abc Make the LSP log view searchable (#2591)
Also, I noticed errors in the logs of the Elixir LSP that we were
sending `goToTypeDefinition` requests, which that server does not
support. We now respect that server capability.

Release Notes:

- N/A
2023-06-08 13:15:05 -07:00
Nate Butler
3719c206c9 Only export color tokens for syntax 2023-06-08 15:14:59 -04:00
Nate Butler
91e1bb8fd4 WIP syntax tokens 2023-06-08 15:12:11 -04:00
Max Brunsfeld
a7f06f962b Enable type definition capability in fake LSPs in tests 2023-06-08 12:09:37 -07:00
Max Brunsfeld
e0dd9e4185 Make the LSP log view searchable 2023-06-08 11:33:20 -07:00
Max Brunsfeld
11dbbcc9dd Respect LSP goToTypeDefinition server capability 2023-06-08 11:33:09 -07:00
Nate Butler
999b2365a8 Export additional tokens, standardize naming convention 2023-06-08 12:21:51 -04:00
Max Brunsfeld
e3f319467a Enhance LSP log viewer to show server logs in addition to RPC trace (#2586)
In debugging what's going on with the Elixir language server, there was
some interesting content in the server's logs (sent to the app via the
`window/logMessage` LSP endpoint). I decided to invest in making
language server issues easier to debug by exposing these `logMessage`
contents in the app.

Also, improve the UI of the view slightly:

* Select one of the servers by default (instead of "no server selected")
* Make it clearer that the menu is clickable
2023-06-08 09:03:52 -07:00
Max Brunsfeld
908de23b72 Tweak LSP log menu styling 2023-06-08 08:58:57 -07:00
Max Brunsfeld
afaff7f9a9 Merge branch 'main' into lsp-log-messages 2023-06-08 08:49:08 -07:00
Max Brunsfeld
817644eb20 Style new lsp log menu, add a test for it 2023-06-08 08:43:25 -07:00
Nate Butler
e2f46d5448 Build layer tokens for each theme 2023-06-08 11:15:59 -04:00
Nate Butler
16e3e04501 Update TSConfig (#2589)
Based on #2558. Also fixes errors resulting from the stricter options.

Release Notes:

- N/A (No public facing changes)
2023-06-08 01:31:21 -04:00
Nate Butler
1e43fec1c5 Update buildLicenses to only include the theme url if there is one 2023-06-08 01:23:19 -04:00
Nate Butler
e996a66596 Update TSCondif
Based on #2558. Also fixes errors resulting from the stricter options.
2023-06-08 01:15:57 -04:00
Nate Butler
a75e9faa83 Add foundation for exporting tokens from our color schemes (#2588)
We removed our Figma Tokens/Tokens Studio export a while back when we
moved to the theme to ColorSchemes. I'd like to get back to exporting
these so we can be working from up to date themes in Figma, especially
with the large amount of UI design work we'll be doing in the next few
weeks on channels.

This PR adds some basic plumbing to start working through the
theme/colorScheme and export the parts as tokens.

I also discovered that Tokens Studio now publishes their types, so we
can use them directly rather than writing our own:
https://github.com/tokens-studio/types

Pulled those in and started connecting them as well.

Running `npm run build-tokens` will export the tokens for each theme to
`styles/target/tokens`.

Currently only a few element's tokens are exported, will expand this
further as time permits.

Release Notes:

- N/A (No public facing changes)
2023-06-08 00:53:16 -04:00
Nate Butler
c8a9d73ea6 Add foundation for exporting tokens 2023-06-08 00:37:00 -04:00
Nate Butler
d4192fc3e9 Organize misc files into theme, themes and styleTrees (#2587)
We've accumulated a number of miscellaneous folders in the styles
folder. Simplifying these down to `theme`, `themes` and `styleTrees`.

Release Notes:

- N/A (No public facing changes)
2023-06-07 22:47:48 -04:00
Mikayla Maki
8216d26a7a Switch to attaching git statuses to their associated entries (#2571)
This rewrites and simplifies the git status system by attaching the git
status to each individual entry. This also improves the git testing
infrastructure to cover more cases and be more accurate to how file
events actually occur. This also fixes several other bugs in the worktree and the buffer, and stops any randomly generated actions from happening inside a `.git` folder. Hopefully, we can undo this change soon once our randomized testing is more robust.

Release Notes:

- Will require a DB migration

TODO:
- [x] Pass randomized tests
- [x] Get ready for merging
2023-06-07 17:11:43 -07:00
Mikayla Maki
fc1f8c5657 Fixed ci 2023-06-07 16:58:55 -07:00
Mikayla Maki
8ca1a7d43d add scan_complete await 2023-06-07 16:51:54 -07:00
Max Brunsfeld
66f215cd13 Restructure LSP log view to show log messages in addition to RPC trace 2023-06-07 16:51:09 -07:00
Mikayla Maki
9e9d8e3a7b add mroe dbg 2023-06-07 16:50:15 -07:00
Mikayla Maki
9d58c4526d Fix warning 2023-06-07 16:45:36 -07:00
Mikayla Maki
5f143f689f Attempting to debug on ci... 2023-06-07 16:44:13 -07:00
Mikayla Maki
572d40381a Add track caller 2023-06-07 16:39:10 -07:00
Mikayla Maki
2c5e83bf72 Fixed a bug where buffer saved clocks would desynchronize in rare execution paths
co-authored-by: Max <max@zed.dev>
2023-06-07 16:10:45 -07:00
Max Brunsfeld
78f9642ac2 Emit project event when a language server sends a logMessage notification 2023-06-07 14:46:08 -07:00
Mikayla Maki
cd63ec2c7f fmt 2023-06-07 14:20:01 -07:00
Mikayla Maki
03a96d2793 Feature gate import 2023-06-07 14:15:20 -07:00
Mikayla Maki
0ac7a3bc21 fmt 2023-06-07 14:13:57 -07:00
Mikayla Maki
28ba27c9c5 Merge branch 'main' into stream-git-statuses 2023-06-07 14:12:58 -07:00
Mikayla Maki
34e134fafb Fix several randomized test failures with the new git status implementation 2023-06-07 14:10:17 -07:00
Max Brunsfeld
351e4863cd Fix some issues found when testing Elixir-LS (#2583)
Closes
https://linear.app/zed-industries/issue/Z-2209/popovers-dont-always-have-syntax-highlighted-code
Closes
https://linear.app/zed-industries/issue/Z-2206/highlight-syntax-in-hover-docs

* Fix a ton of errors in our logs due to us not recognizing that
`elixir-ls` does not support code actions.
* Syntax-highlight elixir code blocks in hover popovers
2023-06-07 12:21:48 -07:00
Max Brunsfeld
11ab1a8cc6 collab 0.13.1 2023-06-07 10:54:27 -07:00
Max Brunsfeld
be8d268eb9 Bump tree-sitter-yaml submodule to avoid broken child submodule 2023-06-07 10:50:03 -07:00
Max Brunsfeld
72372ddf0e Highlight hover code blocks as LSP's own language by default
The elixir LSP does not include a language name on its elixir code blocks.
2023-06-07 10:01:19 -07:00
Nate Butler
86ec43c908 Update createColorScheme to accept ThemeConfig (#2557)
- Added ThemeConfig
- Updated themes use ThemeConfig
- Refactoring
- Fixed TypeScript errors

Release Notes:
- N/A (No public facing changes)
2023-06-07 12:57:34 -04:00
Nate Butler
29de420b59 Organize misc files into theme, themes and styleTrees 2023-06-07 12:50:37 -04:00
Nate Butler
6269cec4f1 Minor updates 2023-06-07 12:40:49 -04:00
Max Brunsfeld
6067575e38 collab 0.13.0 2023-06-07 09:29:58 -07:00
Max Brunsfeld
f56d642b88 Disable scheduled randomized test workflow for now 2023-06-07 09:20:25 -07:00
Joseph Lyons
8882b22c9c v0.91.x dev 2023-06-07 11:57:41 -04:00
Piotr Osiewicz
e94129446d Z 1618/extend comments (#2585)
Fixes Z-1618. In the current state, this only works for line comments
such as `//` (and whatever's set in `{language}.toml` as a
line_comment).

Release Notes:

- Comments are now extended on new line.
2023-06-07 17:37:02 +02:00
Sergii Onufriienko
b1f009cdce Merge branch 'main' into sergey/z-1768-update-createcolorscheme-to-accept-a-theme-in-the 2023-06-07 16:12:13 +01:00
Sergey Onufrienko
4c405e65a3 feat: use theme index to build licenses 2023-06-07 16:10:16 +01:00
Sergey Onufrienko
0ad76ac92c feat: use theme index to build themes 2023-06-07 16:10:02 +01:00
Sergey Onufrienko
02c1efc60d feat: re-export chroma 2023-06-07 16:09:24 +01:00
Sergey Onufrienko
f5d1f314e0 feat: add themes index 2023-06-07 16:08:58 +01:00
Sergey Onufrienko
d2b8501347 feat: change themes to return ThemeConfig 2023-06-07 16:08:48 +01:00
Antonio Scandurra
d5441ba386 Introduce an Assistant Panel (#2584)
This pull request introduces a new assistant panel to Zed that lets
users interact with OpenAI using their API key:

![CleanShot 2023-06-07 at 09 39
10@2x](https://github.com/zed-industries/zed/assets/482957/ba2a5830-9aeb-4c45-a182-a44d6a72675f)

After setting the key up, it will be saved to the keychain and
automatically loaded the next time the assistant panel is opened. The
key can be reset using `assistant: reset key`.

![CleanShot 2023-06-07 at 09 39
23@2x](https://github.com/zed-industries/zed/assets/482957/a6808bb0-0098-45ae-a2e3-f4d88472e626)

From there, users can type messages in a multi-buffer and hit
`cmd-enter` (`assistant: assist`) to stream assistant responses using
the OpenAI API. Responses can be canceled by hitting `escape`.

![CleanShot 2023-06-07 at 09 40
16@2x](https://github.com/zed-industries/zed/assets/482957/749779da-850e-4ad5-af04-74a3ca39f7ad)

Users can quote a selection from the active editor by hitting `cmd->`
(`assistant: quote selection`), which will embed the selected piece of
text in a Markdown fenced code block. Conversations with the assistant
are ephemeral at the moment, but can be easily copy/pasted:

![CleanShot 2023-06-07 at 09 50
33@2x](https://github.com/zed-industries/zed/assets/482957/b3386c10-4c51-4419-a0e0-517112ef6521)

Release Notes:

- Added a new assistant panel feature that enables interacting with
OpenAI using an API key. This replaces the previous experimental `ai:
assist` command that would work on any buffer. The experience is similar
to the one offered by ChatGPT with the added ability to edit, delete or
enhance previous messages. When hitting `cmd-enter`, the assistant will
start streaming responses from OpenAI. A response stream can be canceled
using `escape`. Moreover, the active editor's selection can be quoted in
the assistant panel using `cmd->`, which will automatically embed the
selected piece of text in a Markdown fenced code block.
2023-06-07 16:05:57 +02:00
Antonio Scandurra
d3e0d38bef Fix assistant panel tests 2023-06-07 15:24:08 +02:00
Antonio Scandurra
d26cc2c897 Maintain scroll bottom when streaming assistant responses 2023-06-07 15:01:50 +02:00
Antonio Scandurra
43500dbf60 Fix zed tests 2023-06-07 10:02:35 +02:00
Antonio Scandurra
0dae8f2dd8 Merge remote-tracking branch 'origin/main' into assistant-2 2023-06-07 09:34:16 +02:00
Antonio Scandurra
a6feaf1300 Allow search assistant editors 2023-06-07 09:24:18 +02:00
Joseph Lyons
c93b6cc599 Tweak comment wording 2023-06-07 00:23:58 -04:00
Max Brunsfeld
e8479f23f9 Improve log message on invalid LSP message from server's stdout 2023-06-06 17:13:59 -07:00
Max Brunsfeld
6f2726524e Remove stray println 2023-06-06 17:13:59 -07:00
Max Brunsfeld
fccbac4887 Handle LSP codeActions capability set to false 2023-06-06 17:13:59 -07:00
Max Brunsfeld
0d90c6d02e Improve syntax highlighting and outline view for Elixir (#2582)
Fixes
https://linear.app/zed-industries/issue/Z-2208/outline-view-doesnt-differentiate-between-overloaded-functions
Fixes
https://linear.app/zed-industries/issue/Z-2205/elixir-syntax-highlighting-not-working-properly-for-doc-attributes-and

This PR improves syntax highlighting and outline view in Elixir. It's
common to overload elixir functions, with many different versions of the
function for different patterns of parameters, so I updated the outline
view to show functions' parameters in Elixir. But if we showed functions
the same way in the *breadcrumbs*, it would take up too much space.

So I added a new capture in languages' `outline` queries called
`@context.extra`, which is included in the outline view, but not in
breadcrumbs.

Release Notes:

- Improved syntax highlighting of doc attributes and special macros in
Elixir
- Updated the outline view in Elixir to display function parameters, to
allow differentiating between function overloads.
2023-06-06 16:34:33 -07:00
Max Brunsfeld
4b9a3c66e6 Show function parameters in elixir outline view
Introduce a new capture in the outline query called 'context.extra', which
causes text to appear in the outline, but not in the breadcrumbs.
2023-06-06 16:27:05 -07:00
Max Brunsfeld
7aeaa84657 Fix the order of some patterns in elixir highlight query 2023-06-06 16:06:16 -07:00
Max Brunsfeld
8dc679e74e Upgrade tree-sitter-elixir 2023-06-06 16:06:16 -07:00
Max Brunsfeld
6737ee1495 Avoid panic when failing to load a language's queries 2023-06-06 16:06:16 -07:00
Piotr Osiewicz
cc63d3d048 chore: add targets to rust-toolchain.toml (#2581) 2023-06-07 00:12:47 +02:00
Max Brunsfeld
a9f865d828 Don't apply the 'literal' color to entire markdown code blocks (#2579)
### Before

![Screen Shot 2023-06-06 at 2 32 46
PM](https://github.com/zed-industries/zed/assets/326587/036f1da3-19c6-4979-adc6-c3133f8d63dc)

### After

![Screen Shot 2023-06-06 at 2 32 08
PM](https://github.com/zed-industries/zed/assets/326587/6d4729db-b76a-4052-847e-fd9de346f194)

Release Notes:

* Improved the syntax highlighting of fenced code blocks in markdown.
2023-06-06 14:54:24 -07:00
Piotr Osiewicz
dfd72770e7 chore: bump MSRV to 1.70, add rust-toolchain (#2580)
This time I've added a `components` section to rust-toolchain.toml file
to explicitly require a rustfmt component. Fingers crossed.
2023-06-06 23:49:34 +02:00
Max Brunsfeld
3fc2e0754b Don't apply the 'literal' color to entire markdown code blocks 2023-06-06 14:34:45 -07:00
Max Brunsfeld
bdd3e77e02 Live-reload tree-sitter queries in development (#2578)
This PR adds live reloading of Tree-sitter queries when running in debug
mode, similar to what we do for the themes. This way, you can change a
highlighting query or an outline query, and immediately see the result
in the app.

Release Notes:

- N/A
2023-06-06 14:30:28 -07:00
Max Brunsfeld
7bfb51ee76 Live-reload tree-sitter queries in development 2023-06-06 14:20:21 -07:00
Piotr Osiewicz
559a58d737 Revert "chore: bump MSRV to 1.70" (#2577)
Reverts zed-industries/zed#2573
2023-06-06 22:21:34 +02:00
Piotr Osiewicz
c1c91dc2e3 chore: bump MSRV to 1.70 (#2573)
Add rust-toolchain.toml

Release Notes:

- N/A
2023-06-06 21:50:27 +02:00
Piotr Osiewicz
572c59eec4 chore: Enable full LTO (#2548)
Per conversation with Antonio, I've suggested enabling full LTO; right
now we use a crate-local ThinLTO, which does not inline function calls
across crates.

| Configuration    | Current main (788f97e) | Thin LTO  | Full LTO  |
|------------------|------------------------|-----------|-----------|
| Size in bytes    | 158806721              | 155868753 | 111115553 |
| % of `main` size | 100%                   | 98.14%    | 69.96%    |
| Size in bytes (no debug info) | 129186657 | 127942929 | 108281345 |
2023-06-06 21:50:08 +02:00
Max Brunsfeld
17560cc5b0 Eliminate assets crate (#2575)
Previously, all static assets were embedded into Zed using a single
`RustEmbed` struct called `Assets`, which was in a crate of its own,
also called `assets`. Many crates depended on this crate. The problem
with this situation was that changing *any* static asset file caused
almost every crate in the codebase to need recompilation (because of
recursive dependencies on `assets`).

Now, most of the assets are embedded only into the top-level `zed`
crate. A few assets (such as settings JSON files and AI prompts) are
needed in lower-level crates such as `settings` and `ai`. For these,
I've created separate `RustEmbed` structs in those specific crates,
which embed those specific sub-folders of `assets`.

The result is that now, when you change a theme file, the only crate
that needs to recompile is `zed`.

Release Notes:

- N/A
2023-06-06 12:00:44 -07:00
Max Brunsfeld
dbbd0558c3 Eliminate assets crate 2023-06-06 11:46:46 -07:00
Joseph Lyons
2003d3dbe4 Update initial_user_settings.json 2023-06-06 14:39:56 -04:00
Antonio Scandurra
7a78e64831 Merge remote-tracking branch 'origin/main' into assistant-2 2023-06-06 19:18:22 +02:00
Antonio Scandurra
16090c35ae Insert reply after assistant message when hitting cmd-enter
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-06-06 19:15:06 +02:00
Antonio Scandurra
ef7ec265c8 Cycle message roles on click
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-06-06 18:45:08 +02:00
Max Brunsfeld
53906fd3da Add an application menu item for creating/opening a project-specific settings file (#2572)
Previously, project-specific settings were not discoverable. This PR
adds a `Zed > Preferences > Local Settings` application menu command
that creates a `.zed/settings.json` at the root of your current
worktree. This command works in both local and remote projects.

Limitations:

* Currently, if you have an empty project open, the command just shows a
notification that there are no folders open.
* The JSON-schema-based autocomplete is the same in local settings files
as in your main settings, even though not all settings can be locally
customized.

Release Notes:

- Added an application menu command - `Zed > Preferences > Local
Settings` for creating a folder-specific settings file.
2023-06-06 09:27:36 -07:00
Antonio Scandurra
ac7178068f Include message headers in copied assistant text
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-06-06 18:18:04 +02:00
Max Brunsfeld
cfcfc3bf6b Show notification when attempting to open local settings in a project w/ no folders 2023-06-06 09:03:57 -07:00
Antonio Scandurra
093ce8a9ac Simplify prompt
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-06-06 17:45:58 +02:00
Nate Butler
7b066df7e6 Tighten up spacing in the project panel (#2574)
Following https://github.com/zed-industries/zed/pull/2559 the project
panel entries become pretty wide again. This PR tries to mitigate that
and just make some general improvements to visual density in the project
panel.

- Reduces padding around items
- Removes top margin
- Slightly reduces the height of each item
- Fixes an issue where ignored files had the wrong color chevron

Release Notes:

- Improved density of the project panel and tidied up some visual
issues.
2023-06-06 11:31:53 -04:00
Piotr Osiewicz
a0e2e5db7d project panel/styles: Align child's chevron with parent path (#2559)
Z-1012

Release notes:
- Adjust indent of files in subdirectories.
2023-06-06 16:55:38 +02:00
Antonio Scandurra
2b1aeb07bc Show error message when requests to OpenAI fail
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-06 16:53:56 +02:00
Antonio Scandurra
9c59146026 Set assistant editor's title based on the first question/answer pair
Co-Authored-By: Julia Risley <julia@zed.dev>
2023-06-06 15:59:10 +02:00
Antonio Scandurra
69b8267b6b Show the current model and allow clicking on it to change it 2023-06-06 13:13:44 +02:00
Antonio Scandurra
ada222078c Insert a user reply when hitting cmd-enter in an assistant message 2023-06-06 13:13:41 +02:00
Antonio Scandurra
f4f060667e Add assertion to pinpoint how deletion works across excerpts 2023-06-06 12:36:26 +02:00
Antonio Scandurra
337dda8e3a Only remove excerpts when an edit touches them 2023-06-06 12:27:05 +02:00
Antonio Scandurra
8032324470 Prevent moving across excerpts on Editor::delete 2023-06-06 12:26:53 +02:00
Antonio Scandurra
e46d1549d6 Retain selection's head (as opposed to its end) on insertion
This makes a difference when an edit spans two excerpts and the selection
start won't necessarily be the same as the selection end after the edit.
2023-06-06 10:12:15 +02:00
Nathan Sobo
23836eb251 Not working yet: Remove empty messages unless they contain the cursor
Problem is, I'm trying to trust the excerpt id of the selection head,
but it's a sentinel value and not the actual excerpt id of the message.
I think we probably need to resolve to offsets instead.
2023-06-05 22:58:08 -06:00
Max Brunsfeld
296a0bf510 Populate created local settings file with an empty JSON object and comments 2023-06-05 18:20:02 -07:00
Max Brunsfeld
cb975f1252 Add Zed > Preferences > Local Settings to application menu 2023-06-05 17:45:42 -07:00
Max Brunsfeld
0949ee84d8 🎨 Move OpenSettings action back to the zed crate 2023-06-05 17:43:05 -07:00
Mikayla Maki
a2d58068a7 Improve test generation and implement status propogation
co-authored-by: max <max@zed.dev>
2023-06-05 17:30:12 -07:00
Mikayla Maki
c12bdc894a Silence not found errors 2023-06-05 15:19:59 -07:00
Max Brunsfeld
398b0f303c Apply bounds to all windows when using start-local-collaboration script (#2570)
The `start-local-collaboration` script opens two instances of Zed, each
logged in as a different user, and each one taking up half of the
screen. But previously, when joining a remote project as one of the
collaborators, that newly-opened window would be full screen.

Now, each instance of Zed keeps *all* of its windows on its half of the
screen. This is implemented by respecting the `ZED_WINDOW_{SIZE,BOUNDS}`
env vars, even when joining *remote* projects.

Release Notes:

- N/A
2023-06-05 14:49:39 -07:00
Max Brunsfeld
3d1ba1b363 Apply bounds to all windows when using start-local-collaboration script 2023-06-05 14:36:21 -07:00
Max Brunsfeld
571151173c Fix copilot state corruption when there are multiple buffers with the same remote id in different projects (#2569)
Fixes
https://linear.app/zed-industries/issue/Z-1511/thread-main-panicked-at-assertion-failed-left-==-right-left-local-0-1

Previously, when exchanging messages about buffers with a copilot
language server, we identified buffers using their **remote id**. This
caused problems when there were multiple projects open, where one or
more were remote, because buffers' remote ids are only unique within a
given project.

When you have multiple projects open, and one or more of the projects is
remote, it's pretty easy to have two buffers open with the same remote
id. In my testing, when this happened, copilot would stop working in
both buffers. But I believe that depending on the editing patterns that
occur in the two buffers, it could cause the crash reported in the
Linear issue above.

This PR changes our copilot logic to use buffers' local handle ids for
identifying them. This fixed the problems I was able to reproduce when
using copilot in both remote and local projects.

Release Notes:

- Fixed a crash that would sometimes occur when editing buffers after
having collaborated on a remote project.
2023-06-05 14:33:56 -07:00
Max Brunsfeld
12dd91c89c Use local ids, not remote ids, to identify buffers to copilot 2023-06-05 14:12:19 -07:00
Mikayla Maki
5e4da6433f Add file and line number information to logs (#2568)
This PR adds codegen from rustc to track the file and line number of
calls to `log_err()`. I haven't noticed much longer compile times on my
machine, and looking at the
[implementation](https://rustc-dev-guide.rust-lang.org/backend/implicit-caller-location.html)
it essentially adds an extra argument and secret reference pass.
However, this will show a lot more data in our logs on user machines.
Requesting review from @ForLoveOfCats, who usually knows a bunch about
this kind of thing :)
2023-06-05 13:24:07 -07:00
Mikayla Maki
624467ebca Add file and line number information to logs 2023-06-05 12:53:37 -07:00
Mikayla Maki
9a13a2ba2c WIP: Add status bubbling to project panel 2023-06-05 12:53:04 -07:00
Joseph T. Lyons
7fbafc8030 Remove code sending zed events to mixpanel (#2567)
Release Notes:

- N/A
2023-06-05 15:40:27 -04:00
Joseph Lyons
311074e397 Remove code sending zed events to mixpanel 2023-06-05 15:33:17 -04:00
Mikayla Maki
49c5a3fa86 Add postgres migration 2023-06-05 12:17:21 -07:00
Joseph T. Lyons
2190a27dff Stop sending editor events to mixpanel (#2566)
Release Notes:

- N/A
2023-06-05 15:15:30 -04:00
Joseph Lyons
70c5489c13 Stop sending editor events to mixpanel 2023-06-05 15:09:47 -04:00
Mikayla Maki
4ac5f7b14e Add statuses to test integration 2023-06-05 12:06:23 -07:00
Mikayla Maki
e56fcd69b5 Track git status changes with the changed_paths system 2023-06-05 11:50:23 -07:00
Joseph T. Lyons
4f3165692f Move app version into panic object (#2565)
Final tweaks to clean up how we structure our panic data

Release Notes:

- N/A
2023-06-05 14:40:28 -04:00
Joseph Lyons
917d8949b7 Move app version into panic object 2023-06-05 14:17:48 -04:00
Julia
6b89243902 Update rust-bindgen dev-dependencies (#2563)
I did \*something\* Friday afternoon which changed something about my
system SDK to break the `media` crate's bindings generation. Some of the
types and consts were not being generated despite being able to prove
that they exist in the source, such as when running the header through
the preprocessor myself and feeding that through the generator. Updating
my OS, XCode, command line tools, and reinstalling Rust as well as
working from fresh clones of the repo had no effect.

Updating rust-bindgen resolved the issue and downgrading the version
back to the original version caused the issue to reappear. I'm still not
sure what happened to change the SDK but at this point with being able
to build the project again I'm not going to look a gift horse in the
mouth.

Release Notes:

- N/A
2023-06-05 09:21:07 -04:00
Antonio Scandurra
0ed8bbc818 Query rect_for_text_range on focused view instead of root element (#2564)
This was causing IME input to be drawn in the wrong place when there
were splits or panels in the window.

Release Notes:

- Fixed a bug that was causing IME input to sometimes be rendered in the
wrong position.
2023-06-05 12:11:35 +02:00
Antonio Scandurra
c872f581d1 Query rect_for_text_range on focused view instead of root element
This was causing IME input to be drawn in the wrong place when there
were splits or panels in the window.
2023-06-05 12:00:40 +02:00
Antonio Scandurra
bef6932da7 Avoid accidentally taking the api_key when requesting an assist 2023-06-05 11:25:21 +02:00
Julia
5790d6993e Update rust-bindgen dev-dependencies 2023-06-04 23:34:22 -04:00
Joseph T. Lyons
a89f3ed445 Fix typos (#2562)
Quite literally just ran `typos --write-changes` from
https://crates.io/crates/typos. Its pretty impressive - wonder if we
should run it on CI.

Release Notes:

- N/A
2023-06-02 22:08:45 -04:00
Joseph Lyons
7c60f636d5 Fix typos 2023-06-02 22:02:19 -04:00
Mikayla Maki
ca077408d7 Fix bug where git statuses would not be initialized on startup
move git status queries to be on entry creation

co-authored-by: max <max@zed.dev>
2023-06-02 17:38:39 -07:00
Mikayla Maki
2f97c7a4f1 Remove stale comments
Implement status bubbling query with sum tree traversals

co-authored-by: max <max@zed.dev>
2023-06-02 16:41:01 -07:00
Mikayla Maki
e377459948 Remove stateful bubbling
co-authored-by: max <max@zed.dev>
2023-06-02 15:07:49 -07:00
Mikayla Maki
99a0e11e70 Abandoning stateful bubbling approach
co-authored-by: max <max@zed.dev>
2023-06-02 14:51:40 -07:00
Max Brunsfeld
46d2cbaa4c Improve panic reports (#2560)
* Add an `identifying_backtrace` field that only contains symbols in
*our* own codebase, which can be used for better deduplication.
* In the main backtrace, include file and line numbers for all symbols
in our codebase
* Exclude any stack frames within the panic handling/hooking system
itself, so that the top line of the backtrace is where the panic
originated in our codebase.

This should improve our panic deduplication, and also make panic reports
a bit more readable.

example:

```
{
  "thread": "main",
  "payload": "wtf",
  "location_data": {
    "file": "crates/zed/src/zed.rs",
    "line": 459
  },
  "backtrace": [
    "zed::open_log_file::{{closure}}::{{closure}}::{{closure}}",
    "    crates/zed/src/zed.rs:459",
    "gpui::app::AppContext::spawn_internal::{{closure}}",
    "    crates/gpui/src/app.rs:2073",
    "gpui::executor::any_local_future::{{closure}}",
    "    crates/gpui/src/executor.rs:1026",
    "<core::pin::Pin<P> as core::future::future::Future>::poll",
    "<async_task::runnable::spawn_local::Checked<F> as core::future::future::Future>::poll",
    "async_task::raw::RawTask<F,T,S>::run",
    "async_task::runnable::Runnable::run",
    "<gpui::platform::mac::dispatcher::Dispatcher as gpui::platform::Dispatcher>::run_on_main_thread::trampoline",
    "    crates/gpui/src/platform/mac/dispatcher.rs:40",
    "<() as objc::message::MessageArguments>::invoke",
    "objc::message::platform::send_unverified",
    "objc::message::send_message",
    "<gpui::platform::mac::platform::MacForegroundPlatform as gpui::platform::ForegroundPlatform>::run",
    "    crates/gpui/src/platform/mac/platform.rs:366",
    "gpui::app::App::run",
    "    crates/gpui/src/app.rs:251",
    "Zed::main",
    "    crates/zed/src/main.rs:118",
    "core::ops::function::FnOnce::call_once",
    "std::sys_common::backtrace::__rust_begin_short_backtrace",
    "std::rt::lang_start::{{closure}}",
    "core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once",
    "std::rt::lang_start"
  ],
  "release_channel": "dev",
  "os_name": "macOS",
  "os_version": "12.6.1",
  "architecture": "aarch64",
  "panicked_on": 1685734744050,
  "identifying_backtrace": [
    "zed::open_log_file::{{closure}}::{{closure}}::{{closure}}",
    "gpui::app::AppContext::spawn_internal::{{closure}}",
    "gpui::executor::any_local_future::{{closure}}",
    "<gpui::platform::mac::dispatcher::Dispatcher as gpui::platform::Dispatcher>::run_on_main_thread::trampoline",
    "<gpui::platform::mac::platform::MacForegroundPlatform as gpui::platform::ForegroundPlatform>::run",
    "gpui::app::App::run",
    "Zed::main"
  ]
}
```

Release Notes:

N/A
2023-06-02 13:11:57 -07:00
Max Brunsfeld
33c4c32196 Avoid writing spurious nulls to the settings file when updating it programatically (#2561)
Thanks for catching this @iamnbutler.
2023-06-02 13:11:35 -07:00
Max Brunsfeld
7417835f06 Avoid writing spurious nulls to settings file when updating it programatically 2023-06-02 13:02:17 -07:00
Max Brunsfeld
f6a4706410 Improve panic reports
* Add an 'identifying_backtrace' field that only contains symbols in our
  codebase, which can be used for better deduplication.
* In the main backtrace, include file and line numbers for all symbols
  in our codebase
2023-06-02 12:37:39 -07:00
Joseph T. Lyons
00265c19a0 Add additional panic information to panic events (#2555)
Adds the following to the panic event

release_channel
os_name
os_version
architecture

Merge first: https://github.com/zed-industries/zed.dev/pull/322

Release Notes:

- N/A
2023-06-02 14:30:16 -04:00
Piotr Osiewicz
345fad3e9d editor: add select previous command (#2556)
Added a `select previous` command to complement `select next`.
Release Notes:

- Added "Select previous" editor command, mirroring `Select next`.
Ticket number: Z-366
2023-06-02 17:32:34 +02:00
Antonio Scandurra
f00f16fe37 Show remaining tokens 2023-06-02 17:27:21 +02:00
Sergey Onufrienko
f97999d97f feat: update themes to use ThemeConfig 2023-06-02 14:11:11 +02:00
Sergey Onufrienko
5fbbc1936f fix: typescript errors 2023-06-02 14:10:44 +02:00
Sergey Onufrienko
b38f760fcd feat: add index export 2023-06-02 14:10:19 +02:00
Sergey Onufrienko
d3ed958308 chore: make SyntaxHighlightStyle.color optional 2023-06-02 14:10:02 +02:00
Sergey Onufrienko
6b00db75ad feat: update and refactor colorScheme 2023-06-02 14:08:35 +02:00
Sergey Onufrienko
56ecfaf2f0 feat: add themeConfig types 2023-06-02 14:07:43 +02:00
Antonio Scandurra
3750e64d9f Save OpenAI API key in the keychain 2023-06-02 12:15:25 +02:00
Piotr Osiewicz
571d2f4966 Z-1074/search default label (#2547)
This commit adds a default "Project search" tooltip for empty search
panes. Fixes Linear ticket Z-1074
Release Notes:

- Added default tooltip to empty search panes.
([#1533](https://github.com/zed-industries/community/issues/1533)).
2023-06-02 11:42:40 +02:00
Antonio Scandurra
a81d164ea6 Allow saving the OpenAI API key in the assistant panel 2023-06-02 11:38:02 +02:00
Antonio Scandurra
d0aff65b1c Allow moving the assistant panel to other docks 2023-06-02 10:55:19 +02:00
Antonio Scandurra
55c8c6d3fb Allow adding new contexts 2023-06-02 10:29:22 +02:00
Antonio Scandurra
cf934ab696 Fix compile errors 2023-06-02 10:08:06 +02:00
Antonio Scandurra
20e65a533c Merge remote-tracking branch 'origin/main' into assistant-2 2023-06-02 09:32:11 +02:00
Mikayla Maki
3768851799 WIP: Git statuses 2023-06-01 23:27:49 -07:00
Joseph Lyons
c55aee84d3 Rename field to panicked_on 2023-06-02 01:33:25 -04:00
Joseph Lyons
cc055901e1 Add additional panic information to panic events 2023-06-01 23:34:39 -04:00
Mikayla Maki
5e43dcaab8 WIP: Add stateful status bubbling to worktree 2023-06-01 16:51:34 -07:00
Max Brunsfeld
0bd9d5b1fa Zoom out all panes when opening a dock (#2554)
Fixes
https://linear.app/zed-industries/issue/Z-2051/center-zoom-isnt-unset-when-opening-project-panel

Release Notes:

- Fixed a bug where opening the project panel did not reset the zoom
state of a pane.
2023-06-01 15:26:46 -07:00
Max Brunsfeld
585d13d3db Unzoom all panes when opening a dock 2023-06-01 15:20:15 -07:00
Max Brunsfeld
a55eafa726 Fix ambiguous glob export warnings in rust 1.70 (#2553)
This fixes a warning that is introduced when upgrading to Rust 1.70.

Release Notes:

- N/A
2023-06-01 14:37:36 -07:00
Max Brunsfeld
e67e6e6f70 Fix ambiguous glob export warnings in rust 1.70 2023-06-01 14:07:59 -07:00
Joseph T. Lyons
6d24a8a763 Add panic events (#2552)
Adds a time field to support panic events.  See

https://github.com/zed-industries/zed.dev/pull/321

Release Notes:

- N/A
2023-06-01 16:47:29 -04:00
Joseph Lyons
0065f5715c Add panic events 2023-06-01 16:40:54 -04:00
Mikayla Maki
9d6b3744f7 Redraw the terminal on every wakeup (#2551)
For whatever reason, the optimizations of panes and workspace have
caused the terminal to notify less often then it should. This PR fixes
that oversight.
2023-06-01 10:56:13 -07:00
Mikayla Maki
40c6baf7cb Redraw the terminal on every wakeup
co-authored-by: max <max@zed.dev>
2023-06-01 10:49:31 -07:00
Mikayla Maki
d64dc3960d Make settings store handle no user settings (#2550)
This fixes the crash users have been reporting with the theme selector
2023-06-01 10:25:30 -07:00
Mikayla Maki
2390815d67 Make settings store handle no user settings
co-authored-by: max <max@zed.dev>
2023-06-01 10:09:04 -07:00
Julia
2ffbeca7dd Avoid blocking forever on startup if config files do not exist (#2549)
The files will still get created if the user opens their settings and
saves, otherwise everything will transparently work

Release Notes:

- Fixed an issue where a missing settings file would cause a hang on
startup
([#1590](https://github.com/zed-industries/community/issues/1590)).
2023-06-01 12:13:22 -04:00
Julia
51c82da840 Avoid blocking forever on startup if config files do not exist
The files will still get created if the user opens their settings and
saves, otherwise everything will transparently work

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-06-01 12:04:45 -04:00
Kirill Bulatov
d8ce333cf8 Remove wrong assertion 2023-06-01 16:51:29 +03:00
Piotr Osiewicz
01621972c5 Change "Project search" to "Project Search" 2023-06-01 12:31:37 +02:00
Piotr Osiewicz
e1a6dc9077 search: Add a default tooltip for empty searches
Z-1074
2023-06-01 11:17:33 +02:00
Max Brunsfeld
788f97ec68 Add support for folder-specific settings (#2537)
This PR allows you to customize Zed's settings within a particular
folder by creating a `.zed/settings.json` file within that folder.

Todo

* [x] respect folder-specific settings for local projects
* [x] respect folder-specific settings in remote projects
* [x] pass a path when retrieving editor/language settings
* [x] pass a path when retrieving copilot settings
* [ ] update the `Setting` trait to make it clear which types of
settings are locally overridable

Release Notes:

* Added support for folder-specific settings. You can customize Zed's
settings within a particular folder by creating a `.zed` directory and a
`.zed/settings.json` file within that folder.
2023-05-31 16:27:08 -07:00
Max Brunsfeld
ae7606ce21 collab 0.12.5 2023-05-31 15:38:26 -07:00
Max Brunsfeld
0d281c1b89 🎨 2023-05-31 15:13:01 -07:00
Max Brunsfeld
0dd7694ff5 Make language_settings take a language, not a language name 2023-05-31 15:10:30 -07:00
Max Brunsfeld
03a351fb26 Make language settings accessors take an arc dyn file 2023-05-31 14:57:04 -07:00
Max Brunsfeld
38078b93cc v0.90.x dev 2023-05-31 14:44:48 -07:00
Max Brunsfeld
a2ab7c9eb9 Respect project-specific settings for copilot 2023-05-31 14:42:15 -07:00
Mikayla Maki
6f4fd74f43 Add a dock-keybinding-changed one-time notification (#2546)
This code is intended to be removed at a later date.

Release Notes:

- N/A
2023-05-31 14:28:12 -07:00
Mikayla Maki
bf2016adf5 Remove version check 2023-05-31 14:23:56 -07:00
Mikayla Maki
27ef0e2b52 De-morgans properly 2023-05-31 14:21:38 -07:00
Mikayla Maki
45b42c512d fmt 2023-05-31 14:17:27 -07:00
Mikayla Maki
705e36827c add version check 2023-05-31 14:16:37 -07:00
Mikayla Maki
b875d4ed40 Remove silly panic 2023-05-31 14:10:50 -07:00
Mikayla Maki
ed0b9acb0a Add panic if version is 0.91 2023-05-31 14:06:36 -07:00
Mikayla Maki
ef80b539d1 Fix notification styling, minimize database reads 2023-05-31 13:56:41 -07:00
Max Brunsfeld
62660f2766 Fix more issues with panels and zoom (#2545)
Release Notes:

* Fixed the behavior of panel buttons when their panel is open. Show the
key binding for closing the containing dock (preview only).
* Improved the styling of zoomed panels and panes, giving them a
stronger border, and color the zoom button with an "active" style
(preview only).
* Improved the stability of panels' zoom state. Close their dock instead
of resetting their zoom state when dismissing panels to reveal other
items (preview only).
2023-05-31 13:17:12 -07:00
Max Brunsfeld
193474a346 🎨 2023-05-31 13:07:20 -07:00
Max Brunsfeld
c48fed26bd When revealing items, zoom-out any inactive panes 2023-05-31 13:05:50 -07:00
Mikayla Maki
4717ce1da3 WIP: Move statuses to entries
co-authored-by: julia <julia@zed.dev>
2023-05-31 12:55:31 -07:00
Max Brunsfeld
3f6aa94a5f Use active color for zoom button in a zoomed pane 2023-05-31 12:51:57 -07:00
Max Brunsfeld
893615236d Only include one border edge for zoomed panels 2023-05-31 12:45:55 -07:00
Nate Butler
1f72f9e18b Add un-hooked up zoomIcon styles 2023-05-31 12:45:55 -07:00
Nate Butler
a10933c063 Update pane/panel zoom styling 2023-05-31 12:45:55 -07:00
Max Brunsfeld
ac41564a8f Dismiss zoomed panels by closing their dock, not zooming them out 2023-05-31 12:45:55 -07:00
Mikayla Maki
22e4086658 WIP: Move statuses to be on their associated file entries in worktree
co-authored-by: Julia <julia@zed.dev>
2023-05-31 11:03:11 -07:00
Sergii Onufriienko
c6672dbac2 Update theme license logic (#2541)
Use new theme file structure
```
themes/
  [theme_name]
    [theme_name]-common.ts
    [theme_name]-light.ts
    [theme_name]-dark.ts
    LICENSE
```

Release Notes: 
- N/A
2023-05-31 18:36:13 +01:00
Max Brunsfeld
76927b6d95 Make active panel's button close its dock 2023-05-31 10:29:59 -07:00
Max Brunsfeld
eeba72d775 Bump protocol version 2023-05-31 09:03:45 -07:00
Max Brunsfeld
8f95435548 Replicate project-specific settings when collaborating 2023-05-30 18:08:03 -07:00
Mikayla Maki
c9820fde61 WIP: Add toast when users attempt to use shift-escape for the first time 2023-05-30 17:48:41 -07:00
Mikayla Maki
584e5f7958 Fix usability issues with new panel system. (#2544)
This PR updates the dock key bindings according to the following model:

There are three bits: 
Visible: Opened / closed.
Focus: Panel focused / center focused.
Zoom: Zoomed / Not zoomed.

Each of these variables is 'sticky' in that they won't effect each other
unless they need to. 'Zooming' a panel conceptually merges the visible
and focus bits.

cmd-shift-j/b/r have all been removed.

cmd-j/b/r have been updated to mean 'toggle visibility of a certain
dock', firing them should *always* reveal the panel to you (where you
last left it), or hide it, without moving focus (unless the focused
element is invisible). This means that, when the terminal panel is
zoomed, cmd-j has the same effect as ctrl-`

ctrl-` and cmd-shift-e now toggle a panel's focus, without updating the
zoom state of a panel. Toggling the focus of a zoomed panel causes it to
automatically hide itself, without losing the zoom bit.

When focused or made visible, panels which cannot be zoomed
automatically unzoom everything else so as to preserve user intent of
'show me this panel' and 'everything stays where it is if I don't take
an action'

Release Notes:

- cmd-shift-j/b/r have been removed.  (preview only)
- cmd-j/b/r unconditionally show or hide their associated dock,
respecting zoom settings. (preview only)
- ctrl-` and cmd-shift-e now retain zoom state. (preview only)
- Fixed a bug where terminal dock tab would always be in the active
state (preview only)
- Fixed a bug where terminals would not always open in the terminal
panel
- Changed the look of zoomed panels to fill more of the screen (preview
only)
2023-05-30 16:39:06 -07:00
Mikayla Maki
0122cd61c5 fmt 2023-05-30 16:33:42 -07:00
Mikayla Maki
685e8d7007 Seperate pane and panel styling
co-authored-by: max <max@zed.dev>
2023-05-30 16:23:16 -07:00
Mikayla Maki
9c707eff27 Rework new terminal actions 2023-05-30 16:10:07 -07:00
Mikayla Maki
e80ab5f096 Fix focus bug with new docks
co-authored-by: max <max@zed.dev>
2023-05-30 15:40:30 -07:00
Mikayla Maki
5e4a9abd09 Fix bug in panel button dispatch 2023-05-30 15:17:42 -07:00
Mikayla Maki
5065804388 WIP: iron out bugs in interaction between toggle and toggle focus key bindings
co-authored-by: max <max@zed.dev>
2023-05-30 14:55:49 -07:00
Julia
8f0fc918e9 TextStyle::default() ask system for known existing font family (#2542)
Rather than assuming a specific family exists, try a set of specific
names and if they fail, just grab any old font that the system reports
as existing

Closes
https://linear.app/zed-industries/issue/Z-445/thread-main-panicked-at-called-resultunwrap-on-an-err-value-could-not

Release Notes:

* Fixed crash that could happen if system did not have a specific
fallback font
2023-05-30 16:25:07 -04:00
Mikayla Maki
9c9af5ed94 Remove focus side effects from toggle dock commands
co-authored-by: max <max@zed.dev>
2023-05-30 12:32:00 -07:00
Joseph Lyons
efcf78cbe9 Update pull_request_template.md 2023-05-30 15:15:20 -04:00
Joseph T. Lyons
e9ce85ebc0 Scrape email from feedback editor (#2543)
Add release note lines here:

N/A
2023-05-30 15:13:35 -04:00
Joseph Lyons
501f9ab2c6 Scrape email from feedback editor 2023-05-30 15:06:35 -04:00
Mikayla Maki
1a23fe91b4 WIP: Remove focus side effects from toggle dock commands
co-authored-by: max <max@zed.dev>
2023-05-30 12:00:15 -07:00
Mikayla Maki
84f98f13c4 Seperate open and zoom bits conceptually for new panels
co-authored-by: max <max@zed.dev>
2023-05-30 11:52:14 -07:00
Julia
31516b7863 TextStyle::default() ask system for known existing font family
Rather than assuming a specific family exists, try a set of specific
names and if they fail, just grab any old font that the system reports
as existing

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-05-30 14:46:27 -04:00
Sergey Onufrienko
51c7078f47 chore: remove license_text 2023-05-30 18:16:50 +02:00
Sergey Onufrienko
4568f80f2e feat: read from LICENSE file 2023-05-30 18:12:31 +02:00
Sergey Onufrienko
14efc18eca refactor: add license file and check it exists 2023-05-30 18:11:54 +02:00
Sergey Onufrienko
05d1dd6b11 refactor: move common to theme folder 2023-05-30 16:53:12 +02:00
Sergey Onufrienko
ac63043c27 chore: move similar themes to one folder 2023-05-30 16:52:28 +02:00
Sergey Onufrienko
0d62e76cd6 refactor: colorSchemes 2023-05-30 15:52:32 +02:00
Antonio Scandurra
69e8a166e4 Start on assistant::QuoteSelection 2023-05-30 15:25:53 +02:00
Antonio Scandurra
dc365472a6 Merge remote-tracking branch 'origin/main' into assistant-2 2023-05-30 15:11:06 +02:00
Antonio Scandurra
1fc9103b61 Remove methods taking &mut Workspace from Pane (#2540)
This pull request simplifies the `Pane` struct by replacing methods like
`Pane::add_item` that would previously take a `&mut Workspace` with
methods that take a `&mut self`. When access to the workspace is needed,
we now either emit an event from the `Pane` or directly move the method
to the `Workspace` struct.
2023-05-30 15:01:35 +02:00
Antonio Scandurra
bd494037bb Call ItemHandle::added_to_pane when an item is added to a pane 2023-05-30 14:46:41 +02:00
Sergey Onufrienko
2269c19169 chore: add LICENSE files 2023-05-30 14:06:19 +02:00
Nathan Sobo
8a851ad7f8 Fix annoying system prompt (#2538)
I was having fun, but it was too much. It makes the assistant too
annoying to be useful.

Release Notes:

Use `N/A` in this section if this item should be skipped in the release
notes.

Add release note lines here:

* Dialed back the system prompt on the assistant.
2023-05-29 22:05:40 -06:00
Nathan Sobo
063613bf4e Fix annoying system prompt
I was having fun, but it was too much. It makes the assistant too annoying to be useful.
2023-05-29 21:47:59 -06:00
Max Brunsfeld
ed0fa2404c Use settings JSON schema when editing local settings files 2023-05-29 14:31:54 -07:00
Max Brunsfeld
89446c7fd4 Start work on respecting project-specific settings 2023-05-29 14:25:49 -07:00
Antonio Scandurra
88eb2b2163 Move history navigation logic to Workspace
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-05-29 19:47:59 +02:00
Antonio Scandurra
6d3464fd1f Make Pane::add_item a proper Pane method
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2023-05-29 19:16:22 +02:00
Antonio Scandurra
890c42a75a Show time in assistant messages 2023-05-29 16:23:16 +02:00
Kirill Bulatov
edf8e276af Improves project search panel shortcut handling (#2536)
* ESC (project_search::ToggleFocus) toggles focus from include/exclude
fields to the editor

* Cmd+Shift+F (workspace::NewSearch) can be triggered from the editor,
and moves focus to the query editor

Release Notes:

* Improved project search panel shortcut handling, allowing more actions
to trigger from panel elements
2023-05-29 17:06:38 +03:00
Kirill Bulatov
d593377e45 Improves project search panel shortcut handling
* ESC (project_search::ToggleFocus) toggles focus from
  include/exclude fields to the editor

* Cmd+Shift+F (workspace::NewSearch) can be triggered from the editor,
and moves focus to the query editor
2023-05-29 16:58:06 +03:00
Antonio Scandurra
52e8bf2928 Show custom header for assistant messages 2023-05-29 15:57:55 +02:00
Sergey Onufrienko
cf75b090d3 feat: organize themes by folders with LICENSE 2023-05-29 15:45:13 +02:00
Kirill Bulatov
986b02e217 Update LSP to the newest version (#2535)
Current `lsp-types:0.91.1` crate lacks inlay hints' definitions. Crate's
changelog is not very descriptive, but it appears that `0.92.1` could be
used:
https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md#v0921-2022-03-21
The latest is crate version is `0.94.0` (2023-02-08), the PR updates Zed
to the latest version.


Notable changes:
* workspace symbols may arrive unresolved if the corresponding client
capability is enabled:
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
Zed has this capability disabled, forcing all symbols to arrive
synchronously (?).

Resolve capabilities are important for inlay hints too, but I've not
found any code in Zed for that outside tests, so I'd love to learn more
and implement the resolution for workspace symbols separately.

* since LSP `3.17` (current), watch file changes can use relative glob
patterns:
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles

That seemed just a straightforward extra `match` to use the same Ruse
`Glob` to handle the relative path one.

Release Notes:

N/A
2023-05-29 12:49:37 +03:00
Antonio Scandurra
404bebab63 Set markdown as the assistant's buffer languages 2023-05-29 11:34:26 +02:00
Kirill Bulatov
fdad1adaf6 Update LSP to the newest version 2023-05-28 21:55:08 +03:00
Max Brunsfeld
e4530471de Make worktree UpdatedEntries events fully describe all changes (#2533)
This PR makes the worktree's change events more useful in a few ways:

* The changes are now described by a cheaply clone-able collection, so
that they can be used in background tasks. Right now, I'm using a simple
Arc slice.
* The `UpdatedEntries` event now captures not only changes due to FS
changes, but also newly-loaded paths that are discovered during the
initial scan.
* The `UpdatedGitRepositories` event now includes repositories whose
work-dir changed but git dir didn't change. A boolean flag is included,
to indicate whether the git content changed.
* The `UpdatedEntries` and `UpdatedGitRepositories` events are now
*used* to compute the worktree's `UpdateWorktree` messages, used to sync
changes to guests. This unifies two closely-related code paths, and
makes the host more efficient when collaborating, because the
`UpdateWorktree` message computation used to require walking the entire
`entries` tree on every FS change.
2023-05-26 15:55:14 -07:00
Max Brunsfeld
1f42bfc1bd Include repositories with workdir changes in worktree UpdatedGitRepsositories event 2023-05-26 15:47:37 -07:00
Max Brunsfeld
2db57b5139 Adjust diagnostic transformation test to not wait for two buffer notifications 2023-05-26 15:47:37 -07:00
Max Brunsfeld
02b95ef320 Derive worktree update messages from existing change events 2023-05-26 15:47:37 -07:00
Kirill Bulatov
9cfe39f1aa Display external paths in history (#2534)
Fixes
https://linear.app/zed-industries/issue/Z-1793/file-finder-external-recent-paths-are-not-rendered-properly

Long paths get trimmed, but same do many of our other elements, such as
type definitions, so I think it's ok for now:

![image](https://github.com/zed-industries/zed/assets/2690773/b8b6588d-6d6b-42db-9085-c741a40b7adb)

Also, we seem to do a lot of odd diagnostics handling on every external
stdlib file opened:

![image](https://github.com/zed-industries/zed/assets/2690773/cd82c54e-7849-46fe-a01c-79bfc5402b7b)
Other external files also emit similar messages, but not that much.
@\mikayla-maki mentioned, that this was happening before, so can be
fixed separately.
The PR adds path printing to these logs.

Release Notes:

* Fixed external files not being displayed properly in the recently
opened list in the file finder panel
2023-05-27 01:27:24 +03:00
Kirill Bulatov
5fda9e934d Shorten full paths with ~ 2023-05-27 01:21:38 +03:00
Kirill Bulatov
3a3c1c5a5b Add a test
co-authored-by: Mikayla <mikayla@zed.dev>
2023-05-27 01:12:22 +03:00
Nathan Sobo
ffbfbe422b WIP: Not sure I actually want to rip this out 2023-05-26 16:11:58 -06:00
Nathan Sobo
3904971bd8 Hide assistant gutter 2023-05-26 15:38:03 -06:00
Nathan Sobo
8f6e67f440 Cancel assists on escape 2023-05-26 14:49:27 -06:00
Max Brunsfeld
6628c4df28 Store worktree changes in a sorted arc slice, not a HashMap
We don't need to look-up change types by an arbitrary key, but we
do need to iterate it. It would also be useful to be able to
cheaply clone the changes, to use them in a background task.
2023-05-26 09:24:15 -07:00
Max Brunsfeld
59bfd40679 Make stricter assertions about change events in random worktree test 2023-05-26 09:24:15 -07:00
Max Brunsfeld
f890eefdef Include paths loaded during initial scan in worktree UpdatedEntries event 2023-05-26 09:24:15 -07:00
Nathan Sobo
80080a43e4 Start on an assistant panel based on multi-buffers
Each message is represented as a multibuffer excerpt to allow for
fluid editing of the conversation transcript.

Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-05-26 10:09:55 -06:00
Kirill Bulatov
cf2bbfc85a Better display labels for external files 2023-05-26 18:18:32 +03:00
Kirill Bulatov
508533ebb7 Track abs paths in history 2023-05-26 17:31:35 +03:00
Kirill Bulatov
2fdc960704 Properly display labels for currently opened external files 2023-05-26 15:44:44 +03:00
Kirill Bulatov
b75c27da6f Simplify selected index handling 2023-05-26 14:15:58 +03:00
Kirill Bulatov
364631a155 Add absolute paths to historic elements 2023-05-26 14:03:44 +03:00
Kirill Bulatov
128c19875d Fix duplicate item creation in project panel (#2532)
Closes
https://linear.app/zed-industries/issue/Z-1760/creating-a-new-file-with-the-same-name-as-an-existing-file-overwrites

Now every rename/creation editor won't do anything if a duplicate name
is submitted.

Release Notes:

* Fixed a bug when created in project panel items with duplicate names
were overwriting the old files
2023-05-26 10:14:50 +03:00
Kirill Bulatov
5501dd741c Fix duplicate item creation in project panel 2023-05-26 02:07:25 +03:00
Julia
7900d2a20a Multibuffer edit perf (#2531)
This took so much longer than I wanted, so glad to finally be rid of
this

Release Notes:
* Improved performance when editing many git-tracked files in a
multi-buffer at the same time
2023-05-25 15:01:05 -04:00
Julia
5e39ba596e Clean up final remaining code paths calling old diff update method 2023-05-25 14:41:09 -04:00
Julia
f40c498491 Fix tests 2023-05-25 14:29:28 -04:00
Julia
8d662edb6c Remove concept of git diff refresh from Item trait 2023-05-25 14:29:28 -04:00
Julia
cede296b04 Project level git diff recalc handling
This avoids an issue where in a many-buffer multi-buffer, each modified
buffer could complete its recalc independently, causing a cascade of
repeated notifies

Now all recalcs started at the same time must complete before
 A: Starting another recalc pass
 B: The master notify occurring

Each buffer can still show its new diff if something else triggers it
to notify earlier, this is desirable and does not have the same negative
effects as the notify cascade as those re-layouts would need to happen
anyway

Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-05-25 14:10:27 -04:00
Max Brunsfeld
54421b11f3 wip 2023-05-25 14:10:27 -04:00
Kirill Bulatov
ae3bdd755e Support OnTypeFormatting LSP request (#2517)
Supports
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_onTypeFormatting

rust-analyzer uses this feature to add matching brackets semantically,
e.g.
before:
![Screenshot 2023-05-23 at 17 46
42](https://github.com/zed-industries/zed/assets/2690773/020e8448-23e6-4a38-8dbb-c9edf18062f7)

after:
![Screenshot 2023-05-23 at 17 46
49](https://github.com/zed-industries/zed/assets/2690773/4d140af3-aca6-451d-ac61-e2a9bb31caea)

`use_on_type_format` settings entry was added, enabled by default, to
disable the new feature.

Release Notes:

* Support `OnTypeFormatting` LSP protocol feature, allowing
rust-analyzer to add matching brackets
2023-05-25 18:35:47 +03:00
Kirill Bulatov
739d5ca373 Have proper undo for both client and host 2023-05-25 18:29:57 +03:00
Antonio Scandurra
24098c561d Prevent bottom dock from becoming too large or too small (#2529)
Fixes
https://linear.app/zed-industries/issue/Z-1834/bottom-dock-can-be-dragged-down-far-enough-such-that-the-tab-bar-is

Release Notes:

- Improved the resize experience for the bottom dock, preventing it from
becoming too large or too small. (preview-only)
2023-05-25 15:44:23 +02:00
Antonio Scandurra
4185a178e7 Prevent bottom dock from becoming too large or too small 2023-05-25 15:29:59 +02:00
Antonio Scandurra
f549ada54f Restore the ability to search terminals in the terminal panel (#2528)
Fixes
https://linear.app/zed-industries/issue/Z-1812/find-in-terminal-panel-is-broken

![CleanShot 2023-05-25 at 15 18
01@2x](https://github.com/zed-industries/zed/assets/482957/ddb33b61-d253-4de1-961f-14b24aaa3e46)

This also removes navigation controls from the terminal panel, given
that terminals don't make use of that feature anyway. When the toolbar
is empty, we'll avoid showing it altogether.

![CleanShot 2023-05-25 at 15 17
26@2x](https://github.com/zed-industries/zed/assets/482957/52419f2c-bca0-494a-a9b3-88e183b4c12f)


Release Notes:

- Fixed a regression that was preventing the terminal panel from being
searched.
2023-05-25 15:28:32 +02:00
Antonio Scandurra
aa8c0475b1 Don't show navigation controls for terminal panel 2023-05-25 15:15:20 +02:00
Kirill Bulatov
e2ff829f98 Use Transaction instead of ProjectTransaction
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
2023-05-25 16:12:14 +03:00
Antonio Scandurra
414b97adce Add search bar to terminal panel 2023-05-25 14:46:10 +02:00
Antonio Scandurra
dba7ec4a35 Prevent pane from being erroneously zoomed when toggling the outline pane (#2527)
Fixes
https://linear.app/zed-industries/issue/Z-1818/toggling-the-outline-pane-causes-the-pane-to-zoom

Add release note lines here:

- Fixed a bug that could cause panes to be erroneously zoomed when
toggling modals. (preview-only)
2023-05-25 14:10:08 +02:00
Antonio Scandurra
c2f3ac24a4 Prevent zoom from being restored when focusing a modal 2023-05-25 12:11:11 +02:00
Antonio Scandurra
7e1ea8f274 Remove stray dbg! 2023-05-25 12:04:41 +02:00
Kirill Bulatov
aa58d0fd77 Do not send edits over the wire 2023-05-25 12:55:44 +03:00
Kirill Bulatov
f812151840 Add integration tests 2023-05-25 09:45:50 +03:00
Kirill Bulatov
eca6d2b597 Process remote format typing also 2023-05-25 09:45:50 +03:00
Kirill Bulatov
58a56bdda2 Always use server formatting settings 2023-05-25 09:45:50 +03:00
Kirill Bulatov
d1f4b60fa1 Allow to disable the new feature 2023-05-25 09:45:50 +03:00
Kirill Bulatov
b9dabb165e Use formatting options 2023-05-25 09:45:50 +03:00
Kirill Bulatov
3327e8a6dd Support remote sessions 2023-05-25 09:45:50 +03:00
Kirill Bulatov
f6d7b3d2e8 Send and handle OnTypeFormatting LSP request 2023-05-25 09:45:50 +03:00
Nate Butler
793486b2e8 Update Diff Modified Color (#2526)
For some reason the yellow I used for the modified color in light themes
was really light
.
Release Notes:

* Improved the contrast of diff modified color in the editor in light
themes.
2023-05-24 23:50:37 -04:00
Joseph Lyons
3f79b0c7cc Don't hardcode dates into script 2023-05-24 17:39:02 -04:00
Joseph Lyons
cc3186cb4a Add script to print all prs merged between a previous date and now 2023-05-24 16:58:56 -04:00
Nathan Sobo
0b7d095c0a Introduce the ability to talk to GPT-4 within buffers on preview and dev (#2524)
This adds basic ability to interact with OpenAI inside a buffer.

Release Notes:

* If`OPENAI_API_KEY` is defined in your environment, you can now run the
`ai: assist` command to pass the text of your current buffer to GPT-4.
If you're editing a file with a `.zmd` extension, you can also invoke
the model with `cmd-enter.` (preview-only)
2023-05-24 12:00:43 -06:00
Joseph Lyons
37d35db3d7 v0.89.x dev 2023-05-24 13:22:00 -04:00
Nathan Sobo
26051de160 Clean up 2023-05-24 11:08:00 -06:00
Nathan Sobo
747322a02d Merge remote-tracking branch 'origin/main' into zmd 2023-05-24 11:04:07 -06:00
Nathan Sobo
d495c1b804 Only run AI agent on dev and preview 2023-05-24 10:58:33 -06:00
Antonio Scandurra
35f9996a4f WIP 2023-05-24 18:45:47 +02:00
Nathan Sobo
ba79a8ba94 WIP 2023-05-24 10:26:14 -06:00
Nathan Sobo
50e1e17d09 WIP 2023-05-24 09:53:08 -06:00
Nathan Sobo
051c7566cc WIP 2023-05-24 09:38:37 -06:00
Nathan Sobo
8abaf66602 WIP 2023-05-24 09:21:50 -06:00
Nathan Sobo
6368ade1da WIP 2023-05-24 09:12:16 -06:00
Nathan Sobo
637ed79603 WIP 2023-05-23 18:16:49 -06:00
Nathan Sobo
65fc50745b WIP 2023-05-23 18:04:33 -06:00
Nathan Sobo
d934da1905 WIP 2023-05-23 15:25:34 -06:00
Nathan Sobo
7be41e19f7 WIP 2023-05-23 08:52:21 -06:00
Nathan Sobo
073967c80b WIP 2023-05-23 01:56:28 -06:00
Nathan Sobo
5362e7d346 WIP 2023-05-23 01:52:37 -06:00
Nathan Sobo
394e87d17c WIP 2023-05-23 01:23:01 -06:00
Nathan Sobo
0de5a444d3 WIP 2023-05-23 00:22:21 -06:00
Nathan Sobo
234dbc3ca9 WIP 2023-05-23 00:16:44 -06:00
Nathan Sobo
c4d88bc529 WIP 2023-05-22 23:43:02 -06:00
Nathan Sobo
30de64845f WIP 2023-05-22 23:11:27 -06:00
Nathan Sobo
7e6cccfa3d WIP: Stream in completions
Drop dependency on tokio introduced by async-openai and do it ourselves.

The approach I'm taking of replacing instead of appending is causing issues. Need to just append.
2023-05-22 20:28:22 -06:00
Nate Butler
48dcacc7ce Sort & add some docs 2023-05-22 10:03:10 -04:00
Nathan Sobo
912fd23006 Parse markdown into an OpenAI chat completion request 2023-05-20 09:43:12 -06:00
Nate Butler
32849d1e43 Add initial internal docs from Meta 2023-05-19 11:02:18 -04:00
335 changed files with 18276 additions and 9087 deletions

View File

@@ -2,12 +2,11 @@
Release Notes:
Use `N/A` in this section if this item should be skipped in the release notes.
- N/A
Add release note lines here:
or
* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
* ...
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
These will be removed by the person making the release.

View File

@@ -93,7 +93,6 @@ jobs:
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_MIXPANEL_TOKEN: ${{ secrets.ZED_MIXPANEL_TOKEN }}
steps:
- name: Install Rust
run: |

View File

@@ -6,8 +6,8 @@ on:
push:
branches:
- randomized-tests-runner
schedule:
- cron: '0 * * * *'
# schedule:
# - cron: '0 * * * *'
env:
CARGO_TERM_COLOR: always

View File

@@ -21,19 +21,3 @@ jobs:
${{ github.event.release.body }}
```
mixpanel_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10.5"
architecture: "x64"
cache: "pip"
- run: pip install -r script/mixpanel_release/requirements.txt
- run: >
python script/mixpanel_release/main.py
${{ github.event.release.tag_name }}
${{ secrets.MIXPANEL_PROJECT_ID }}
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_USERNAME }}
${{ secrets.MIXPANEL_SERVICE_ACCOUNT_SECRET }}

1
.gitignore vendored
View File

@@ -18,4 +18,5 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.swiftpm
**/*.db

317
Cargo.lock generated
View File

@@ -95,6 +95,32 @@ dependencies = [
"memchr",
]
[[package]]
name = "ai"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"collections",
"editor",
"fs",
"futures 0.3.28",
"gpui",
"isahc",
"language",
"menu",
"schemars",
"search",
"serde",
"serde_json",
"settings",
"smol",
"theme",
"tiktoken-rs",
"util",
"workspace",
]
[[package]]
name = "alacritty_config"
version = "0.1.1-dev"
@@ -164,15 +190,6 @@ dependencies = [
"libc",
]
[[package]]
name = "ansi_term"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anyhow"
version = "1.0.71"
@@ -203,15 +220,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "assets"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"rust-embed",
]
[[package]]
name = "async-broadcast"
version = "0.4.1"
@@ -388,7 +396,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -436,7 +444,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -479,7 +487,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -586,7 +594,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"itoa",
"itoa 1.0.6",
"matchit",
"memchr",
"mime",
@@ -699,27 +707,42 @@ dependencies = [
[[package]]
name = "bindgen"
version = "0.59.2"
version = "0.65.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"clap 2.34.0",
"env_logger 0.9.3",
"lazy_static",
"lazycell",
"log",
"peeking_take_while",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.18",
"which",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -845,6 +868,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
dependencies = [
"memchr",
"once_cell",
"regex-automata",
"serde",
]
@@ -1069,21 +1094,6 @@ dependencies = [
"libloading",
]
[[package]]
name = "clap"
version = "2.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim 0.8.0",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
[[package]]
name = "clap"
version = "3.2.25"
@@ -1096,9 +1106,9 @@ dependencies = [
"clap_lex",
"indexmap",
"once_cell",
"strsim 0.10.0",
"strsim",
"termcolor",
"textwrap 0.16.0",
"textwrap",
]
[[package]]
@@ -1128,7 +1138,7 @@ name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 3.2.25",
"clap",
"core-foundation",
"core-services",
"dirs 3.0.2",
@@ -1230,7 +1240,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.12.4"
version = "0.14.2"
dependencies = [
"anyhow",
"async-tungstenite",
@@ -1238,7 +1248,7 @@ dependencies = [
"axum-extra",
"base64 0.13.1",
"call",
"clap 3.2.25",
"clap",
"client",
"collections",
"ctor",
@@ -1410,7 +1420,6 @@ name = "copilot_button"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"context_menu",
"copilot",
"editor",
@@ -1781,7 +1790,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -1798,7 +1807,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -2195,6 +2204,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fancy-regex"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
dependencies = [
"bit-set",
"regex",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@@ -2220,6 +2239,7 @@ dependencies = [
"log",
"postage",
"project",
"regex",
"search",
"serde",
"serde_derive",
@@ -2421,6 +2441,7 @@ dependencies = [
"parking_lot 0.11.2",
"regex",
"rope",
"rpc",
"serde",
"serde_derive",
"serde_json",
@@ -2570,7 +2591,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -2991,7 +3012,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes 1.4.0",
"fnv",
"itoa",
"itoa 1.0.6",
]
[[package]]
@@ -3050,7 +3071,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa",
"itoa 1.0.6",
"pin-project-lite 0.2.9",
"socket2",
"tokio",
@@ -3316,6 +3337,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "itoa"
version = "1.0.6"
@@ -3376,12 +3403,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json_comments"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5"
[[package]]
name = "jwt"
version = "0.16.0"
@@ -3495,6 +3516,29 @@ dependencies = [
"workspace",
]
[[package]]
name = "language_tools"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"env_logger 0.9.3",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"tree-sitter",
"unindent",
"util",
"workspace",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -3728,9 +3772,9 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.91.1"
version = "0.94.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
dependencies = [
"bitflags",
"serde",
@@ -3739,26 +3783,6 @@ dependencies = [
"url",
]
[[package]]
name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"editor",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "mach"
version = "0.3.2"
@@ -4331,7 +4355,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -4776,6 +4800,16 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1"
dependencies = [
"proc-macro2",
"syn 2.0.18",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
@@ -4811,9 +4845,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.56"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
dependencies = [
"unicode-ident",
]
@@ -5090,9 +5124,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.27"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
dependencies = [
"proc-macro2",
]
@@ -5634,7 +5668,7 @@ dependencies = [
"bitflags",
"errno 0.2.8",
"io-lifetimes 0.5.3",
"itoa",
"itoa 1.0.6",
"libc",
"linux-raw-sys 0.0.42",
"once_cell",
@@ -6036,7 +6070,7 @@ checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -6066,7 +6100,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"indexmap",
"itoa",
"itoa 1.0.6",
"ryu",
"serde",
]
[[package]]
name = "serde_json_lenient"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add"
dependencies = [
"indexmap",
"itoa 0.4.8",
"ryu",
"serde",
]
@@ -6079,7 +6125,7 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -6089,7 +6135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"itoa 1.0.6",
"ryu",
"serde",
]
@@ -6111,19 +6157,20 @@ name = "settings"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"collections",
"fs",
"futures 0.3.28",
"gpui",
"json_comments",
"indoc",
"lazy_static",
"postage",
"pretty_assertions",
"rust-embed",
"schemars",
"serde",
"serde_derive",
"serde_json",
"serde_json_lenient",
"smallvec",
"sqlez",
"staff_mode",
@@ -6474,7 +6521,7 @@ dependencies = [
"hkdf",
"hmac 0.12.1",
"indexmap",
"itoa",
"itoa 1.0.6",
"libc",
"libsqlite3-sys",
"log",
@@ -6566,12 +6613,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.10.0"
@@ -6643,9 +6684,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.15"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
dependencies = [
"proc-macro2",
"quote",
@@ -6793,6 +6834,7 @@ dependencies = [
"procinfo",
"project",
"rand 0.8.5",
"search",
"serde",
"serde_derive",
"settings",
@@ -6830,15 +6872,6 @@ dependencies = [
"util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
@@ -6912,7 +6945,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -6942,6 +6975,21 @@ dependencies = [
"weezl",
]
[[package]]
name = "tiktoken-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba161c549e2c0686f35f5d920e63fad5cafba2c28ad2caceaf07e5d9fa6e8c4"
dependencies = [
"anyhow",
"base64 0.21.0",
"bstr",
"fancy-regex",
"lazy_static",
"parking_lot 0.12.1",
"rustc-hash",
]
[[package]]
name = "time"
version = "0.1.45"
@@ -6959,7 +7007,7 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
dependencies = [
"itoa",
"itoa 1.0.6",
"serde",
"time-core",
"time-macros",
@@ -7070,7 +7118,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -7258,7 +7306,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]
@@ -7325,8 +7373,8 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
version = "0.20.10"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=49226023693107fba9a1191136a4f47f38cdca73#49226023693107fba9a1191136a4f47f38cdca73"
dependencies = [
"cc",
"regex",
@@ -7363,8 +7411,8 @@ dependencies = [
[[package]]
name = "tree-sitter-elixir"
version = "0.19.0"
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2"
version = "0.1.0"
source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e"
dependencies = [
"cc",
"tree-sitter",
@@ -7389,6 +7437,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-heex"
version = "0.0.1"
source = "git+https://github.com/phoenixframework/tree-sitter-heex?rev=2e1348c3cf2c9323e87c2744796cf3f3868aa82a#2e1348c3cf2c9323e87c2744796cf3f3868aa82a"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-html"
version = "0.19.0"
@@ -7526,7 +7583,7 @@ dependencies = [
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=f545a41f57502e1b5ddf2a6668896c1b0620f930#f545a41f57502e1b5ddf2a6668896c1b0620f930"
dependencies = [
"cc",
"tree-sitter",
@@ -7760,6 +7817,7 @@ dependencies = [
"lazy_static",
"log",
"rand 0.8.5",
"rust-embed",
"serde",
"serde_json",
"smol",
@@ -7819,12 +7877,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.4"
@@ -7836,7 +7888,6 @@ name = "vim"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"async-compat",
"async-trait",
"collections",
@@ -7988,7 +8039,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
"wasm-bindgen-shared",
]
@@ -8022,7 +8073,7 @@ checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -8664,7 +8715,6 @@ name = "workspace"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"async-recursion 1.0.4",
"bincode",
"call",
@@ -8759,11 +8809,11 @@ dependencies = [
[[package]]
name = "zed"
version = "0.88.0"
version = "0.92.1"
dependencies = [
"activity_indicator",
"ai",
"anyhow",
"assets",
"async-compression",
"async-recursion 0.3.2",
"async-tar",
@@ -8803,11 +8853,11 @@ dependencies = [
"journal",
"language",
"language_selector",
"language_tools",
"lazy_static",
"libc",
"log",
"lsp",
"lsp_log",
"node_runtime",
"num_cpus",
"outline",
@@ -8849,6 +8899,7 @@ dependencies = [
"tree-sitter-elixir",
"tree-sitter-embedded-template",
"tree-sitter-go",
"tree-sitter-heex",
"tree-sitter-html",
"tree-sitter-json 0.20.0",
"tree-sitter-lua",
@@ -8888,7 +8939,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 2.0.18",
]
[[package]]

View File

@@ -1,7 +1,7 @@
[workspace]
members = [
"crates/activity_indicator",
"crates/assets",
"crates/ai",
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
@@ -32,10 +32,10 @@ members = [
"crates/journal",
"crates/language",
"crates/language_selector",
"crates/language_tools",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp_log",
"crates/media",
"crates/menu",
"crates/node_runtime",
@@ -78,6 +78,8 @@ ctor = { version = "0.1" }
env_logger = { version = "0.9" }
futures = { version = "0.3" }
globset = { version = "0.4" }
indoc = "1"
isahc = "1.7.2"
lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" }
@@ -85,6 +87,7 @@ parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" }
regex = { version = "1.5" }
rust-embed = { version = "6.3", features = ["include-exclude"] }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -95,10 +98,11 @@ tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
tree-sitter = "0.20"
unindent = { version = "0.1.7" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
@@ -113,3 +117,4 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true
lto = "thin"

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.65-bullseye as builder
FROM rust:1.70-bullseye as builder
WORKDIR app
COPY . .

View File

@@ -0,0 +1,6 @@
User input begins on a line starting with /.
Don't apologize ever.
Never say "I apologize".
Use simple language and don't flatter the users.
Keep it short.
Risk being rude.

View File

@@ -0,0 +1,4 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 4C2.5 2.89531 3.39688 2 4.5 2H9.5C10.6031 2 11.5 2.89531 11.5 4V8C11.5 9.10312 10.6031 10 9.5 10H4.5C3.39688 10 2.5 9.10312 2.5 8V4ZM5 4C4.44687 4 4 4.44687 4 5C4 5.55313 4.44687 6 5 6C5.55313 6 6 5.55313 6 5C6 4.44687 5.55313 4 5 4ZM9 6C9.55313 6 10 5.55313 10 5C10 4.44687 9.55313 4 9 4C8.44687 4 8 4.44687 8 5C8 5.55313 8.44687 6 9 6ZM5 8.5C5.275 8.5 5.5 8.275 5.5 8C5.5 7.725 5.275 7.5 5 7.5C4.725 7.5 4.5 7.725 4.5 8C4.5 8.275 4.725 8.5 5 8.5ZM7 7.5C6.725 7.5 6.5 7.725 6.5 8C6.5 8.275 6.725 8.5 7 8.5C7.275 8.5 7.5 8.275 7.5 8C7.5 7.725 7.275 7.5 7 7.5ZM9 8.5C9.275 8.5 9.5 8.275 9.5 8C9.5 7.725 9.275 7.5 9 7.5C8.725 7.5 8.5 7.725 8.5 8C8.5 8.275 8.725 8.5 9 8.5ZM0 14C0 12.3156 1.34312 11 3 11H11C12.6562 11 14 12.3156 14 14V15C14 15.5531 13.5531 16 13 16H11V14C11 13.4469 10.5531 13 10 13H4C3.44687 13 3 13.4469 3 14V16H1C0.447812 16 0 15.5531 0 15V14Z" fill="#808080"/>
<path d="M7.5 2H6.5V0.5C6.5 0.22375 6.725 0 7 0C7.275 0 7.5 0.22375 7.5 0.5V2ZM1.5 4.5V7.5C1.5 7.775 1.27625 8 1 8C0.72375 8 0.5 7.775 0.5 7.5V4.5C0.5 4.225 0.72375 4 1 4C1.27625 4 1.5 4.225 1.5 4.5ZM5.5 16H4.5V14.5C4.5 14.225 4.725 14 5 14C5.275 14 5.5 14.225 5.5 14.5V16ZM7.5 16H6.5V14.5C6.5 14.225 6.725 14 7 14C7.275 14 7.5 14.225 7.5 14.5V16ZM9 14C9.275 14 9.5 14.225 9.5 14.5V16H8.5V14.5C8.5 14.225 8.725 14 9 14ZM13.5 7.5C13.5 7.775 13.275 8 13 8C12.725 8 12.5 7.775 12.5 7.5V4.5C12.5 4.225 12.725 4 13 4C13.275 4 13.5 4.225 13.5 4.5V7.5Z" fill="#808080"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.01077 0.000234794C2.69085 0.000234794 0.000639074 2.18612 0.000639074 4.88385C0.000639074 6.0491 0.501914 7.11387 1.33823 7.95254C1.04475 9.13517 0.0640321 10.1894 0.0522927 10.2011C-0.00053487 10.2539 -0.0153266 10.3356 0.0170743 10.4061C0.0464229 10.4763 0.111459 10.5185 0.187766 10.5185C1.74324 10.5185 2.89019 9.77286 3.4889 9.31197C4.25431 9.60052 5.10894 9.76722 6.01053 9.76722C9.33045 9.76722 12 7.58063 12 4.88361C12 2.18659 9.33045 0 6.01053 0L6.01077 0.000234794Z" fill="#FAFAFA"/>
</svg>

Before

Width:  |  Height:  |  Size: 636 B

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -16,6 +16,12 @@
"replace_newest": true
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
@@ -49,7 +55,40 @@
"context": "Pane",
"bindings": {
"alt-cmd-/": "search::ToggleRegex",
"ctrl-0": "project_panel::ToggleFocus"
"ctrl-0": "project_panel::ToggleFocus",
"cmd-1": [
"pane::ActivateItem",
0
],
"cmd-2": [
"pane::ActivateItem",
1
],
"cmd-3": [
"pane::ActivateItem",
2
],
"cmd-4": [
"pane::ActivateItem",
3
],
"cmd-5": [
"pane::ActivateItem",
4
],
"cmd-6": [
"pane::ActivateItem",
5
],
"cmd-7": [
"pane::ActivateItem",
6
],
"cmd-8": [
"pane::ActivateItem",
7
],
"cmd-9": "pane::ActivateLastItem"
}
},
{

View File

@@ -185,7 +185,8 @@
],
"alt-\\": "copilot::Suggest",
"alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion"
"alt-[": "copilot::PreviousSuggestion",
"cmd->": "assistant::QuoteSelection"
}
},
{
@@ -195,6 +196,15 @@
"cmd-alt-enter": "editor::NewlineBelow"
}
},
{
"context": "AssistantEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
}
},
{
"context": "BufferSearchBar",
"bindings": {
@@ -244,12 +254,24 @@
"replace_newest": false
}
],
"ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-k cmd-d": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"cmd-k ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"cmd-k cmd-i": "editor::Hover",
"cmd-/": [
"editor::ToggleComments",
@@ -367,30 +389,9 @@
"workspace::ActivatePane",
8
],
"cmd-b": [
"workspace::ToggleLeftDock",
{ "focus": true }
],
"cmd-shift-b": [
"workspace::ToggleLeftDock",
{ "focus": false }
],
"cmd-r": [
"workspace::ToggleRightDock",
{ "focus": true }
],
"cmd-shift-r": [
"workspace::ToggleRightDock",
{ "focus": false }
],
"cmd-j": [
"workspace::ToggleBottomDock",
{ "focus": true }
],
"cmd-shift-j": [
"workspace::ToggleBottomDock",
{ "focus": false }
],
"cmd-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"cmd-shift-f": "workspace::NewSearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
@@ -519,7 +520,7 @@
"terminal::SendText",
"\u0001"
],
// Terminal.app compatability
// Terminal.app compatibility
"alt-left": [
"terminal::SendText",
"\u001bb"

View File

@@ -26,6 +26,12 @@
"replace_newest": false
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-/": [
"editor::ToggleComments",
{

View File

@@ -39,6 +39,9 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
@@ -78,6 +81,14 @@
// Default width of the project panel.
"default_width": 240
},
"assistant": {
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
"default_width": 450,
// Default height when the assistant is docked to the bottom.
"default_height": 320
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
@@ -97,6 +108,8 @@
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,
@@ -105,9 +118,9 @@
// How to perform a buffer format. This setting can take two values:
//
// 1. Format code using the current language server:
// "format_on_save": "language_server"
// "formatter": "language_server"
// 2. Format code using an external command:
// "format_on_save": {
// "formatter": {
// "external": {
// "command": "prettier",
// "arguments": ["--stdin-filepath", "{buffer_path}"]
@@ -242,7 +255,7 @@
// copy to the system clipboard.
"copy_on_select": false,
// Any key-value pairs added to this list will be added to the terminal's
// enviroment. Use `:` to seperate multiple values.
// environment. Use `:` to separate multiple values.
"env": {
// "KEY": "value1:value2"
},

View File

@@ -0,0 +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
{}

35
crates/ai/Cargo.toml Normal file
View File

@@ -0,0 +1,35 @@
[package]
name = "ai"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/ai.rs"
doctest = false
[dependencies]
collections = { path = "../collections"}
editor = { path = "../editor" }
fs = { path = "../fs" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
chrono = "0.4"
futures.workspace = true
isahc.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tiktoken-rs = "0.4"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

63
crates/ai/README.zmd Normal file
View File

@@ -0,0 +1,63 @@
> Hello, I'm Nathan Sobo.
I'm currently your sole creator and the leader of Zed Industries.
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
@nathan> #zed, can you describe the conversation UI as you currently understand it.
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
@nathan> What details around the conversation view are worth exploring?
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
@nathan> @nate, which of these would you like to explore?
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
#zed describe a hypothetical conversation UI that meets the description.
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.

3
crates/ai/features.zmd Normal file
View File

@@ -0,0 +1,3 @@
Push content to a deeper layer.
A context can have multiple sublayers.
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.

98
crates/ai/src/ai.rs Normal file
View File

@@ -0,0 +1,98 @@
pub mod assistant;
mod assistant_settings;
pub use assistant::AssistantPanel;
use gpui::AppContext;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
// Data types for chat completion requests
#[derive(Debug, Serialize)]
struct OpenAIRequest {
model: String,
messages: Vec<RequestMessage>,
stream: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RequestMessage {
role: Role,
content: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ResponseMessage {
role: Option<Role>,
content: Option<String>,
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Role {
User,
Assistant,
System,
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
}
}
}
impl Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "User"),
Role::Assistant => write!(f, "Assistant"),
Role::System => write!(f, "System"),
}
}
}
#[derive(Deserialize, Debug)]
struct OpenAIResponseStreamEvent {
pub id: Option<String>,
pub object: String,
pub created: u32,
pub model: String,
pub choices: Vec<ChatChoiceDelta>,
pub usage: Option<Usage>,
}
#[derive(Deserialize, Debug)]
struct Usage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
}
#[derive(Deserialize, Debug)]
struct ChatChoiceDelta {
pub index: u32,
pub delta: ResponseMessage,
pub finish_reason: Option<String>,
}
#[derive(Deserialize, Debug)]
struct OpenAIUsage {
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
}
#[derive(Deserialize, Debug)]
struct OpenAIChoice {
text: String,
index: u32,
logprobs: Option<serde_json::Value>,
finish_reason: Option<String>,
}
pub fn init(cx: &mut AppContext) {
assistant::init(cx);
}

1960
crates/ai/src/assistant.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
use anyhow;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
Right,
Bottom,
}
#[derive(Deserialize, Debug)]
pub struct AssistantSettings {
pub dock: AssistantDockPosition,
pub default_width: f32,
pub default_height: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
pub dock: Option<AssistantDockPosition>,
pub default_width: Option<f32>,
pub default_height: Option<f32>,
}
impl Setting for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
type FileContent = AssistantSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@@ -1,14 +0,0 @@
[package]
name = "assets"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/assets.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
anyhow.workspace = true
rust-embed = { version = "6.3", features = ["include-exclude"] }

View File

@@ -1,29 +0,0 @@
use std::process::Command;
fn main() {
let output = Command::new("npm")
.current_dir("../../styles")
.args(["install", "--no-save"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"failed to install theme dependencies {}",
String::from_utf8_lossy(&output.stderr)
);
}
let output = Command::new("npm")
.current_dir("../../styles")
.args(["run", "build"])
.output()
.expect("failed to run npm");
if !output.status.success() {
panic!(
"build script failed {}",
String::from_utf8_lossy(&output.stderr)
);
}
println!("cargo:rerun-if-changed=../../styles/src");
}

View File

@@ -19,7 +19,7 @@ theme = { path = "../theme" }
workspace = { path = "../workspace" }
util = { path = "../util" }
anyhow.workspace = true
isahc = "1.7"
isahc.workspace = true
lazy_static.workspace = true
log.workspace = true
serde.workspace = true

View File

@@ -159,10 +159,7 @@ impl Bundle {
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath {
executable: excutable,
..
} => excutable,
Self::LocalPath { executable, .. } => executable,
}
}

View File

@@ -776,15 +776,6 @@ impl Client {
if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some();
if read_from_keychain {
cx.read(|cx| {
self.telemetry().report_mixpanel_event(
"read credentials from keychain",
Default::default(),
*settings::get::<TelemetrySettings>(cx),
);
});
}
}
if credentials.is_none() {
let mut status_rx = self.status();
@@ -1072,11 +1063,8 @@ impl Client {
) -> Task<Result<Credentials>> {
let platform = cx.platform();
let executor = cx.background();
let telemetry = self.telemetry.clone();
let http = self.http.clone();
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by
@@ -1159,12 +1147,6 @@ impl Client {
.context("failed to decrypt access token")?;
platform.activate(true);
telemetry.report_mixpanel_event(
"authenticate with browser",
Default::default(),
telemetry_settings,
);
Ok(Credentials {
user_id: user_id.parse()?,
access_token,

View File

@@ -1,25 +1,13 @@
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
executor::Background,
serde_json::{self, value::Map, Value},
AppContext, Task,
};
use gpui::{executor::Background, serde_json, AppContext, Task};
use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use serde_json::json;
use std::{
env,
io::Write,
mem,
path::PathBuf,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
use util::{channel::ReleaseChannel, TryFutureExt};
use uuid::Uuid;
pub struct Telemetry {
@@ -37,23 +25,15 @@ struct TelemetryState {
os_name: &'static str,
os_version: Option<Arc<str>>,
architecture: &'static str,
mixpanel_events_queue: Vec<MixpanelEvent>,
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
next_mixpanel_event_id: usize,
flush_mixpanel_events_task: Option<Task<()>>,
flush_clickhouse_events_task: Option<Task<()>>,
log_file: Option<NamedTempFile>,
is_staff: Option<bool>,
}
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
lazy_static! {
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
.ok()
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
static ref CLICKHOUSE_EVENTS_URL: String =
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
}
@@ -72,7 +52,6 @@ struct ClickhouseEventRequestBody {
#[derive(Serialize, Debug)]
struct ClickhouseEventWrapper {
time: u128,
signed_in: bool,
#[serde(flatten)]
event: ClickhouseEvent,
@@ -95,47 +74,6 @@ pub enum ClickhouseEvent {
},
}
#[derive(Serialize, Debug)]
struct MixpanelEvent {
event: String,
properties: MixpanelEventProperties,
}
#[derive(Serialize, Debug)]
struct MixpanelEventProperties {
// Mixpanel required fields
#[serde(skip_serializing_if = "str::is_empty")]
token: &'static str,
time: u128,
#[serde(rename = "distinct_id")]
installation_id: Option<Arc<str>>,
#[serde(rename = "$insert_id")]
insert_id: usize,
// Custom fields
#[serde(skip_serializing_if = "Option::is_none", flatten)]
event_properties: Option<Map<String, Value>>,
#[serde(rename = "OS Name")]
os_name: &'static str,
#[serde(rename = "OS Version")]
os_version: Option<Arc<str>>,
#[serde(rename = "Release Channel")]
release_channel: Option<&'static str>,
#[serde(rename = "App Version")]
app_version: Option<Arc<str>>,
#[serde(rename = "Signed In")]
signed_in: bool,
}
#[derive(Serialize)]
struct MixpanelEngageRequest {
#[serde(rename = "$token")]
token: &'static str,
#[serde(rename = "$distinct_id")]
installation_id: Arc<str>,
#[serde(rename = "$set")]
set: Value,
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 1;
@@ -168,29 +106,13 @@ impl Telemetry {
release_channel,
installation_id: None,
metrics_id: None,
mixpanel_events_queue: Default::default(),
clickhouse_events_queue: Default::default(),
flush_mixpanel_events_task: Default::default(),
flush_clickhouse_events_task: Default::default(),
next_mixpanel_event_id: 0,
log_file: None,
is_staff: None,
}),
});
if MIXPANEL_TOKEN.is_some() {
this.executor
.spawn({
let this = this.clone();
async move {
if let Some(tempfile) = NamedTempFile::new().log_err() {
this.state.lock().log_file = Some(tempfile);
}
}
})
.detach();
}
this
}
@@ -218,20 +140,9 @@ impl Telemetry {
let mut state = this.state.lock();
state.installation_id = Some(installation_id.clone());
for event in &mut state.mixpanel_events_queue {
event
.properties
.installation_id
.get_or_insert_with(|| installation_id.clone());
}
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
drop(state);
if has_mixpanel_events {
this.flush_mixpanel_events();
}
drop(state);
if has_clickhouse_events {
this.flush_clickhouse_events();
@@ -256,37 +167,11 @@ impl Telemetry {
return;
}
let this = self.clone();
let mut state = self.state.lock();
let installation_id = state.installation_id.clone();
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
state.metrics_id = metrics_id.clone();
state.is_staff = Some(is_staff);
drop(state);
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
self.executor
.spawn(
async move {
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
token,
installation_id,
set: json!({
"Staff": is_staff,
"ID": metrics_id,
"App": true
}),
}])?;
this.http_client
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}
pub fn report_clickhouse_event(
@@ -300,17 +185,12 @@ impl Telemetry {
let mut state = self.state.lock();
let signed_in = state.metrics_id.is_some();
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
signed_in,
event,
});
state
.clickhouse_events_queue
.push(ClickhouseEventWrapper { signed_in, event });
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_clickhouse_events();
} else {
@@ -324,55 +204,6 @@ impl Telemetry {
}
}
pub fn report_mixpanel_event(
self: &Arc<Self>,
kind: &str,
properties: Value,
telemetry_settings: TelemetrySettings,
) {
if !telemetry_settings.metrics {
return;
}
let mut state = self.state.lock();
let event = MixpanelEvent {
event: kind.into(),
properties: MixpanelEventProperties {
token: "",
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
installation_id: state.installation_id.clone(),
insert_id: post_inc(&mut state.next_mixpanel_event_id),
event_properties: if let Value::Object(properties) = properties {
Some(properties)
} else {
None
},
os_name: state.os_name,
os_version: state.os_version.clone(),
release_channel: state.release_channel,
app_version: state.app_version.clone(),
signed_in: state.metrics_id.is_some(),
},
};
state.mixpanel_events_queue.push(event);
if state.installation_id.is_some() {
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
drop(state);
self.flush_mixpanel_events();
} else {
let this = self.clone();
let executor = self.executor.clone();
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
executor.timer(DEBOUNCE_INTERVAL).await;
this.flush_mixpanel_events();
}));
}
}
}
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().metrics_id.clone()
}
@@ -385,44 +216,6 @@ impl Telemetry {
self.state.lock().is_staff
}
fn flush_mixpanel_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.mixpanel_events_queue);
state.flush_mixpanel_events_task.take();
drop(state);
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
let this = self.clone();
self.executor
.spawn(
async move {
let mut json_bytes = Vec::new();
if let Some(file) = &mut this.state.lock().log_file {
let file = file.as_file_mut();
for event in &mut events {
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, event)?;
file.write_all(&json_bytes)?;
file.write(b"\n")?;
event.properties.token = token;
}
}
json_bytes.clear();
serde_json::to_writer(&mut json_bytes, &events)?;
this.http_client
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
.await?;
anyhow::Ok(())
}
.log_err(),
)
.detach();
}
}
fn flush_clickhouse_events(self: &Arc<Self>) {
let mut state = self.state.lock();
let mut events = mem::take(&mut state.clickhouse_events_queue);

View File

@@ -66,6 +66,7 @@ impl<'a> AddAssign<&'a Local> for Local {
}
}
/// A vector clock
#[derive(Clone, Default, Hash, Eq, PartialEq)]
pub struct Global(SmallVec<[u32; 8]>);

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
version = "0.12.4"
version = "0.14.2"
publish = false
[[bin]]
@@ -76,7 +76,7 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
indoc = "1.0.4"
indoc.workspace = true
util = { path = "../util" }
lazy_static.workspace = true
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }

View File

@@ -104,6 +104,8 @@ spec:
key: secret
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE
value: "1"
- name: RUST_LOG
value: ${RUST_LOG}
- name: LOG_JSON

View File

@@ -76,6 +76,7 @@ CREATE TABLE "worktree_entries" (
"is_symlink" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL,
"git_status" INTEGER,
PRIMARY KEY(project_id, worktree_id, id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
@@ -96,22 +97,16 @@ CREATE TABLE "worktree_repositories" (
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
"path" VARCHAR NOT NULL,
"content" TEXT,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL,

View File

@@ -0,0 +1,10 @@
CREATE TABLE "worktree_settings_files" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"path" VARCHAR NOT NULL,
"content" TEXT NOT NULL,
PRIMARY KEY(project_id, worktree_id, path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");

View File

@@ -0,0 +1,2 @@
ALTER TABLE "worktree_entries"
ADD "git_status" INT8;

View File

@@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
mod worktree_entry;
mod worktree_repository;
mod worktree_repository_statuses;
mod worktree_settings_file;
use crate::executor::Executor;
use crate::{Error, Result};
@@ -1494,6 +1495,7 @@ impl Database {
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
@@ -1537,6 +1539,7 @@ impl Database {
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
@@ -1571,54 +1574,6 @@ impl Database {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
});
}
}
}
// Repository Status Entries
for repository in worktree.updated_repositories.iter_mut() {
let repository_status_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_repository_statuses =
worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id),
)
.add(repository_status_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_repository_statuses.next().await {
let db_status_entry = db_status_entry?;
if db_status_entry.is_deleted {
repository
.removed_repo_paths
.push(db_status_entry.repo_path);
} else {
repository.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
}
@@ -1638,6 +1593,25 @@ impl Database {
})
.collect::<Vec<_>>();
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees
.iter_mut()
.find(|w| w.id == db_settings_file.worktree_id as u64)
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
@@ -2375,6 +2349,7 @@ impl Database {
mtime_nanos: ActiveValue::set(mtime.nanos as i32),
is_symlink: ActiveValue::set(entry.is_symlink),
is_ignored: ActiveValue::set(entry.is_ignored),
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
is_deleted: ActiveValue::set(false),
scan_id: ActiveValue::set(update.scan_id as i64),
}
@@ -2393,6 +2368,7 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::IsSymlink,
worktree_entry::Column::IsIgnored,
worktree_entry::Column::GitStatus,
worktree_entry::Column::ScanId,
])
.to_owned(),
@@ -2446,68 +2422,6 @@ impl Database {
)
.exec(&*tx)
.await?;
for repository in update.updated_repositories.iter() {
if !repository.updated_statuses.is_empty() {
worktree_repository_statuses::Entity::insert_many(
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
}
}),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
worktree_repository_statuses::Column::IsDeleted,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
if !repository.removed_repo_paths.is_empty() {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id as i64),
)
.and(worktree_repository_statuses::Column::RepoPath.is_in(
repository.removed_repo_paths.iter().map(String::as_str),
)),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
}
if !update.removed_repositories.is_empty() {
@@ -2637,6 +2551,58 @@ impl Database {
.await
}
pub async fn update_worktree_settings(
&self,
update: &proto::UpdateWorktreeSettings,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.host_connection()? != connection {
return Err(anyhow!("can't update a project hosted by someone else"))?;
}
if let Some(content) = &update.content {
worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
content: ActiveValue::Set(content.clone()),
})
.on_conflict(
OnConflict::columns([
worktree_settings_file::Column::ProjectId,
worktree_settings_file::Column::WorktreeId,
worktree_settings_file::Column::Path,
])
.update_column(worktree_settings_file::Column::Content)
.to_owned(),
)
.exec(&*tx)
.await?;
} else {
worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
project_id: ActiveValue::Set(project_id),
worktree_id: ActiveValue::Set(update.worktree_id as i64),
path: ActiveValue::Set(update.path.clone()),
..Default::default()
})
.exec(&*tx)
.await?;
}
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
Ok(connection_ids)
})
.await
}
pub async fn join_project(
&self,
project_id: ProjectId,
@@ -2707,6 +2673,7 @@ impl Database {
entries: Default::default(),
repository_entries: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
},
@@ -2738,6 +2705,7 @@ impl Database {
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
@@ -2763,41 +2731,12 @@ impl Database {
proto::RepositoryEntry {
work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
},
);
}
}
}
{
let mut db_status_entries = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_status_entries.next().await {
let db_status_entry = db_status_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
{
if let Some(repository_entry) = worktree
.repository_entries
.get_mut(&(db_status_entry.work_directory_id as u64))
{
repository_entry.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
}
}
}
// Populate worktree diagnostic summaries.
{
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
@@ -2819,6 +2758,25 @@ impl Database {
}
}
// Populate worktree settings files
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) =
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
// Populate language servers.
let language_servers = project
.find_related(language_server::Entity)
@@ -3482,6 +3440,7 @@ pub struct RejoinedWorktree {
pub updated_repositories: Vec<proto::RepositoryEntry>,
pub removed_repositories: Vec<u64>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
@@ -3537,10 +3496,17 @@ pub struct Worktree {
pub entries: Vec<proto::Entry>,
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub settings_files: Vec<WorktreeSettingsFile>,
pub scan_id: u64,
pub completed_scan_id: u64,
}
#[derive(Debug)]
pub struct WorktreeSettingsFile {
pub path: String,
pub content: String,
}
#[cfg(test)]
pub use test::*;

View File

@@ -15,6 +15,7 @@ pub struct Model {
pub inode: i64,
pub mtime_seconds: i64,
pub mtime_nanos: i32,
pub git_status: Option<i64>,
pub is_symlink: bool,
pub is_ignored: bool,
pub is_deleted: bool,

View File

@@ -0,0 +1,19 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_settings_files")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub path: String,
pub content: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -200,6 +200,7 @@ impl Server {
.add_message_handler(start_language_server)
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
.add_request_handler(forward_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -223,6 +224,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(update_buffer_file)
@@ -1087,6 +1089,18 @@ async fn rejoin_room(
},
)?;
}
for settings_file in worktree.settings_files {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project.id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
}
for language_server in &project.language_servers {
@@ -1409,6 +1423,18 @@ async fn join_project(
},
)?;
}
for settings_file in worktree.settings_files {
session.peer.send(
session.connection_id,
proto::UpdateWorktreeSettings {
project_id: project_id.to_proto(),
worktree_id: worktree.id,
path: settings_file.path,
content: Some(settings_file.content),
},
)?;
}
}
for language_server in &project.language_servers {
@@ -1524,6 +1550,29 @@ async fn update_diagnostic_summary(
Ok(())
}
async fn update_worktree_settings(
message: proto::UpdateWorktreeSettings,
session: Session,
) -> Result<()> {
let guest_connection_ids = session
.db()
.await
.update_worktree_settings(&message, session.connection_id)
.await?;
broadcast(
Some(session.connection_id),
guest_connection_ids.iter().copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, message.clone())
},
);
Ok(())
}
async fn start_language_server(
request: proto::StartLanguageServer,
session: Session,

View File

@@ -39,7 +39,12 @@ use std::{
},
};
use unindent::Unindent as _;
use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace};
use workspace::{
dock::{test::TestPanel, DockPosition},
item::{test::TestItem, ItemHandle as _},
shared_screen::SharedScreen,
SplitDirection, Workspace,
};
#[ctor::ctor]
fn init_logger() {
@@ -2415,14 +2420,10 @@ async fn test_git_diff_base_change(
"
.unindent();
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), diff_base.clone())],
)
.await;
client_a.fs.as_fake().set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), diff_base.clone())],
);
// Create the buffer
let buffer_local_a = project_local
@@ -2464,14 +2465,10 @@ async fn test_git_diff_base_change(
);
});
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), new_diff_base.clone())],
)
.await;
client_a.fs.as_fake().set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), new_diff_base.clone())],
);
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
@@ -2513,14 +2510,10 @@ async fn test_git_diff_base_change(
"
.unindent();
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), diff_base.clone())],
)
.await;
client_a.fs.as_fake().set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), diff_base.clone())],
);
// Create the buffer
let buffer_local_b = project_local
@@ -2562,14 +2555,10 @@ async fn test_git_diff_base_change(
);
});
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), new_diff_base.clone())],
)
.await;
client_a.fs.as_fake().set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), new_diff_base.clone())],
);
// Wait for buffer_local_b to receive it
deterministic.run_until_parked();
@@ -2646,8 +2635,7 @@ async fn test_git_branch_name(
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"))
.await;
.set_branch_name(Path::new("/dir/.git"), Some("branch-1"));
// Wait for it to catch up to the new branch
deterministic.run_until_parked();
@@ -2673,8 +2661,7 @@ async fn test_git_branch_name(
client_a
.fs
.as_fake()
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"))
.await;
.set_branch_name(Path::new("/dir/.git"), Some("branch-2"));
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
@@ -2726,17 +2713,13 @@ async fn test_git_status_sync(
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
client_a
.fs
.as_fake()
.set_status_for_repo(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Added),
(&Path::new(B_TXT), GitFileStatus::Added),
],
)
.await;
client_a.fs.as_fake().set_status_for_repo_via_git_operation(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Added),
(&Path::new(B_TXT), GitFileStatus::Added),
],
);
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
@@ -2763,8 +2746,7 @@ async fn test_git_status_sync(
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
let root_entry = snapshot.root_git_entry().unwrap();
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
assert_eq!(snapshot.status_for_file(file), status);
}
// Smoke test status reading
@@ -2780,14 +2762,13 @@ async fn test_git_status_sync(
client_a
.fs
.as_fake()
.set_status_for_repo(
.set_status_for_repo_via_working_copy_change(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Modified),
(&Path::new(B_TXT), GitFileStatus::Modified),
],
)
.await;
);
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
@@ -3114,6 +3095,135 @@ async fn test_fs_operations(
});
}
#[gpui::test(iterations = 10)]
async fn test_local_settings(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// As client A, open a project that contains some local settings files
client_a
.fs
.insert_tree(
"/dir",
json!({
".zed": {
"settings.json": r#"{ "tab_size": 2 }"#
},
"a": {
".zed": {
"settings.json": r#"{ "tab_size": 8 }"#
},
"a.txt": "a-contents",
},
"b": {
"b.txt": "b-contents",
}
}),
)
.await;
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// As client B, join that project and observe the local settings.
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, update a settings file. As Client B, see the changed settings.
client_a
.fs
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("").into(), r#"{}"#.to_string()),
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
]
)
});
// As client A, create and remove some settings files. As client B, see the changed settings.
client_a
.fs
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
client_a
.fs
.create_dir("/dir/b/.zed".as_ref())
.await
.unwrap();
client_a
.fs
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
.await;
deterministic.run_until_parked();
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
]
)
});
// As client B, disconnect.
server.forbid_connections();
server.disconnect_client(client_b.peer_id().unwrap());
// As client A, change and remove settings files while client B is disconnected.
client_a
.fs
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
.await;
client_a
.fs
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
.await
.unwrap();
deterministic.run_until_parked();
// As client B, reconnect and see the changed settings.
server.allow_connections();
deterministic.advance_clock(RECEIVE_TIMEOUT);
cx_b.read(|cx| {
let store = cx.global::<SettingsStore>();
assert_eq!(
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
)
});
}
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(
deterministic: Arc<Deterministic>,
@@ -5010,19 +5120,21 @@ async fn test_project_symbols(
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
#[allow(deprecated)]
Ok(Some(vec![lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
fake_language_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(|_, _| async move {
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
#[allow(deprecated)]
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
tags: None,
container_name: None,
deprecated: None,
},
kind: lsp::SymbolKind::CONSTANT,
tags: None,
container_name: None,
deprecated: None,
}]))
])))
});
// Request the definition of a symbol as the guest.
@@ -6606,7 +6718,7 @@ async fn test_basic_following(
// When client A navigates back and forth, client B does so as well.
workspace_a
.update(cx_a, |workspace, cx| {
workspace::Pane::go_back(workspace, None, cx)
workspace.go_back(workspace.active_pane().downgrade(), cx)
})
.await
.unwrap();
@@ -6617,7 +6729,7 @@ async fn test_basic_following(
workspace_a
.update(cx_a, |workspace, cx| {
workspace::Pane::go_back(workspace, None, cx)
workspace.go_back(workspace.active_pane().downgrade(), cx)
})
.await
.unwrap();
@@ -6628,7 +6740,7 @@ async fn test_basic_following(
workspace_a
.update(cx_a, |workspace, cx| {
workspace::Pane::go_forward(workspace, None, cx)
workspace.go_forward(workspace.active_pane().downgrade(), cx)
})
.await
.unwrap();
@@ -6740,12 +6852,43 @@ async fn test_basic_following(
)
});
// Client B activates an external window again, and the previously-opened screen-sharing item
// gets activated.
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.add_view(workspace_b.window_id(), |_| {
TestPanel::new(DockPosition::Left)
});
workspace_b.update(cx_b, |workspace, cx| {
workspace.add_panel(panel, cx);
workspace.toggle_panel_focus::<TestPanel>(cx);
});
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
.active_item(cx)
.unwrap()
.id()),
shared_screen.id()
);
// Toggling the focus back to the pane causes client A to return to the multibuffer.
workspace_b.update(cx_b, |workspace, cx| {
workspace.toggle_panel_focus::<TestPanel>(cx);
});
deterministic.run_until_parked();
workspace_a.read_with(cx_a, |workspace, cx| {
assert_eq!(
workspace.active_item(cx).unwrap().id(),
multibuffer_editor_a.id()
)
});
// Client B activates an item that doesn't implement following,
// so the previously-opened screen-sharing item gets activated.
let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new());
workspace_b.update(cx_b, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
})
});
deterministic.run_until_parked();
assert_eq!(
workspace_a.read_with(cx_a, |workspace, cx| workspace
@@ -7377,6 +7520,265 @@ async fn test_peers_simultaneously_following_each_other(
});
}
#[gpui::test(iterations = 10)]
async fn test_on_input_format_from_host_to_guest(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
first_trigger_character: ":".to_string(),
more_trigger_character: Some(vec![">".to_string()]),
}),
..Default::default()
},
..Default::default()
}))
.await;
client_a.language_registry.add(Arc::new(language));
client_a
.fs
.insert_tree(
"/a",
json!({
"main.rs": "fn main() { a }",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
// Open a file in an editor as the host.
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
let (window_a, _) = cx_a.add_window(|_| EmptyView);
let editor_a = cx_a.add_view(window_a, |cx| {
Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_b.foreground().run_until_parked();
// Receive an OnTypeFormatting request as the host's language server.
// Return some formattings from the host's language server.
fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
);
assert_eq!(
params.text_document_position.position,
lsp::Position::new(0, 14),
);
Ok(Some(vec![lsp::TextEdit {
new_text: "~<".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
}]))
},
);
// Open the buffer on the guest and see that the formattings worked
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
// Type a on type formatting trigger character as the guest.
editor_a.update(cx_a, |editor, cx| {
cx.focus(&editor_a);
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(">", cx);
});
cx_b.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a>~< }")
});
// Undo should remove LSP edits first
editor_a.update(cx_a, |editor, cx| {
assert_eq!(editor.text(cx), "fn main() { a>~< }");
editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a> }");
});
cx_b.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a> }")
});
editor_a.update(cx_a, |editor, cx| {
assert_eq!(editor.text(cx), "fn main() { a> }");
editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a }");
});
cx_b.foreground().run_until_parked();
buffer_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a }")
});
}
#[gpui::test(iterations = 10)]
async fn test_on_input_format_from_guest_to_host(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
first_trigger_character: ":".to_string(),
more_trigger_character: Some(vec![">".to_string()]),
}),
..Default::default()
},
..Default::default()
}))
.await;
client_a.language_registry.add(Arc::new(language));
client_a
.fs
.insert_tree(
"/a",
json!({
"main.rs": "fn main() { a }",
"other.rs": "// Test file",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
// Open a file in an editor as the guest.
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
let (window_b, _) = cx_b.add_window(|_| EmptyView);
let editor_b = cx_b.add_view(window_b, |cx| {
Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
});
let fake_language_server = fake_language_servers.next().await.unwrap();
cx_a.foreground().run_until_parked();
// Type a on type formatting trigger character as the guest.
editor_b.update(cx_b, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(":", cx);
cx.focus(&editor_b);
});
// Receive an OnTypeFormatting request as the host's language server.
// Return some formattings from the host's language server.
cx_a.foreground().start_waiting();
fake_language_server
.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
);
assert_eq!(
params.text_document_position.position,
lsp::Position::new(0, 14),
);
Ok(Some(vec![lsp::TextEdit {
new_text: "~:".to_string(),
range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
}]))
})
.next()
.await
.unwrap();
cx_a.foreground().finish_waiting();
// Open the buffer on the host and see that the formattings worked
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
.await
.unwrap();
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a:~: }")
});
// Undo should remove LSP edits first
editor_b.update(cx_b, |editor, cx| {
assert_eq!(editor.text(cx), "fn main() { a:~: }");
editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a: }");
});
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a: }")
});
editor_b.update(cx_b, |editor, cx| {
assert_eq!(editor.text(cx), "fn main() { a: }");
editor.undo(&Undo, cx);
assert_eq!(editor.text(cx), "fn main() { a }");
});
cx_a.foreground().run_until_parked();
buffer_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.text(), "fn main() { a }")
});
}
#[derive(Debug, Eq, PartialEq)]
struct RoomParticipants {
remote: Vec<String>,

View File

@@ -422,7 +422,7 @@ async fn apply_client_operation(
);
ensure_project_shared(&project, client, cx).await;
if !client.fs.paths().contains(&new_root_path) {
if !client.fs.paths(false).contains(&new_root_path) {
client.fs.create_dir(&new_root_path).await.unwrap();
}
project
@@ -628,12 +628,13 @@ async fn apply_client_operation(
ensure_project_shared(&project, client, cx).await;
let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
let save = project.update(cx, |project, cx| project.save_buffer(buffer, cx));
let save = cx.background().spawn(async move {
let (saved_version, _, _) = save
.await
let save = project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
let save = cx.spawn(|cx| async move {
save.await
.map_err(|err| anyhow!("save request failed: {:?}", err))?;
assert!(saved_version.observed_all(&requested_version));
assert!(buffer
.read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
.observed_all(&requested_version));
anyhow::Ok(())
});
if detach {
@@ -743,7 +744,7 @@ async fn apply_client_operation(
} => {
if !client
.fs
.directories()
.directories(false)
.contains(&path.parent().unwrap().to_owned())
{
return Err(TestError::Inapplicable);
@@ -770,10 +771,16 @@ async fn apply_client_operation(
repo_path,
contents,
} => {
if !client.fs.directories().contains(&repo_path) {
if !client.fs.directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
for (path, _) in contents.iter() {
if !client.fs.files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable);
}
}
log::info!(
"{}: writing git index for repo {:?}: {:?}",
client.username,
@@ -789,13 +796,13 @@ async fn apply_client_operation(
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
client.fs.set_index_for_repo(&dot_git_dir, &contents);
}
GitOperation::WriteGitBranch {
repo_path,
new_branch,
} => {
if !client.fs.directories().contains(&repo_path) {
if !client.fs.directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
@@ -810,15 +817,21 @@ async fn apply_client_operation(
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
client.fs.set_branch_name(&dot_git_dir, new_branch);
}
GitOperation::WriteGitStatuses {
repo_path,
statuses,
git_operation,
} => {
if !client.fs.directories().contains(&repo_path) {
if !client.fs.directories(false).contains(&repo_path) {
return Err(TestError::Inapplicable);
}
for (path, _) in statuses.iter() {
if !client.fs.files().contains(&repo_path.join(path)) {
return Err(TestError::Inapplicable);
}
}
log::info!(
"{}: writing git statuses for repo {:?}: {:?}",
@@ -838,10 +851,16 @@ async fn apply_client_operation(
client.fs.create_dir(&dot_git_dir).await?;
}
client
.fs
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
.await;
if git_operation {
client
.fs
.set_status_for_repo_via_git_operation(&dot_git_dir, statuses.as_slice());
} else {
client.fs.set_status_for_repo_via_working_copy_change(
&dot_git_dir,
statuses.as_slice(),
);
}
}
},
}
@@ -913,9 +932,10 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
assert_eq!(
guest_snapshot.entries(false).collect::<Vec<_>>(),
host_snapshot.entries(false).collect::<Vec<_>>(),
"{} has different snapshot than the host for worktree {:?} and project {:?}",
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
client.username,
host_snapshot.abs_path(),
id,
guest_project.remote_id(),
);
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
@@ -1230,6 +1250,7 @@ enum GitOperation {
WriteGitStatuses {
repo_path: PathBuf,
statuses: Vec<(PathBuf, GitFileStatus)>,
git_operation: bool,
},
}
@@ -1575,7 +1596,7 @@ impl TestPlan {
.choose(&mut self.rng)
.cloned() else { continue };
let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs.paths();
let mut paths = client.fs.paths(false);
paths.remove(0);
let new_root_path = if paths.is_empty() || self.rng.gen() {
Path::new("/").join(&self.next_root_dir_name(user_id))
@@ -1755,7 +1776,7 @@ impl TestPlan {
let is_dir = self.rng.gen::<bool>();
let content;
let mut path;
let dir_paths = client.fs.directories();
let dir_paths = client.fs.directories(false);
if is_dir {
content = String::new();
@@ -1809,7 +1830,7 @@ impl TestPlan {
let repo_path = client
.fs
.directories()
.directories(false)
.choose(&mut self.rng)
.unwrap()
.clone();
@@ -1855,9 +1876,12 @@ impl TestPlan {
})
.collect::<Vec<_>>();
let git_operation = self.rng.gen::<bool>();
GitOperation::WriteGitStatuses {
repo_path,
statuses,
git_operation,
}
}
_ => unreachable!(),

View File

@@ -472,7 +472,7 @@ impl CollabTitlebarItem {
Stack::new()
.with_child(
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistant width for both text variations
//TODO: Ensure this button has consistent width for both text variations
let style = titlebar.share_button.style_for(state, false);
Label::new(label, style.text.clone())
.contained()

View File

@@ -4,7 +4,7 @@ mod sign_in;
use anyhow::{anyhow, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use collections::HashMap;
use collections::{HashMap, HashSet};
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
use gpui::{
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
@@ -127,7 +127,7 @@ impl CopilotServer {
struct RunningCopilotServer {
lsp: Arc<LanguageServer>,
sign_in_status: SignInStatus,
registered_buffers: HashMap<u64, RegisteredBuffer>,
registered_buffers: HashMap<usize, RegisteredBuffer>,
}
#[derive(Clone, Debug)]
@@ -163,7 +163,6 @@ impl Status {
}
struct RegisteredBuffer {
id: u64,
uri: lsp::Url,
language_id: String,
snapshot: BufferSnapshot,
@@ -178,13 +177,13 @@ impl RegisteredBuffer {
buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Copilot>,
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
let id = self.id;
let (done_tx, done_rx) = oneshot::channel();
if buffer.read(cx).version() == self.snapshot.version {
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
} else {
let buffer = buffer.downgrade();
let id = buffer.id();
let prev_pending_change =
mem::replace(&mut self.pending_buffer_change, Task::ready(None));
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
@@ -268,7 +267,7 @@ pub struct Copilot {
http: Arc<dyn HttpClient>,
node_runtime: Arc<NodeRuntime>,
server: CopilotServer,
buffers: HashMap<u64, WeakModelHandle<Buffer>>,
buffers: HashSet<WeakModelHandle<Buffer>>,
}
impl Entity for Copilot {
@@ -318,7 +317,7 @@ impl Copilot {
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
if all_language_settings(cx).copilot_enabled(None, None) {
if all_language_settings(None, cx).copilot_enabled(None, None) {
if matches!(self.server, CopilotServer::Disabled) {
let start_task = cx
.spawn({
@@ -375,7 +374,7 @@ impl Copilot {
server
.on_notification::<LogMessage, _>(|params, _cx| {
match params.level {
// Copilot is pretty agressive about logging
// Copilot is pretty aggressive about logging
0 => debug!("copilot: {}", params.message),
1 => debug!("copilot: {}", params.message),
_ => error!("copilot: {}", params.message),
@@ -559,8 +558,8 @@ impl Copilot {
}
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
let buffer_id = buffer.read(cx).remote_id();
self.buffers.insert(buffer_id, buffer.downgrade());
let weak_buffer = buffer.downgrade();
self.buffers.insert(weak_buffer.clone());
if let CopilotServer::Running(RunningCopilotServer {
lsp: server,
@@ -573,8 +572,7 @@ impl Copilot {
return;
}
let buffer_id = buffer.read(cx).remote_id();
registered_buffers.entry(buffer_id).or_insert_with(|| {
registered_buffers.entry(buffer.id()).or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
@@ -592,7 +590,6 @@ impl Copilot {
.log_err();
RegisteredBuffer {
id: buffer_id,
uri,
language_id,
snapshot,
@@ -603,8 +600,8 @@ impl Copilot {
this.handle_buffer_event(buffer, event, cx).log_err();
}),
cx.observe_release(buffer, move |this, _buffer, _cx| {
this.buffers.remove(&buffer_id);
this.unregister_buffer(buffer_id);
this.buffers.remove(&weak_buffer);
this.unregister_buffer(&weak_buffer);
}),
],
}
@@ -619,8 +616,7 @@ impl Copilot {
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Ok(server) = self.server.as_running() {
let buffer_id = buffer.read(cx).remote_id();
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
match event {
language::Event::Edited => {
let _ = registered_buffer.report_changes(&buffer, cx);
@@ -674,9 +670,9 @@ impl Copilot {
Ok(())
}
fn unregister_buffer(&mut self, buffer_id: u64) {
fn unregister_buffer(&mut self, buffer: &WeakModelHandle<Buffer>) {
if let Ok(server) = self.server.as_running() {
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) {
server
.lsp
.notify::<lsp::notification::DidCloseTextDocument>(
@@ -779,16 +775,12 @@ impl Copilot {
Err(error) => return Task::ready(Err(error)),
};
let lsp = server.lsp.clone();
let buffer_id = buffer.read(cx).remote_id();
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer);
let settings = language_settings(
buffer.language_at(position).map(|l| l.name()).as_deref(),
cx,
);
let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs;
let relative_path = buffer
@@ -853,7 +845,7 @@ impl Copilot {
lsp_status: request::SignInStatus,
cx: &mut ModelContext<Self>,
) {
self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
self.buffers.retain(|buffer| buffer.is_upgradable(cx));
if let Ok(server) = self.server.as_running() {
match lsp_status {
@@ -861,7 +853,7 @@ impl Copilot {
| request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => {
server.sign_in_status = SignInStatus::Authorized;
for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
if let Some(buffer) = buffer.upgrade(cx) {
self.register_buffer(&buffer, cx);
}
@@ -869,14 +861,14 @@ impl Copilot {
}
request::SignInStatus::NotAuthorized { .. } => {
server.sign_in_status = SignInStatus::Unauthorized;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id);
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);
}
}
request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
self.unregister_buffer(buffer_id);
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);
}
}
}
@@ -899,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
} else {
format!("buffer://{}", buffer.read(cx).remote_id())
.parse()
.unwrap()
format!("buffer://{}", buffer.id()).parse().unwrap()
}
}
@@ -1175,6 +1165,10 @@ mod tests {
fn to_proto(&self) -> rpc::proto::File {
unimplemented!()
}
fn worktree_id(&self) -> usize {
0
}
}
impl language::LocalFile for File {

View File

@@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
doctest = false
[dependencies]
assets = { path = "../assets" }
copilot = { path = "../copilot" }
editor = { path = "../editor" }
fs = { path = "../fs" }

View File

@@ -9,7 +9,10 @@ use gpui::{
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::language_settings::{self, all_language_settings, AllLanguageSettings};
use language::{
language_settings::{self, all_language_settings, AllLanguageSettings},
File, Language,
};
use settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
@@ -26,8 +29,8 @@ pub struct CopilotButton {
popup_menu: ViewHandle<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<str>>,
path: Option<Arc<Path>>,
language: Option<Arc<Language>>,
file: Option<Arc<dyn File>>,
fs: Arc<dyn Fs>,
}
@@ -41,7 +44,7 @@ impl View for CopilotButton {
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let all_language_settings = &all_language_settings(cx);
let all_language_settings = all_language_settings(None, cx);
if !all_language_settings.copilot.feature_enabled {
return Empty::new().into_any();
}
@@ -165,7 +168,7 @@ impl CopilotButton {
editor_subscription: None,
editor_enabled: None,
language: None,
path: None,
file: None,
fs,
}
}
@@ -197,14 +200,13 @@ impl CopilotButton {
if let Some(language) = self.language.clone() {
let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(language.as_ref()), cx)
.show_copilot_suggestions;
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
.show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" },
language
language.name()
),
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
));
@@ -212,9 +214,9 @@ impl CopilotButton {
let settings = settings::get::<AllLanguageSettings>(cx);
if let Some(path) = self.path.as_ref() {
let path_enabled = settings.copilot_enabled_for_path(path);
let path = path.clone();
if let Some(file) = &self.file {
let path = file.path().clone();
let path_enabled = settings.copilot_enabled_for_path(&path);
menu_options.push(ContextMenuItem::handler(
format!(
"{} Suggestions for This Path",
@@ -276,17 +278,15 @@ impl CopilotButton {
let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot
.language_at(suggestion_anchor)
.map(|language| language.name());
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
let language = snapshot.language_at(suggestion_anchor);
let file = snapshot.file_at(suggestion_anchor).cloned();
self.editor_enabled = Some(
all_language_settings(cx)
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
all_language_settings(self.file.as_ref(), cx)
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
);
self.language = language_name;
self.path = path.cloned();
self.language = language.cloned();
self.file = file;
cx.notify()
}
@@ -315,9 +315,7 @@ async fn configure_disabled_globs(
let settings_editor = workspace
.update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || {
settings::initial_user_settings_content(&assets::Assets)
.as_ref()
.into()
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
@@ -363,17 +361,18 @@ async fn configure_disabled_globs(
}
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
});
}
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions =
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file.languages
.entry(language)
.entry(language.name())
.or_default()
.show_copilot_suggestions = Some(!show_copilot_suggestions);
});

View File

@@ -18,7 +18,7 @@ sqlez = { path = "../sqlez" }
sqlez_macros = { path = "../sqlez_macros" }
util = { path = "../util" }
anyhow.workspace = true
indoc = "1.0.4"
indoc.workspace = true
async-trait.workspace = true
lazy_static.workspace = true
log.workspace = true

View File

@@ -430,7 +430,7 @@ impl ProjectDiagnosticsEditor {
});
self.editor.update(cx, |editor, cx| {
editor.remove_blocks(blocks_to_remove, cx);
editor.remove_blocks(blocks_to_remove, None, cx);
let block_ids = editor.insert_blocks(
blocks_to_add.into_iter().map(|block| {
let (excerpt_id, text_anchor) = block.position;
@@ -442,6 +442,7 @@ impl ProjectDiagnosticsEditor {
disposition: block.disposition,
}
}),
Some(Autoscroll::fit()),
cx,
);
@@ -609,15 +610,6 @@ impl Item for ProjectDiagnosticsEditor {
unreachable!()
}
fn git_diff_recalc(
&mut self,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.editor
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
Editor::to_item_events(event)
}
@@ -1508,6 +1500,7 @@ mod tests {
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
});
}
@@ -1516,7 +1509,8 @@ mod tests {
let snapshot = editor.snapshot(cx);
snapshot
.blocks_in_range(0..snapshot.max_point().row())
.filter_map(|(row, block)| {
.enumerate()
.filter_map(|(ix, (row, block))| {
let name = match block {
TransformBlock::Custom(block) => block
.render(&mut BlockContext {
@@ -1527,6 +1521,7 @@ mod tests {
gutter_width: 0.,
line_height: 0.,
em_width: 0.,
block_id: ix,
})
.name()?
.to_string(),

View File

@@ -83,7 +83,7 @@ ctor.workspace = true
env_logger.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter = "0.20"
tree-sitter.workspace = true
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }

View File

@@ -272,12 +272,11 @@ impl DisplayMap {
}
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let language_name = buffer
let language = buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).language())
.map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
.and_then(|buffer| buffer.read(cx).language());
language_settings(language.as_deref(), None, cx).tab_size
}
#[cfg(test)]
@@ -476,7 +475,7 @@ impl DisplaySnapshot {
})
}
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
/// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn find_while<'a>(
&'a self,
@@ -487,7 +486,7 @@ impl DisplaySnapshot {
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
}
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
/// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn reverse_find_while<'a>(
&'a self,

View File

@@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> {
pub gutter_padding: f32,
pub em_width: f32,
pub line_height: f32,
pub block_id: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -243,7 +244,7 @@ impl BlockMap {
// Preserve any old transforms that precede this edit.
let old_start = WrapRow(edit.old.start);
let new_start = WrapRow(edit.new.start);
new_transforms.push_tree(cursor.slice(&old_start, Bias::Left, &()), &());
new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
if let Some(transform) = cursor.item() {
if transform.is_isomorphic() && old_start == cursor.end(&()) {
new_transforms.push(transform.clone(), &());
@@ -425,7 +426,7 @@ impl BlockMap {
push_isomorphic(&mut new_transforms, extent_after_edit);
}
new_transforms.push_tree(cursor.suffix(&()), &());
new_transforms.append(cursor.suffix(&()), &());
debug_assert_eq!(
new_transforms.summary().input_rows,
wrap_snapshot.max_point().row() + 1

View File

@@ -115,10 +115,10 @@ impl<'a> FoldMapWriter<'a> {
let mut new_tree = SumTree::new();
let mut cursor = self.0.folds.cursor::<Fold>();
for fold in folds {
new_tree.push_tree(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
new_tree.append(cursor.slice(&fold, Bias::Right, &buffer), &buffer);
new_tree.push(fold, &buffer);
}
new_tree.push_tree(cursor.suffix(&buffer), &buffer);
new_tree.append(cursor.suffix(&buffer), &buffer);
new_tree
};
@@ -165,10 +165,10 @@ impl<'a> FoldMapWriter<'a> {
let mut cursor = self.0.folds.cursor::<usize>();
let mut folds = SumTree::new();
for fold_ix in fold_ixs_to_delete {
folds.push_tree(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
folds.append(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer);
cursor.next(&buffer);
}
folds.push_tree(cursor.suffix(&buffer), &buffer);
folds.append(cursor.suffix(&buffer), &buffer);
folds
};
@@ -302,7 +302,7 @@ impl FoldMap {
cursor.seek(&0, Bias::Right, &());
while let Some(mut edit) = buffer_edits_iter.next() {
new_transforms.push_tree(cursor.slice(&edit.old.start, Bias::Left, &()), &());
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
edit.new.start -= edit.old.start - cursor.start();
edit.old.start = *cursor.start();
@@ -412,7 +412,7 @@ impl FoldMap {
}
}
new_transforms.push_tree(cursor.suffix(&()), &());
new_transforms.append(cursor.suffix(&()), &());
if new_transforms.is_empty() {
let text_summary = new_buffer.text_summary();
new_transforms.push(

View File

@@ -353,7 +353,7 @@ impl WrapSnapshot {
}
old_cursor.next(&());
new_transforms.push_tree(
new_transforms.append(
old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
&(),
);
@@ -366,7 +366,7 @@ impl WrapSnapshot {
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.push_tree(old_cursor.suffix(&()), &());
new_transforms.append(old_cursor.suffix(&()), &());
}
}
}
@@ -500,7 +500,7 @@ impl WrapSnapshot {
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.push_tree(
new_transforms.append(
old_cursor.slice(
&TabPoint::new(next_edit.old_rows.start, 0),
Bias::Right,
@@ -517,7 +517,7 @@ impl WrapSnapshot {
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.push_tree(old_cursor.suffix(&()), &());
new_transforms.append(old_cursor.suffix(&()), &());
}
}
}

View File

@@ -10,7 +10,7 @@ pub mod items;
mod link_go_to_definition;
mod mouse_context_menu;
pub mod movement;
mod multi_buffer;
pub mod multi_buffer;
mod persistence;
pub mod scroll;
pub mod selections_collection;
@@ -31,7 +31,9 @@ use copilot::Copilot;
pub use display_map::DisplayPoint;
use display_map::*;
pub use editor_settings::EditorSettings;
pub use element::*;
pub use element::{
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
};
use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -44,9 +46,9 @@ use gpui::{
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
serde_json::{self, json},
AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity,
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -109,6 +111,12 @@ pub struct SelectNext {
pub replace_newest: bool,
}
#[derive(Clone, Deserialize, PartialEq, Default)]
pub struct SelectPrevious {
#[serde(default)]
pub replace_newest: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct SelectToBeginningOfLine {
#[serde(default)]
@@ -270,6 +278,7 @@ impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectToBeginningOfLine,
SelectToEndOfLine,
ToggleCodeActions,
@@ -365,6 +374,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::add_selection_above);
cx.add_action(Editor::add_selection_below);
cx.add_action(Editor::select_next);
cx.add_action(Editor::select_previous);
cx.add_action(Editor::toggle_comments);
cx.add_action(Editor::select_larger_syntax_node);
cx.add_action(Editor::select_smaller_syntax_node);
@@ -482,6 +492,7 @@ pub struct Editor {
columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
selection_history: SelectionHistory,
autoclose_regions: Vec<AutocloseRegion>,
snippet_stack: InvalidationStack<SnippetState>,
@@ -496,6 +507,7 @@ pub struct Editor {
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
mode: EditorMode,
show_gutter: bool,
placeholder_text: Option<Arc<str>>,
highlighted_rows: Option<Range<u32>>,
#[allow(clippy::type_complexity)]
@@ -526,6 +538,7 @@ pub struct Editor {
pub struct EditorSnapshot {
pub mode: EditorMode,
pub show_gutter: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
@@ -537,6 +550,7 @@ pub struct EditorSnapshot {
struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>,
select_next_state: Option<SelectNextState>,
select_prev_state: Option<SelectNextState>,
add_selections_state: Option<AddSelectionsState>,
}
@@ -1284,6 +1298,7 @@ impl Editor {
columnar_selection_tail: None,
add_selections_state: None,
select_next_state: None,
select_prev_state: None,
selection_history: Default::default(),
autoclose_regions: Default::default(),
snippet_stack: Default::default(),
@@ -1297,6 +1312,7 @@ impl Editor {
blink_manager: blink_manager.clone(),
show_local_selections: true,
mode,
show_gutter: mode == EditorMode::Full,
placeholder_text: None,
highlighted_rows: None,
background_highlights: Default::default(),
@@ -1393,6 +1409,7 @@ impl Editor {
pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
@@ -1505,6 +1522,7 @@ impl Editor {
let buffer = &display_map.buffer_snapshot;
self.add_selections_state = None;
self.select_next_state = None;
self.select_prev_state = None;
self.select_larger_syntax_node_stack.clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
self.snippet_stack
@@ -2122,6 +2140,15 @@ impl Editor {
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
// When buffer contents is updated and caret is moved, try triggering on type formatting.
if settings::get::<EditorSettings>(cx).use_on_type_format {
if let Some(on_type_format_task) =
this.trigger_on_type_formatting(text.to_string(), cx)
{
on_type_format_task.detach_and_log_err(cx);
}
}
if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) {
@@ -2138,8 +2165,8 @@ impl Editor {
self.transact(cx, |this, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx).snapshot(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = multi_buffer.snapshot(cx);
selections
.iter()
.map(|selection| {
@@ -2148,9 +2175,11 @@ impl Editor {
indent.len = cmp::min(indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let mut insert_extra_newline = false;
if let Some(language) = buffer.language_scope_at(start) {
let is_cursor = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
&language_scope
{
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
@@ -2163,25 +2192,71 @@ impl Editor {
.map(|c| c.len_utf8())
.sum::<usize>();
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
let insert_extra_newline =
language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
enabled
&& pair.newline
&& buffer
.contains_str_at(end + trailing_whitespace_len, pair_end)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
pair_start,
)
enabled
&& pair.newline
&& buffer.contains_str_at(
end + trailing_whitespace_len,
pair_end,
)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
pair_start,
)
});
// Comment extension on newline is allowed only for cursor selections
let comment_delimiter = language.line_comment_prefix().filter(|_| {
let is_comment_extension_enabled =
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
is_cursor && is_comment_extension_enabled
});
}
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
buffer
.buffer_line_for_row(start_point.row)
.is_some_and(|(snapshot, range)| {
let mut index_of_first_non_whitespace = 0;
let line_starts_with_comment = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.take(delimiter.len())
.eq(delimiter.chars());
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + delimiter.len()
<= start_point.column as usize;
line_starts_with_comment
&& cursor_is_placed_after_comment_marker
})
.then(|| delimiter.clone())
} else {
None
};
(comment_delimiter, insert_extra_newline)
} else {
(None, false)
};
let mut new_text = String::with_capacity(1 + indent.len as usize);
new_text.push('\n');
let capacity_for_delimiter = comment_delimiter
.as_deref()
.map(str::len)
.unwrap_or_default();
let mut new_text =
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
new_text.push_str("\n");
new_text.extend(indent.chars());
if let Some(delimiter) = &comment_delimiter {
new_text.push_str(&delimiter);
}
if insert_extra_newline {
new_text = new_text.repeat(2);
}
@@ -2363,7 +2438,7 @@ impl Editor {
old_selections
.iter()
.map(|s| {
let anchor = snapshot.anchor_after(s.end);
let anchor = snapshot.anchor_after(s.head());
s.map(|_| anchor)
})
.collect::<Vec<_>>()
@@ -2500,6 +2575,52 @@ impl Editor {
}
}
fn trigger_on_type_formatting(
&self,
input: String,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if input.len() != 1 {
return None;
}
let project = self.project.as_ref()?;
let position = self.selections.newest_anchor().head();
let (buffer, buffer_position) = self
.buffer
.read(cx)
.text_anchor_for_position(position.clone(), cx)?;
// OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
// hence we do LSP request & edit on host side only — add formats to host's history.
let push_to_lsp_host_history = true;
// If this is not the host, append its history with new edits.
let push_to_client_history = project.read(cx).is_remote();
let on_type_formatting = project.update(cx, |project, cx| {
project.on_type_format(
buffer.clone(),
buffer_position,
input,
push_to_lsp_host_history,
cx,
)
});
Some(cx.spawn(|editor, mut cx| async move {
if let Some(transaction) = on_type_formatting.await? {
if push_to_client_history {
buffer.update(&mut cx, |buffer, _| {
buffer.push_transaction(transaction, Instant::now());
});
}
editor.update(&mut cx, |editor, cx| {
editor.refresh_document_highlights(cx);
})?;
}
Ok(())
}))
}
fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
if self.pending_rename.is_some() {
return;
@@ -3152,12 +3273,10 @@ impl Editor {
snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>,
) -> bool {
let path = snapshot.file_at(location).map(|file| file.path().as_ref());
let language_name = snapshot
.language_at(location)
.map(|language| language.name());
let settings = all_language_settings(cx);
settings.copilot_enabled(language_name.as_deref(), path)
let file = snapshot.file_at(location);
let language = snapshot.language_at(location);
let settings = all_language_settings(file, cx);
settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
}
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@@ -3494,7 +3613,9 @@ impl Editor {
s.move_with(|map, selection| {
if selection.is_empty() && !line_mode {
let cursor = movement::right(map, selection.head());
selection.set_head(cursor, SelectionGoal::None);
selection.end = cursor;
selection.reversed = true;
selection.goal = SelectionGoal::None;
}
})
});
@@ -4876,12 +4997,12 @@ impl Editor {
}
fn push_to_nav_history(
&self,
&mut self,
cursor_anchor: Anchor,
new_position: Option<Point>,
cx: &mut ViewContext<Self>,
) {
if let Some(nav_history) = &self.nav_history {
if let Some(nav_history) = self.nav_history.as_mut() {
let buffer = self.buffer.read(cx).read(cx);
let cursor_position = cursor_anchor.to_point(&buffer);
let scroll_state = self.scroll_manager.anchor();
@@ -5158,6 +5279,101 @@ impl Editor {
}
}
pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
self.push_to_selection_history();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let mut selections = self.selections.all::<usize>(cx);
if let Some(mut select_prev_state) = self.select_prev_state.take() {
let query = &select_prev_state.query;
if !select_prev_state.done {
let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
let mut next_selected_range = None;
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
let bytes_before_last_selection =
buffer.reversed_bytes_in_range(0..last_selection.start);
let bytes_after_first_selection =
buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
let query_matches = query
.stream_find_iter(bytes_before_last_selection)
.map(|result| (last_selection.start, result))
.chain(
query
.stream_find_iter(bytes_after_first_selection)
.map(|result| (buffer.len(), result)),
);
for (end_offset, query_match) in query_matches {
let query_match = query_match.unwrap(); // can only fail due to I/O
let offset_range =
end_offset - query_match.end()..end_offset - query_match.start();
let display_range = offset_range.start.to_display_point(&display_map)
..offset_range.end.to_display_point(&display_map);
if !select_prev_state.wordwise
|| (!movement::is_inside_word(&display_map, display_range.start)
&& !movement::is_inside_word(&display_map, display_range.end))
{
next_selected_range = Some(offset_range);
break;
}
}
if let Some(next_selected_range) = next_selected_range {
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
if action.replace_newest {
s.delete(s.newest_anchor().id);
}
s.insert_range(next_selected_range);
});
} else {
select_prev_state.done = true;
}
}
self.select_prev_state = Some(select_prev_state);
} else if selections.len() == 1 {
let selection = selections.last_mut().unwrap();
if selection.start == selection.end {
let word_range = movement::surrounding_word(
&display_map,
selection.start.to_display_point(&display_map),
);
selection.start = word_range.start.to_offset(&display_map, Bias::Left);
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
selection.goal = SelectionGoal::None;
selection.reversed = false;
let query = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
let query = query.chars().rev().collect::<String>();
let select_state = SelectNextState {
query: AhoCorasick::new_auto_configured(&[query]),
wordwise: true,
done: false,
};
self.unfold_ranges([selection.start..selection.end], false, true, cx);
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select(selections);
});
self.select_prev_state = Some(select_state);
} else {
let query = buffer
.text_for_range(selection.start..selection.end)
.collect::<String>();
let query = query.chars().rev().collect::<String>();
self.select_prev_state = Some(SelectNextState {
query: AhoCorasick::new_auto_configured(&[query]),
wordwise: false,
done: false,
});
self.select_previous(action, cx);
}
}
}
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let mut selections = this.selections.all::<Point>(cx);
@@ -5276,7 +5492,7 @@ impl Editor {
let mut all_selection_lines_are_comments = true;
for row in start_row..=end_row {
if snapshot.is_line_blank(row) {
if snapshot.is_line_blank(row) && start_row < end_row {
continue;
}
@@ -5531,6 +5747,7 @@ impl Editor {
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
@@ -5543,6 +5760,7 @@ impl Editor {
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
self.select_next_state = entry.select_next_state;
self.select_prev_state = entry.select_prev_state;
self.add_selections_state = entry.add_selections_state;
self.request_autoscroll(Autoscroll::newest(), cx);
}
@@ -6050,6 +6268,7 @@ impl Editor {
}),
disposition: BlockDisposition::Below,
}],
Some(Autoscroll::fit()),
cx,
)[0];
this.pending_rename = Some(RenameState {
@@ -6116,7 +6335,11 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Option<RenameState> {
let rename = self.pending_rename.take()?;
self.remove_blocks([rename.block_id].into_iter().collect(), cx);
self.remove_blocks(
[rename.block_id].into_iter().collect(),
Some(Autoscroll::fit()),
cx,
);
self.clear_text_highlights::<Rename>(cx);
self.show_local_selections = true;
@@ -6320,6 +6543,7 @@ impl Editor {
self.selection_history.push(SelectionHistoryEntry {
selections: self.selections.disjoint_anchors(),
select_next_state: self.select_next_state.clone(),
select_prev_state: self.select_prev_state.clone(),
add_selections_state: self.add_selections_state.clone(),
});
}
@@ -6501,29 +6725,43 @@ impl Editor {
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) -> Vec<BlockId> {
let blocks = self
.display_map
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
self.request_autoscroll(Autoscroll::fit(), cx);
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
blocks
}
pub fn replace_blocks(
&mut self,
blocks: HashMap<BlockId, RenderBlock>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) {
self.display_map
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
self.request_autoscroll(Autoscroll::fit(), cx);
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
}
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
pub fn remove_blocks(
&mut self,
block_ids: HashSet<BlockId>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) {
self.display_map.update(cx, |display_map, cx| {
display_map.remove_blocks(block_ids, cx)
});
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
}
pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
@@ -6599,6 +6837,11 @@ impl Editor {
cx.notify();
}
pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
self.show_gutter = show_gutter;
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@@ -6818,11 +7061,12 @@ impl Editor {
multi_buffer::Event::Saved => cx.emit(Event::Saved),
multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
multi_buffer::Event::Closed => cx.emit(Event::Closed),
multi_buffer::Event::DiagnosticsUpdated => {
self.refresh_active_diagnostics(cx);
}
multi_buffer::Event::LanguageChanged => {}
_ => {}
}
}
@@ -6863,7 +7107,7 @@ impl Editor {
let mut new_selections_by_buffer = HashMap::default();
for selection in editor.selections.all::<usize>(cx) {
for (buffer, mut range) in
for (buffer, mut range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
if selection.reversed {
@@ -7020,22 +7264,24 @@ impl Editor {
};
// If None, we are in a file without an extension
let file_extension = file_extension.or(self
let file = self
.buffer
.read(cx)
.as_singleton()
.and_then(|b| b.read(cx).file())
.and_then(|b| b.read(cx).file());
let file_extension = file_extension.or(file
.as_ref()
.and_then(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let vim_mode = cx
.global::<SettingsStore>()
.untyped_user_settings()
.raw_user_settings()
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
let copilot_enabled_for_language = self
.buffer
.read(cx)
@@ -7043,15 +7289,6 @@ impl Editor {
.show_copilot_suggestions;
let telemetry = project.read(cx).client().telemetry().clone();
telemetry.report_mixpanel_event(
match name {
"open" => "open editor",
"save" => "save editor",
_ => name,
},
json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }),
telemetry_settings,
);
let event = ClickhouseEvent::Editor {
file_extension,
vim_mode,
@@ -7206,11 +7443,13 @@ pub enum Event {
DirtyChanged,
Saved,
TitleChanged,
DiffBaseChanged,
SelectionsChanged {
local: bool,
},
ScrollPositionChanged {
local: bool,
autoscroll: bool,
},
Closed,
}
@@ -7330,7 +7569,7 @@ impl View for Editor {
false
}
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
@@ -7346,10 +7585,18 @@ impl View for Editor {
Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"),
None => {}
}
for layer in self.keymap_context_layers.values() {
keymap.extend(layer);
}
if let Some(extension) = self
.buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
{
keymap.add_key("extension", extension.to_string());
}
}
fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
@@ -7702,6 +7949,7 @@ impl Deref for EditorStyle {
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
let mut highlighted_lines = Vec::new();
for (index, line) in diagnostic.message.lines().enumerate() {
let line = match &diagnostic.source {
Some(source) if index == 0 => {
@@ -7713,36 +7961,55 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
};
highlighted_lines.push(line);
}
let message = diagnostic.message;
Arc::new(move |cx: &mut BlockContext| {
let message = message.clone();
let settings = settings::get::<ThemeSettings>(cx);
let tooltip_style = settings.theme.tooltip.clone();
let theme = &settings.theme.editor;
let style = diagnostic_style(diagnostic.severity, is_valid, theme);
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
Flex::column()
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
Label::new(
line.clone(),
style.message.clone().with_font_size(font_size),
)
.with_highlights(highlights.clone())
.contained()
.with_margin_left(cx.anchor_x)
}))
.aligned()
.left()
.into_any()
let anchor_x = cx.anchor_x;
enum BlockContextToolip {}
MouseEventHandler::<BlockContext, _>::new(cx.block_id, cx, |_, _| {
Flex::column()
.with_children(highlighted_lines.iter().map(|(line, highlights)| {
Label::new(
line.clone(),
style.message.clone().with_font_size(font_size),
)
.with_highlights(highlights.clone())
.contained()
.with_margin_left(anchor_x)
}))
.aligned()
.left()
.into_any()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
})
// We really need to rethink this ID system...
.with_tooltip::<BlockContextToolip>(
cx.block_id,
"Copy diagnostic message".to_string(),
None,
tooltip_style,
cx,
)
.into_any()
})
}
pub fn highlight_diagnostic_message(
inital_highlights: Vec<usize>,
initial_highlights: Vec<usize>,
message: &str,
) -> (String, Vec<usize>) {
let mut message_without_backticks = String::new();
let mut prev_offset = 0;
let mut inside_block = false;
let mut highlights = inital_highlights;
let mut highlights = initial_highlights;
for (match_ix, (offset, _)) in message
.match_indices('`')
.chain([(message.len(), "")])

View File

@@ -7,6 +7,7 @@ pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub use_on_type_format: bool,
pub scrollbar: Scrollbar,
}
@@ -30,6 +31,7 @@ pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
}

View File

@@ -9,7 +9,8 @@ use gpui::{
executor::Deterministic,
geometry::{rect::RectF, vector::vec2f},
platform::{WindowBounds, WindowOptions},
serde_json, TestAppContext,
serde_json::{self, json},
TestAppContext,
};
use indoc::indoc;
use language::{
@@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor;
let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
invalid_anchor.text_anchor.buffer_id = Some(999);
let invalid_point = Point::new(9999, 0);
editor.navigate(
@@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
cursor_anchor: invalid_anchor,
cursor_position: invalid_point,
scroll_anchor: ScrollAnchor {
top_anchor: invalid_anchor,
anchor: invalid_anchor,
offset: Default::default(),
},
scroll_top_row: invalid_point.row,
@@ -1246,7 +1247,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
@@ -1358,7 +1359,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon
#[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height));
@@ -1473,7 +1474,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("one «two threeˇ» four");
cx.update_editor(|editor, cx| {
editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
@@ -1637,7 +1638,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
.unwrap(),
);
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
const a: ˇA = (
@@ -1685,7 +1686,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
.unwrap(),
);
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
const a: ˇA = (
@@ -1718,6 +1719,56 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("//".into()),
..LanguageConfig::default()
},
None,
));
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
// Fooˇ
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
// Foo
//ˇ
"});
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
cx.set_state(indoc! {"
ˇ// Foo
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
ˇ// Foo
"});
}
// Ensure that comment continuations can be disabled.
update_test_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
// Fooˇ
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
// Foo
ˇ
"});
}
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -1751,7 +1802,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
settings.defaults.tab_size = NonZeroU32::new(3)
});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
ˇabˇc
ˇ🏀ˇ🏀ˇefg
@@ -1779,7 +1830,7 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(
Language::new(
LanguageConfig::default(),
@@ -1850,7 +1901,7 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
.unwrap(),
);
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
fn a() {
@@ -1876,7 +1927,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
settings.defaults.tab_size = NonZeroU32::new(4);
});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
«oneˇ» «twoˇ»
@@ -1949,7 +2000,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
settings.defaults.hard_tabs = Some(true);
});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
// select two ranges on one line
cx.set_state(indoc! {"
@@ -2156,7 +2207,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
async fn test_backspace(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
// Basic backspace
cx.set_state(indoc! {"
@@ -2205,7 +2256,7 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) {
async fn test_delete(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
onˇe two three
fou«rˇ» five six
@@ -2444,6 +2495,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
height: 1,
render: Arc::new(|_| Empty::new().into_any()),
}],
Some(Autoscroll::fit()),
cx,
);
editor.change_selections(None, cx, |s| {
@@ -2559,7 +2611,7 @@ fn test_transpose(cx: &mut TestAppContext) {
async fn test_clipboard(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
cx.update_editor(|e, cx| e.cut(&Cut, cx));
@@ -2641,7 +2693,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::language()),
@@ -3085,7 +3137,7 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) {
async fn test_select_next(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx));
@@ -3107,6 +3159,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
#[gpui::test]
async fn test_select_previous(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
{
// `Select previous` without a selection (selects wordwise)
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
}
{
// `Select previous` with a selection
let mut cx = EditorTestContext::new(cx).await;
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
}
}
#[gpui::test]
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -3314,7 +3417,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig {
@@ -3485,7 +3588,7 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let html_language = Arc::new(
Language::new(
@@ -3721,7 +3824,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let rust_language = Arc::new(
Language::new(
@@ -4270,7 +4373,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
);
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server
// Set rust language override and assert overridden tabsize is sent to language server
update_test_settings(cx, |settings| {
settings.languages.insert(
"Rust".into(),
@@ -4384,7 +4487,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
);
assert!(!cx.read(|cx| editor.is_dirty(cx)));
// Set rust language override and assert overriden tabsize is sent to language server
// Set rust language override and assert overridden tabsize is sent to language server
update_test_settings(cx, |settings| {
settings.languages.insert(
"Rust".into(),
@@ -4725,7 +4828,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
two
threeˇ
"},
"overlapping aditional edit",
"overlapping additional edit",
),
(
indoc! {"
@@ -4842,7 +4945,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("// ".into()),
@@ -4850,77 +4953,95 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let text = "
// If multiple selections intersect a line, the line is only toggled once.
cx.set_state(indoc! {"
fn a() {
//b();
// c();
// d();
«//b();
ˇ»// «c();
//ˇ» d();
}
"
.unindent();
"});
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
view.update(cx, |editor, cx| {
// If multiple selections intersect a line, the line is only
// toggled once.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3),
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
b();
c();
d();
}
"
.unindent()
);
cx.assert_editor_state(indoc! {"
fn a() {
«b();
c();
ˇ» d();
}
"});
// The comment prefix is inserted at the same column for every line
// in a selection.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
// b();
// c();
// d();
}
"
.unindent()
);
// The comment prefix is inserted at the same column for every line in a
// selection.
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
// If a selection ends at the beginning of a line, that line is not toggled.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
// b();
c();
// d();
}
"
.unindent()
);
});
cx.assert_editor_state(indoc! {"
fn a() {
// «b();
// c();
ˇ»// d();
}
"});
// If a selection ends at the beginning of a line, that line is not toggled.
cx.set_selections_state(indoc! {"
fn a() {
// b();
«// c();
ˇ» // d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
// b();
«c();
ˇ» // d();
}
"});
// If a selection span a single line and is empty, the line is toggled.
cx.set_state(indoc! {"
fn a() {
a();
b();
ˇ
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
a();
b();
//•ˇ
}
"});
// If a selection span multiple lines, empty lines are not toggled.
cx.set_state(indoc! {"
fn a() {
«a();
c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «a();
// c();ˇ»
}
"});
}
#[gpui::test]
@@ -4938,7 +5059,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let registry = Arc::new(LanguageRegistry::test());
registry.add(language.clone());
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(registry);
buffer.set_language(Some(language), cx);
@@ -5060,7 +5181,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let html_language = Arc::new(
Language::new(
@@ -5225,7 +5346,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
Point::new(0, 1)..Point::new(0, 1),
Point::new(1, 1)..Point::new(1, 1),
]
)
);
// Ensure the cursor's head is respected when deleting across an excerpt boundary.
view.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
});
view.backspace(&Default::default(), cx);
assert_eq!(view.text(cx), "Xa\nbbb");
assert_eq!(
view.selections.ranges(cx),
[Point::new(1, 0)..Point::new(1, 0)]
);
view.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
});
view.backspace(&Default::default(), cx);
assert_eq!(view.text(cx), "X\nbb");
assert_eq!(
view.selections.ranges(cx),
[Point::new(0, 1)..Point::new(0, 1)]
);
});
}
@@ -5742,7 +5884,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
follower.set_scroll_anchor(
ScrollAnchor {
top_anchor,
anchor: top_anchor,
offset: vec2f(0.0, 0.5),
},
cx,
@@ -5985,7 +6127,7 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
let mut cx = EditorTestContext::new(cx).await;
let diff_base = r#"
use some::mod;

View File

@@ -40,7 +40,10 @@ use language::{
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
Selection,
};
use project::ProjectPath;
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
ProjectPath,
};
use smallvec::SmallVec;
use std::{
borrow::Cow,
@@ -51,7 +54,7 @@ use std::{
sync::Arc,
};
use text::Point;
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
use workspace::item::Item;
enum FoldMarkers {}
@@ -88,7 +91,6 @@ impl SelectionLayout {
}
}
#[derive(Clone)]
pub struct EditorElement {
style: Arc<EditorStyle>,
}
@@ -551,11 +553,8 @@ impl EditorElement {
let scroll_top = scroll_position.y() * line_height;
let show_gutter = matches!(
settings::get::<WorkspaceSettings>(cx)
.git
.git_gutter
.unwrap_or_default(),
GitGutterSetting::TrackedFiles
settings::get::<ProjectSettings>(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
if show_gutter {
@@ -1465,11 +1464,10 @@ impl EditorElement {
line_height: f32,
style: &EditorStyle,
line_layouts: &[LineWithInvisibles],
include_root: bool,
editor: &mut Editor,
cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) {
let tooltip_style = theme::current(cx).tooltip.clone();
let mut block_id = 0;
let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone())
@@ -1477,7 +1475,7 @@ impl EditorElement {
TransformBlock::ExcerptHeader { .. } => false,
TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
});
let mut render_block = |block: &TransformBlock, width: f32| {
let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
let mut element = match block {
TransformBlock::Custom(block) => {
let align_to = block
@@ -1502,6 +1500,7 @@ impl EditorElement {
scroll_x,
gutter_width,
em_width,
block_id,
})
}
TransformBlock::ExcerptHeader {
@@ -1511,7 +1510,12 @@ impl EditorElement {
starts_new_buffer,
..
} => {
let id = *id;
let tooltip_style = theme::current(cx).tooltip.clone();
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
@@ -1524,7 +1528,7 @@ impl EditorElement {
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
@@ -1556,7 +1560,7 @@ impl EditorElement {
}
})
.with_tooltip::<JumpIcon>(
id.into(),
(*id).into(),
"Jump to Buffer".to_string(),
Some(Box::new(crate::OpenExcerpts)),
tooltip_style.clone(),
@@ -1567,9 +1571,9 @@ impl EditorElement {
});
if *starts_new_buffer {
let style = &self.style.diagnostic_path_header;
let font_size =
(style.text_scale_factor * self.style.text.font_size).round();
let editor_font_size = style.text.font_size;
let style = &style.diagnostic_path_header;
let font_size = (style.text_scale_factor * editor_font_size).round();
let path = buffer.resolve_file_path(cx, include_root);
let mut filename = None;
@@ -1605,7 +1609,7 @@ impl EditorElement {
.expanded()
.into_any_named("path header block")
} else {
let text_style = self.style.text.clone();
let text_style = style.text.clone();
Flex::row()
.with_child(Label::new("", text_style))
.with_children(jump_icon)
@@ -1632,7 +1636,8 @@ impl EditorElement {
let mut fixed_block_max_width = 0f32;
let mut blocks = Vec::new();
for (row, block) in fixed_blocks {
let element = render_block(block, f32::INFINITY);
let element = render_block(block, f32::INFINITY, block_id);
block_id += 1;
fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
blocks.push(BlockLayout {
row,
@@ -1652,7 +1657,8 @@ impl EditorElement {
.max(gutter_width + scroll_width),
BlockStyle::Fixed => unreachable!(),
};
let element = render_block(block, width);
let element = render_block(block, width, block_id);
block_id += 1;
blocks.push(BlockLayout {
row,
element,
@@ -1899,7 +1905,7 @@ impl Element<Editor> for EditorElement {
let gutter_padding;
let gutter_width;
let gutter_margin;
if snapshot.mode == EditorMode::Full {
if snapshot.show_gutter {
let em_width = style.text.em_width(cx.font_cache());
gutter_padding = (em_width * style.gutter_padding_factor).round();
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
@@ -2080,12 +2086,6 @@ impl Element<Editor> for EditorElement {
ShowScrollbar::Never => false,
};
let include_root = editor
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
.into_iter()
.map(|(id, fold)| {
@@ -2144,7 +2144,6 @@ impl Element<Editor> for EditorElement {
line_height,
&style,
&line_layouts,
include_root,
editor,
cx,
);
@@ -2888,6 +2887,7 @@ mod tests {
position: Anchor::min(),
render: Arc::new(|_| Empty::new().into_any()),
}],
None,
cx,
);
@@ -3080,7 +3080,7 @@ mod tests {
editor_width: f32,
) -> Vec<Invisible> {
info!(
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
"Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
);
let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&input_text, cx);

View File

@@ -221,6 +221,7 @@ fn show_hover(
project: project.clone(),
symbol_range: range,
blocks: hover_result.contents,
language: hover_result.language,
rendered_content: None,
})
});
@@ -253,6 +254,7 @@ fn render_blocks(
theme_id: usize,
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
style: &EditorStyle,
) -> RenderedInfo {
let mut text = String::new();
@@ -351,11 +353,13 @@ fn render_blocks(
}
Tag::CodeBlock(kind) => {
new_paragraph(&mut text, &mut list_stack);
if let CodeBlockKind::Fenced(language) = kind {
current_language = language_registry
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.now_or_never()
.and_then(Result::ok);
.and_then(Result::ok)
} else {
language.cloned()
}
}
Tag::Emphasis => italic_depth += 1,
@@ -414,10 +418,6 @@ fn render_blocks(
}
}
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
RenderedInfo {
theme_id,
text,
@@ -524,6 +524,7 @@ pub struct InfoPopover {
pub project: ModelHandle<Project>,
pub symbol_range: Range<Anchor>,
pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RenderedInfo>,
}
@@ -559,6 +560,7 @@ impl InfoPopover {
style.theme_id,
&self.blocks,
self.project.read(cx).languages(),
self.language.as_ref(),
style,
)
});
@@ -588,10 +590,7 @@ impl InfoPopover {
MouseRegion::new::<Self>(view_id, region_id, bounds)
.on_click::<Editor, _>(
MouseButton::Left,
move |_, _, cx| {
println!("clicked link {url}");
cx.platform().open_url(&url);
},
move |_, _, cx| cx.platform().open_url(&url),
),
);
}
@@ -906,7 +905,7 @@ mod tests {
text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three\n".to_string(),
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
weight: Some(Weight::BOLD),
..Default::default()
@@ -918,7 +917,7 @@ mod tests {
text: "one [two](the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three\n".to_string(),
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
@@ -937,8 +936,7 @@ mod tests {
- b
* two
- [c](the-url)
- d
"
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
@@ -949,8 +947,7 @@ mod tests {
- b
- two
- «c»
- d
"
- d"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
@@ -973,9 +970,8 @@ mod tests {
nine
* ten
* six
"
.unindent(),
* six"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
@@ -985,9 +981,8 @@ mod tests {
nine
- ten
- six
"
.unindent(),
- six"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(Underline {
thickness: 1.0.into(),
@@ -1004,7 +999,7 @@ mod tests {
expected_styles,
} in &rows[0..]
{
let rendered = render_blocks(0, &blocks, &Default::default(), &style);
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges

View File

@@ -196,7 +196,7 @@ impl FollowableItem for Editor {
singleton: buffer.is_singleton(),
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
excerpts,
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
scroll_x: scroll_anchor.offset.x(),
scroll_y: scroll_anchor.offset.y(),
selections: self
@@ -253,7 +253,7 @@ impl FollowableItem for Editor {
}
Event::ScrollPositionChanged { .. } => {
let scroll_anchor = self.scroll_manager.anchor();
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
update.scroll_x = scroll_anchor.offset.x();
update.scroll_y = scroll_anchor.offset.y();
true
@@ -294,7 +294,7 @@ impl FollowableItem for Editor {
match event {
Event::Edited => true,
Event::SelectionsChanged { local } => *local,
Event::ScrollPositionChanged { local } => *local,
Event::ScrollPositionChanged { local, .. } => *local,
_ => false,
}
}
@@ -412,7 +412,7 @@ async fn update_editor_from_message(
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
editor.set_scroll_anchor_remote(
ScrollAnchor {
top_anchor: scroll_top_anchor,
anchor: scroll_top_anchor,
offset: vec2f(message.scroll_x, message.scroll_y),
},
cx,
@@ -510,8 +510,8 @@ impl Item for Editor {
};
let mut scroll_anchor = data.scroll_anchor;
if !buffer.can_resolve(&scroll_anchor.top_anchor) {
scroll_anchor.top_anchor = buffer.anchor_before(
if !buffer.can_resolve(&scroll_anchor.anchor) {
scroll_anchor.anchor = buffer.anchor_before(
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
);
}
@@ -720,17 +720,6 @@ impl Item for Editor {
})
}
fn git_diff_recalc(
&mut self,
_project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |multibuffer, cx| {
multibuffer.git_diff_recalc(cx);
});
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
let mut result = SmallVec::new();
match event {
@@ -1242,6 +1231,10 @@ mod tests {
unimplemented!()
}
fn worktree_id(&self) -> usize {
0
}
fn is_deleted(&self) -> bool {
unimplemented!()
}

View File

@@ -312,6 +312,7 @@ mod tests {
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
..Default::default()
},
cx,

View File

@@ -64,8 +64,12 @@ pub enum Event {
ExcerptsRemoved {
ids: Vec<ExcerptId>,
},
ExcerptsEdited {
ids: Vec<ExcerptId>,
},
Edited,
Reloaded,
DiffBaseChanged,
LanguageChanged,
Reparsed,
Saved,
@@ -195,6 +199,13 @@ pub struct MultiBufferBytes<'a> {
chunk: &'a [u8],
}
pub struct ReversedMultiBufferBytes<'a> {
range: Range<usize>,
excerpts: Cursor<'a, Excerpt, usize>,
excerpt_bytes: Option<ExcerptBytes<'a>>,
chunk: &'a [u8],
}
struct ExcerptChunks<'a> {
content_chunks: BufferChunks<'a>,
footer_height: usize,
@@ -343,17 +354,6 @@ impl MultiBuffer {
self.read(cx).symbols_containing(offset, theme)
}
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
let buffers = self.buffers.borrow();
for buffer_state in buffers.values() {
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
buffer_state
.buffer
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
}
}
}
pub fn edit<I, S, T>(
&mut self,
edits: I,
@@ -397,6 +397,7 @@ impl MultiBuffer {
original_indent_column: u32,
}
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
let mut edited_excerpt_ids = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>();
for (ix, (range, new_text)) in edits.enumerate() {
let new_text: Arc<str> = new_text.into();
@@ -413,6 +414,7 @@ impl MultiBuffer {
.start
.to_offset(&start_excerpt.buffer)
+ start_overshoot;
edited_excerpt_ids.push(start_excerpt.id);
cursor.seek(&range.end, Bias::Right, &());
if cursor.item().is_none() && range.end == *cursor.start() {
@@ -438,6 +440,7 @@ impl MultiBuffer {
original_indent_column,
});
} else {
edited_excerpt_ids.push(end_excerpt.id);
let start_excerpt_range = buffer_start
..start_excerpt
.range
@@ -484,6 +487,7 @@ impl MultiBuffer {
is_insertion: false,
original_indent_column,
});
edited_excerpt_ids.push(excerpt.id);
cursor.next(&());
}
}
@@ -556,6 +560,10 @@ impl MultiBuffer {
buffer.edit(insertions, insertion_autoindent_mode, cx);
})
}
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
}
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
@@ -1002,7 +1010,7 @@ impl MultiBuffer {
let suffix = cursor.suffix(&());
let changed_trailing_excerpt = suffix.is_empty();
new_excerpts.push_tree(suffix, &());
new_excerpts.append(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
snapshot.excerpt_ids = new_excerpt_ids;
@@ -1110,7 +1118,7 @@ impl MultiBuffer {
&self,
point: T,
cx: &AppContext,
) -> Option<(ModelHandle<Buffer>, usize)> {
) -> Option<(ModelHandle<Buffer>, usize, ExcerptId)> {
let snapshot = self.read(cx);
let offset = point.to_offset(&snapshot);
let mut cursor = snapshot.excerpts.cursor::<usize>();
@@ -1124,7 +1132,7 @@ impl MultiBuffer {
let buffer_point = excerpt_start + offset - *cursor.start();
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
(buffer, buffer_point)
(buffer, buffer_point, excerpt.id)
})
}
@@ -1132,7 +1140,7 @@ impl MultiBuffer {
&self,
range: Range<T>,
cx: &AppContext,
) -> Vec<(ModelHandle<Buffer>, Range<usize>)> {
) -> Vec<(ModelHandle<Buffer>, Range<usize>, ExcerptId)> {
let snapshot = self.read(cx);
let start = range.start.to_offset(&snapshot);
let end = range.end.to_offset(&snapshot);
@@ -1157,7 +1165,7 @@ impl MultiBuffer {
let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
result.push((buffer, start..end));
result.push((buffer, start..end, excerpt.id));
cursor.next(&());
}
@@ -1185,7 +1193,7 @@ impl MultiBuffer {
while let Some(excerpt_id) = excerpt_ids.next() {
// Seek to the next excerpt to remove, preserving any preceding excerpts.
let locator = snapshot.excerpt_locator_for_id(excerpt_id);
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
if let Some(mut excerpt) = cursor.item() {
if excerpt.id != excerpt_id {
@@ -1237,7 +1245,7 @@ impl MultiBuffer {
}
let suffix = cursor.suffix(&());
let changed_trailing_excerpt = suffix.is_empty();
new_excerpts.push_tree(suffix, &());
new_excerpts.append(suffix, &());
drop(cursor);
snapshot.excerpts = new_excerpts;
@@ -1312,6 +1320,7 @@ impl MultiBuffer {
language::Event::Saved => Event::Saved,
language::Event::FileHandleChanged => Event::FileHandleChanged,
language::Event::Reloaded => Event::Reloaded,
language::Event::DiffBaseChanged => Event::DiffBaseChanged,
language::Event::LanguageChanged => Event::LanguageChanged,
language::Event::Reparsed => Event::Reparsed,
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
@@ -1378,7 +1387,7 @@ impl MultiBuffer {
cx: &'a AppContext,
) -> Option<Arc<Language>> {
self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
}
pub fn settings_at<'a, T: ToOffset>(
@@ -1386,8 +1395,14 @@ impl MultiBuffer {
point: T,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(point, cx);
language_settings(language.map(|l| l.name()).as_deref(), cx)
let mut language = None;
let mut file = None;
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
let buffer = buffer.read(cx);
language = buffer.language_at(offset);
file = buffer.file();
}
language_settings(language.as_ref(), file, cx)
}
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
@@ -1494,7 +1509,7 @@ impl MultiBuffer {
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>();
for (locator, buffer, buffer_edited) in excerpts_to_edit {
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &());
let old_excerpt = cursor.item().unwrap();
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
@@ -1534,7 +1549,7 @@ impl MultiBuffer {
new_excerpts.push(new_excerpt, &());
cursor.next(&());
}
new_excerpts.push_tree(cursor.suffix(&()), &());
new_excerpts.append(cursor.suffix(&()), &());
drop(cursor);
snapshot.excerpts = new_excerpts;
@@ -1550,6 +1565,13 @@ impl MultiBuffer {
cx.add_model(|cx| Self::singleton(buffer, cx))
}
pub fn build_from_buffer(
buffer: ModelHandle<Buffer>,
cx: &mut gpui::AppContext,
) -> ModelHandle<Self> {
cx.add_model(|cx| Self::singleton(buffer, cx))
}
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
@@ -1963,7 +1985,6 @@ impl MultiBufferSnapshot {
} else {
None
};
MultiBufferBytes {
range,
excerpts,
@@ -1972,6 +1993,33 @@ impl MultiBufferSnapshot {
}
}
pub fn reversed_bytes_in_range<T: ToOffset>(
&self,
range: Range<T>,
) -> ReversedMultiBufferBytes {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpts = self.excerpts.cursor::<usize>();
excerpts.seek(&range.end, Bias::Left, &());
let mut chunk = &[][..];
let excerpt_bytes = if let Some(excerpt) = excerpts.item() {
let mut excerpt_bytes = excerpt.reversed_bytes_in_range(
range.start - excerpts.start()..range.end - excerpts.start(),
);
chunk = excerpt_bytes.next().unwrap_or(&[][..]);
Some(excerpt_bytes)
} else {
None
};
ReversedMultiBufferBytes {
range,
excerpts,
excerpt_bytes,
chunk,
}
}
pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows {
let mut result = MultiBufferRows {
buffer_row_range: 0..0,
@@ -2787,9 +2835,13 @@ impl MultiBufferSnapshot {
point: T,
cx: &'a AppContext,
) -> &'a LanguageSettings {
self.point_to_buffer_offset(point)
.map(|(buffer, offset)| buffer.settings_at(offset, cx))
.unwrap_or_else(|| language_settings(None, cx))
let mut language = None;
let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
language = buffer.language_at(offset);
file = buffer.file();
}
language_settings(language, file, cx)
}
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
@@ -3401,6 +3453,26 @@ impl Excerpt {
}
}
fn reversed_bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
let content_start = self.range.context.start.to_offset(&self.buffer);
let bytes_start = content_start + range.start;
let bytes_end = content_start + cmp::min(range.end, self.text_summary.len);
let footer_height = if self.has_trailing_newline
&& range.start <= self.text_summary.len
&& range.end > self.text_summary.len
{
1
} else {
0
};
let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end);
ExcerptBytes {
content_bytes,
footer_height,
}
}
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
if text_anchor
.cmp(&self.range.context.start, &self.buffer)
@@ -3719,6 +3791,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> {
}
}
impl<'a> ReversedMultiBufferBytes<'a> {
fn consume(&mut self, len: usize) {
self.range.end -= len;
self.chunk = &self.chunk[..self.chunk.len() - len];
if !self.range.is_empty() && self.chunk.is_empty() {
if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) {
self.chunk = chunk;
} else {
self.excerpts.next(&());
if let Some(excerpt) = self.excerpts.item() {
let mut excerpt_bytes =
excerpt.bytes_in_range(0..self.range.end - self.excerpts.start());
self.chunk = excerpt_bytes.next().unwrap();
self.excerpt_bytes = Some(excerpt_bytes);
}
}
}
}
}
impl<'a> io::Read for ReversedMultiBufferBytes<'a> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let len = cmp::min(buf.len(), self.chunk.len());
buf[..len].copy_from_slice(&self.chunk[..len]);
buf[..len].reverse();
if len > 0 {
self.consume(len);
}
Ok(len)
}
}
impl<'a> Iterator for ExcerptBytes<'a> {
type Item = &'a [u8];
@@ -3870,10 +3974,13 @@ where
#[cfg(test)]
mod tests {
use crate::editor_tests::init_test;
use super::*;
use futures::StreamExt;
use gpui::{AppContext, TestAppContext};
use language::{Buffer, Rope};
use project::{FakeFs, Project};
use rand::prelude::*;
use settings::SettingsStore;
use std::{env, rc::Rc};
@@ -4564,73 +4671,85 @@ mod tests {
#[gpui::test]
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
use git::diff::DiffHunkStatus;
init_test(cx, |_| {});
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
// buffer has two modified hunks with two rows each
let buffer_1 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent(),
cx,
);
let buffer_1 = project
.update(cx, |project, cx| {
project.create_buffer(
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent()
.as_str(),
None,
cx,
)
})
.unwrap();
buffer_1.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
buffer
});
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = cx.add_model(|cx| {
let mut buffer = Buffer::new(
0,
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent(),
cx,
);
let buffer_2 = project
.update(cx, |project, cx| {
project.create_buffer(
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent()
.as_str(),
None,
cx,
)
})
.unwrap();
buffer_2.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
buffer
});
cx.foreground().run_until_parked();
@@ -5077,7 +5196,7 @@ mod tests {
.range_to_buffer_ranges(start_ix..end_ix, cx);
let excerpted_buffers_text = excerpted_buffer_ranges
.iter()
.map(|(buffer, buffer_range)| {
.map(|(buffer, buffer_range, _)| {
buffer
.read(cx)
.text_for_range(buffer_range.clone())
@@ -5224,7 +5343,7 @@ mod tests {
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
// An undo in the multibuffer undoes the multibuffer transaction
// and also any individual buffer edits that have occured since
// and also any individual buffer edits that have occurred since
// that transaction.
multibuffer.undo(cx);
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");

View File

@@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor {
pub offset: Vector2F,
pub top_anchor: Anchor,
pub anchor: Anchor,
}
impl ScrollAnchor {
fn new() -> Self {
Self {
offset: Vector2F::zero(),
top_anchor: Anchor::min(),
anchor: Anchor::min(),
}
}
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
let mut scroll_position = self.offset;
if self.top_anchor != Anchor::min() {
let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32;
if self.anchor != Anchor::min() {
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
scroll_position.set_y(scroll_top + scroll_position.y());
} else {
scroll_position.set_y(0.);
@@ -59,7 +59,7 @@ impl ScrollAnchor {
}
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
self.top_anchor.to_point(buffer).row
self.anchor.to_point(buffer).row
}
}
@@ -173,13 +173,14 @@ impl ScrollManager {
scroll_position: Vector2F,
map: &DisplaySnapshot,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
let (new_anchor, top_row) = if scroll_position.y() <= 0. {
(
ScrollAnchor {
top_anchor: Anchor::min(),
anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)),
},
0,
@@ -193,7 +194,7 @@ impl ScrollManager {
(
ScrollAnchor {
top_anchor,
anchor: top_anchor,
offset: vec2f(
scroll_position.x(),
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
@@ -203,7 +204,7 @@ impl ScrollManager {
)
};
self.set_anchor(new_anchor, top_row, local, workspace_id, cx);
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
}
fn set_anchor(
@@ -211,11 +212,12 @@ impl ScrollManager {
anchor: ScrollAnchor,
top_row: u32,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local });
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
@@ -296,21 +298,28 @@ impl Editor {
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
self.set_scroll_position_internal(scroll_position, true, cx);
self.set_scroll_position_internal(scroll_position, true, false, cx);
}
pub(crate) fn set_scroll_position_internal(
&mut self,
scroll_position: Vector2F,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
autoscroll,
workspace_id,
cx,
);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
@@ -322,11 +331,11 @@ impl Editor {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
@@ -337,11 +346,11 @@ impl Editor {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.top_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
@@ -377,7 +386,7 @@ impl Editor {
let screen_top = self
.scroll_manager
.anchor
.top_anchor
.anchor
.to_display_point(&snapshot);
if screen_top > newest_head {
@@ -408,7 +417,7 @@ impl Editor {
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor {
offset: Vector2F::new(x, y),
top_anchor,
anchor: top_anchor,
};
self.set_scroll_anchor(scroll_anchor, cx);
}

View File

@@ -86,7 +86,7 @@ impl Editor {
editor.set_scroll_anchor(
ScrollAnchor {
top_anchor: new_anchor,
anchor: new_anchor,
offset: Default::default(),
},
cx,
@@ -113,7 +113,7 @@ impl Editor {
editor.set_scroll_anchor(
ScrollAnchor {
top_anchor: new_anchor,
anchor: new_anchor,
offset: Default::default(),
},
cx,
@@ -143,7 +143,7 @@ impl Editor {
editor.set_scroll_anchor(
ScrollAnchor {
top_anchor: new_anchor,
anchor: new_anchor,
offset: Default::default(),
},
cx,

View File

@@ -136,23 +136,23 @@ impl Editor {
if target_top < start_row {
scroll_position.set_y(target_top);
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
} else if target_bottom >= end_row {
scroll_position.set_y(target_bottom - visible_lines);
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
AutoscrollStrategy::Center => {
scroll_position.set_y((first_cursor_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.set_y((first_cursor_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Bottom => {
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, cx);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}

View File

@@ -76,6 +76,9 @@ impl SelectionsCollection {
count
}
/// The non-pending, non-overlapping selections. There could still be a pending
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
/// selections over Anchors.
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
self.disjoint.clone()
}

View File

@@ -48,8 +48,8 @@ pub fn marked_display_snapshot(
}
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), umarked_text);
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
}

View File

@@ -7,6 +7,7 @@ use gpui::{
};
use indoc::indoc;
use language::{Buffer, BufferSnapshot};
use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
@@ -25,11 +26,16 @@ pub struct EditorTestContext<'a> {
}
impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
let buffer = project
.update(cx, |project, cx| project.create_buffer("", None, cx))
.unwrap();
let (window_id, editor) = cx.update(|cx| {
cx.add_window(Default::default(), |cx| {
cx.focus_self();
build_editor(MultiBuffer::build_simple("", cx), cx)
build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx)
})
});

View File

@@ -16,6 +16,7 @@ editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
project = { path = "../project" }
regex.workspace = true
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
@@ -27,7 +28,7 @@ futures.workspace = true
anyhow.workspace = true
smallvec.workspace = true
human_bytes = "0.4.1"
isahc = "1.7"
isahc.workspace = true
lazy_static.workspace = true
postage.workspace = true
serde.workspace = true

View File

@@ -14,6 +14,7 @@ use isahc::Request;
use language::Buffer;
use postage::prelude::Stream;
use project::Project;
use regex::Regex;
use serde::Serialize;
use smallvec::SmallVec;
use std::{
@@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) {
#[derive(Serialize)]
struct FeedbackRequestBody<'a> {
feedback_text: &'a str,
email: Option<String>,
metrics_id: Option<Arc<str>>,
installation_id: Option<Arc<str>>,
system_specs: SystemSpecs,
@@ -157,8 +159,18 @@ impl FeedbackEditor {
let is_staff = telemetry.is_staff();
let http_client = zed_client.http_client();
let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
let emails: Vec<&str> = re
.captures_iter(feedback_text)
.map(|capture| capture.get(0).unwrap().as_str())
.collect();
let email = emails.first().map(|e| e.to_string());
let request = FeedbackRequestBody {
feedback_text: &feedback_text,
email,
metrics_id,
installation_id,
system_specs,

View File

@@ -34,7 +34,7 @@ impl View for FeedbackInfoText {
Flex::row()
.with_child(
Text::new(
"We read whatever you submit here. For issues and discussions, visit the ",
"Share your feedback. Include your email for replies. For issues and discussions, visit the ",
theme.feedback.info_text_default.text.clone(),
)
.with_soft_wrap(false)
@@ -60,7 +60,7 @@ impl View for FeedbackInfoText {
}),
)
.with_child(
Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
Text::new(".", theme.feedback.info_text_default.text.clone())
.with_soft_wrap(false)
.aligned(),
)

View File

@@ -6,6 +6,8 @@ use std::{env, fmt::Display};
use sysinfo::{System, SystemExt};
use util::channel::ReleaseChannel;
// TODO: Move this file out of feedback and into a more general place
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
#[serde(serialize_with = "serialize_app_version")]

View File

@@ -6,7 +6,7 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
path::Path,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
@@ -25,11 +25,57 @@ pub struct FileFinderDelegate {
latest_search_id: usize,
latest_search_did_cancel: bool,
latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
currently_opened_path: Option<ProjectPath>,
matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>,
currently_opened_path: Option<FoundPath>,
matches: Matches,
selected_index: Option<usize>,
cancel_flag: Arc<AtomicBool>,
history_items: Vec<ProjectPath>,
history_items: Vec<FoundPath>,
}
#[derive(Debug)]
enum Matches {
History(Vec<FoundPath>),
Search(Vec<PathMatch>),
}
#[derive(Debug)]
enum Match<'a> {
History(&'a FoundPath),
Search(&'a PathMatch),
}
impl Matches {
fn len(&self) -> usize {
match self {
Self::History(items) => items.len(),
Self::Search(items) => items.len(),
}
}
fn get(&self, index: usize) -> Option<Match<'_>> {
match self {
Self::History(items) => items.get(index).map(Match::History),
Self::Search(items) => items.get(index).map(Match::Search),
}
}
}
impl Default for Matches {
fn default() -> Self {
Self::History(Vec::new())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct FoundPath {
project: ProjectPath,
absolute: Option<PathBuf>,
}
impl FoundPath {
fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
Self { project, absolute }
}
}
actions!(file_finder, [Toggle]);
@@ -43,10 +89,41 @@ const MAX_RECENT_SELECTIONS: usize = 20;
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx);
let project = workspace.project().read(cx);
let currently_opened_path = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx));
.and_then(|item| item.project_path(cx))
.map(|project_path| {
let abs_path = project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
FoundPath::new(project_path, abs_path)
});
// if exists, bubble the currently opened path to the top
let history_items = currently_opened_path
.clone()
.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())
})
.map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
)
.collect();
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
@@ -87,37 +164,11 @@ impl FileSearchQuery {
}
impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
- file_name.chars().count();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
if pos >= &file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
(file_name, file_name_positions, full_path, path_positions)
}
pub fn new(
fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
currently_opened_path: Option<ProjectPath>,
history_items: Vec<ProjectPath>,
currently_opened_path: Option<FoundPath>,
history_items: Vec<FoundPath>,
cx: &mut ViewContext<FileFinder>,
) -> Self {
cx.observe(&project, |picker, _, cx| {
@@ -132,8 +183,8 @@ impl FileFinderDelegate {
latest_search_did_cancel: false,
latest_search_query: None,
currently_opened_path,
matches: Vec::new(),
selected: None,
matches: Matches::default(),
selected_index: None,
cancel_flag: Arc::new(AtomicBool::new(false)),
history_items,
}
@@ -147,7 +198,7 @@ impl FileFinderDelegate {
let relative_to = self
.currently_opened_path
.as_ref()
.map(|project_path| Arc::clone(&project_path.path));
.map(|found_path| Arc::clone(&found_path.project.path));
let worktrees = self
.project
.read(cx)
@@ -188,13 +239,13 @@ impl FileFinderDelegate {
.update(&mut cx, |picker, cx| {
picker
.delegate_mut()
.set_matches(search_id, did_cancel, query, matches, cx)
.set_search_matches(search_id, did_cancel, query, matches, cx)
})
.log_err();
})
}
fn set_matches(
fn set_search_matches(
&mut self,
search_id: usize,
did_cancel: bool,
@@ -211,15 +262,126 @@ impl FileFinderDelegate {
.as_ref()
.map(|query| query.path_like.path_query())
{
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
match &mut self.matches {
Matches::History(_) => self.matches = Matches::Search(matches),
Matches::Search(search_matches) => {
util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
b.cmp(a)
})
}
}
} else {
self.matches = matches;
self.matches = Matches::Search(matches);
}
self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel;
cx.notify();
}
}
fn labels_for_match(
&self,
path_match: Match,
cx: &AppContext,
ix: usize,
) -> (String, Vec<usize>, String, Vec<usize>) {
let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
Match::History(found_path) => {
let worktree_id = found_path.project.worktree_id;
let project_relative_path = &found_path.project.path;
let has_worktree = self
.project
.read(cx)
.worktree_for_id(worktree_id, cx)
.is_some();
if !has_worktree {
if let Some(absolute_path) = &found_path.absolute {
return (
absolute_path
.file_name()
.map_or_else(
|| project_relative_path.to_string_lossy(),
|file_name| file_name.to_string_lossy(),
)
.to_string(),
Vec::new(),
absolute_path.to_string_lossy().to_string(),
Vec::new(),
);
}
}
let mut path = Arc::clone(project_relative_path);
if project_relative_path.as_ref() == Path::new("") {
if let Some(absolute_path) = &found_path.absolute {
path = Arc::from(absolute_path.as_path());
}
}
self.labels_for_path_match(&PathMatch {
score: ix as f64,
positions: Vec::new(),
worktree_id: worktree_id.to_usize(),
path,
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
})
}
Match::Search(path_match) => self.labels_for_path_match(path_match),
};
if file_name_positions.is_empty() {
if let Some(user_home_path) = std::env::var("HOME").ok() {
let user_home_path = user_home_path.trim();
if !user_home_path.is_empty() {
if (&full_path).starts_with(user_home_path) {
return (
file_name,
file_name_positions,
full_path.replace(user_home_path, "~"),
full_path_positions,
);
}
}
}
}
(
file_name,
file_name_positions,
full_path,
full_path_positions,
)
}
fn labels_for_path_match(
&self,
path_match: &PathMatch,
) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path;
let path_string = path.to_string_lossy();
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
let path_positions = path_match.positions.clone();
let file_name = path.file_name().map_or_else(
|| path_match.path_prefix.to_string(),
|file_name| file_name.to_string_lossy().to_string(),
);
let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
- file_name.chars().count();
let file_name_positions = path_positions
.iter()
.filter_map(|pos| {
if pos >= &file_name_start {
Some(pos - file_name_start)
} else {
None
}
})
.collect();
(file_name, file_name_positions, full_path, path_positions)
}
}
impl PickerDelegate for FileFinderDelegate {
@@ -232,45 +394,35 @@ impl PickerDelegate for FileFinderDelegate {
}
fn selected_index(&self) -> usize {
if let Some(selected) = self.selected.as_ref() {
for (ix, path_match) in self.matches.iter().enumerate() {
if (path_match.worktree_id, path_match.path.as_ref())
== (selected.0, selected.1.as_ref())
{
return ix;
}
}
}
0
self.selected_index.unwrap_or(0)
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
let mat = &self.matches[ix];
self.selected = Some((mat.worktree_id, mat.path.clone()));
self.selected_index = Some(ix);
cx.notify();
}
fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if raw_query.is_empty() {
let project = self.project.read(cx);
self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear();
self.matches = self
.currently_opened_path
.iter() // if exists, bubble the currently opened path to the top
.chain(self.history_items.iter().filter(|history_item| {
Some(*history_item) != self.currently_opened_path.as_ref()
}))
.enumerate()
.map(|(i, history_item)| PathMatch {
score: i as f64,
positions: Vec::new(),
worktree_id: history_item.worktree_id.to_usize(),
path: Arc::clone(&history_item.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
})
.collect();
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
.as_ref()
.filter(|abs_path| abs_path.exists())
.is_some())
})
.cloned()
.collect(),
);
cx.notify();
Task::ready(())
} else {
@@ -293,16 +445,52 @@ impl PickerDelegate for FileFinderDelegate {
fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
if let Some(m) = self.matches.get(self.selected_index()) {
if let Some(workspace) = self.workspace.upgrade(cx) {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
};
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path.clone(), None, true, cx)
let open_task = workspace.update(cx, |workspace, cx| match m {
Match::History(history_match) => {
let worktree_id = history_match.project.worktree_id;
if workspace
.project()
.read(cx)
.worktree_for_id(worktree_id, cx)
.is_some()
{
workspace.open_path(
ProjectPath {
worktree_id,
path: Arc::clone(&history_match.project.path),
},
None,
true,
cx,
)
} else {
match history_match.absolute.as_ref() {
Some(abs_path) => {
workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
}
None => workspace.open_path(
ProjectPath {
worktree_id,
path: Arc::clone(&history_match.project.path),
},
None,
true,
cx,
),
}
}
}
Match::Search(m) => workspace.open_path(
ProjectPath {
worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(),
},
None,
true,
cx,
),
});
let workspace = workspace.downgrade();
let row = self
.latest_search_query
.as_ref()
@@ -333,6 +521,7 @@ impl PickerDelegate for FileFinderDelegate {
}
}
workspace
.downgrade()
.update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
.log_err();
@@ -352,11 +541,14 @@ impl PickerDelegate for FileFinderDelegate {
selected: bool,
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let path_match = &self.matches[ix];
let path_match = self
.matches
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match);
self.labels_for_match(path_match, cx, ix);
Flex::column()
.with_child(
Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
@@ -373,7 +565,7 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(test)]
mod tests {
use std::{assert_eq, collections::HashMap, time::Duration};
use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
use super::*;
use editor::Editor;
@@ -649,12 +841,16 @@ mod tests {
finder.update(cx, |finder, cx| {
let delegate = finder.delegate_mut();
let matches = delegate.matches.clone();
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
}
.clone();
// Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found.
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
delegate.set_search_matches(
delegate.latest_search_id,
true, // did-cancel
query.clone(),
@@ -664,7 +860,7 @@ mod tests {
// Simulate another cancellation.
drop(delegate.spawn_search(query.clone(), cx));
delegate.set_matches(
delegate.set_search_matches(
delegate.latest_search_id,
true, // did-cancel
query.clone(),
@@ -672,7 +868,12 @@ mod tests {
cx,
);
assert_eq!(delegate.matches, matches[0..4])
match &delegate.matches {
Matches::Search(new_matches) => {
assert_eq!(new_matches.as_slice(), &matches[0..4])
}
_ => panic!("Search matches expected"),
};
});
}
@@ -772,10 +973,14 @@ mod tests {
cx.read(|cx| {
let finder = finder.read(cx);
let delegate = finder.delegate();
assert_eq!(delegate.matches.len(), 1);
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert_eq!(matches.len(), 1);
let (file_name, file_name_positions, full_path, full_path_positions) =
delegate.labels_for_match(&delegate.matches[0]);
delegate.labels_for_path_match(&matches[0]);
assert_eq!(file_name, "the-file");
assert_eq!(file_name_positions, &[0, 1, 4]);
assert_eq!(full_path, "the-file");
@@ -876,10 +1081,10 @@ mod tests {
// When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier
let b_path = Some(ProjectPath {
let b_path = Some(dummy_found_path(ProjectPath {
worktree_id,
path: Arc::from(Path::new("/root/dir2/b.txt")),
});
}));
let (_, finder) = cx.add_window(|cx| {
Picker::new(
FileFinderDelegate::new(
@@ -901,8 +1106,12 @@ mod tests {
finder.read_with(cx, |f, _| {
let delegate = f.delegate();
assert_eq!(delegate.matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(delegate.matches[1].path.as_ref(), Path::new("dir1/a.txt"));
let matches = match &delegate.matches {
Matches::Search(path_matches) => path_matches,
_ => panic!("Search matches expected"),
};
assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
});
}
@@ -1012,10 +1221,13 @@ mod tests {
.await;
assert_eq!(
history_after_first,
vec![ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
}],
vec![FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
)],
"Should show 1st opened item in the history when opening the 2nd item"
);
@@ -1032,14 +1244,20 @@ mod tests {
assert_eq!(
history_after_second,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
),
],
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
2nd item should be the first in the history, as the last opened."
@@ -1058,18 +1276,27 @@ mod tests {
assert_eq!(
history_after_third,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
Some(PathBuf::from("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
),
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
3rd item should be the first in the history, as the last opened."
@@ -1088,24 +1315,162 @@ mod tests {
assert_eq!(
history_after_second_again,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
Some(PathBuf::from("/src/test/third.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
Some(PathBuf::from("/src/test/first.rs"))
),
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
2nd item, as the last opened, 3rd item should go next as it was opened right before."
);
}
#[gpui::test]
async fn test_external_files_history(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
}
}),
)
.await;
app_state
.fs
.as_fake()
.insert_tree(
"/external-src",
json!({
"test": {
"third.rs": "// Third Rust file",
"fourth.rs": "// Fourth Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
cx.update(|cx| {
project.update(cx, |project, cx| {
project.find_or_create_local_worktree("/external-src", false, cx)
})
})
.detach();
deterministic.run_until_parked();
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1,);
WorktreeId::from_usize(worktrees[0].id())
});
workspace
.update(cx, |workspace, cx| {
workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
})
.detach();
deterministic.run_until_parked();
let external_worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(
worktrees.len(),
2,
"External file should get opened in a new worktree"
);
WorktreeId::from_usize(
worktrees
.into_iter()
.find(|worktree| worktree.id() != worktree_id.to_usize())
.expect("New worktree should have a different id")
.id(),
)
});
close_active_item(&workspace, &deterministic, cx).await;
let initial_history_items = open_close_queried_buffer(
"sec",
1,
"second.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
initial_history_items,
vec![FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
path: Arc::from(Path::new("")),
},
Some(PathBuf::from("/external-src/test/third.rs"))
)],
"Should show external file with its full path in the history after it was open"
);
let updated_history_items = open_close_queried_buffer(
"fir",
1,
"first.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
updated_history_items,
vec![
FoundPath::new(
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
Some(PathBuf::from("/src/test/second.rs"))
),
FoundPath::new(
ProjectPath {
worktree_id: external_worktree_id,
path: Arc::from(Path::new("")),
},
Some(PathBuf::from("/external-src/test/third.rs"))
),
],
"Should keep external file with history updates",
);
}
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,
@@ -1114,7 +1479,7 @@ mod tests {
workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic,
cx: &mut gpui::TestAppContext,
) -> Vec<ProjectPath> {
) -> Vec<FoundPath> {
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
@@ -1152,6 +1517,16 @@ mod tests {
);
});
close_active_item(workspace, deterministic, cx).await;
history_items
}
async fn close_active_item(
workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic,
cx: &mut TestAppContext,
) {
let mut original_items = HashMap::new();
cx.read(|cx| {
for pane in workspace.read(cx).panes() {
@@ -1161,6 +1536,8 @@ mod tests {
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
}
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
active_pane
.update(cx, |pane, cx| {
pane.close_active_item(&workspace::CloseActiveItem, cx)
@@ -1185,8 +1562,10 @@ mod tests {
}
}
});
history_items
assert!(
original_items.len() <= 1,
"At most one panel should got closed"
);
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
@@ -1198,6 +1577,7 @@ mod tests {
super::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
@@ -1215,4 +1595,11 @@ mod tests {
})
.unwrap()
}
fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
FoundPath {
project: project_path,
absolute: None,
}
}
}

View File

@@ -14,6 +14,8 @@ lsp = { path = "../lsp" }
rope = { path = "../rope" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
rpc = { path = "../rpc" }
anyhow.workspace = true
async-trait.workspace = true
futures.workspace = true

View File

@@ -29,10 +29,12 @@ use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))]
use repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))]
use std::ffi::OsStr;
#[cfg(any(test, feature = "test-support"))]
use std::sync::Weak;
lazy_static! {
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -77,13 +79,13 @@ impl LineEnding {
}
pub fn normalize(text: &mut String) {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
@@ -501,6 +503,11 @@ impl FakeFsState {
}
}
#[cfg(any(test, feature = "test-support"))]
lazy_static! {
pub static ref FS_DOT_GIT: &'static OsStr = OsStr::new(".git");
}
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {
pub fn new(executor: Arc<gpui::executor::Background>) -> Arc<Self> {
@@ -619,7 +626,7 @@ impl FakeFs {
.boxed()
}
pub fn with_git_state<F>(&self, dot_git: &Path, f: F)
pub fn with_git_state<F>(&self, dot_git: &Path, emit_git_event: bool, f: F)
where
F: FnOnce(&mut FakeGitRepositoryState),
{
@@ -633,18 +640,22 @@ impl FakeFs {
f(&mut repo_state);
state.emit_event([dot_git]);
if emit_git_event {
state.emit_event([dot_git]);
}
} else {
panic!("not a directory");
}
}
pub async fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, |state| state.branch_name = branch.map(Into::into))
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, true, |state| {
state.branch_name = branch.map(Into::into)
})
}
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
self.with_git_state(dot_git, |state| {
pub fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
self.with_git_state(dot_git, true, |state| {
state.index_contents.clear();
state.index_contents.extend(
head_state
@@ -654,8 +665,32 @@ impl FakeFs {
});
}
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
self.with_git_state(dot_git, |state| {
pub fn set_status_for_repo_via_working_copy_change(
&self,
dot_git: &Path,
statuses: &[(&Path, GitFileStatus)],
) {
self.with_git_state(dot_git, false, |state| {
state.worktree_statuses.clear();
state.worktree_statuses.extend(
statuses
.iter()
.map(|(path, content)| ((**path).into(), content.clone())),
);
});
self.state.lock().emit_event(
statuses
.iter()
.map(|(path, _)| dot_git.parent().unwrap().join(path)),
);
}
pub fn set_status_for_repo_via_git_operation(
&self,
dot_git: &Path,
statuses: &[(&Path, GitFileStatus)],
) {
self.with_git_state(dot_git, true, |state| {
state.worktree_statuses.clear();
state.worktree_statuses.extend(
statuses
@@ -665,7 +700,7 @@ impl FakeFs {
});
}
pub fn paths(&self) -> Vec<PathBuf> {
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
@@ -675,12 +710,18 @@ impl FakeFs {
queue.push_back((path.join(name), entry.clone()));
}
}
result.push(path);
if include_dot_git
|| !path
.components()
.any(|component| component.as_os_str() == *FS_DOT_GIT)
{
result.push(path);
}
}
result
}
pub fn directories(&self) -> Vec<PathBuf> {
pub fn directories(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
@@ -689,7 +730,13 @@ impl FakeFs {
for (name, entry) in entries {
queue.push_back((path.join(name), entry.clone()));
}
result.push(path);
if include_dot_git
|| !path
.components()
.any(|component| component.as_os_str() == *FS_DOT_GIT)
{
result.push(path);
}
}
}
result

View File

@@ -1,6 +1,8 @@
use anyhow::Result;
use collections::HashMap;
use git2::ErrorCode;
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
use std::{
cmp::Ordering,
@@ -24,7 +26,7 @@ pub trait GitRepository: Send {
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -91,9 +93,18 @@ impl GitRepository for LibGitRepository {
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
let status = self.status_file(path).log_err()?;
read_status(status)
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
let status = self.status_file(path);
match status {
Ok(status) => Ok(read_status(status)),
Err(e) => {
if e.code() == ErrorCode::NotFound {
Ok(None)
} else {
Err(e.into())
}
}
}
}
}
@@ -155,9 +166,9 @@ impl GitRepository for FakeGitRepository {
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
let state = self.state.lock();
state.worktree_statuses.get(path).cloned()
Ok(state.worktree_statuses.get(path).cloned())
}
}
@@ -197,8 +208,51 @@ pub enum GitFileStatus {
Conflict,
}
impl GitFileStatus {
pub fn merge(
this: Option<GitFileStatus>,
other: Option<GitFileStatus>,
prefer_other: bool,
) -> Option<GitFileStatus> {
if prefer_other {
return other;
} else {
match (this, other) {
(Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
Some(GitFileStatus::Conflict)
}
(Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
Some(GitFileStatus::Modified)
}
(Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
Some(GitFileStatus::Added)
}
_ => None,
}
}
}
pub fn from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
git_status.and_then(|status| {
proto::GitStatus::from_i32(status).map(|status| match status {
proto::GitStatus::Added => GitFileStatus::Added,
proto::GitStatus::Modified => GitFileStatus::Modified,
proto::GitStatus::Conflict => GitFileStatus::Conflict,
})
})
}
pub fn to_proto(self) -> i32 {
match self {
GitFileStatus::Added => proto::GitStatus::Added as i32,
GitFileStatus::Modified => proto::GitStatus::Modified as i32,
GitFileStatus::Conflict => proto::GitStatus::Conflict as i32,
}
}
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(PathBuf);
pub struct RepoPath(pub PathBuf);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {

View File

@@ -161,13 +161,6 @@ impl BufferDiff {
self.tree = SumTree::new();
}
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
match &self.last_buffer_version {
Some(last) => buffer.version().changed_since(last),
None => true,
}
}
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
let mut tree = SumTree::new();

View File

@@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0"
[build-dependencies]
bindgen = "0.59.2"
bindgen = "0.65.1"
cc = "1.0.67"
[dev-dependencies]

View File

@@ -445,7 +445,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut WindowContext
type KeystrokeCallback =
Box<dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool>;
type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
type DeserializeActionCallback = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
pub struct AppContext {
@@ -624,14 +624,14 @@ impl AppContext {
pub fn deserialize_action(
&self,
name: &str,
argument: Option<&str>,
argument: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> {
let callback = self
.action_deserializers
.get(name)
.ok_or_else(|| anyhow!("unknown action {}", name))?
.1;
callback(argument.unwrap_or("{}"))
callback(argument.unwrap_or_else(|| serde_json::Value::Object(Default::default())))
.with_context(|| format!("invalid data for action {}", name))
}
@@ -5573,7 +5573,7 @@ mod tests {
let action1 = cx
.deserialize_action(
"test::something::ComplexAction",
Some(r#"{"arg": "a", "count": 5}"#),
Some(serde_json::from_str(r#"{"arg": "a", "count": 5}"#).unwrap()),
)
.unwrap();
let action2 = cx
@@ -6335,9 +6335,9 @@ mod tests {
#[crate::test(self)]
async fn test_labeled_tasks(cx: &mut TestAppContext) {
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
let (mut sender, mut reciever) = postage::oneshot::channel::<()>();
let (mut sender, mut receiver) = postage::oneshot::channel::<()>();
let task = cx
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await }));
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await }));
assert_eq!(
Some("Test Label"),

View File

@@ -11,7 +11,7 @@ pub trait Action: 'static {
fn qualified_name() -> &'static str
where
Self: Sized;
fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
fn from_json_str(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
where
Self: Sized;
}
@@ -38,7 +38,7 @@ macro_rules! actions {
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new(Self))
}
}
@@ -58,8 +58,8 @@ macro_rules! impl_actions {
$crate::__impl_action! {
$namespace,
$name,
fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
Ok(Box::new($crate::serde_json::from_value::<Self>(json)?))
}
}
)*

View File

@@ -434,7 +434,9 @@ impl<T: Entity> ModelHandle<T> {
Duration::from_secs(1)
};
let executor = cx.background().clone();
async move {
executor.start_waiting();
let notification = crate::util::timeout(duration, rx.next())
.await
.expect("next notification timed out");

View File

@@ -394,7 +394,7 @@ impl<'a> WindowContext<'a> {
.iter()
.filter_map(move |(name, (type_id, deserialize))| {
if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
let action = deserialize("{}").ok()?;
let action = deserialize(serde_json::Value::Object(Default::default())).ok()?;
let bindings = self
.keystroke_matcher
.bindings_for_action_type(*type_id)
@@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> {
}
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
let root_view_id = self.window.root_view().id();
let focused_view_id = self.window.focused_view_id?;
self.window
.rendered_views
.get(&root_view_id)?
.get(&focused_view_id)?
.rect_for_text_range(range_utf16, self)
.log_err()
.flatten()

View File

@@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler {
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
self.app
.borrow_mut()
.update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
.borrow()
.read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
.flatten()
}
}

View File

@@ -211,7 +211,7 @@ impl<V: View> Element<V> for List<V> {
let mut cursor = old_items.cursor::<Count>();
if state.rendered_range.start < new_rendered_range.start {
new_items.push_tree(
new_items.append(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
@@ -221,7 +221,7 @@ impl<V: View> Element<V> for List<V> {
cursor.next(&());
}
}
new_items.push_tree(
new_items.append(
cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
&(),
);
@@ -230,7 +230,7 @@ impl<V: View> Element<V> for List<V> {
cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
if new_rendered_range.end < state.rendered_range.start {
new_items.push_tree(
new_items.append(
cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
&(),
);
@@ -240,7 +240,7 @@ impl<V: View> Element<V> for List<V> {
cursor.next(&());
}
new_items.push_tree(cursor.suffix(&()), &());
new_items.append(cursor.suffix(&()), &());
state.items = new_items;
state.rendered_range = new_rendered_range;
@@ -413,7 +413,7 @@ impl<V: View> ListState<V> {
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
new_heights.push_tree(old_heights.suffix(&()), &());
new_heights.append(old_heights.suffix(&()), &());
drop(old_heights);
state.items = new_heights;
}

View File

@@ -36,7 +36,7 @@ struct StateInner {
scroll_to: Option<ScrollTarget>,
}
pub struct LayoutState<V: View> {
pub struct UniformListLayoutState<V: View> {
scroll_max: f32,
item_height: f32,
items: Vec<AnyElement<V>>,
@@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
}
impl<V: View> Element<V> for UniformList<V> {
type LayoutState = LayoutState<V>;
type LayoutState = UniformListLayoutState<V>;
type PaintState = ();
fn layout(
@@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
let no_items = (
constraint.min,
LayoutState {
UniformListLayoutState {
item_height: 0.,
scroll_max: 0.,
items: Default::default(),
@@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
(
size,
LayoutState {
UniformListLayoutState {
item_height,
scroll_max,
items,

View File

@@ -876,6 +876,14 @@ impl Background {
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn start_waiting(&self) {
match self {
Self::Deterministic { executor, .. } => executor.start_waiting(),
_ => panic!("this method can only be called on a deterministic executor"),
}
}
}
impl Default for Background {
@@ -960,7 +968,7 @@ impl<T: 'static, E: 'static + Display> Task<Result<T, E>> {
pub fn detach_and_log_err(self, cx: &mut AppContext) {
cx.spawn(|_| async move {
if let Err(err) = self.await {
log::error!("{}", err);
log::error!("{:#}", err);
}
})
.detach();

View File

@@ -25,8 +25,9 @@ struct Family {
pub struct FontCache(RwLock<FontCacheState>);
pub struct FontCacheState {
fonts: Arc<dyn platform::FontSystem>,
font_system: Arc<dyn platform::FontSystem>,
families: Vec<Family>,
default_family: Option<FamilyId>,
font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
metrics: HashMap<FontId, Metrics>,
wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
@@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
impl FontCache {
pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
Self(RwLock::new(FontCacheState {
fonts,
font_system: fonts,
families: Default::default(),
default_family: None,
font_selections: Default::default(),
metrics: Default::default(),
wrapper_pool: Default::default(),
@@ -73,14 +75,14 @@ impl FontCache {
let mut state = RwLockUpgradableReadGuard::upgrade(state);
if let Ok(font_ids) = state.fonts.load_family(name, features) {
if let Ok(font_ids) = state.font_system.load_family(name, features) {
if font_ids.is_empty() {
continue;
}
let family_id = FamilyId(state.families.len());
for font_id in &font_ids {
if state.fonts.glyph_for_char(*font_id, 'm').is_none() {
if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
return Err(anyhow!("font must contain a glyph for the 'm' character"));
}
}
@@ -99,6 +101,31 @@ impl FontCache {
))
}
/// Returns an arbitrary font family that is available on the system.
pub fn known_existing_family(&self) -> FamilyId {
if let Some(family_id) = self.0.read().default_family {
return family_id;
}
let default_family = self
.load_family(
&["Courier", "Helvetica", "Arial", "Verdana"],
&Default::default(),
)
.unwrap_or_else(|_| {
let all_family_names = self.0.read().font_system.all_families();
let all_family_names: Vec<_> = all_family_names
.iter()
.map(|string| string.as_str())
.collect();
self.load_family(&all_family_names, &Default::default())
.expect("could not load any default font family")
});
self.0.write().default_family = Some(default_family);
default_family
}
pub fn default_font(&self, family_id: FamilyId) -> FontId {
self.select_font(family_id, &Properties::default()).unwrap()
}
@@ -115,7 +142,7 @@ impl FontCache {
let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
let family = &inner.families[family_id.0];
let font_id = inner
.fonts
.font_system
.select_font(&family.font_ids, properties)
.unwrap_or(family.font_ids[0]);
@@ -137,7 +164,7 @@ impl FontCache {
if let Some(metrics) = state.metrics.get(&font_id) {
f(metrics)
} else {
let metrics = state.fonts.font_metrics(font_id);
let metrics = state.font_system.font_metrics(font_id);
let metric = f(&metrics);
let mut state = RwLockUpgradableReadGuard::upgrade(state);
state.metrics.insert(font_id, metrics);
@@ -157,8 +184,11 @@ impl FontCache {
let bounds;
{
let state = self.0.read();
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
bounds = state
.font_system
.typographic_bounds(font_id, glyph_id)
.unwrap();
}
bounds.width() * self.em_scale(font_id, font_size)
}
@@ -168,8 +198,8 @@ impl FontCache {
let advance;
{
let state = self.0.read();
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
advance = state.fonts.advance(font_id, glyph_id).unwrap();
glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
advance = state.font_system.advance(font_id, glyph_id).unwrap();
}
advance.x() * self.em_scale(font_id, font_size)
}
@@ -214,7 +244,7 @@ impl FontCache {
.or_default();
let wrapper = wrappers
.pop()
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone()));
.unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone()));
LineWrapperHandle {
wrapper: Some(wrapper),
font_cache: self.clone(),

View File

@@ -295,13 +295,14 @@ impl Default for TextStyle {
.as_ref()
.expect("TextStyle::default can only be called within a call to with_font_cache");
let font_family_name = Arc::from("Courier");
let font_family_id = font_cache
.load_family(&[&font_family_name], &Default::default())
.unwrap();
let font_family_id = font_cache.known_existing_family();
let font_id = font_cache
.select_font(font_family_id, &Default::default())
.unwrap();
.expect("did not have any font in system-provided family");
let font_family_name = font_cache
.family_name(font_family_id)
.expect("we loaded this family from the font cache, so this should work");
Self {
color: Default::default(),
font_family_name,

View File

@@ -67,7 +67,7 @@ impl KeymapMatcher {
/// MatchResult::Pending =>
/// There exist bindings which are still waiting for more keys.
/// MatchResult::Complete(matches) =>
/// 1 or more bindings have recieved the necessary key presses.
/// 1 or more bindings have received the necessary key presses.
/// The order of the matched actions is by position of the matching first,
// and order in the keymap second.
pub fn push_keystroke(

View File

@@ -343,6 +343,7 @@ pub enum RasterizationOptions {
pub trait FontSystem: Send + Sync {
fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
fn all_families(&self) -> Vec<String>;
fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
fn select_font(
&self,

View File

@@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
self.0.write().add_fonts(fonts)
}
fn all_families(&self) -> Vec<String> {
self.0
.read()
.system_source
.all_families()
.expect("core text should never return an error")
}
fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
self.0.write().load_family(name, features)
}

View File

@@ -786,7 +786,7 @@ impl platform::Platform for MacPlatform {
fn set_cursor_style(&self, style: CursorStyle) {
unsafe {
let cursor: id = match style {
let new_cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
CursorStyle::ResizeLeftRight => {
msg_send![class!(NSCursor), resizeLeftRightCursor]
@@ -795,7 +795,11 @@ impl platform::Platform for MacPlatform {
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
};
let _: () = msg_send![cursor, set];
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
if new_cursor != old_cursor {
let _: () = msg_send![new_cursor, set];
}
}
}

View File

@@ -55,7 +55,7 @@ serde_json.workspace = true
similar = "1.3"
smallvec.workspace = true
smol.workspace = true
tree-sitter = "0.20"
tree-sitter.workspace = true
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-typescript = { version = "*", optional = true }
unicase = "2.6"
@@ -70,8 +70,10 @@ settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
indoc = "1.0.4"
indoc.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter-embedded-template = "*"
tree-sitter-html = "*"
tree-sitter-javascript = "*"
@@ -81,4 +83,3 @@ tree-sitter-rust = "*"
tree-sitter-python = "*"
tree-sitter-typescript = "*"
tree-sitter-ruby = "*"
unindent.workspace = true

View File

@@ -8,7 +8,8 @@ use crate::{
language_settings::{language_settings, LanguageSettings},
outline::OutlineItem,
syntax_map::{
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot,
ToTreeSitterPoint,
},
CodeLabel, LanguageScope, Outline,
};
@@ -50,16 +51,10 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity;
struct GitDiffStatus {
diff: git::diff::BufferDiff,
update_in_progress: bool,
update_requested: bool,
}
pub struct Buffer {
text: TextBuffer,
diff_base: Option<String>,
git_diff_status: GitDiffStatus,
git_diff: git::diff::BufferDiff,
file: Option<Arc<dyn File>>,
saved_version: clock::Global,
saved_version_fingerprint: RopeFingerprint,
@@ -195,6 +190,7 @@ pub enum Event {
Saved,
FileHandleChanged,
Reloaded,
DiffBaseChanged,
LanguageChanged,
Reparsed,
DiagnosticsUpdated,
@@ -221,6 +217,11 @@ pub trait File: Send + Sync {
/// of its worktree, then this method will return the name of the worktree itself.
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
/// Returns the id of the worktree to which this file belongs.
///
/// This is needed for looking up project-specific settings.
fn worktree_id(&self) -> usize;
fn is_deleted(&self) -> bool;
fn as_any(&self) -> &dyn Any;
@@ -466,11 +467,7 @@ impl Buffer {
was_dirty_before_starting_transaction: None,
text: buffer,
diff_base,
git_diff_status: GitDiffStatus {
diff: git::diff::BufferDiff::new(),
update_in_progress: false,
update_requested: false,
},
git_diff: git::diff::BufferDiff::new(),
file,
syntax_map: Mutex::new(SyntaxMap::new()),
parsing_in_background: false,
@@ -501,7 +498,7 @@ impl Buffer {
BufferSnapshot {
text,
syntax,
git_diff: self.git_diff_status.diff.clone(),
git_diff: self.git_diff.clone(),
file: self.file.clone(),
remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(),
@@ -620,7 +617,6 @@ impl Buffer {
cx,
);
}
self.git_diff_recalc(cx);
cx.emit(Event::Reloaded);
cx.notify();
}
@@ -676,50 +672,29 @@ impl Buffer {
pub fn set_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
self.diff_base = diff_base;
self.git_diff_recalc(cx);
cx.emit(Event::DiffBaseChanged);
}
pub fn needs_git_diff_recalc(&self) -> bool {
self.git_diff_status.diff.needs_update(self)
}
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) -> Option<Task<()>> {
let diff_base = self.diff_base.clone()?; // TODO: Make this an Arc
let snapshot = self.snapshot();
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
if self.git_diff_status.update_in_progress {
self.git_diff_status.update_requested = true;
return;
}
let mut diff = self.git_diff.clone();
let diff = cx.background().spawn(async move {
diff.update(&diff_base, &snapshot).await;
diff
});
if let Some(diff_base) = &self.diff_base {
let snapshot = self.snapshot();
let diff_base = diff_base.clone();
let mut diff = self.git_diff_status.diff.clone();
let diff = cx.background().spawn(async move {
diff.update(&diff_base, &snapshot).await;
diff
});
cx.spawn_weak(|this, mut cx| async move {
let buffer_diff = diff.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.git_diff_status.diff = buffer_diff;
this.git_diff_update_count += 1;
cx.notify();
this.git_diff_status.update_in_progress = false;
if this.git_diff_status.update_requested {
this.git_diff_recalc(cx);
}
})
}
})
.detach()
} else {
let snapshot = self.snapshot();
self.git_diff_status.diff.clear(&snapshot);
self.git_diff_update_count += 1;
cx.notify();
}
let handle = cx.weak_handle();
Some(cx.spawn_weak(|_, mut cx| async move {
let buffer_diff = diff.await;
if let Some(this) = handle.upgrade(&mut cx) {
this.update(&mut cx, |this, _| {
this.git_diff = buffer_diff;
this.git_diff_update_count += 1;
})
}
}))
}
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
@@ -1833,8 +1808,7 @@ impl BufferSnapshot {
}
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let language_name = self.language_at(position).map(|language| language.name());
let settings = language_settings(language_name.as_deref(), cx);
let settings = language_settings(self.language_at(position), self.file(), cx);
if settings.hard_tabs {
IndentSize::tab()
} else {
@@ -2143,12 +2117,20 @@ impl BufferSnapshot {
}
}
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
self.syntax.layers_for_range(0..self.len(), &self.text)
}
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.filter(|l| l.node().end_byte() > offset)
.last()
}
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
self.syntax_layer_at(position)
.map(|info| info.language)
.or(self.language.as_ref())
}
@@ -2158,8 +2140,7 @@ impl BufferSnapshot {
position: D,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(position);
language_settings(language.map(|l| l.name()).as_deref(), cx)
language_settings(self.language_at(position), self.file.as_ref(), cx)
}
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
@@ -2168,7 +2149,7 @@ impl BufferSnapshot {
if let Some(layer_info) = self
.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.filter(|l| l.node().end_byte() > offset)
.last()
{
Some(LanguageScope {
@@ -2216,7 +2197,7 @@ impl BufferSnapshot {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<Range<usize>> = None;
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
let mut cursor = layer.node.walk();
let mut cursor = layer.node().walk();
// Descend to the first leaf that touches the start of the range,
// and if the range is non-empty, extends beyond the start.
@@ -2281,7 +2262,7 @@ impl BufferSnapshot {
}
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
self.outline_items_containing(0..self.len(), theme)
self.outline_items_containing(0..self.len(), true, theme)
.map(Outline::new)
}
@@ -2293,6 +2274,7 @@ impl BufferSnapshot {
let position = position.to_offset(self);
let mut items = self.outline_items_containing(
position.saturating_sub(1)..self.len().min(position + 1),
false,
theme,
)?;
let mut prev_depth = None;
@@ -2307,6 +2289,7 @@ impl BufferSnapshot {
fn outline_items_containing(
&self,
range: Range<usize>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> {
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
@@ -2341,7 +2324,10 @@ impl BufferSnapshot {
let node_is_name;
if capture.index == config.name_capture_ix {
node_is_name = true;
} else if Some(capture.index) == config.context_capture_ix {
} else if Some(capture.index) == config.context_capture_ix
|| (Some(capture.index) == config.extra_context_capture_ix
&& include_extra_context)
{
node_is_name = false;
} else {
continue;
@@ -2368,10 +2354,12 @@ impl BufferSnapshot {
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
true,
);
let mut last_buffer_range_end = 0;
for (buffer_range, is_name) in buffer_ranges {
if !text.is_empty() {
if !text.is_empty() && buffer_range.start > last_buffer_range_end {
text.push(' ');
}
last_buffer_range_end = buffer_range.end;
if is_name {
let mut start = text.len();
let end = start + buffer_range.len();

View File

@@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
);
}
#[gpui::test]
async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
let language = javascript_lang()
.with_outline_query(
r#"
(function_declaration
"function" @context
name: (_) @name
parameters: (formal_parameters
"(" @context.extra
")" @context.extra)) @item
"#,
)
.unwrap();
let text = r#"
function a() {}
function b(c) {}
"#
.unindent();
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
// extra context nodes are included in the outline.
let outline = snapshot.outline(None).unwrap();
assert_eq!(
outline
.items
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a()", 0), ("function b( )", 0),]
);
// extra context nodes do not appear in breadcrumbs.
let symbols = snapshot.symbols_containing(3, None).unwrap();
assert_eq!(
symbols
.iter()
.map(|item| (item.text.as_str(), item.depth))
.collect::<Vec<_>>(),
&[("function a", 0)]
);
}
#[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"
@@ -2196,7 +2242,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
layers[0].node.to_sexp()
layers[0].node().to_sexp()
})
}

View File

@@ -17,7 +17,7 @@ use futures::{
future::{BoxFuture, Shared},
FutureExt, TryFutureExt as _,
};
use gpui::{executor::Background, AppContext, Task};
use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
use highlight_map::HighlightMap;
use lazy_static::lazy_static;
use lsp::CodeActionKind;
@@ -34,7 +34,7 @@ use std::{
fmt::Debug,
hash::Hash,
mem,
ops::Range,
ops::{Not, Range},
path::{Path, PathBuf},
str,
sync::{
@@ -57,6 +57,7 @@ pub use buffer::*;
pub use diagnostic_set::DiagnosticEntry;
pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem};
pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
pub use tree_sitter::{Parser, Tree};
pub fn init(cx: &mut AppContext) {
@@ -124,27 +125,46 @@ impl CachedLspAdapter {
pub async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
self.adapter.fetch_latest_server_version(http).await
self.adapter.fetch_latest_server_version(delegate).await
}
pub fn will_fetch_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Result<()>>> {
self.adapter.will_fetch_server(delegate, cx)
}
pub fn will_start_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Result<()>>> {
self.adapter.will_start_server(delegate, cx)
}
pub async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
self.adapter
.fetch_server_binary(version, http, container_dir)
.fetch_server_binary(version, container_dir, delegate)
.await
}
pub async fn cached_server_binary(
&self,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
self.adapter.cached_server_binary(container_dir).await
self.adapter
.cached_server_binary(container_dir, delegate)
.await
}
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -186,23 +206,48 @@ impl CachedLspAdapter {
}
}
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
}
#[async_trait]
pub trait LspAdapter: 'static + Send + Sync {
async fn name(&self) -> LanguageServerName;
async fn fetch_latest_server_version(
&self,
http: Arc<dyn HttpClient>,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>>;
fn will_fetch_server(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncAppContext,
) -> Option<Task<Result<()>>> {
None
}
fn will_start_server(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncAppContext,
) -> Option<Task<Result<()>>> {
None
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
http: Arc<dyn HttpClient>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary>;
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary>;
async fn cached_server_binary(
&self,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary>;
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
@@ -455,6 +500,7 @@ struct OutlineConfig {
item_capture_ix: u32,
name_capture_ix: u32,
context_capture_ix: Option<u32>,
extra_context_capture_ix: Option<u32>,
}
struct InjectionConfig {
@@ -500,6 +546,7 @@ struct AvailableLanguage {
grammar: tree_sitter::Language,
lsp_adapters: Vec<Arc<dyn LspAdapter>>,
get_queries: fn(&str) -> LanguageQueries,
loaded: bool,
}
pub struct LanguageRegistry {
@@ -510,10 +557,7 @@ pub struct LanguageRegistry {
login_shell_env_loaded: Shared<Task<()>>,
#[allow(clippy::type_complexity)]
lsp_binary_paths: Mutex<
HashMap<
LanguageServerName,
Shared<BoxFuture<'static, Result<LanguageServerBinary, Arc<anyhow::Error>>>>,
>,
HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
>,
executor: Option<Arc<Background>>,
}
@@ -527,6 +571,7 @@ struct LanguageRegistryState {
subscription: (watch::Sender<()>, watch::Receiver<()>),
theme: Option<Arc<Theme>>,
version: usize,
reload_count: usize,
}
pub struct PendingLanguageServer {
@@ -547,6 +592,7 @@ impl LanguageRegistry {
subscription: watch::channel(),
theme: Default::default(),
version: 0,
reload_count: 0,
}),
language_server_download_dir: None,
lsp_binary_statuses_tx,
@@ -566,6 +612,14 @@ impl LanguageRegistry {
self.executor = Some(executor);
}
/// Clear out all of the loaded languages and reload them from scratch.
///
/// This is useful in development, when queries have changed.
#[cfg(debug_assertions)]
pub fn reload(&self) {
self.state.write().reload();
}
pub fn register(
&self,
path: &'static str,
@@ -582,6 +636,7 @@ impl LanguageRegistry {
grammar,
lsp_adapters,
get_queries,
loaded: false,
});
}
@@ -590,7 +645,7 @@ impl LanguageRegistry {
let mut result = state
.available_languages
.iter()
.map(|l| l.config.name.to_string())
.filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
.chain(state.languages.iter().map(|l| l.config.name.to_string()))
.collect::<Vec<_>>();
result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
@@ -603,6 +658,7 @@ impl LanguageRegistry {
state
.available_languages
.iter()
.filter(|l| !l.loaded)
.flat_map(|l| l.lsp_adapters.clone())
.chain(
state
@@ -639,10 +695,17 @@ impl LanguageRegistry {
self.state.read().subscription.1.clone()
}
/// The number of times that the registry has been changed,
/// by adding languages or reloading.
pub fn version(&self) -> usize {
self.state.read().version
}
/// The number of times that the registry has been reloaded.
pub fn reload_count(&self) -> usize {
self.state.read().reload_count
}
pub fn set_theme(&self, theme: Arc<Theme>) {
let mut state = self.state.write();
state.theme = Some(theme.clone());
@@ -721,7 +784,7 @@ impl LanguageRegistry {
if let Some(language) = state
.available_languages
.iter()
.find(|l| callback(&l.config))
.find(|l| !l.loaded && callback(&l.config))
.cloned()
{
let txs = state
@@ -743,9 +806,7 @@ impl LanguageRegistry {
let language = Arc::new(language);
let mut state = this.state.write();
state.add(language.clone());
state
.available_languages
.retain(|language| language.id != id);
state.mark_language_loaded(id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Ok(language.clone()));
@@ -753,10 +814,9 @@ impl LanguageRegistry {
}
}
Err(err) => {
log::error!("failed to load language {name} - {err}");
let mut state = this.state.write();
state
.available_languages
.retain(|language| language.id != id);
state.mark_language_loaded(id);
if let Some(mut txs) = state.loading_languages.remove(&id) {
for tx in txs.drain(..) {
let _ = tx.send(Err(anyhow!(
@@ -793,9 +853,15 @@ impl LanguageRegistry {
language: Arc<Language>,
adapter: Arc<CachedLspAdapter>,
root_path: Arc<Path>,
http_client: Arc<dyn HttpClient>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AppContext,
) -> Option<PendingLanguageServer> {
let server_id = self.state.write().next_language_server_id();
log::info!(
"starting language server name:{}, path:{root_path:?}, id:{server_id}",
adapter.name.0
);
#[cfg(any(test, feature = "test-support"))]
if language.fake_adapter.is_some() {
let task = cx.spawn(|cx| async move {
@@ -825,7 +891,6 @@ impl LanguageRegistry {
Ok(server)
});
let server_id = self.state.write().next_language_server_id();
return Some(PendingLanguageServer { server_id, task });
}
@@ -834,39 +899,42 @@ impl LanguageRegistry {
.clone()
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
.log_err()?;
let this = self.clone();
let language = language.clone();
let http_client = http_client.clone();
let download_dir = download_dir.clone();
let root_path = root_path.clone();
let adapter = adapter.clone();
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
let server_id = self.state.write().next_language_server_id();
let task = cx.spawn(|cx| async move {
let task = cx.spawn(|mut cx| async move {
login_shell_env_loaded.await;
let mut lock = this.lsp_binary_paths.lock();
let entry = lock
let entry = this
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
get_binary(
adapter.clone(),
language.clone(),
http_client,
download_dir,
lsp_binary_statuses,
)
.map_err(Arc::new)
.boxed()
cx.spawn(|cx| {
get_binary(
adapter.clone(),
language.clone(),
delegate.clone(),
download_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();
drop(lock);
let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
task.await?;
}
let server = lsp::LanguageServer::new(
server_id,
&binary.path,
@@ -902,6 +970,28 @@ impl LanguageRegistryState {
self.version += 1;
*self.subscription.0.borrow_mut() = ();
}
#[cfg(debug_assertions)]
fn reload(&mut self) {
self.languages.clear();
self.version += 1;
self.reload_count += 1;
for language in &mut self.available_languages {
language.loaded = false;
}
*self.subscription.0.borrow_mut() = ();
}
/// Mark the given language a having been loaded, so that the
/// language registry won't try to load it again.
fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
for language in &mut self.available_languages {
if language.id == id {
language.loaded = true;
break;
}
}
}
}
#[cfg(any(test, feature = "test-support"))]
@@ -914,9 +1004,10 @@ impl Default for LanguageRegistry {
async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
delegate: Arc<dyn LspAdapterDelegate>,
download_dir: Arc<Path>,
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
mut cx: AsyncAppContext,
) -> Result<LanguageServerBinary> {
let container_dir = download_dir.join(adapter.name.0.as_ref());
if !container_dir.exists() {
@@ -925,17 +1016,24 @@ async fn get_binary(
.context("failed to create container directory")?;
}
if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) {
task.await?;
}
let binary = fetch_latest_binary(
adapter.clone(),
language.clone(),
http_client,
delegate.as_ref(),
&container_dir,
statuses.clone(),
)
.await;
if let Err(error) = binary.as_ref() {
if let Some(cached) = adapter.cached_server_binary(container_dir).await {
if let Some(cached) = adapter
.cached_server_binary(container_dir, delegate.as_ref())
.await
{
statuses
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
.await?;
@@ -957,7 +1055,7 @@ async fn get_binary(
async fn fetch_latest_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
http_client: Arc<dyn HttpClient>,
delegate: &dyn LspAdapterDelegate,
container_dir: &Path,
lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
) -> Result<LanguageServerBinary> {
@@ -968,14 +1066,12 @@ async fn fetch_latest_binary(
LanguageServerBinaryStatus::CheckingForUpdate,
))
.await?;
let version_info = adapter
.fetch_latest_server_version(http_client.clone())
.await?;
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
.await?;
let binary = adapter
.fetch_server_binary(version_info, http_client, container_dir.to_path_buf())
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;
lsp_binary_statuses_tx
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
@@ -1018,34 +1114,22 @@ impl Language {
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
.with_highlights_query(query.as_ref())
.expect("failed to evaluate highlights query");
self = self.with_highlights_query(query.as_ref())?;
}
if let Some(query) = queries.brackets {
self = self
.with_brackets_query(query.as_ref())
.expect("failed to load brackets query");
self = self.with_brackets_query(query.as_ref())?;
}
if let Some(query) = queries.indents {
self = self
.with_indents_query(query.as_ref())
.expect("failed to load indents query");
self = self.with_indents_query(query.as_ref())?;
}
if let Some(query) = queries.outline {
self = self
.with_outline_query(query.as_ref())
.expect("failed to load outline query");
self = self.with_outline_query(query.as_ref())?;
}
if let Some(query) = queries.injections {
self = self
.with_injection_query(query.as_ref())
.expect("failed to load injection query");
self = self.with_injection_query(query.as_ref())?;
}
if let Some(query) = queries.overrides {
self = self
.with_override_query(query.as_ref())
.expect("failed to load override query");
self = self.with_override_query(query.as_ref())?;
}
Ok(self)
}
@@ -1061,12 +1145,14 @@ impl Language {
let mut item_capture_ix = None;
let mut name_capture_ix = None;
let mut context_capture_ix = None;
let mut extra_context_capture_ix = None;
get_capture_indices(
&query,
&mut [
("item", &mut item_capture_ix),
("name", &mut name_capture_ix),
("context", &mut context_capture_ix),
("context.extra", &mut extra_context_capture_ix),
],
);
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@@ -1075,6 +1161,7 @@ impl Language {
item_capture_ix,
name_capture_ix,
context_capture_ix,
extra_context_capture_ix,
});
}
Ok(self)
@@ -1508,7 +1595,7 @@ impl LspAdapter for Arc<FakeLspAdapter> {
async fn fetch_latest_server_version(
&self,
_: Arc<dyn HttpClient>,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!();
}
@@ -1516,13 +1603,17 @@ impl LspAdapter for Arc<FakeLspAdapter> {
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: Arc<dyn HttpClient>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
unreachable!();
}
async fn cached_server_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
unreachable!();
}

View File

@@ -1,3 +1,4 @@
use crate::{File, Language};
use anyhow::Result;
use collections::HashMap;
use globset::GlobMatcher;
@@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
settings::register::<AllLanguageSettings>(cx);
}
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
settings::get::<AllLanguageSettings>(cx).language(language)
pub fn language_settings<'a>(
language: Option<&Arc<Language>>,
file: Option<&Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language_name = language.map(|l| l.name());
all_language_settings(file, cx).language(language_name.as_deref())
}
pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
settings::get::<AllLanguageSettings>(cx)
pub fn all_language_settings<'a>(
file: Option<&Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a AllLanguageSettings {
let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
settings::get_local(location, cx)
}
#[derive(Debug, Clone)]
@@ -41,6 +51,7 @@ pub struct LanguageSettings {
pub enable_language_server: bool,
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
pub extend_comment_on_newline: bool,
}
#[derive(Clone, Debug, Default)]
@@ -85,6 +96,8 @@ pub struct LanguageSettingsContent {
pub show_copilot_suggestions: Option<bool>,
#[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>,
#[serde(default)]
pub extend_comment_on_newline: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -155,7 +168,7 @@ impl AllLanguageSettings {
.any(|glob| glob.is_match(path))
}
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
if !self.copilot.feature_enabled {
return false;
}
@@ -166,7 +179,8 @@ impl AllLanguageSettings {
}
}
self.language(language_name).show_copilot_suggestions
self.language(language.map(|l| l.name()).as_deref())
.show_copilot_suggestions
}
}
@@ -253,7 +267,7 @@ impl settings::Setting for AllLanguageSettings {
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
// settings with specific languages.
assert!(root_schema
.definitions
.contains_key("LanguageSettingsContent"));
@@ -329,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.show_copilot_suggestions,
);
merge(&mut settings.show_whitespaces, src.show_whitespaces);
merge(
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
);
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
[package]
name = "lsp_log"
name = "language_tools"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/lsp_log.rs"
path = "src/language_tools.rs"
doctest = false
[dependencies]
@@ -22,9 +22,12 @@ lsp = { path = "../lsp" }
futures.workspace = true
serde.workspace = true
anyhow.workspace = true
tree-sitter.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
env_logger.workspace = true
unindent.workspace = true

View File

@@ -0,0 +1,15 @@
mod lsp_log;
mod syntax_tree_view;
#[cfg(test)]
mod lsp_log_tests;
use gpui::AppContext;
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
pub fn init(cx: &mut AppContext) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
}

View File

@@ -0,0 +1,786 @@
use collections::HashMap;
use editor::Editor;
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
elements::{
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, Stack,
},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, View, ViewContext,
ViewHandle, WeakModelHandle,
};
use language::{Buffer, LanguageServerId, LanguageServerName};
use project::{Project, Worktree};
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
};
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
pub struct LogStore {
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
}
struct ProjectState {
servers: HashMap<LanguageServerId, LanguageServerState>,
_subscriptions: [gpui::Subscription; 2],
}
struct LanguageServerState {
log_buffer: ModelHandle<Buffer>,
rpc_state: Option<LanguageServerRpcState>,
}
struct LanguageServerRpcState {
buffer: ModelHandle<Buffer>,
last_message_kind: Option<MessageKind>,
_subscription: lsp::Subscription,
}
pub struct LspLogView {
pub(crate) editor: ViewHandle<Editor>,
log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
project: ModelHandle<Project>,
}
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
menu_open: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum MessageKind {
Send,
Receive,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct LogMenuItem {
pub server_id: LanguageServerId,
pub server_name: LanguageServerName,
pub worktree: ModelHandle<Worktree>,
pub rpc_trace_enabled: bool,
pub rpc_trace_selected: bool,
pub logs_selected: bool,
}
actions!(debug, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
let log_store = cx.add_model(|cx| LogStore::new(cx));
cx.subscribe_global::<WorkspaceCreated, _>({
let log_store = log_store.clone();
move |event, cx| {
let workspace = &event.0;
if let Some(workspace) = workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(&project, cx);
});
}
}
}
})
.detach();
cx.add_action(
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item(
Box::new(cx.add_view(|cx| {
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
})),
cx,
);
}
},
);
}
impl LogStore {
pub fn new(cx: &mut ModelContext<Self>) -> Self {
let (io_tx, mut io_rx) = mpsc::unbounded();
let this = Self {
projects: HashMap::default(),
io_tx,
};
cx.spawn_weak(|this, mut cx| async move {
while let Some((project, server_id, is_output, mut message)) = io_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
message.push('\n');
this.on_io(project, server_id, is_output, &message, cx);
});
}
}
anyhow::Ok(())
})
.detach();
this
}
pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
use project::Event::*;
let weak_project = project.downgrade();
self.projects.insert(
weak_project,
ProjectState {
servers: HashMap::default(),
_subscriptions: [
cx.observe_release(&project, move |this, _, _| {
this.projects.remove(&weak_project);
}),
cx.subscribe(project, |this, project, event, cx| match event {
LanguageServerAdded(id) => {
this.add_language_server(&project, *id, cx);
}
LanguageServerRemoved(id) => {
this.remove_language_server(&project, *id, cx);
}
LanguageServerLog(id, message) => {
this.add_language_server_log(&project, *id, message, cx);
}
_ => {}
}),
],
},
);
}
fn add_language_server(
&mut self,
project: &ModelHandle<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
let project_state = self.projects.get_mut(&project.downgrade())?;
Some(
project_state
.servers
.entry(id)
.or_insert_with(|| {
cx.notify();
LanguageServerState {
rpc_state: None,
log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(),
}
})
.log_buffer
.clone(),
)
}
fn add_language_server_log(
&mut self,
project: &ModelHandle<Project>,
id: LanguageServerId,
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let buffer = self.add_language_server(&project, id, cx)?;
buffer.update(cx, |buffer, cx| {
let len = buffer.len();
let has_newline = message.ends_with("\n");
buffer.edit([(len..len, message)], None, cx);
if !has_newline {
let len = buffer.len();
buffer.edit([(len..len, "\n")], None, cx);
}
});
cx.notify();
Some(())
}
fn remove_language_server(
&mut self,
project: &ModelHandle<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let project_state = self.projects.get_mut(&project.downgrade())?;
project_state.servers.remove(&id);
cx.notify();
Some(())
}
pub fn log_buffer_for_server(
&self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
) -> Option<ModelHandle<Buffer>> {
let weak_project = project.downgrade();
let project_state = self.projects.get(&weak_project)?;
let server_state = project_state.servers.get(&server_id)?;
Some(server_state.log_buffer.clone())
}
pub fn enable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<ModelHandle<Buffer>> {
let weak_project = project.downgrade();
let project_state = self.projects.get_mut(&weak_project)?;
let server_state = project_state.servers.get_mut(&server_id)?;
let server = project.read(cx).language_server_for_id(server_id)?;
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
let io_tx = self.io_tx.clone();
let language = project.read(cx).languages().language_for_name("JSON");
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
cx.spawn_weak({
let buffer = buffer.clone();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
});
}
})
.detach();
LanguageServerRpcState {
buffer,
last_message_kind: None,
_subscription: server.on_io(move |is_received, json| {
io_tx
.unbounded_send((weak_project, server_id, is_received, json.to_string()))
.ok();
}),
}
});
Some(rpc_state.buffer.clone())
}
pub fn disable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
server_id: LanguageServerId,
_: &mut ModelContext<Self>,
) -> Option<()> {
let project = project.downgrade();
let project_state = self.projects.get_mut(&project)?;
let server_state = project_state.servers.get_mut(&server_id)?;
server_state.rpc_state.take();
Some(())
}
fn on_io(
&mut self,
project: WeakModelHandle<Project>,
language_server_id: LanguageServerId,
is_received: bool,
message: &str,
cx: &mut AppContext,
) -> Option<()> {
let state = self
.projects
.get_mut(&project)?
.servers
.get_mut(&language_server_id)?
.rpc_state
.as_mut()?;
state.buffer.update(cx, |buffer, cx| {
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
};
if state.last_message_kind != Some(kind) {
let len = buffer.len();
let line = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
buffer.edit([(len..len, line)], None, cx);
state.last_message_kind = Some(kind);
}
let len = buffer.len();
buffer.edit([(len..len, message)], None, cx);
});
Some(())
}
}
impl LspLogView {
pub fn new(
project: ModelHandle<Project>,
log_store: ModelHandle<LogStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let server_id = log_store
.read(cx)
.projects
.get(&project.downgrade())
.and_then(|project| project.servers.keys().copied().next());
let buffer = cx.add_model(|cx| Buffer::new(0, "", cx));
let mut this = Self {
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
project,
log_store,
current_server_id: None,
is_showing_rpc_trace: false,
};
if let Some(server_id) = server_id {
this.show_logs_for_server(server_id, cx);
}
this
}
fn editor_for_buffer(
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: &mut ViewContext<Self>,
) -> ViewHandle<Editor> {
let editor = cx.add_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
editor.set_read_only(true);
editor.move_to_end(&Default::default(), cx);
editor
});
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
.detach();
editor
}
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
let log_store = self.log_store.read(cx);
let state = log_store.projects.get(&self.project.downgrade())?;
let mut rows = self
.project
.read(cx)
.language_servers()
.filter_map(|(server_id, language_server_name, worktree_id)| {
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
let state = state.servers.get(&server_id)?;
Some(LogMenuItem {
server_id,
server_name: language_server_name,
worktree,
rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
})
})
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.server_id);
rows.dedup_by_key(|row| row.server_id);
Some(rows)
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let buffer = self
.log_store
.read(cx)
.log_buffer_for_server(&self.project, server_id);
if let Some(buffer) = buffer {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = false;
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
cx.notify();
}
}
fn show_rpc_trace_for_server(
&mut self,
server_id: LanguageServerId,
cx: &mut ViewContext<Self>,
) {
let buffer = self.log_store.update(cx, |log_set, cx| {
log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
});
if let Some(buffer) = buffer {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = true;
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
cx.notify();
}
}
fn toggle_rpc_trace_for_server(
&mut self,
server_id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
self.log_store.update(cx, |log_store, cx| {
if enabled {
log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
} else {
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
}
});
if !enabled && Some(server_id) == self.current_server_id {
self.show_logs_for_server(server_id, cx);
cx.notify();
}
}
}
impl View for LspLogView {
fn ui_name() -> &'static str {
"LspLogView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
}
}
impl Item for LspLogView {
fn tab_content<V: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<V> {
Label::new("LSP Logs", style.label.clone()).into_any()
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
}
impl SearchableItem for LspLogView {
type Match = <Editor as SearchableItem>::Match;
fn to_search_event(event: &Self::Event) -> Option<workspace::searchable::SearchEvent> {
Editor::to_search_event(event)
}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |e, cx| e.clear_matches(cx))
}
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |e, cx| e.update_matches(matches, cx))
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
self.editor.update(cx, |e, cx| e.query_suggestion(cx))
}
fn activate_match(
&mut self,
index: usize,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) {
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, cx))
}
fn find_matches(
&mut self,
query: project::search::SearchQuery,
cx: &mut ViewContext<Self>,
) -> gpui::Task<Vec<Self::Match>> {
self.editor.update(cx, |e, cx| e.find_matches(query, cx))
}
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
self.editor
.update(cx, |e, cx| e.active_match_index(matches, cx))
}
}
impl ToolbarItemView for LspLogToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
self.log_view = Some(log_view.clone());
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
}
}
self.log_view = None;
ToolbarItemLocation::Hidden
}
}
impl View for LspLogToolbarItemView {
fn ui_name() -> &'static str {
"LspLogView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
let log_view = log_view.read(cx);
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
let current_server_id = log_view.current_server_id;
let current_server = current_server_id.and_then(|current_server_id| {
if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
Some(menu_rows[ix].clone())
} else {
None
}
});
enum Menu {}
Stack::new()
.with_child(Self::render_language_server_menu_header(
current_server,
&theme,
cx,
))
.with_children(if self.menu_open {
Some(
Overlay::new(
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
Flex::column()
.with_children(menu_rows.into_iter().map(|row| {
Self::render_language_server_menu_item(
row.server_id,
row.server_name,
row.worktree,
row.rpc_trace_enabled,
row.logs_selected,
row.rpc_trace_selected,
&theme,
cx,
)
}))
.contained()
.with_style(theme.toolbar_dropdown_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.menu_open = false;
cx.notify()
}),
)
.with_hoverable(true)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left(),
)
} else {
None
})
.aligned()
.left()
.clipped()
.into_any()
}
}
const RPC_MESSAGES: &str = "RPC Messages";
const SERVER_LOGS: &str = "Server Logs";
impl LspLogToolbarItemView {
pub fn new() -> Self {
Self {
menu_open: false,
log_view: None,
}
}
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
self.menu_open = !self.menu_open;
cx.notify();
}
fn toggle_logging_for_server(
&mut self,
id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| {
log_view.toggle_rpc_trace_for_server(id, enabled, cx);
if !enabled && Some(id) == log_view.current_server_id {
log_view.show_logs_for_server(id, cx);
cx.notify();
}
});
}
cx.notify();
}
fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
self.menu_open = false;
cx.notify();
}
}
fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
self.menu_open = false;
cx.notify();
}
}
fn render_language_server_menu_header(
current_server: Option<LogMenuItem>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, cx| {
let label: Cow<str> = current_server
.and_then(|row| {
let worktree = row.worktree.read(cx);
Some(
format!(
"{} ({}) - {}",
row.server_name.0,
worktree.root_name(),
if row.rpc_trace_selected {
RPC_MESSAGES
} else {
SERVER_LOGS
},
)
.into(),
)
})
.unwrap_or_else(|| "No server selected".into());
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx);
})
}
fn render_language_server_menu_item(
id: LanguageServerId,
name: LanguageServerName,
worktree: ModelHandle<Worktree>,
rpc_trace_enabled: bool,
logs_selected: bool,
rpc_trace_selected: bool,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ActivateLog {}
enum ActivateRpcTrace {}
Flex::column()
.with_child({
let style = &theme.toolbar_dropdown_menu.section_header;
Label::new(
format!("{} ({})", name.0, worktree.read(cx).root_name()),
style.text.clone(),
)
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_child(
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, logs_selected);
Label::new(SERVER_LOGS, style.text.clone())
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.show_logs_for_server(id, cx);
}),
)
.with_child(
MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, rpc_trace_selected);
Flex::row()
.with_child(
Label::new(RPC_MESSAGES, style.text.clone())
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height),
)
.with_child(
ui::checkbox_with_label::<Self, _, Self, _>(
Empty::new(),
&theme.welcome.checkbox,
rpc_trace_enabled,
id.0,
cx,
move |this, enabled, cx| {
this.toggle_logging_for_server(id, enabled, cx);
},
)
.flex_float(),
)
.align_children_center()
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.show_rpc_trace_for_server(id, cx);
}),
)
}
}
impl Entity for LogStore {
type Event = ();
}
impl Entity for LspLogView {
type Event = editor::Event;
}
impl Entity for LspLogToolbarItemView {
type Event = ();
}

View File

@@ -0,0 +1,99 @@
use std::sync::Arc;
use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, TestAppContext};
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
use project::{FakeFs, Project};
use settings::SettingsStore;
#[gpui::test]
async fn test_lsp_logs(cx: &mut TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
init_test(cx);
let mut rust_language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_rust_servers = rust_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-rust-language-server",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/the-root",
json!({
"test.rs": "",
"package.json": "",
}),
)
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages().add(Arc::new(rust_language));
});
let log_store = cx.add_model(|cx| LogStore::new(cx));
log_store.update(cx, |store, cx| store.add_project(&project, cx));
let _rust_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test.rs", cx)
})
.await
.unwrap();
let mut language_server = fake_rust_servers.next().await.unwrap();
language_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await;
let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
message: "hello from the server".into(),
typ: lsp::MessageType::INFO,
});
cx.foreground().run_until_parked();
log_view.read_with(cx, |view, cx| {
assert_eq!(
view.menu_items(cx).unwrap(),
&[LogMenuItem {
server_id: language_server.server.server_id(),
server_name: LanguageServerName("the-rust-language-server".into()),
worktree: project.read(cx).worktrees(cx).next().unwrap(),
rpc_trace_enabled: false,
rpc_trace_selected: false,
logs_selected: true,
}]
);
assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");
});
}
fn init_test(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
client::init_settings(cx);
Project::init_settings(cx);
editor::init_settings(cx);
});
}

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