Compare commits

..

57 Commits

Author SHA1 Message Date
Ben Kunkle
a037c39868 use extra context 2025-05-19 11:25:22 +02:00
Ben Kunkle
b1a6c35e3f update python/outline.scm for decorator support 2025-05-19 11:25:22 +02:00
Ben Kunkle
a61d89feba multiline entries in outline panel
Co-Authored-By: Kirill <kirill@zed.dev>
2025-05-19 11:25:22 +02:00
Sergei Kartsev
a829281841 Fix prevent zero value for buffer line height (#30832)
Closes #30802 

Release Notes:

- Fixed issue where setting `buffer_line_height.custom` to 0 would cause
text to disappear

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-05-19 00:55:35 +00:00
Aleksei Voronin
592568ff87 docs: Add a missing comma in AI configuration docs (#30928) 2025-05-19 00:22:16 +02:00
Kirill Bulatov
83afe56a61 Add a way to import ssh host names from the ssh config (#30926)
Closes https://github.com/zed-industries/zed/issues/20016

Use `"read_ssh_config": false` to disable the new behavior.

Release Notes:

- Added a way to import ssh host names from the ssh config

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-05-18 20:34:47 +00:00
Michael Sloan
e468f9d2da Remove unsaved text thread from recent history when switching away (#30918)
Bug found by @SomeoneToIgnore 

Release Notes:

- N/A
2025-05-18 20:11:06 +00:00
Max Brunsfeld
1ce2652a89 agent: Create checkpoints when editing a past message (#30831)
Release Notes:

- N/A
2025-05-18 09:02:15 -07:00
Michael Sloan
784d51c40f Fix pane deduplication for unsaved buffers that have no path (#30834)
For example, running `zed some-new-path` multiple times would open
multiple tabs.

Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>
2025-05-18 14:22:06 +00:00
Smit Barmase
0079c99c2c editor: Add python indentation tests (#30902)
This PR add tests for a recent PR: [language: Fix indent suggestions for
significant indented languages like
Python](https://github.com/zed-industries/zed/pull/29625)

It also covers cases from past related issues so that we don't end up
circling back to them on future fixes.

- [Python incorrect auto-indentation for
except:](https://github.com/zed-industries/zed/issues/10832)
- [Python for/while...else indention overridden by if statement
](https://github.com/zed-industries/zed/issues/30795)
- [Python: erroneous indent on newline when comment ends in
:](https://github.com/zed-industries/zed/issues/25416)
- [Newline in Python file does not indent
](https://github.com/zed-industries/zed/issues/16288)
- [Tab Indentation works incorrectly when there are multiple
cursors](https://github.com/zed-industries/zed/issues/26157)

Release Notes:

- N/A
2025-05-18 07:29:25 +05:30
Peter Tripp
230eb12f72 docs: Fix incorrect info in C# documentation (#30891)
`ignore_system_version` does not work for extensions.

Release Notes:

- N/A
2025-05-17 19:05:52 +00:00
Marshall Bowers
dd3956eaf1 Add a picker for jj bookmark list (#30883)
This PR adds a new picker for viewing a list of jj bookmarks, like you
would with `jj bookmark list`.

This is an exploration around what it would look like to begin adding
some dedicated jj features to Zed.

This is behind the `jj-ui` feature flag.

Release Notes:

- N/A
2025-05-17 16:42:45 +00:00
Marshall Bowers
122d6c9e4d Upgrade tempfile to v3.20.0 (#30886)
This PR upgrades our `tempfile` dependency to v3.20.0.

Pulling out of https://github.com/zed-industries/zed/pull/30883.

Release Notes:

- N/A
2025-05-17 16:25:09 +00:00
Danilo Leal
19e89a8b2d agent: Scroll to the bottom after sending a new message (#30878)
Closes https://github.com/zed-industries/zed/issues/30572

Release Notes:

- agent: Improved UX by scrolling to the bottom of the thread after
submitting a new message or editing a previous one.
2025-05-17 12:57:00 -03:00
Danilo Leal
919ffe7655 docs: Refine some agent-related pages (#30884)
Release Notes:

- N/A
2025-05-17 12:56:45 -03:00
Marshall Bowers
841a4e35ea Update .mailmap (#30874)
This PR updates the `.mailmap` file to merge some more commit authors.

Release Notes:

- N/A
2025-05-17 12:34:42 +00:00
Marshall Bowers
175ce05fd1 docs: Fix broken links in AI docs (#30872)
This PR fixes some broken links in the AI docs.

Release Notes:

- N/A
2025-05-17 11:54:42 +00:00
Marshall Bowers
e518941445 Add PR 15352 to .git-blame-ignore-revs (#30870)
This PR adds https://github.com/zed-industries/zed/pull/15352 to the
`.git-blame-ignore-revs` file.

Release Notes:

- N/A
2025-05-17 11:35:58 +00:00
Logan Blyth
10b8174c1b docs: Inform users about the supports_tools flag (#30839)
Closes #30115 

Release Notes:

- Improved documentation on Ollama `supports_tools` feature.

---------

Signed-off-by: Logan Blyth <logan@githedgehog.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-05-17 07:13:03 -04:00
Zsolt Cserna
21fd1c8b80 python: Fix highlighting of built-in types for isinstance and issubclass (#30807)
When built-in types such as `list` is specified in calls like
`isinstance()`, the parameter is highlighted as a type.
    
The issue is caused by a change which removed `list` and others in
bf9e5b4f76.
    
This commit makes two special cases for `isinstance` and `issubclass`
ensuring tree sitter to highlight the parameters correctly.

Fixes #30331

Release Notes:

- python: Fixed syntax highlighting for `isinstance()` and
`issubclass()` calls

Co-authored-by: László Vaskó <1771332+vlaci@users.noreply.github.com>
2025-05-17 06:37:59 -04:00
Marshall Bowers
c80bd698f8 language_models: Don't mark local subscription binding as unused (#30867)
This PR removes an instance of marking a local `Subscription` binding as
unused.

While we `_` the field to prevent unused warnings, the locals shouldn't
be marked as unused as we do use them (and want them to participate in
usage tracking).

Release Notes:

- N/A
2025-05-17 10:23:08 +00:00
Marshall Bowers
03419da6f1 ui_macros: Remove DerivePathStr macro (#30862)
This PR removes the `DerivePathStr` macro, as it is no longer used.

Also removes the `PathStaticStr` macro from `gpui_macros`, which was
also unused.

Release Notes:

- N/A
2025-05-17 10:05:55 +00:00
Ben Kunkle
f56960ab5b Fix project search unsaved edits (#30864)
Closes #30820

Release Notes:

- Fixed an issue where entering a new search in the project search would
drop unsaved edits in the project search buffer

---------

Co-authored-by: Mark Janssen <20283+praseodym@users.noreply.github.com>
2025-05-17 05:59:51 -04:00
Marshall Bowers
4d827924f0 ui: Remove usage of DerivePathStr macro (#30861)
This PR updates the `KnockoutIconName` and `VectorName` enums to
manually implement the `path` method instead of using the
`DerivePathStr` macro.

Release Notes:

- N/A
2025-05-17 09:05:58 +00:00
Vivien Maisonneuve
25b4591539 docs: Fix duplicate and misordered YAML patterns in Ansible config (#30859)
Release Notes:

- N/A
2025-05-17 04:21:22 -04:00
Marshall Bowers
afbf527aa2 Remove Repology badge from README (#30857)
This PR removes the Repology badge from the README.

At time of writing, the majority of the packages listed here are
woefully out of date:

<img width="299" alt="Screenshot 2025-05-17 at 8 44 16 AM"
src="https://github.com/user-attachments/assets/c45afba3-72ac-488d-a067-1fb0e237c7c0"
/>

This isn't a good look for someone coming to the Zed repository for the
first time.

I've added a link to the Repology list in the "Linux" section of the
docs for people who are interested in checking the packaging status in
various repos.

Release Notes:

- N/A
2025-05-17 07:01:46 +00:00
Erik Funder Carstensen
eb9ea20313 Add missing "no" in .rules (#30748)
I have no clue how much this does/does not impact model behavior - if
you don't think it matters, just close the PR

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-17 06:31:56 +00:00
Stanislav Alekseev
3d2ab4e58c build: Remove -all_load linker argument on macOS (#30656)
This fixes builds in nix development shell on macOS

Release Notes:

- N/A
2025-05-17 07:20:23 +02:00
Conrad Irwin
ff0060aa36 Remove unnecessary result in line shaping (#30721)
Updates #29879

Release Notes:

- N/A
2025-05-16 23:48:36 +02:00
Alex Shen
d791c6cdb1 vim: Add g M motion to go to the middle of a line (#30227)
Adds the "g M" vim motion to go to the middle of the line.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-16 21:21:30 +00:00
Gen Tamura
c7725e31d9 terminal: Implement basic Japanese IME support on macOS (#29879)
## Description

This PR implements basic support for Japanese Input Method Editors
(IMEs) in the Zed terminal on macOS, addressing issue #9900. Previously,
users had to switch input modes to confirm Japanese text, and pre-edit
(marked) text was not displayed.

With these changes:

- **Marked Text Display:** Pre-edit text (e.g., underlined characters
during Japanese composition) is now rendered directly in the terminal at
the cursor's current position.
- **Composition Confirmation:** Pressing Enter correctly finalizes the
IME composition, clears the marked text, and sends the confirmed string
to the underlying PTY process. This allows for a more natural input flow
similar to other macOS applications like iTerm2.
- **State Management:** IME state (marked text and its selected range
within the marked text) is now managed within the `TerminalView` struct.
- **Input Handling:** `TerminalInputHandler` has been updated to
correctly process IME callbacks (`replace_and_mark_text_in_range`,
`replace_text_in_range`, `unmark_text`, `marked_text_range`) by
interacting with `TerminalView`.
- **Painting Logic:** `TerminalElement::paint` now fetches the marked
text and its range from `TerminalView` and renders it with an underline.
The standard terminal cursor is hidden when marked text is present to
avoid visual clutter.
- **Candidate Window Positioning:**
`TerminalInputHandler::bounds_for_range` now attempts to provide more
accurate bounds for the IME candidate window by using the actual painted
bounds of the pre-edit text, falling back to a cursor-based
approximation if necessary.

This significantly improves the usability of the Zed terminal for users
who need to input Japanese characters, bringing the experience closer to
system-standard IME behavior.

## Movies


https://github.com/user-attachments/assets/be6c7597-7b65-49a6-b376-e1adff6da974

---

Closes #9900

Release Notes:

- **Terminal:** Implemented basic support for Japanese Input Method
Editors (IMEs) on macOS. Users can now see pre-edit (marked) text as
they type Japanese and confirm their input with the Enter key directly
in the terminal. This provides a more natural and efficient experience
for Japanese language input. (Fixes #9900)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-16 23:10:41 +02:00
Nate Butler
e26620d1cf gpui: Add a standard text example (#30747)
This is a dumb first pass at a standard text example. We'll use this to
start digging in to some text/scale rendering issues.

There will be a ton of follow-up features to this, but starting simple.

Release Notes:

- N/A
2025-05-16 17:35:44 +02:00
Ben Brandt
9dabf491f0 agent: Only focus on the context strip if it has items to display (#30379) 2025-05-16 12:05:03 -03:00
Danilo Leal
f2dcc98216 agent: Improve layout shift in the previous message editor (#30825)
This PR also moves the context strip to be at the top, so it matches the
main message editor, making the arrow-up keyboard interaction to focus
on it to work the same way.

Release Notes:

- agent: Made the previous message editing UX more consistent with the
main message editor.
2025-05-16 11:36:37 -03:00
Jakob Herpel
23bbfc4b94 Run ignored test when running single test (#30830)
Release Notes:

- languages: Run ignored test if user wants to run one specific test
2025-05-16 14:23:27 +00:00
张小白
98aefcca83 windows: Some refactor (#30826)
Release Notes:

- N/A
2025-05-16 14:14:42 +00:00
Remco Smits
9be1e9aab1 debugger: Prevent pane context menu from showing on secondary mouse click in list entries (#30781)
This PR prevents the debug panel pane context menu from showing when you
click your secondary mouse button in **stackframe**, **breakpoint** and
**module** list entries.

Release Notes:

- N/A
2025-05-16 15:43:12 +02:00
Anthony Eid
33b60bc16d debugger: Fix inline values panic when selecting stack frames (#30821)
Release Notes:

- debugger beta: Fix panic that could occur when selecting a stack frame
- debugger beta: Fix inline values not showing in stack trace view

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-16 15:42:09 +02:00
Smit Barmase
0355b9dfab editor: Fix line comments not extending when adding new line immediately after slash (#30824)
This PR fixes a bug where comments don't extend when cursor is right
next to the second slash. We added `// ` as a prefix character to
correctly position the cursor after a new line, but this broke comment
validation by including that trailing space, which it shouldn't.

Now both line comments and block comments (already handled in JSDoc PR)
can extend right after the prefix without needing an additional space.

Before:


https://github.com/user-attachments/assets/ca4d4c1b-b9b9-4f1b-b47a-56ae35776f41

After:


https://github.com/user-attachments/assets/b3408e1e-3efe-4787-ba68-d33cd2ea8563

Release Notes:

- Fixed issue where comments weren't extending when adding new line
immediately after comment prefix (`//`).
2025-05-16 19:11:37 +05:30
Danilo Leal
6bec76cd5d agent: Allow dismissing previous message by clicking on the backdrop (#30822)
Release Notes:

- agent: Improved UX for dismissing an edit to a previous message.
2025-05-16 10:25:21 -03:00
张小白
d4f47aa653 client: Add support for HTTP/HTTPS proxy (#30812)
Closes #30732

I tested it on my machine, and the HTTP proxy is working properly now.

Release Notes:

- N/A
2025-05-16 20:35:30 +08:00
Oleksiy Syvokon
5112fcebeb evals: Make LLMs configurable in edit_agent evals (#30813)
Release Notes:

- N/A
2025-05-16 11:10:15 +00:00
Ben Kunkle
dcf7f714f7 Revert "Revert "python: Enable subroot detection for pylsp and pyright (#27364)" (#29658)" (#30810)
Revert "Revert "python: Enable subroot detection for pylsp and pyright
(#27364)" (#29658)"

This reverts commit 59708ef56c.

Closes #29699

Release Notes:

- N/A
2025-05-16 07:05:33 -04:00
Smit Barmase
16f668b8e3 editor: Add astrick on new line in multiline comment for Go, Rust, C, and C++ (#30808)
Add asterisk on new line in multiline comments for Go, Rust, C, and C++.
While `*` is entirely for style. There's no actual need for it. It can
be disabled from setting. More:
https://doc.rust-lang.org/rust-by-example/hello/comment.html

<img width="491" alt="image"
src="https://github.com/user-attachments/assets/385b1eb5-be81-446c-b7cf-34165d6b384a"
/>

Release Notes:

- Added automatic asterisk insertion for new lines in multiline comments
for Go, Rust, C, and C++. This can be disable by setting
`extend_comment_on_newline` to `false`.
2025-05-16 15:30:04 +05:30
Danilo Leal
0f4e52bde8 agent: Ensure background color is the same even while zoomed in (#30804)
Release Notes:

- agent: Fixed the background color of the agent panel changing if you
zoomed it in.
2025-05-16 06:48:22 -03:00
Danilo Leal
dfe37b0a07 agent: Make Markdown codeblocks expanded by default (#30806)
Release Notes:

- N/A
2025-05-16 06:48:15 -03:00
Ben Kunkle
2da37988b5 fix bedrock name in assistant settings schema (#30805)
Closes #30778 

Release Notes:

- Fixed an issue with the assistant settings where `amazon-bedrock` was
incorrectly called `bedrock` in the settings schema
2025-05-16 09:29:58 +00:00
Peter Tripp
05955e4faa keymap: Move 'project_panel::NewSearchInDirectory' to a dedicated bind (#29681)
Previously cmd-shift-f / ctrl-shift-f had different behavior when
invoked from the project panel context than from an editor (for project
panel `include` field was populated from the currently select project
panel directory).

Change this so that it has it's own keybind of cmd-alt-shift-f /
ctrl-alt-shift-f so cmd-shift-f and ctrl-shift-f has consistent behavior
(`pane::DeploySearch`) everywhere.

Release Notes:

- Add dedicated keybind for "Find in Folder..." from the project panel
(cmd-alt-shift-f, ctrl-alt-shift-f).
2025-05-16 11:05:13 +02:00
Ben Kunkle
1d043b37fb askpass: Workaround rust lang 69343 (#30774)
Closes #ISSUE

Work around https://github.com/rust-lang/rust/issues/69343 in askpass

Release Notes:

- linux: Fixed an issue with askpass where the Zed binary path would be incorrect after an auto-update is installed
but not yet applied
2025-05-16 05:04:36 -04:00
Smit Barmase
18d39e3f81 editor: Improve JSDoc extend comment on newline to follow convention (#30800)
Follow up for https://github.com/zed-industries/zed/pull/30768

This PR makes JSDoc auto comment on new line lot better by:

- Inserting delimiters regardless of whether previous delimiters have
trailing spaces or not
- When on start tag, auto-indenting both prefix and end tag upon new
line

This makes it correct as per convention out of the box. No need to
manually adjust spaces on every new line.


https://github.com/user-attachments/assets/81b8e05a-fe8a-4459-9e90-c8a3d70a51a2

Release Notes:

- Improved JSDoc auto-commenting on newline which now correctly indents
as per convention.
2025-05-16 12:42:11 +05:30
Oleksiy Syvokon
cc3a28a8e8 agent: Fix unnecessary "tool result too long" (#30798)
Release Notes:

- N/A
2025-05-16 06:39:02 +00:00
Piotr Osiewicz
0f17e82154 chore: Bump Rust to 1.87 (#30739)
Closes #ISSUE

Release Notes:

- N/A
2025-05-15 22:28:52 +00:00
morgankrey
a316428686 docs: Update Claude 3.5 Sonnet context window (#30518)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-15 13:15:36 -04:00
Ben Brandt
355266988d extension: Update wasi preview adapter (#30759)
Replace dynamic downloading of WASI adapter with the provided crate.

More importantly, this makes sure we are using the same adapter version
as our version of wasmtime, which includes several fixes.

Arguably we could also at this point update to wasm32-wasip2 target and
remove this dependency as well if we want, but that might need further
testing.

Release Notes:

- N/A
2025-05-15 19:10:13 +02:00
Danilo Leal
72007c9a62 docs: Polish AI content (#30770)
Release Notes:

- N/A
2025-05-15 13:59:17 -03:00
Smit Barmase
c2feffac9d editor: Add prefix on newline in documentation block (e.g. JSDoc) (#30768)
Closes #8973

- [x] Tests


https://github.com/user-attachments/assets/7fc6608f-1c11-4c70-a69b-34bfa8f789a2

Release Notes:

- Added auto-insertion of asterisk (*) prefix when creating new lines
within JSDoc comment blocks.
2025-05-15 20:30:06 +05:30
张小白
4b7b5db58c windows: Remove unnecessay helper function (#30764)
Release Notes:

- N/A
2025-05-15 14:22:04 +00:00
176 changed files with 5700 additions and 2035 deletions

View File

@@ -13,12 +13,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-all_load"]
[target.'cfg(target_os = "windows")']
rustflags = [
"--cfg",

View File

@@ -30,3 +30,7 @@ ffdda588b41f7d9d270ffe76cab116f828ad545e
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
# https://github.com/zed-industries/zed/pull/13887
813cc3f5e537372fc86720b5e71b6e1c815440ab
# 2024-07-24 docs: Format docs
# https://github.com/zed-industries/zed/pull/15352
3a44a59f8ec114ac1ba22f7da1652717ef7e4e5c

View File

@@ -19,6 +19,8 @@ amtoaer <amtoaer@gmail.com>
amtoaer <amtoaer@gmail.com> <amtoaer@outlook.com>
Andrei Zvonimir Crnković <andrei@0x7f.dev>
Andrei Zvonimir Crnković <andrei@0x7f.dev> <andreicek@0x7f.dev>
Angelk90 <angelo.k90@hotmail.it>
Angelk90 <angelo.k90@hotmail.it> <20476002+Angelk90@users.noreply.github.com>
Antonio Scandurra <me@as-cii.com>
Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Ben Kunkle <ben@zed.dev>
@@ -38,6 +40,8 @@ Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Edwin Aronsson <75266237+4teapo@users.noreply.github.com>
Elvis Pranskevichus <elvis@geldata.com>
Elvis Pranskevichus <elvis@geldata.com> <elvis@magic.io>
Evren Sen <nervenes@icloud.com>
Evren Sen <nervenes@icloud.com> <146845123+evrensen467@users.noreply.github.com>
Evren Sen <nervenes@icloud.com> <146845123+evrsen@users.noreply.github.com>
@@ -69,6 +73,8 @@ Lilith Iris <itslirissama@gmail.com> <83819417+Irilith@users.noreply.github.com>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marko Kungla <marko.kungla@gmail.com>
Marko Kungla <marko.kungla@gmail.com> <marko@mkungla.dev>
Marshall Bowers <git@maxdeviant.com>
Marshall Bowers <git@maxdeviant.com> <elliott.codes@gmail.com>
Marshall Bowers <git@maxdeviant.com> <marshall@zed.dev>
@@ -84,6 +90,7 @@ Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Morgan Krey <morgan@zed.dev>
Muhammad Talal Anwar <mail@talal.io>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Nate Butler <iamnbutler@gmail.com>
@@ -116,11 +123,18 @@ Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Smit Barmase <0xtimsb@gmail.com>
Smit Barmase <0xtimsb@gmail.com> <smit@zed.dev>
Thomas <github.thomaub@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry94@gmail.com>
Thomas <github.thomaub@gmail.com> <thomas.aubry@paylead.fr>
Thomas Heartman <thomasheartman+github@gmail.com>
Thomas Heartman <thomasheartman+github@gmail.com> <thomas@getunleash.io>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com>
Thomas Mickley-Doyle <tmickleydoyle@gmail.com> <thomas@zed.dev>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <thorsten@zed.dev>
Thorsten Ball <thorsten@zed.dev> <me@thorstenball.com>
Thorsten Ball <thorsten@zed.dev> <mrnugget@gmail.com>
Thorsten Ball <mrnugget@gmail.com>
Thorsten Ball <mrnugget@gmail.com> <me@thorstenball.com>
Thorsten Ball <mrnugget@gmail.com> <thorsten@zed.dev>
Tristan Hume <tris.hume@gmail.com>
Tristan Hume <tris.hume@gmail.com> <tristan@anthropic.com>
Uladzislau Kaminski <i@uladkaminski.com>

2
.rules
View File

@@ -115,7 +115,7 @@ Other entities can then register a callback to handle these events by doing `cx.
GPUI has had some changes to its APIs. Always write code using the new APIs:
* `spawn` methods now take async closures (`AsyncFn`), and so should be called like `cx.spawn(async move |cx| ...)`.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which longer exists and should NEVER be used.
* Use `Entity<T>`. This replaces `Model<T>` and `View<T>` which no longer exist and should NEVER be used.
* Use `App` references. This replaces `AppContext` which no longer exists and should NEVER be used.
* Use `Context<T>` references. This replaces `ModelContext<T>` which no longer exists and should NEVER be used.
* `Window` is now passed around explicitly. The new interface adds a `Window` reference parameter to some methods, and adds some new "*_in" methods for plumbing `Window`. The old types `WindowContext` and `ViewContext<T>` should NEVER be used.

View File

@@ -2,16 +2,14 @@
{
"label": "Debug Zed (CodeLLDB)",
"adapter": "CodeLLDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT"
"program": "target/debug/zed",
"request": "launch"
},
{
"label": "Debug Zed (GDB)",
"adapter": "GDB",
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
"program": "target/debug/zed",
"request": "launch",
"cwd": "$ZED_WORKTREE_ROOT",
"initialize_args": {
"stopAtBeginningOfMainSubprogram": true
}

1349
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -74,6 +74,8 @@ members = [
"crates/inline_completion",
"crates/inline_completion_button",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/language",
"crates/language_extension",
@@ -279,6 +281,8 @@ indexed_docs = { path = "crates/indexed_docs" }
inline_completion = { path = "crates/inline_completion" }
inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
@@ -458,6 +462,7 @@ indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
jj-lib = { git = "https://github.com/jj-vcs/jj", rev = "e18eb8e05efaa153fad5ef46576af145bba1807f" }
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -548,7 +553,7 @@ syn = { version = "1.0.72", features = ["full", "extra-traits"] }
sys-locale = "0.3.1"
sysinfo = "0.31.0"
take-until = "0.2.0"
tempfile = "3.9.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = "0.6.0"
time = { version = "0.3", features = [
@@ -594,6 +599,7 @@ url = "2.2"
urlencoding = "2.1.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.3"
wasi-preview1-component-adapter-provider = "29"
wasm-encoder = "0.221"
wasmparser = "0.221"
wasmtime = { version = "29", default-features = false, features = [
@@ -787,6 +793,9 @@ let_underscore_future = "allow"
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.86-bookworm as builder
FROM rust:1.87-bookworm as builder
WORKDIR app
COPY . .

View File

@@ -8,10 +8,6 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of
### Installation
<a href="https://repology.org/project/zed-editor/versions">
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
</a>
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
Other platforms are not yet available:

View File

@@ -766,7 +766,7 @@
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"

View File

@@ -825,7 +825,7 @@
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
"escape": "menu::Cancel"

View File

@@ -152,6 +152,7 @@
"g end": ["vim::EndOfLine", { "display_lines": true }],
"g 0": ["vim::StartOfLine", { "display_lines": true }],
"g home": ["vim::StartOfLine", { "display_lines": true }],
"g shift-m": ["vim::MiddleOfLine", { "display_lines": true }],
"g ^": ["vim::FirstNonWhitespace", { "display_lines": true }],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",

View File

@@ -1715,6 +1715,8 @@
// }
// ]
"ssh_connections": [],
// Whether to read ~/.ssh/config for ssh connection sources.
"read_ssh_config": true,
// Configures context servers for use by the agent.
"context_servers": {},
"debugger": {

View File

@@ -99,9 +99,6 @@
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"version_control.conflict_marker.border": "#d3b0201f",
"conflict": "#dec184ff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",
@@ -481,11 +478,6 @@
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.conflict.ours_background": "#FF0000",
"version_control.conflict.ours_border": "#FF0000",
"version_control.conflict.theirs_background": "#e2e2faff",
"version_control.conflict.theirs_border": "#cbcdf6ff",
"version_control.conflict.divider_background": "#faf2e6ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",
"conflict.border": "#f4e7d1ff",

View File

@@ -185,12 +185,14 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
line_height: Some(line_height.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
@@ -1012,6 +1014,7 @@ impl ActiveThread {
self.push_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1025,6 +1028,7 @@ impl ActiveThread {
self.edited_message(message_id, &message_segments, window, cx);
}
self.scroll_to_bottom(cx);
self.save_thread(cx);
cx.notify();
}
@@ -1538,11 +1542,15 @@ impl ActiveThread {
let project = self.thread.read(cx).project().clone();
let prompt_store = self.thread_store.read(cx).prompt_store().clone();
let git_store = project.read(cx).git_store().clone();
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
let load_context_task =
crate::context::load_context(new_context, &project, &prompt_store, cx);
self._load_edited_message_context_task =
Some(cx.spawn_in(window, async move |this, cx| {
let context = load_context_task.await;
let (context, checkpoint) =
futures::future::join(load_context_task, checkpoint).await;
let _ = this
.update_in(cx, |this, window, cx| {
this.thread.update(cx, |thread, cx| {
@@ -1551,6 +1559,7 @@ impl ActiveThread {
Role::User,
vec![MessageSegment::Text(edited_text)],
Some(context.loaded_context),
checkpoint.ok(),
cx,
);
for message_id in this.messages_after(message_id) {
@@ -1720,10 +1729,11 @@ impl ActiveThread {
.on_action(cx.listener(Self::confirm_editing_message))
.capture_action(cx.listener(Self::paste))
.min_h_6()
.flex_grow()
.w_full()
.flex_grow()
.gap_2()
.child(EditorElement::new(
.child(state.context_strip.clone())
.child(div().pt(px(-3.)).px_neg_0p5().child(EditorElement::new(
&state.editor,
EditorStyle {
background: colors.editor_background,
@@ -1732,8 +1742,7 @@ impl ActiveThread {
syntax: cx.theme().syntax().clone(),
..Default::default()
},
))
.child(state.context_strip.clone())
)))
}
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -1921,16 +1930,6 @@ impl ActiveThread {
v_flex()
.w_full()
.gap_1()
.when(!message_is_empty, |parent| {
parent.child(div().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.when(!added_context.is_empty(), |parent| {
parent.child(h_flex().flex_wrap().gap_1().children(
added_context.into_iter().map(|added_context| {
@@ -1949,6 +1948,16 @@ impl ActiveThread {
}),
))
})
.when(!message_is_empty, |parent| {
parent.child(div().pt_0p5().min_h_6().child(self.render_message_content(
message_id,
rendered_message,
has_tool_uses,
workspace.clone(),
window,
cx,
)))
})
.into_any_element()
}
});
@@ -1974,6 +1983,7 @@ impl ActiveThread {
h_flex()
.p_2p5()
.gap_1()
.items_end()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
@@ -1987,6 +1997,7 @@ impl ActiveThread {
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2004,11 +2015,12 @@ impl ActiveThread {
.child(
IconButton::new(
"confirm-edit-message",
IconName::Check,
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Success)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -2028,9 +2040,6 @@ impl ActiveThread {
)
}),
)
.when(editing_message_state.is_none(), |this| {
this.tooltip(Tooltip::text("Click To Edit"))
})
.on_click(cx.listener({
let message_segments = message.segments.clone();
move |this, _, window, cx| {
@@ -2071,6 +2080,16 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
let backdrop = div()
.id("backdrop")
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8)
.on_click(cx.listener(Self::handle_cancel_click));
v_flex()
.w_full()
.map(|parent| {
@@ -2240,15 +2259,7 @@ impl ActiveThread {
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
parent.relative().child(backdrop)
})
.into_any()
}
@@ -2363,6 +2374,7 @@ impl ActiveThread {
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
@@ -2370,6 +2382,7 @@ impl ActiveThread {
let is_expanded = active_thread
.read(cx)
.is_codeblock_expanded(message_id, range.start);
if is_expanded {
return el;
}
@@ -3392,16 +3405,21 @@ impl ActiveThread {
self.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
.unwrap_or(true)
}
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
let is_expanded = self
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(false);
.or_insert(true);
*is_expanded = !*is_expanded;
}
pub fn scroll_to_bottom(&mut self, cx: &mut Context<Self>) {
self.list_state.reset(self.messages.len());
cx.notify();
}
}
pub enum ActiveThreadEvent {
@@ -3415,6 +3433,7 @@ impl Render for ActiveThread {
v_flex()
.size_full()
.relative()
.bg(cx.theme().colors().panel_background)
.on_mouse_move(cx.listener(|this, _, _, cx| {
this.show_scrollbar = true;
this.hide_scrollbar_later(cx);

View File

@@ -30,7 +30,6 @@ pub(crate) struct ConfigureContextServerModal {
context_server_store: Entity<ContextServerStore>,
}
#[allow(clippy::large_enum_variant)]
enum Configuration {
NotAvailable,
Required(ConfigurationRequiredState),

View File

@@ -567,6 +567,15 @@ impl AgentPanel {
menu = menu.header("Recently Opened");
for entry in recently_opened.iter() {
if let RecentEntry::Context(context) = entry {
if context.read(cx).path().is_none() {
log::error!(
"bug: text thread in recent history list was never saved"
);
continue;
}
}
let summary = entry.summary(cx);
menu = menu.entry_with_end_slot_on_hover(
@@ -1290,14 +1299,26 @@ impl AgentPanel {
let new_is_history = matches!(new_view, ActiveView::History);
match &self.active_view {
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
ActiveView::Thread { thread, .. } => {
if let Some(thread) = thread.upgrade() {
if thread.read(cx).is_empty() {
let id = thread.read(cx).id().clone();
store.remove_recently_opened_thread(id, cx);
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_thread(id, cx);
});
}
}
}),
}
ActiveView::PromptEditor { context_editor, .. } => {
let context = context_editor.read(cx).context();
// When switching away from an unsaved text thread, delete its entry.
if context.read(cx).path().is_none() {
let context = context.clone();
self.history_store.update(cx, |store, cx| {
store.remove_recently_opened_entry(&RecentEntry::Context(context), cx);
});
}
}
_ => {}
}
@@ -2135,6 +2156,7 @@ impl AgentPanel {
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(recent_history.is_empty(), |this| {
let configuration_error_ref = &configuration_error;
this.child(

View File

@@ -84,6 +84,12 @@ impl ContextStrip {
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
@@ -104,14 +110,14 @@ impl ContextStrip {
}
}
fn suggested_context(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::File => self.suggested_file(cx),
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_file(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_file(&self, cx: &App) -> Option<SuggestedContext> {
let workspace = self.workspace.upgrade()?;
let active_item = workspace.read(cx).active_item(cx)?;
@@ -138,7 +144,7 @@ impl ContextStrip {
})
}
fn suggested_thread(&self, cx: &Context<Self>) -> Option<SuggestedContext> {
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}

View File

@@ -451,7 +451,7 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
} else {
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -401,7 +401,7 @@ impl MessageEditor {
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
if self.context_picker_menu_handle.is_deployed() {
cx.propagate();
} else {
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
}
}

View File

@@ -214,7 +214,7 @@ pub struct GitState {
pub diff: Option<String>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct ThreadCheckpoint {
message_id: MessageId,
git_checkpoint: GitStoreCheckpoint,
@@ -996,6 +996,7 @@ impl Thread {
new_role: Role,
new_segments: Vec<MessageSegment>,
loaded_context: Option<LoadedContext>,
checkpoint: Option<GitStoreCheckpoint>,
cx: &mut Context<Self>,
) -> bool {
let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else {
@@ -1006,6 +1007,15 @@ impl Thread {
if let Some(context) = loaded_context {
message.loaded_context = context;
}
if let Some(git_checkpoint) = checkpoint {
self.checkpoints_by_message.insert(
id,
ThreadCheckpoint {
message_id: id,
git_checkpoint,
},
);
}
self.touch_updated_at();
cx.emit(ThreadEvent::MessageEdited(id));
true

View File

@@ -425,16 +425,17 @@ impl ToolUseState {
let content = match tool_result {
ToolResultContent::Text(text) => {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
LanguageModelToolResultContent::Text(
let text = if text.len() < tool_output_limit {
text
} else {
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
format!(
"Tool result too long. The first {} bytes:\n\n{}",
truncated.len(),
truncated
)
.into(),
)
};
LanguageModelToolResultContent::Text(text.into())
}
ToolResultContent::Image(language_model_image) => {
if language_model_image.estimate_tokens() < tool_output_limit {

View File

@@ -163,8 +163,10 @@ impl AskPassSession {
#[cfg(unix)]
fn get_shell_safe_zed_path() -> anyhow::Result<String> {
let zed_path = std::env::current_exe()
.context("Failed to figure out current executable path for use in askpass")?
.context("Failed to determine current executable path for use in askpass")?
.to_string_lossy()
// see https://github.com/rust-lang/rust/issues/69343
.trim_end_matches(" (deleted)")
.to_string();
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have

View File

@@ -692,7 +692,7 @@ impl JsonSchema for LanguageModelProviderSetting {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"bedrock".into(),
"amazon-bedrock".into(),
"google".into(),
"lmstudio".into(),
"ollama".into(),

View File

@@ -15,7 +15,7 @@ use gpui::{AppContext, TestAppContext};
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId,
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
};
use project::Project;
use rand::prelude::*;
@@ -25,6 +25,7 @@ use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
str::FromStr,
sync::mpsc,
};
use util::path;
@@ -1216,7 +1217,7 @@ fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usiz
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
@@ -1255,13 +1256,21 @@ impl EditAgentTest {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let agent_model = SelectedModel::from_str(
&std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let judge_model = SelectedModel::from_str(
&std::env::var("ZED_JUDGE_MODEL")
.unwrap_or("anthropic/claude-3-7-sonnet-latest".into()),
)
.unwrap();
let (agent_model, judge_model) = cx
.update(|cx| {
cx.spawn(async move |cx| {
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let judge_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
let agent_model = Self::load_model(&agent_model, cx).await;
let judge_model = Self::load_model(&judge_model, cx).await;
(agent_model.unwrap(), judge_model.unwrap())
})
})
@@ -1276,15 +1285,17 @@ impl EditAgentTest {
}
async fn load_model(
provider: &str,
id: &str,
selected_model: &SelectedModel,
cx: &mut AsyncApp,
) -> Result<Arc<dyn LanguageModel>> {
let (provider, model) = cx.update(|cx| {
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
.find(|model| model.provider_id().0 == provider && model.id().0 == id)
.find(|model| {
model.provider_id() == selected_model.provider
&& model.id() == selected_model.model
})
.unwrap();
let provider = models.provider(&model.provider_id()).unwrap();
(provider, model)

View File

@@ -1249,7 +1249,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,

View File

@@ -19,6 +19,7 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
@@ -29,6 +30,7 @@ gpui.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
http_client_tls.workspace = true
httparse = "1.10"
log.workspace = true
paths.workspace = true
parking_lot.workspace = true
@@ -47,6 +49,7 @@ text.workspace = true
thiserror.workspace = true
time.workspace = true
tiny_http = "0.8"
tokio-native-tls = "0.3"
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
url.workspace = true
util.workspace = true

View File

@@ -1,7 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod socks;
mod proxy;
pub mod telemetry;
pub mod user;
pub mod zed_urls;
@@ -24,13 +24,13 @@ use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
use rand::prelude::*;
use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use socks::connect_socks_proxy_stream;
use std::pin::Pin;
use std::{
any::TypeId,
@@ -1156,7 +1156,7 @@ impl Client {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
match proxy {
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
};

View File

@@ -0,0 +1,66 @@
//! client proxy
mod http_proxy;
mod socks_proxy;
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
pub(crate) async fn connect_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect((proxy_domain.as_str(), proxy_port))
.await
.context("Failed to connect to proxy")?;
let proxy_stream = match proxy_type {
ProxyType::SocksProxy(proxy) => connect_socks_proxy_stream(stream, proxy, rpc_host).await?,
ProxyType::HttpProxy(proxy) => {
connect_http_proxy_stream(stream, proxy, rpc_host, &proxy_domain).await?
}
};
Ok(proxy_stream)
}
enum ProxyType<'t> {
SocksProxy(SocksVersion<'t>),
HttpProxy(HttpProxyType<'t>),
}
fn parse_proxy_type<'t>(proxy: &'t Url) -> Option<((String, u16), ProxyType<'t>)> {
let scheme = proxy.scheme();
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
let proxy_type = match scheme {
scheme if scheme.starts_with("socks") => {
Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
}
scheme if scheme.starts_with("http") => {
Some(ProxyType::HttpProxy(parse_http_proxy(scheme, proxy)))
}
_ => None,
}?;
Some(((host, port), proxy_type))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}

View File

@@ -0,0 +1,171 @@
use anyhow::{Context, Result};
use base64::Engine;
use httparse::{EMPTY_HEADER, Response};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufStream},
net::TcpStream,
};
use tokio_native_tls::{TlsConnector, native_tls};
use url::Url;
use super::AsyncReadWrite;
pub(super) enum HttpProxyType<'t> {
HTTP(Option<HttpProxyAuthorization<'t>>),
HTTPS(Option<HttpProxyAuthorization<'t>>),
}
pub(super) struct HttpProxyAuthorization<'t> {
username: &'t str,
password: &'t str,
}
pub(super) fn parse_http_proxy<'t>(scheme: &str, proxy: &'t Url) -> HttpProxyType<'t> {
let auth = proxy.password().map(|password| HttpProxyAuthorization {
username: proxy.username(),
password,
});
if scheme.starts_with("https") {
HttpProxyType::HTTPS(auth)
} else {
HttpProxyType::HTTP(auth)
}
}
pub(crate) async fn connect_http_proxy_stream(
stream: TcpStream,
http_proxy: HttpProxyType<'_>,
rpc_host: (&str, u16),
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>> {
match http_proxy {
HttpProxyType::HTTP(auth) => http_connect(stream, rpc_host, auth).await,
HttpProxyType::HTTPS(auth) => https_connect(stream, rpc_host, auth, proxy_domain).await,
}
.context("error connecting to http/https proxy")
}
async fn http_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let mut stream = BufStream::new(stream);
let request = make_request(target, auth);
stream.write_all(request.as_bytes()).await?;
stream.flush().await?;
check_response(&mut stream).await?;
Ok(Box::new(stream))
}
async fn https_connect<T>(
stream: T,
target: (&str, u16),
auth: Option<HttpProxyAuthorization<'_>>,
proxy_domain: &str,
) -> Result<Box<dyn AsyncReadWrite>>
where
T: AsyncReadWrite,
{
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
let stream = tls_connector.connect(proxy_domain, stream).await?;
http_connect(stream, target, auth).await
}
fn make_request(target: (&str, u16), auth: Option<HttpProxyAuthorization<'_>>) -> String {
let (host, port) = target;
let mut request = format!(
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\nProxy-Connection: Keep-Alive\r\n"
);
if let Some(HttpProxyAuthorization { username, password }) = auth {
let auth =
base64::prelude::BASE64_STANDARD.encode(format!("{username}:{password}").as_bytes());
let auth = format!("Proxy-Authorization: Basic {auth}\r\n");
request.push_str(&auth);
}
request.push_str("\r\n");
request
}
async fn check_response<T>(stream: &mut BufStream<T>) -> Result<()>
where
T: AsyncReadWrite,
{
let response = recv_response(stream).await?;
let mut dummy_headers = [EMPTY_HEADER; MAX_RESPONSE_HEADERS];
let mut parser = Response::new(&mut dummy_headers);
parser.parse(response.as_bytes())?;
match parser.code {
Some(code) => {
if code == 200 {
Ok(())
} else {
Err(anyhow::anyhow!(
"Proxy connection failed with HTTP code: {code}"
))
}
}
None => Err(anyhow::anyhow!(
"Proxy connection failed with no HTTP code: {}",
parser.reason.unwrap_or("Unknown reason")
)),
}
}
const MAX_RESPONSE_HEADER_LENGTH: usize = 4096;
const MAX_RESPONSE_HEADERS: usize = 16;
async fn recv_response<T>(stream: &mut BufStream<T>) -> Result<String>
where
T: AsyncReadWrite,
{
let mut response = String::new();
loop {
if stream.read_line(&mut response).await? == 0 {
return Err(anyhow::anyhow!("End of stream"));
}
if MAX_RESPONSE_HEADER_LENGTH < response.len() {
return Err(anyhow::anyhow!("Maximum response header length exceeded"));
}
if response.ends_with("\r\n\r\n") {
return Ok(response);
}
}
}
#[cfg(test)]
mod tests {
use url::Url;
use super::{HttpProxyAuthorization, HttpProxyType, parse_http_proxy};
#[test]
fn test_parse_http_proxy() {
let proxy = Url::parse("http://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(version, HttpProxyType::HTTP(None)))
}
#[test]
fn test_parse_http_proxy_with_auth() {
let proxy = Url::parse("http://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let version = parse_http_proxy(scheme, &proxy);
assert!(matches!(
version,
HttpProxyType::HTTP(Some(HttpProxyAuthorization {
username: "username",
password: "password"
}))
))
}
}

View File

@@ -1,15 +1,19 @@
//! socks proxy
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use anyhow::{Context, Result};
use tokio::net::TcpStream;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
use url::Url;
use super::AsyncReadWrite;
/// Identification to a Socks V4 Proxy
struct Socks4Identification<'a> {
pub(super) struct Socks4Identification<'a> {
user_id: &'a str,
}
/// Authorization to a Socks V5 Proxy
struct Socks5Authorization<'a> {
pub(super) struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
@@ -18,45 +22,50 @@ struct Socks5Authorization<'a> {
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
enum SocksVersion<'a> {
pub(super) enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
}
pub(crate) async fn connect_socks_proxy_stream(
proxy: &Url,
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
}
}
pub(super) async fn connect_socks_proxy_stream(
stream: TcpStream,
socks_version: SocksVersion<'_>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect(socks_proxy)
.await
.context("Failed to connect to socks proxy")?;
let socks: Box<dyn AsyncReadWrite> = match version {
match socks_version {
SocksVersion::V4(None) => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V5(None) => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
let socks = Socks5Stream::connect_with_password_and_socket(
@@ -64,44 +73,9 @@ pub(crate) async fn connect_socks_proxy_stream(
)
.await
.context("error connecting to socks")?;
Box::new(socks)
Ok(Box::new(socks))
}
};
Ok(socks)
}
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
let scheme = proxy.scheme();
let socks_version = if scheme.starts_with("socks4") {
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else if scheme.starts_with("socks") {
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
} else {
return None;
};
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
Some(((host, port), socks_version))
}
pub(crate) trait AsyncReadWrite:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
{
}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
for T
{
}
}
#[cfg(test)]
@@ -113,20 +87,18 @@ mod tests {
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V4(None)))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
@@ -136,20 +108,18 @@ mod tests {
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(version, SocksVersion::V5(None)))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let scheme = proxy.scheme();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
let version = parse_socks_proxy(scheme, &proxy);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
@@ -158,19 +128,4 @@ mod tests {
}))
))
}
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
/// SOCKS proxies are often used in contexts where security and privacy are critical,
/// so any fallback could expose users to significant risks.
#[tokio::test]
async fn fails_on_bad_proxy() {
// Should fail connecting because http is not a valid Socks proxy scheme
let proxy = Url::parse("http://localhost:2313").unwrap();
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
match result {
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
Ok(_) => panic!("Connecting on bad proxy should fail"),
};
}
}

View File

@@ -543,7 +543,7 @@ pub struct MembershipUpdated {
/// The result of setting a member's role.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum SetMemberRoleResult {
InviteUpdated(Channel),
MembershipUpdated(MembershipUpdated),

View File

@@ -36,6 +36,7 @@ use util::{ResultExt as _, maybe};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
#[expect(clippy::result_large_err)]
#[tokio::main]
async fn main() -> Result<()> {
if let Err(error) = env::load_dotenv() {

View File

@@ -36,8 +36,8 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
room.read_with(cx, |room, _| {
let mut remote = room
.remote_participants()
.iter()
.map(|(_, participant)| participant.user.github_login.clone())
.values()
.map(|participant| participant.user.github_login.clone())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()

View File

@@ -7,11 +7,11 @@ use crate::notifications::collab_notification::CollabNotification;
pub struct CollabNotificationStory;
impl Render for CollabNotificationStory {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let window_container = |width, height| div().w(px(width)).h(px(height));
Story::container()
.child(Story::title_for::<CollabNotification>())
Story::container(cx)
.child(Story::title_for::<CollabNotification>(cx))
.child(
StorySection::new().child(StoryItem::new(
"Incoming Call Notification",

View File

@@ -21,8 +21,8 @@ use project::{
use ui::{
App, Clickable, Color, Context, Div, Icon, IconButton, IconName, Indicator, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Window, div,
h_flex, px, v_flex,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Tooltip, Window,
div, h_flex, px, v_flex,
};
use util::{ResultExt, maybe};
use workspace::Workspace;
@@ -259,6 +259,11 @@ impl LineBreakpoint {
dir, name, line
)))
.cursor_pointer()
.tooltip(Tooltip::text(if breakpoint.state.is_enabled() {
"Disable Breakpoint"
} else {
"Enable Breakpoint"
}))
.on_click({
let weak = weak.clone();
let path = path.clone();
@@ -290,6 +295,9 @@ impl LineBreakpoint {
)))
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.end_hover_slot(
IconButton::new(
SharedString::from(format!(
@@ -423,12 +431,20 @@ impl ExceptionBreakpoint {
self.id
)))
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.start_slot(
div()
.id(SharedString::from(format!(
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(Tooltip::text(if self.is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
}))
.on_click(move |_, _, cx| {
list.update(cx, |this, cx| {
this.session.update(cx, |this, cx| {

View File

@@ -129,6 +129,9 @@ impl ModuleList {
.w_full()
.group("")
.id(("module-list", ix))
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.when(module.path.is_some(), |this| {
this.on_click({
let path = module

View File

@@ -39,7 +39,6 @@ pub struct StackFrameList {
_refresh_task: Task<()>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, PartialEq, Eq)]
pub enum StackFrameEntry {
Normal(dap::StackFrame),
@@ -394,6 +393,9 @@ impl StackFrameList {
.when(is_selected_frame, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);
@@ -481,6 +483,9 @@ impl StackFrameList {
.when(is_selected, |this| {
this.bg(cx.theme().colors().element_hover)
})
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_ix = Some(ix);
this.activate_selected_entry(window, cx);

View File

@@ -79,6 +79,7 @@ theme.workspace = true
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
tree-sitter-python = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unicode-script.workspace = true
unindent = { workspace = true, optional = true }

View File

@@ -40,7 +40,6 @@ pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
#[allow(clippy::large_enum_variant)]
pub enum CodeContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
@@ -928,7 +927,6 @@ impl CodeActionContents {
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum CodeActionsItem {
Task(TaskSourceKind, ResolvedTask),

View File

@@ -1026,9 +1026,7 @@ impl DisplaySnapshot {
}
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
text_system
.layout_line(&line, font_size, &runs)
.expect("we expect the font to be loaded because it's rendered by the editor")
text_system.layout_line(&line, font_size, &runs)
}
pub fn x_for_display_point(

View File

@@ -282,7 +282,6 @@ struct Transform {
block: Option<Block>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum Block {
Custom(Arc<CustomBlock>),

View File

@@ -107,9 +107,9 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
TransactionId, TreeSitterOptions, WordsQuery,
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -1311,7 +1311,7 @@ pub struct ActiveDiagnosticGroup {
}
#[derive(Debug, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum ActiveDiagnostic {
None,
All,
@@ -3912,7 +3912,7 @@ impl Editor {
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction);
self.transact(window, cx, |this, window, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = multi_buffer.snapshot(cx);
@@ -3920,17 +3920,21 @@ impl Editor {
.iter()
.map(|selection| {
let start_point = selection.start.to_point(&buffer);
let mut indent =
let mut existing_indent =
buffer.indent_size_for_line(MultiBufferRow(start_point.row));
indent.len = cmp::min(indent.len, start_point.column);
existing_indent.len = cmp::min(existing_indent.len, start_point.column);
let start = selection.start;
let end = selection.end;
let selection_is_empty = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
&language_scope
{
let insert_extra_newline =
let (
comment_delimiter,
doc_delimiter,
insert_extra_newline,
indent_on_newline,
indent_on_extra_newline,
) = if let Some(language) = &language_scope {
let mut insert_extra_newline =
insert_extra_newline_brackets(&buffer, start..end, language)
|| insert_extra_newline_tree_sitter(&buffer, start..end);
@@ -3950,63 +3954,208 @@ impl Editor {
let (snapshot, range) =
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
let mut index_of_first_non_whitespace = 0;
let num_of_whitespaces = snapshot
.chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let comment_candidate = 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
})
.skip(num_of_whitespaces)
.take(max_len_of_delimiter)
.collect::<String>();
let comment_prefix = delimiters.iter().find(|comment_prefix| {
comment_candidate.starts_with(comment_prefix.as_ref())
})?;
let (delimiter, trimmed_len) =
delimiters.iter().find_map(|delimiter| {
let trimmed = delimiter.trim_end();
if comment_candidate.starts_with(trimmed) {
Some((delimiter, trimmed.len()))
} else {
None
}
})?;
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + comment_prefix.len()
<= start_point.column as usize;
num_of_whitespaces + trimmed_len <= start_point.column as usize;
if cursor_is_placed_after_comment_marker {
Some(comment_prefix.clone())
Some(delimiter.clone())
} else {
None
}
});
(comment_delimiter, insert_extra_newline)
let mut indent_on_newline = IndentSize::spaces(0);
let mut indent_on_extra_newline = IndentSize::spaces(0);
let doc_delimiter = maybe!({
if !selection_is_empty {
return None;
}
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
return None;
}
let DocumentationConfig {
start: start_tag,
end: end_tag,
prefix: delimiter,
tab_size: len,
} = language.documentation()?;
let (snapshot, range) =
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
let num_of_whitespaces = snapshot
.chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let cursor_is_after_start_tag = {
let start_tag_len = start_tag.len();
let start_tag_line = snapshot
.chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(start_tag_len)
.collect::<String>();
if start_tag_line.starts_with(start_tag.as_ref()) {
num_of_whitespaces + start_tag_len
<= start_point.column as usize
} else {
false
}
};
let cursor_is_after_delimiter = {
let delimiter_trim = delimiter.trim_end();
let delimiter_line = snapshot
.chars_for_range(range.clone())
.skip(num_of_whitespaces)
.take(delimiter_trim.len())
.collect::<String>();
if delimiter_line.starts_with(delimiter_trim) {
num_of_whitespaces + delimiter_trim.len()
<= start_point.column as usize
} else {
false
}
};
let cursor_is_before_end_tag_if_exists = {
let num_of_whitespaces_rev = snapshot
.reversed_chars_for_range(range.clone())
.take_while(|c| c.is_whitespace())
.count();
let mut line_iter = snapshot
.reversed_chars_for_range(range)
.skip(num_of_whitespaces_rev);
let end_tag_exists = end_tag
.chars()
.rev()
.all(|char| line_iter.next() == Some(char));
if end_tag_exists {
let max_point = snapshot.line_len(start_point.row) as usize;
let ordering = (num_of_whitespaces_rev
+ end_tag.len()
+ start_point.column as usize)
.cmp(&max_point);
let cursor_is_before_end_tag =
ordering != Ordering::Greater;
if cursor_is_after_start_tag {
if cursor_is_before_end_tag {
insert_extra_newline = true;
}
let cursor_is_at_start_of_end_tag =
ordering == Ordering::Equal;
if cursor_is_at_start_of_end_tag {
indent_on_extra_newline.len = (*len).into();
}
}
cursor_is_before_end_tag
} else {
true
}
};
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
&& cursor_is_before_end_tag_if_exists
{
if cursor_is_after_start_tag {
indent_on_newline.len = (*len).into();
}
Some(delimiter.clone())
} else {
None
}
});
(
comment_delimiter,
doc_delimiter,
insert_extra_newline,
indent_on_newline,
indent_on_extra_newline,
)
} else {
(None, false)
(
None,
None,
false,
IndentSize::default(),
IndentSize::default(),
)
};
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);
let prevent_auto_indent = doc_delimiter.is_some();
let delimiter = comment_delimiter.or(doc_delimiter);
let capacity_for_delimiter =
delimiter.as_deref().map(str::len).unwrap_or_default();
let mut new_text = String::with_capacity(
1 + capacity_for_delimiter
+ existing_indent.len as usize
+ indent_on_newline.len as usize
+ indent_on_extra_newline.len as usize,
);
new_text.push('\n');
new_text.extend(indent.chars());
if let Some(delimiter) = &comment_delimiter {
new_text.extend(existing_indent.chars());
new_text.extend(indent_on_newline.chars());
if let Some(delimiter) = &delimiter {
new_text.push_str(delimiter);
}
if insert_extra_newline {
new_text = new_text.repeat(2);
new_text.push('\n');
new_text.extend(existing_indent.chars());
new_text.extend(indent_on_extra_newline.chars());
}
let anchor = buffer.anchor_after(end);
let new_selection = selection.map(|_| anchor);
(
(start..end, new_text),
((start..end, new_text), prevent_auto_indent),
(insert_extra_newline, new_selection),
)
})
.unzip()
};
this.edit_with_autoindent(edits, cx);
let mut auto_indent_edits = Vec::new();
let mut edits = Vec::new();
for (edit, prevent_auto_indent) in edits_with_flags {
if prevent_auto_indent {
edits.push(edit);
} else {
auto_indent_edits.push(edit);
}
}
if !edits.is_empty() {
this.edit(edits, cx);
}
if !auto_indent_edits.is_empty() {
this.edit_with_autoindent(auto_indent_edits, cx);
}
let buffer = this.buffer.read(cx).snapshot(cx);
let new_selections = selection_fixup_info
let new_selections = selection_info
.into_iter()
.map(|(extra_newline_inserted, new_selection)| {
let mut cursor = new_selection.end.to_point(&buffer);
@@ -17918,10 +18067,6 @@ impl Editor {
.and_then(|lines| lines.last().map(|line| line.range.start));
self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| {
let snapshot = editor
.update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx))
.ok()?;
let inline_values = editor
.update(cx, |editor, cx| {
let Some(current_execution_position) = current_execution_position else {
@@ -17949,22 +18094,40 @@ impl Editor {
.context("refreshing debugger inlays")
.log_err()?;
let (excerpt_id, buffer_id) = snapshot
.excerpts()
.next()
.map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?;
let mut buffer_inline_values: HashMap<BufferId, Vec<InlayHint>> = HashMap::default();
for (buffer_id, inline_value) in inline_values
.into_iter()
.filter_map(|hint| Some((hint.position.buffer_id?, hint)))
{
buffer_inline_values
.entry(buffer_id)
.or_default()
.push(inline_value);
}
editor
.update(cx, |editor, cx| {
let new_inlays = inline_values
.into_iter()
.map(|debugger_value| {
Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position),
debugger_value.text(),
)
})
.collect::<Vec<_>>();
let snapshot = editor.buffer.read(cx).snapshot(cx);
let mut new_inlays = Vec::default();
for (excerpt_id, buffer_snapshot, _) in snapshot.excerpts() {
let buffer_id = buffer_snapshot.remote_id();
buffer_inline_values
.get(&buffer_id)
.into_iter()
.flatten()
.for_each(|hint| {
let inlay = Inlay::debugger_hint(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
);
new_inlays.push(inlay);
});
}
let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect();
std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids);
@@ -20188,8 +20351,8 @@ impl EditorSnapshot {
let participant_indices = collaboration_hub.user_participant_indices(cx);
let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
let collaborators_by_replica_id = collaborators_by_peer_id
.iter()
.map(|(_, collaborator)| (collaborator.replica_id, collaborator))
.values()
.map(|collaborator| (collaborator.replica_id, collaborator))
.collect::<HashMap<_, _>>();
self.buffer_snapshot
.selections_in_range(range, false)

View File

@@ -26,6 +26,7 @@ use language::{
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
LanguageSettingsContent, LspInsertMode, PrettierSettings,
},
tree_sitter_python,
};
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use lsp::CompletionParams;
@@ -2755,7 +2756,7 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["//".into()],
line_comments: vec!["// ".into()],
..LanguageConfig::default()
},
None,
@@ -2770,7 +2771,29 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
// Foo
// ˇ
"});
// Ensure that we add comment prefix when existing line contains space
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
// Foo
//s
// ˇ
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that we add comment prefix when existing line does not contain space
cx.set_state(indoc! {"
// Foo
//ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, 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! {"
@@ -2797,6 +2820,177 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new(Language::new(
LanguageConfig {
documentation: Some(language::DocumentationConfig {
start: "/**".into(),
end: "*/".into(),
prefix: "* ".into(),
tab_size: NonZeroU32::new(1).unwrap(),
}),
..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! {"
/**ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
"});
// Ensure that if cursor is before the comment start,
// we do not actually insert a comment prefix.
cx.set_state(indoc! {"
ˇ/**
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
ˇ/**
"});
// Ensure that if cursor is between it doesn't add comment prefix.
cx.set_state(indoc! {"
/*ˇ*
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/*
ˇ*
"});
// Ensure that if suffix exists on same line after cursor it adds new line.
cx.set_state(indoc! {"
/**ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor with space it adds new line.
cx.set_state(indoc! {"
/**ˇ */
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor with space it adds new line.
cx.set_state(indoc! {"
/** ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
/**s
* ˇ
*/
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that delimiter space is preserved when newline on already
// spaced delimiter.
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(
indoc! {"
/**s
*s
* ˇ
*/
"}
.replace("s", " ") // s is used as space placeholder to prevent format on save
.as_str(),
);
// Ensure that delimiter space is preserved when space is not
// on existing delimiter.
cx.set_state(indoc! {"
/**
*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
* ˇ
*/
"});
// Ensure that if suffix exists on same line after cursor it
// doesn't add extra new line if prefix is not on same line.
cx.set_state(indoc! {"
/**
ˇ*/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ*/
"});
// Ensure that it detects suffix after existing prefix.
cx.set_state(indoc! {"
/**ˇ/
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ/
"});
// Ensure that if suffix exists on same line before
// cursor it does not add comment prefix.
cx.set_state(indoc! {"
/** */ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/** */
ˇ
"});
// Ensure that if suffix exists on same line before
// cursor it does not add comment prefix.
cx.set_state(indoc! {"
/**
*
*/ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
*
*/
ˇ
"});
}
// Ensure that comment continuations can be disabled.
update_test_language_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
/**ˇ
"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
/**
ˇ
"});
}
#[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -20017,6 +20211,330 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
);
}
#[gpui::test]
async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
// test cursor move to start of each line on tab
// for `if`, `elif`, `else`, `while`, `with` and `for`
cx.set_state(indoc! {"
def main():
ˇ for item in items:
ˇ while item.active:
ˇ if item.value > 10:
ˇ continue
ˇ elif item.value < 0:
ˇ break
ˇ else:
ˇ with item.context() as ctx:
ˇ yield count
ˇ else:
ˇ log('while else')
ˇ else:
ˇ log('for else')
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
def main():
ˇfor item in items:
ˇwhile item.active:
ˇif item.value > 10:
ˇcontinue
ˇelif item.value < 0:
ˇbreak
ˇelse:
ˇwith item.context() as ctx:
ˇyield count
ˇelse:
ˇlog('while else')
ˇelse:
ˇlog('for else')
"});
// test relative indent is preserved when tab
// for `if`, `elif`, `else`, `while`, `with` and `for`
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
def main():
ˇfor item in items:
ˇwhile item.active:
ˇif item.value > 10:
ˇcontinue
ˇelif item.value < 0:
ˇbreak
ˇelse:
ˇwith item.context() as ctx:
ˇyield count
ˇelse:
ˇlog('while else')
ˇelse:
ˇlog('for else')
"});
// test cursor move to start of each line on tab
// for `try`, `except`, `else`, `finally`, `match` and `def`
cx.set_state(indoc! {"
def main():
ˇ try:
ˇ fetch()
ˇ except ValueError:
ˇ handle_error()
ˇ else:
ˇ match value:
ˇ case _:
ˇ finally:
ˇ def status():
ˇ return 0
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
def main():
ˇtry:
ˇfetch()
ˇexcept ValueError:
ˇhandle_error()
ˇelse:
ˇmatch value:
ˇcase _:
ˇfinally:
ˇdef status():
ˇreturn 0
"});
// test relative indent is preserved when tab
// for `try`, `except`, `else`, `finally`, `match` and `def`
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
def main():
ˇtry:
ˇfetch()
ˇexcept ValueError:
ˇhandle_error()
ˇelse:
ˇmatch value:
ˇcase _:
ˇfinally:
ˇdef status():
ˇreturn 0
"});
}
#[gpui::test]
async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
// test `else` auto outdents when typed inside `if` block
cx.set_state(indoc! {"
def main():
if i == 2:
return
ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
cx.assert_editor_state(indoc! {"
def main():
if i == 2:
return
else:ˇ
"});
// test `except` auto outdents when typed inside `try` block
cx.set_state(indoc! {"
def main():
try:
i = 2
ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input("except:", window, cx);
});
cx.assert_editor_state(indoc! {"
def main():
try:
i = 2
except:ˇ
"});
// test `else` auto outdents when typed inside `except` block
cx.set_state(indoc! {"
def main():
try:
i = 2
except:
j = 2
ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
cx.assert_editor_state(indoc! {"
def main():
try:
i = 2
except:
j = 2
else:ˇ
"});
// test `finally` auto outdents when typed inside `else` block
cx.set_state(indoc! {"
def main():
try:
i = 2
except:
j = 2
else:
k = 2
ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input("finally:", window, cx);
});
cx.assert_editor_state(indoc! {"
def main():
try:
i = 2
except:
j = 2
else:
k = 2
finally:ˇ
"});
// TODO: test `except` auto outdents when typed inside `try` block right after for block
// cx.set_state(indoc! {"
// def main():
// try:
// for i in range(n):
// pass
// ˇ
// "});
// cx.update_editor(|editor, window, cx| {
// editor.handle_input("except:", window, cx);
// });
// cx.assert_editor_state(indoc! {"
// def main():
// try:
// for i in range(n):
// pass
// except:ˇ
// "});
// TODO: test `else` auto outdents when typed inside `except` block right after for block
// cx.set_state(indoc! {"
// def main():
// try:
// i = 2
// except:
// for i in range(n):
// pass
// ˇ
// "});
// cx.update_editor(|editor, window, cx| {
// editor.handle_input("else:", window, cx);
// });
// cx.assert_editor_state(indoc! {"
// def main():
// try:
// i = 2
// except:
// for i in range(n):
// pass
// else:ˇ
// "});
// TODO: test `finally` auto outdents when typed inside `else` block right after for block
// cx.set_state(indoc! {"
// def main():
// try:
// i = 2
// except:
// j = 2
// else:
// for i in range(n):
// pass
// ˇ
// "});
// cx.update_editor(|editor, window, cx| {
// editor.handle_input("finally:", window, cx);
// });
// cx.assert_editor_state(indoc! {"
// def main():
// try:
// i = 2
// except:
// j = 2
// else:
// for i in range(n):
// pass
// finally:ˇ
// "});
// test `else` stays at correct indent when typed after `for` block
cx.set_state(indoc! {"
def main():
for i in range(10):
if i == 3:
break
ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.handle_input("else:", window, cx);
});
cx.assert_editor_state(indoc! {"
def main():
for i in range(10):
if i == 3:
break
else:ˇ
"});
}
#[gpui::test]
async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
init_test(cx, |_| {});
update_test_language_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
// test correct indent after newline on comment
cx.set_state(indoc! {"
# COMMENT:ˇ
"});
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
cx.assert_editor_state(indoc! {"
# COMMENT:
ˇ
"});
// test correct indent after newline in curly brackets
cx.set_state(indoc! {"
{ˇ}
"});
cx.update_editor(|editor, window, cx| {
editor.newline(&Newline, window, cx);
});
cx.run_until_parked();
cx.assert_editor_state(indoc! {"
{
ˇ
}
"});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -1343,7 +1343,7 @@ impl EditorElement {
None
}
})
.and_then(|text| {
.map(|text| {
let len = text.len();
let font = cursor_row_layout
@@ -1369,21 +1369,18 @@ impl EditorElement {
cx.theme().colors().editor_background
};
window
.text_system()
.shape_line(
text,
cursor_row_layout.font_size,
&[TextRun {
len,
font,
color,
background_color: None,
strikethrough: None,
underline: None,
}],
)
.log_err()
window.text_system().shape_line(
text,
cursor_row_layout.font_size,
&[TextRun {
len,
font,
color,
background_color: None,
strikethrough: None,
underline: None,
}],
)
})
} else {
None
@@ -2690,9 +2687,8 @@ impl EditorElement {
}
})
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
let shaped_line = self
.shape_line_number(SharedString::from(&line_number), color, window)
.log_err()?;
let shaped_line =
self.shape_line_number(SharedString::from(&line_number), color, window);
let scroll_top = scroll_position.y * line_height;
let line_origin = gutter_hitbox.map(|hitbox| {
hitbox.origin
@@ -2808,7 +2804,7 @@ impl EditorElement {
.chain(iter::repeat(""))
.take(rows.len());
placeholder_lines
.filter_map(move |line| {
.map(move |line| {
let run = TextRun {
len: line.len(),
font: style.text.font(),
@@ -2817,17 +2813,17 @@ impl EditorElement {
underline: None,
strikethrough: None,
};
window
.text_system()
.shape_line(line.to_string().into(), font_size, &[run])
.log_err()
})
.map(|line| LineWithInvisibles {
width: line.width,
len: line.len,
fragments: smallvec![LineFragment::Text(line)],
invisibles: Vec::new(),
font_size,
let line =
window
.text_system()
.shape_line(line.to_string().into(), font_size, &[run]);
LineWithInvisibles {
width: line.width,
len: line.len,
fragments: smallvec![LineFragment::Text(line)],
invisibles: Vec::new(),
font_size,
}
})
.collect()
} else {
@@ -4764,13 +4760,7 @@ impl EditorElement {
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
let color = cx.theme().colors().editor_hover_line_number;
let Some(line) = self
.shape_line_number(shaped_line.text.clone(), color, window)
.log_err()
else {
continue;
};
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
line.paint(hitbox.origin, line_height, window, cx).log_err()
} else {
shaped_line
@@ -6137,21 +6127,18 @@ impl EditorElement {
fn column_pixels(&self, column: usize, window: &mut Window, _: &mut App) -> Pixels {
let style = &self.style;
let font_size = style.text.font_size.to_pixels(window.rem_size());
let layout = window
.text_system()
.shape_line(
SharedString::from(" ".repeat(column)),
font_size,
&[TextRun {
len: column,
font: style.text.font(),
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
let layout = window.text_system().shape_line(
SharedString::from(" ".repeat(column)),
font_size,
&[TextRun {
len: column,
font: style.text.font(),
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
}],
);
layout.width
}
@@ -6171,7 +6158,7 @@ impl EditorElement {
text: SharedString,
color: Hsla,
window: &mut Window,
) -> anyhow::Result<ShapedLine> {
) -> ShapedLine {
let run = TextRun {
len: text.len(),
font: self.style.text.font(),
@@ -6393,7 +6380,6 @@ pub(crate) struct LineWithInvisibles {
font_size: Pixels,
}
#[allow(clippy::large_enum_variant)]
enum LineFragment {
Text(ShapedLine),
Element {
@@ -6452,10 +6438,10 @@ impl LineWithInvisibles {
}]) {
if let Some(replacement) = highlighted_chunk.replacement {
if !line.is_empty() {
let shaped_line = window
.text_system()
.shape_line(line.clone().into(), font_size, &styles)
.unwrap();
let shaped_line =
window
.text_system()
.shape_line(line.clone().into(), font_size, &styles);
width += shaped_line.width;
len += shaped_line.len;
fragments.push(LineFragment::Text(shaped_line));
@@ -6471,14 +6457,11 @@ impl LineWithInvisibles {
} else {
SharedString::from(Arc::from(highlighted_chunk.text))
};
let shaped_line = window
.text_system()
.shape_line(
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
)
.unwrap();
let shaped_line = window.text_system().shape_line(
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
);
AvailableSpace::Definite(shaped_line.width)
} else {
AvailableSpace::MinContent
@@ -6523,7 +6506,6 @@ impl LineWithInvisibles {
let line_layout = window
.text_system()
.shape_line(x, font_size, &[run])
.unwrap()
.with_len(highlighted_chunk.text.len());
width += line_layout.width;
@@ -6534,10 +6516,11 @@ impl LineWithInvisibles {
} else {
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
if ix > 0 {
let shaped_line = window
.text_system()
.shape_line(line.clone().into(), font_size, &styles)
.unwrap();
let shaped_line = window.text_system().shape_line(
line.clone().into(),
font_size,
&styles,
);
width += shaped_line.width;
len += shaped_line.len;
fragments.push(LineFragment::Text(shaped_line));
@@ -8039,36 +8022,30 @@ impl Element for EditorElement {
});
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = window
.text_system()
.shape_line(
"".into(),
invisible_symbol_font_size,
&[TextRun {
len: "".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
let space_invisible = window
.text_system()
.shape_line(
"".into(),
invisible_symbol_font_size,
&[TextRun {
len: "".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
)
.unwrap();
let tab_invisible = window.text_system().shape_line(
"".into(),
invisible_symbol_font_size,
&[TextRun {
len: "".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
);
let space_invisible = window.text_system().shape_line(
"".into(),
invisible_symbol_font_size,
&[TextRun {
len: "".len(),
font: self.style.text.font(),
color: cx.theme().colors().editor_invisible,
background_color: None,
underline: None,
strikethrough: None,
}],
);
let mode = snapshot.mode.clone();

View File

@@ -33,6 +33,7 @@ serde_json.workspace = true
task.workspace = true
toml.workspace = true
util.workspace = true
wasi-preview1-component-adapter-provider.workspace = true
wasm-encoder.workspace = true
wasmparser.workspace = true
wit-component.workspace = true

View File

@@ -4,7 +4,6 @@ use crate::{
use anyhow::{Context as _, Result, anyhow, bail};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::AsyncReadExt;
use futures::io::BufReader;
use heck::ToSnakeCase;
use http_client::{self, AsyncBody, HttpClient};
@@ -15,6 +14,7 @@ use std::{
process::Stdio,
sync::Arc,
};
use wasi_preview1_component_adapter_provider::WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER;
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;
use wit_component::ComponentEncoder;
@@ -26,7 +26,6 @@ use wit_component::ComponentEncoder;
/// Once Rust 1.78 is released, there will be a `wasm32-wasip2` target available, so we will
/// not need the adapter anymore.
const RUST_TARGET: &str = "wasm32-wasip1";
const WASI_ADAPTER_URL: &str = "https://github.com/bytecodealliance/wasmtime/releases/download/v18.0.2/wasi_snapshot_preview1.reactor.wasm";
/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc
/// and clang's runtime library. The `wasi-sdk` provides these binaries.
@@ -137,7 +136,6 @@ impl ExtensionBuilder {
options: CompileExtensionOptions,
) -> Result<(), anyhow::Error> {
self.install_rust_wasm_target_if_needed()?;
let adapter_bytes = self.install_wasi_preview1_adapter_if_needed().await?;
let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?;
let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?;
@@ -186,7 +184,10 @@ impl ExtensionBuilder {
let mut encoder = ComponentEncoder::default()
.module(&wasm_bytes)?
.adapter("wasi_snapshot_preview1", &adapter_bytes)
.adapter(
"wasi_snapshot_preview1",
WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
)
.context("failed to load adapter module")?
.validate(true);
@@ -395,38 +396,6 @@ impl ExtensionBuilder {
Ok(())
}
async fn install_wasi_preview1_adapter_if_needed(&self) -> Result<Vec<u8>> {
let cache_path = self.cache_dir.join("wasi_snapshot_preview1.reactor.wasm");
if let Ok(content) = fs::read(&cache_path) {
if Parser::is_core_wasm(&content) {
return Ok(content);
}
}
fs::remove_file(&cache_path).ok();
log::info!(
"downloading wasi adapter module to {}",
cache_path.display()
);
let mut response = self
.http
.get(WASI_ADAPTER_URL, AsyncBody::default(), true)
.await?;
let mut content = Vec::new();
let mut body = BufReader::new(response.body_mut());
body.read_to_end(&mut content).await?;
fs::write(&cache_path, &content)
.with_context(|| format!("failed to save file {}", cache_path.display()))?;
if !Parser::is_core_wasm(&content) {
bail!("downloaded wasi adapter is invalid");
}
Ok(content)
}
async fn install_wasi_sdk_if_needed(&self) -> Result<PathBuf> {
let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME {
format!("{WASI_SDK_URL}/{asset_name}")

View File

@@ -91,6 +91,12 @@ impl FeatureFlag for ThreadAutoCaptureFeatureFlag {
}
}
pub struct JjUiFeatureFlag {}
impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui";
}
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where

View File

@@ -324,7 +324,8 @@ fn update_conflict_highlighting(
cx: &mut Context<Editor>,
) {
log::debug!("update conflict highlighting for {conflict:?}");
let theme = cx.theme().clone();
let colors = theme.colors();
let outer_start = buffer
.anchor_in_excerpt(excerpt_id, conflict.range.start)
.unwrap();
@@ -344,9 +345,11 @@ fn update_conflict_highlighting(
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
.unwrap();
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
let divider_background = cx.theme().colors().version_control_conflict_marker_border;
let ours_background = colors.version_control_conflict_ours_background;
let ours_marker = colors.version_control_conflict_ours_marker_background;
let theirs_background = colors.version_control_conflict_theirs_background;
let theirs_marker = colors.version_control_conflict_theirs_marker_background;
let divider_background = colors.version_control_conflict_divider_background;
let options = RowHighlightOptions {
include_gutter: false,
@@ -354,14 +357,14 @@ fn update_conflict_highlighting(
};
// Prevent diff hunk highlighting within the entire conflict region.
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(
outer_start..our_start,
ours_background,
editor.highlight_rows::<ConflictsOuter>(
outer_start..outer_end,
divider_background,
options,
cx,
);
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
editor.highlight_rows::<ConflictsTheirs>(
their_start..their_end,
theirs_background,
@@ -370,7 +373,7 @@ fn update_conflict_highlighting(
);
editor.highlight_rows::<ConflictsTheirsMarker>(
their_end..outer_end,
theirs_background,
theirs_marker,
options,
cx,
);

View File

@@ -257,6 +257,10 @@ path = "examples/image/image.rs"
name = "input"
path = "examples/input.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/on_window_close_quit.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"
@@ -277,6 +281,10 @@ path = "examples/shadow.rs"
name = "svg"
path = "examples/svg/svg.rs"
[[example]]
name = "text"
path = "examples/text.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"
@@ -288,7 +296,3 @@ path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/on_window_close_quit.rs"

View File

@@ -481,8 +481,7 @@ impl Element for TextElement {
let font_size = style.font_size.to_pixels(window.rem_size());
let line = window
.text_system()
.shape_line(display_text, font_size, &runs)
.unwrap();
.shape_line(display_text, font_size, &runs);
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {

View File

@@ -0,0 +1,333 @@
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use gpui::{
AbsoluteLength, App, Application, Context, DefiniteLength, ElementId, Global, Hsla, Menu,
SharedString, TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds,
colors::DefaultColors, div, point, prelude::*, px, relative, rgb, size,
};
use std::iter;
#[derive(Clone, Debug)]
pub struct TextContext {
font_size: f32,
line_height: f32,
type_scale: f32,
}
impl Default for TextContext {
fn default() -> Self {
TextContext {
font_size: 16.0,
line_height: 1.3,
type_scale: 1.33,
}
}
}
impl TextContext {
pub fn get_global(cx: &App) -> &Arc<TextContext> {
&cx.global::<GlobalTextContext>().0
}
}
#[derive(Clone, Debug)]
pub struct GlobalTextContext(pub Arc<TextContext>);
impl Deref for GlobalTextContext {
type Target = Arc<TextContext>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for GlobalTextContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Global for GlobalTextContext {}
pub trait ActiveTextContext {
fn text_context(&self) -> &Arc<TextContext>;
}
impl ActiveTextContext for App {
fn text_context(&self) -> &Arc<TextContext> {
&self.global::<GlobalTextContext>().0
}
}
#[derive(Clone, PartialEq)]
pub struct SpecimenTheme {
pub bg: Hsla,
pub fg: Hsla,
}
impl Default for SpecimenTheme {
fn default() -> Self {
Self {
bg: gpui::white(),
fg: gpui::black(),
}
}
}
impl SpecimenTheme {
pub fn invert(&self) -> Self {
Self {
bg: self.fg,
fg: self.bg,
}
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct Specimen {
id: ElementId,
scale: f32,
text_style: Option<TextStyle>,
string: SharedString,
invert: bool,
}
impl Specimen {
pub fn new(id: usize) -> Self {
let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
let id_string = format!("specimen-{}", id);
let id = ElementId::Name(id_string.into());
Self {
id,
scale: 1.0,
text_style: None,
string,
invert: false,
}
}
pub fn invert(mut self) -> Self {
self.invert = !self.invert;
self
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for Specimen {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let rem_size = window.rem_size();
let scale = self.scale;
let global_style = cx.text_context();
let style_override = self.text_style;
let mut font_size = global_style.font_size;
let mut line_height = global_style.line_height;
if let Some(style_override) = style_override {
font_size = style_override.font_size.to_pixels(rem_size).0;
line_height = match style_override.line_height {
DefiniteLength::Absolute(absolute_len) => match absolute_len {
AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).0,
AbsoluteLength::Pixels(absolute_len) => absolute_len.0,
},
DefiniteLength::Fraction(value) => value,
};
}
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
div()
.id(self.id)
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(font_size * scale))
.line_height(relative(line_height))
.p(px(10.0))
.child(self.string.clone())
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct CharacterGrid {
scale: f32,
invert: bool,
text_style: Option<TextStyle>,
}
impl CharacterGrid {
pub fn new() -> Self {
Self {
scale: 1.0,
invert: false,
text_style: None,
}
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for CharacterGrid {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
let characters = vec![
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
"Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
"r", "s", "t", "u", "v", "w", "x", "y", "z", "", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
"Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
"χ", "ψ", "", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "", "µ",
"", "<=", "!=", "==", "--", "++", "=>", "->",
];
let columns = 11;
let rows = characters.len().div_ceil(columns);
let grid_rows = (0..rows).map(|row_idx| {
let start_idx = row_idx * columns;
let end_idx = (start_idx + columns).min(characters.len());
div()
.w_full()
.flex()
.flex_row()
.children((start_idx..end_idx).map(|i| {
div()
.text_center()
.size(px(62.))
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(24.0))
.line_height(relative(1.0))
.child(characters[i])
}))
.when(end_idx - start_idx < columns, |d| {
d.children(
iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
)
})
});
div().p_4().gap_2().flex().flex_col().children(grid_rows)
}
}
struct TextExample {
next_id: usize,
}
impl TextExample {
fn next_id(&mut self) -> usize {
self.next_id += 1;
self.next_id
}
}
impl Render for TextExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let tcx = cx.text_context();
let colors = cx.default_colors().clone();
let type_scale = tcx.type_scale;
let step_down_2 = 1.0 / (type_scale * type_scale);
let step_down_1 = 1.0 / type_scale;
let base = 1.0;
let step_up_1 = base * type_scale;
let step_up_2 = step_up_1 * type_scale;
let step_up_3 = step_up_2 * type_scale;
let step_up_4 = step_up_3 * type_scale;
let step_up_5 = step_up_4 * type_scale;
let step_up_6 = step_up_5 * type_scale;
div()
.size_full()
.child(
div()
.id("text-example")
.overflow_y_scroll()
.overflow_x_hidden()
.bg(rgb(0xffffff))
.size_full()
.child(div().child(CharacterGrid::new().scale(base)))
.child(
div()
.child(Specimen::new(self.next_id()).scale(step_down_2))
.child(Specimen::new(self.next_id()).scale(step_down_2).invert())
.child(Specimen::new(self.next_id()).scale(step_down_1))
.child(Specimen::new(self.next_id()).scale(step_down_1).invert())
.child(Specimen::new(self.next_id()).scale(base))
.child(Specimen::new(self.next_id()).scale(base).invert())
.child(Specimen::new(self.next_id()).scale(step_up_1))
.child(Specimen::new(self.next_id()).scale(step_up_1).invert())
.child(Specimen::new(self.next_id()).scale(step_up_2))
.child(Specimen::new(self.next_id()).scale(step_up_2).invert())
.child(Specimen::new(self.next_id()).scale(step_up_3))
.child(Specimen::new(self.next_id()).scale(step_up_3).invert())
.child(Specimen::new(self.next_id()).scale(step_up_4))
.child(Specimen::new(self.next_id()).scale(step_up_4).invert())
.child(Specimen::new(self.next_id()).scale(step_up_5))
.child(Specimen::new(self.next_id()).scale(step_up_5).invert())
.child(Specimen::new(self.next_id()).scale(step_up_6))
.child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
),
)
.child(div().w(px(240.)).h_full().bg(colors.container))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.set_menus(vec![Menu {
name: "GPUI Typography".into(),
items: vec![],
}]);
cx.init_colors();
cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
let window = cx
.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("GPUI Typography".into()),
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(bounds(
point(px(0.0), px(0.0)),
size(px(920.), px(720.)),
))),
..Default::default()
},
|_window, cx| cx.new(|_cx| TextExample { next_id: 0 }),
)
.unwrap();
window
.update(cx, |_view, _window, cx| {
cx.activate(true);
})
.unwrap();
});
}

View File

@@ -38,7 +38,9 @@ use crate::{
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptHandle, PromptLevel,
Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString,
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
WindowHandle, WindowId, WindowInvalidator, current_platform, hash, init_app_menus,
WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
mod async_context;
@@ -1656,6 +1658,13 @@ impl App {
_ = window.drop_image(image);
}
}
/// Initializes gpui's default colors for the application.
///
/// These colors can be accessed through `cx.default_colors()`.
pub fn init_colors(&mut self) {
self.set_global(GlobalColors(Arc::new(Colors::default())));
}
}
impl AppContext for App {

122
crates/gpui/src/colors.rs Normal file
View File

@@ -0,0 +1,122 @@
use crate::{App, Global, Rgba, Window, WindowAppearance, rgb};
use std::ops::Deref;
use std::sync::Arc;
/// The default set of colors for gpui.
///
/// These are used for styling base components, examples and more.
#[derive(Clone, Debug)]
pub struct Colors {
/// Text color
pub text: Rgba,
/// Selected text color
pub selected_text: Rgba,
/// Background color
pub background: Rgba,
/// Disabled color
pub disabled: Rgba,
/// Selected color
pub selected: Rgba,
/// Border color
pub border: Rgba,
/// Separator color
pub separator: Rgba,
/// Container color
pub container: Rgba,
}
impl Default for Colors {
fn default() -> Self {
Self::light()
}
}
impl Colors {
/// Returns the default colors for the given window appearance.
pub fn for_appearance(window: &Window) -> Self {
match window.appearance() {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::light(),
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::dark(),
}
}
/// Returns the default dark colors.
pub fn dark() -> Self {
Self {
text: rgb(0xffffff),
selected_text: rgb(0xffffff),
disabled: rgb(0x565656),
selected: rgb(0x2457ca),
background: rgb(0x222222),
border: rgb(0x000000),
separator: rgb(0xd9d9d9),
container: rgb(0x262626),
}
}
/// Returns the default light colors.
pub fn light() -> Self {
Self {
text: rgb(0x252525),
selected_text: rgb(0xffffff),
background: rgb(0xffffff),
disabled: rgb(0xb0b0b0),
selected: rgb(0x2a63d9),
border: rgb(0xd9d9d9),
separator: rgb(0xe6e6e6),
container: rgb(0xf4f5f5),
}
}
/// Get [Colors] from the global state
pub fn get_global(cx: &App) -> &Arc<Colors> {
&cx.global::<GlobalColors>().0
}
}
/// Get [Colors] from the global state
#[derive(Clone, Debug)]
pub struct GlobalColors(pub Arc<Colors>);
impl Deref for GlobalColors {
type Target = Arc<Colors>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Global for GlobalColors {}
/// Implement this trait to allow global [Color] access via `cx.default_colors()`.
pub trait DefaultColors {
/// Returns the default [`gpui::Colors`]
fn default_colors(&self) -> &Arc<Colors>;
}
impl DefaultColors for App {
fn default_colors(&self) -> &Arc<Colors> {
&self.global::<GlobalColors>().0
}
}
/// The appearance of the base GPUI colors, used to style GPUI elements
///
/// Varies based on the system's current [`WindowAppearance`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DefaultAppearance {
/// Use the set of colors for light appearances.
#[default]
Light,
/// Use the set of colors for dark appearances.
Dark,
}
impl From<WindowAppearance> for DefaultAppearance {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
}
}
}

View File

@@ -1,115 +0,0 @@
use crate::{Hsla, Rgba, WindowAppearance, rgb};
/// The appearance of the base GPUI colors, used to style GPUI elements
///
/// Varies based on the system's current [`WindowAppearance`].
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DefaultThemeAppearance {
/// Use the set of colors for light appearances.
#[default]
Light,
/// Use the set of colors for dark appearances.
Dark,
}
impl From<WindowAppearance> for DefaultThemeAppearance {
fn from(appearance: WindowAppearance) -> Self {
match appearance {
WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light,
WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark,
}
}
}
/// Returns the default colors for the given appearance.
pub fn colors(appearance: DefaultThemeAppearance) -> DefaultColors {
match appearance {
DefaultThemeAppearance::Light => DefaultColors::light(),
DefaultThemeAppearance::Dark => DefaultColors::dark(),
}
}
/// A collection of colors.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DefaultColors {
text: Rgba,
selected_text: Rgba,
background: Rgba,
disabled: Rgba,
selected: Rgba,
border: Rgba,
separator: Rgba,
container: Rgba,
}
impl DefaultColors {
/// Returns the default dark colors.
pub fn dark() -> Self {
Self {
text: rgb(0xffffff),
selected_text: rgb(0xffffff),
disabled: rgb(0x565656),
selected: rgb(0x2457ca),
background: rgb(0x222222),
border: rgb(0x000000),
separator: rgb(0xd9d9d9),
container: rgb(0x262626),
}
}
/// Returns the default light colors.
pub fn light() -> Self {
Self {
text: rgb(0x252525),
selected_text: rgb(0xffffff),
background: rgb(0xffffff),
disabled: rgb(0xb0b0b0),
selected: rgb(0x2a63d9),
border: rgb(0xd9d9d9),
separator: rgb(0xe6e6e6),
container: rgb(0xf4f5f5),
}
}
}
/// A default GPUI color.
#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumIter)]
pub enum DefaultColor {
/// Text color
Text,
/// Selected text color
SelectedText,
/// Background color
Background,
/// Disabled color
Disabled,
/// Selected color
Selected,
/// Border color
Border,
/// Separator color
Separator,
/// Container color
Container,
}
impl DefaultColor {
/// Returns the RGBA color for the given color type.
pub fn color(&self, colors: &DefaultColors) -> Rgba {
match self {
DefaultColor::Text => colors.text,
DefaultColor::SelectedText => colors.selected_text,
DefaultColor::Background => colors.background,
DefaultColor::Disabled => colors.disabled,
DefaultColor::Selected => colors.selected,
DefaultColor::Border => colors.border,
DefaultColor::Separator => colors.separator,
DefaultColor::Container => colors.container,
}
}
/// Returns the HSLA color for the given color type.
pub fn hsla(&self, colors: &DefaultColors) -> Hsla {
self.color(colors).into()
}
}

View File

@@ -1,7 +1,6 @@
mod anchored;
mod animation;
mod canvas;
mod common;
mod deferred;
mod div;
mod image_cache;
@@ -15,7 +14,6 @@ mod uniform_list;
pub use anchored::*;
pub use animation::*;
pub use canvas::*;
pub use common::*;
pub use deferred::*;
pub use div::*;
pub use image_cache::*;

View File

@@ -73,6 +73,8 @@ mod asset_cache;
mod assets;
mod bounds_tree;
mod color;
/// The default colors used by GPUI.
pub mod colors;
mod element;
mod elements;
mod executor;

View File

@@ -426,8 +426,8 @@ impl<P: LinuxClient + 'static> Platform for P {
fn app_path(&self) -> Result<PathBuf> {
// get the path of the executable of the current process
let exe_path = env::current_exe()?;
Ok(exe_path)
let app_path = env::current_exe()?;
return Ok(app_path);
}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {

View File

@@ -983,7 +983,7 @@ impl Clipboard {
// format that the contents can be converted to
format_atoms[0..IMAGE_FORMAT_COUNT].copy_from_slice(&image_format_atoms);
format_atoms[IMAGE_FORMAT_COUNT..].copy_from_slice(&text_format_atoms);
debug_assert!(!format_atoms.iter().any(|&a| a == atom_none));
debug_assert!(!format_atoms.contains(&atom_none));
let result = self.inner.read(&format_atoms, selection)?;

View File

@@ -137,10 +137,7 @@ fn add_recent_folders(
let tasks: IObjectCollection =
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
for folder_path in entries
.iter()
.filter(|path| !is_item_in_array(path, removed))
{
for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
let argument = HSTRING::from(
folder_path
.iter()
@@ -181,11 +178,6 @@ fn add_recent_folders(
}
}
#[inline]
fn is_item_in_array(item: &SmallVec<[PathBuf; 2]>, removed: &Vec<SmallVec<[PathBuf; 2]>>) -> bool {
removed.iter().any(|removed_item| removed_item == item)
}
fn create_shell_link(
argument: HSTRING,
description: HSTRING,

View File

@@ -293,37 +293,35 @@ fn handle_mouse_move_msg(
start_tracking_mouse(handle, &state_ptr, TME_LEAVE);
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
drop(lock);
let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
flags if flags.contains(MK_XBUTTON1) => {
Some(MouseButton::Navigate(NavigationDirection::Back))
}
flags if flags.contains(MK_XBUTTON2) => {
Some(MouseButton::Navigate(NavigationDirection::Forward))
}
_ => None,
};
let x = lparam.signed_loword() as f32;
let y = lparam.signed_hiword() as f32;
let event = MouseMoveEvent {
position: logical_point(x, y, scale_factor),
pressed_button,
modifiers: current_modifiers(),
};
let result = if callback(PlatformInput::MouseMove(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
return result;
}
Some(1)
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
let scale_factor = lock.scale_factor;
drop(lock);
let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
flags if flags.contains(MK_XBUTTON1) => {
Some(MouseButton::Navigate(NavigationDirection::Back))
}
flags if flags.contains(MK_XBUTTON2) => {
Some(MouseButton::Navigate(NavigationDirection::Forward))
}
_ => None,
};
let x = lparam.signed_loword() as f32;
let y = lparam.signed_hiword() as f32;
let input = PlatformInput::MouseMove(MouseMoveEvent {
position: logical_point(x, y, scale_factor),
pressed_button,
modifiers: current_modifiers(),
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if handled { Some(0) } else { Some(1) }
}
fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
@@ -439,14 +437,10 @@ fn handle_keyup_msg(
};
drop(lock);
let result = if func(input).default_prevented {
Some(0)
} else {
Some(1)
};
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
if handled { Some(0) } else { Some(1) }
}
fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
@@ -479,32 +473,27 @@ fn handle_mouse_down_msg(
) -> Option<isize> {
unsafe { SetCapture(handle) };
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let x = lparam.signed_loword() as f32;
let y = lparam.signed_hiword() as f32;
let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
let click_count = lock.click_state.update(button, physical_point);
let scale_factor = lock.scale_factor;
drop(lock);
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
let x = lparam.signed_loword();
let y = lparam.signed_hiword();
let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
let click_count = lock.click_state.update(button, physical_point);
let scale_factor = lock.scale_factor;
drop(lock);
let event = MouseDownEvent {
button,
position: logical_point(x, y, scale_factor),
modifiers: current_modifiers(),
click_count,
first_mouse: false,
};
let result = if callback(PlatformInput::MouseDown(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
let input = PlatformInput::MouseDown(MouseDownEvent {
button,
position: logical_point(x as f32, y as f32, scale_factor),
modifiers: current_modifiers(),
click_count,
first_mouse: false,
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
} else {
Some(1)
}
if handled { Some(0) } else { Some(1) }
}
fn handle_mouse_up_msg(
@@ -515,30 +504,25 @@ fn handle_mouse_up_msg(
) -> Option<isize> {
unsafe { ReleaseCapture().log_err() };
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let x = lparam.signed_loword() as f32;
let y = lparam.signed_hiword() as f32;
let click_count = lock.click_state.current_count;
let scale_factor = lock.scale_factor;
drop(lock);
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
let x = lparam.signed_loword() as f32;
let y = lparam.signed_hiword() as f32;
let click_count = lock.click_state.current_count;
let scale_factor = lock.scale_factor;
drop(lock);
let event = MouseUpEvent {
button,
position: logical_point(x, y, scale_factor),
modifiers: current_modifiers(),
click_count,
};
let result = if callback(PlatformInput::MouseUp(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
let input = PlatformInput::MouseUp(MouseUpEvent {
button,
position: logical_point(x, y, scale_factor),
modifiers: current_modifiers(),
click_count,
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
result
} else {
Some(1)
}
if handled { Some(0) } else { Some(1) }
}
fn handle_xbutton_msg(
@@ -564,46 +548,42 @@ fn handle_mouse_wheel_msg(
) -> Option<isize> {
let modifiers = current_modifiers();
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
let wheel_scroll_amount = match modifiers.shift {
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
};
drop(lock);
let wheel_distance =
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let event = ScrollWheelEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
delta: ScrollDelta::Lines(match modifiers.shift {
true => Point {
x: wheel_distance,
y: 0.0,
},
false => Point {
y: wheel_distance,
x: 0.0,
},
}),
modifiers: current_modifiers(),
touch_phase: TouchPhase::Moved,
};
let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
let scale_factor = lock.scale_factor;
let wheel_scroll_amount = match modifiers.shift {
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
};
drop(lock);
result
} else {
Some(1)
}
let wheel_distance =
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
delta: ScrollDelta::Lines(match modifiers.shift {
true => Point {
x: wheel_distance,
y: 0.0,
},
false => Point {
y: wheel_distance,
x: 0.0,
},
}),
modifiers,
touch_phase: TouchPhase::Moved,
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if handled { Some(0) } else { Some(1) }
}
fn handle_mouse_horizontal_wheel_msg(
@@ -613,37 +593,33 @@ fn handle_mouse_horizontal_wheel_msg(
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
drop(lock);
let wheel_distance =
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let event = ScrollWheelEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
delta: ScrollDelta::Lines(Point {
x: wheel_distance,
y: 0.0,
}),
modifiers: current_modifiers(),
touch_phase: TouchPhase::Moved,
};
let result = if callback(PlatformInput::ScrollWheel(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
let Some(mut func) = lock.callbacks.input.take() else {
return Some(1);
};
let scale_factor = lock.scale_factor;
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
drop(lock);
result
} else {
Some(1)
}
let wheel_distance =
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let event = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
delta: ScrollDelta::Lines(Point {
x: wheel_distance,
y: 0.0,
}),
modifiers: current_modifiers(),
touch_phase: TouchPhase::Moved,
});
let handled = !func(event).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if handled { Some(0) } else { Some(1) }
}
fn retrieve_caret_position(state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<POINT> {
@@ -808,10 +784,10 @@ fn handle_activate_msg(
.executor
.spawn(async move {
let mut lock = this.state.borrow_mut();
if let Some(mut cb) = lock.callbacks.active_status_change.take() {
if let Some(mut func) = lock.callbacks.active_status_change.take() {
drop(lock);
cb(activated);
this.state.borrow_mut().callbacks.active_status_change = Some(cb);
func(activated);
this.state.borrow_mut().callbacks.active_status_change = Some(func);
}
})
.detach();
@@ -878,7 +854,7 @@ fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>)
// Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize
// are handled there.
// So we only care about if monitor is disconnected.
let previous_monitor = state_ptr.as_ref().state.borrow().display;
let previous_monitor = state_ptr.state.borrow().display;
if WindowsDisplay::is_connected(previous_monitor.handle) {
// we are fine, other display changed
return None;
@@ -896,7 +872,7 @@ fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>)
return None;
}
let new_display = WindowsDisplay::new_with_handle(new_monitor);
state_ptr.as_ref().state.borrow_mut().display = new_display;
state_ptr.state.borrow_mut().display = new_display;
Some(0)
}
@@ -980,30 +956,24 @@ fn handle_nc_mouse_move_msg(
start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT);
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
drop(lock);
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let event = MouseMoveEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
pressed_button: None,
modifiers: current_modifiers(),
};
let result = if callback(PlatformInput::MouseMove(event)).default_prevented {
Some(0)
} else {
Some(1)
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
let mut func = lock.callbacks.input.take()?;
let scale_factor = lock.scale_factor;
drop(lock);
result
} else {
None
}
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let input = PlatformInput::MouseMove(MouseMoveEvent {
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
pressed_button: None,
modifiers: current_modifiers(),
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if handled { Some(0) } else { None }
}
fn handle_nc_mouse_down_msg(
@@ -1018,7 +988,7 @@ fn handle_nc_mouse_down_msg(
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
if let Some(mut func) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
@@ -1028,22 +998,19 @@ fn handle_nc_mouse_down_msg(
let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y));
let click_count = lock.click_state.update(button, physical_point);
drop(lock);
let event = MouseDownEvent {
let input = PlatformInput::MouseDown(MouseDownEvent {
button,
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
modifiers: current_modifiers(),
click_count,
first_mouse: false,
};
let result = if callback(PlatformInput::MouseDown(event)).default_prevented {
Some(0)
} else {
None
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if result.is_some() {
return result;
if handled {
return Some(0);
}
} else {
drop(lock);
@@ -1075,28 +1042,26 @@ fn handle_nc_mouse_up_msg(
}
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut callback) = lock.callbacks.input.take() {
if let Some(mut func) = lock.callbacks.input.take() {
let scale_factor = lock.scale_factor;
drop(lock);
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
};
unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
let event = MouseUpEvent {
let input = PlatformInput::MouseUp(MouseUpEvent {
button,
position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
modifiers: current_modifiers(),
click_count: 1,
};
let result = if callback(PlatformInput::MouseUp(event)).default_prevented {
Some(0)
} else {
None
};
state_ptr.state.borrow_mut().callbacks.input = Some(callback);
if result.is_some() {
return result;
});
let handled = !func(input).propagate;
state_ptr.state.borrow_mut().callbacks.input = Some(func);
if handled {
return Some(0);
}
} else {
drop(lock);
@@ -1104,35 +1069,27 @@ fn handle_nc_mouse_up_msg(
let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take();
if button == MouseButton::Left && last_pressed.is_some() {
let last_button = last_pressed.unwrap();
let mut handled = false;
match wparam.0 as u32 {
HTMINBUTTON => {
if last_button == HTMINBUTTON {
unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
handled = true;
}
let handled = match (wparam.0 as u32, last_pressed.unwrap()) {
(HTMINBUTTON, HTMINBUTTON) => {
unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
true
}
HTMAXBUTTON => {
if last_button == HTMAXBUTTON {
if state_ptr.state.borrow().is_maximized() {
unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() };
} else {
unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() };
}
handled = true;
(HTMAXBUTTON, HTMAXBUTTON) => {
if state_ptr.state.borrow().is_maximized() {
unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() };
} else {
unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() };
}
true
}
HTCLOSE => {
if last_button == HTCLOSE {
unsafe {
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
.log_err()
};
handled = true;
}
(HTCLOSE, HTCLOSE) => {
unsafe {
PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
.log_err()
};
true
}
_ => {}
_ => false,
};
if handled {
return Some(0);

View File

@@ -343,7 +343,7 @@ impl WindowTextSystem {
text: SharedString,
font_size: Pixels,
runs: &[TextRun],
) -> Result<ShapedLine> {
) -> ShapedLine {
debug_assert!(
text.find('\n').is_none(),
"text argument should not contain newlines"
@@ -370,13 +370,13 @@ impl WindowTextSystem {
});
}
let layout = self.layout_line(&text, font_size, runs)?;
let layout = self.layout_line(&text, font_size, runs);
Ok(ShapedLine {
ShapedLine {
layout,
text,
decoration_runs,
})
}
}
/// Shape a multi line string of text, at the given font_size, for painting to the screen.
@@ -510,7 +510,7 @@ impl WindowTextSystem {
text: Text,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>>
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
@@ -537,7 +537,7 @@ impl WindowTextSystem {
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
Ok(layout)
layout
}
}

View File

@@ -1,73 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Attribute, Data, DeriveInput, Lit, Meta, NestedMeta, parse_macro_input};
pub fn derive_path_static_str(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let prefix = get_attr_value(&input.attrs, "prefix").unwrap_or_else(|| "".to_string());
let suffix = get_attr_value(&input.attrs, "suffix").unwrap_or_else(|| "".to_string());
let delimiter = get_attr_value(&input.attrs, "delimiter").unwrap_or_else(|| "/".to_string());
let path_str_impl = impl_path_str(name, &input.data, &prefix, &suffix, &delimiter);
let expanded = quote! {
impl #name {
pub fn path_str(&self) -> &'static str {
#path_str_impl
}
}
};
TokenStream::from(expanded)
}
fn impl_path_str(
name: &syn::Ident,
data: &Data,
prefix: &str,
suffix: &str,
delimiter: &str,
) -> proc_macro2::TokenStream {
match *data {
Data::Enum(ref data) => {
let match_arms = data.variants.iter().map(|variant| {
let ident = &variant.ident;
let path = format!("{}{}{}{}{}", prefix, delimiter, ident, delimiter, suffix);
quote! {
#name::#ident => #path,
}
});
quote! {
match self {
#(#match_arms)*
}
}
}
_ => panic!("DerivePathStr only supports enums"),
}
}
fn get_attr_value(attrs: &[Attribute], key: &str) -> Option<String> {
attrs
.iter()
.filter(|attr| attr.path.is_ident("derive_path_static_str"))
.find_map(|attr| {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
meta_list.nested.iter().find_map(|nested_meta| {
if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested_meta {
if name_value.path.is_ident(key) {
if let Lit::Str(lit_str) = &name_value.lit {
return Some(lit_str.value());
}
}
}
None
})
} else {
None
}
})
}

View File

@@ -1,6 +1,5 @@
mod derive_app_context;
mod derive_into_element;
mod derive_path_static_str;
mod derive_render;
mod derive_visual_context;
mod register_action;
@@ -31,12 +30,6 @@ pub fn derive_render(input: TokenStream) -> TokenStream {
derive_render::derive_render(input)
}
#[proc_macro_derive(PathStaticStr)]
#[doc(hidden)]
pub fn derive_path_static_str(input: TokenStream) -> TokenStream {
derive_path_static_str::derive_path_static_str(input)
}
/// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App`
/// Note that a `#[app]` attribute is required to identify the variable holding the &mut App.
///

18
crates/jj/Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "jj"
version = "0.1.0"
publish.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/jj.rs"
[dependencies]
anyhow.workspace = true
gpui.workspace = true
jj-lib.workspace = true
workspace-hack.workspace = true

1
crates/jj/LICENSE-GPL Symbolic link
View File

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

5
crates/jj/src/jj.rs Normal file
View File

@@ -0,0 +1,5 @@
mod jj_repository;
mod jj_store;
pub use jj_repository::*;
pub use jj_store::*;

View File

@@ -0,0 +1,72 @@
use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use gpui::SharedString;
use jj_lib::config::StackedConfig;
use jj_lib::repo::StoreFactories;
use jj_lib::settings::UserSettings;
use jj_lib::workspace::{self, DefaultWorkspaceLoaderFactory, WorkspaceLoaderFactory};
#[derive(Debug, Clone)]
pub struct Bookmark {
pub ref_name: SharedString,
}
pub trait JujutsuRepository: Send + Sync {
fn list_bookmarks(&self) -> Vec<Bookmark>;
}
pub struct RealJujutsuRepository {
repository: Arc<jj_lib::repo::ReadonlyRepo>,
}
impl RealJujutsuRepository {
pub fn new(cwd: &Path) -> Result<Self> {
let workspace_loader_factory = DefaultWorkspaceLoaderFactory;
let workspace_loader = workspace_loader_factory.create(Self::find_workspace_dir(cwd))?;
let config = StackedConfig::with_defaults();
let settings = UserSettings::from_config(config)?;
let workspace = workspace_loader.load(
&settings,
&StoreFactories::default(),
&workspace::default_working_copy_factories(),
)?;
let repo_loader = workspace.repo_loader();
let repository = repo_loader.load_at_head()?;
Ok(Self { repository })
}
fn find_workspace_dir(cwd: &Path) -> &Path {
cwd.ancestors()
.find(|path| path.join(".jj").is_dir())
.unwrap_or(cwd)
}
}
impl JujutsuRepository for RealJujutsuRepository {
fn list_bookmarks(&self) -> Vec<Bookmark> {
let bookmarks = self
.repository
.view()
.bookmarks()
.map(|(ref_name, _target)| Bookmark {
ref_name: ref_name.as_str().to_string().into(),
})
.collect();
bookmarks
}
}
pub struct FakeJujutsuRepository {}
impl JujutsuRepository for FakeJujutsuRepository {
fn list_bookmarks(&self) -> Vec<Bookmark> {
Vec::new()
}
}

41
crates/jj/src/jj_store.rs Normal file
View File

@@ -0,0 +1,41 @@
use std::path::Path;
use std::sync::Arc;
use gpui::{App, Entity, Global, prelude::*};
use crate::{JujutsuRepository, RealJujutsuRepository};
/// Note: We won't ultimately be storing the jj store in a global, we're just doing this for exploration purposes.
struct GlobalJujutsuStore(Entity<JujutsuStore>);
impl Global for GlobalJujutsuStore {}
pub struct JujutsuStore {
repository: Arc<dyn JujutsuRepository>,
}
impl JujutsuStore {
pub fn init_global(cx: &mut App) {
let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else {
return;
};
let repository = Arc::new(repository);
let jj_store = cx.new(|cx| JujutsuStore::new(repository, cx));
cx.set_global(GlobalJujutsuStore(jj_store));
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalJujutsuStore>()
.map(|global| global.0.clone())
}
pub fn new(repository: Arc<dyn JujutsuRepository>, _cx: &mut Context<Self>) -> Self {
Self { repository }
}
pub fn repository(&self) -> &Arc<dyn JujutsuRepository> {
&self.repository
}
}

25
crates/jj_ui/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "jj_ui"
version = "0.1.0"
publish.workspace = true
edition.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/jj_ui.rs"
[dependencies]
command_palette_hooks.workspace = true
feature_flags.workspace = true
fuzzy.workspace = true
gpui.workspace = true
jj.workspace = true
picker.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true

1
crates/jj_ui/LICENSE-GPL Symbolic link
View File

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

View File

@@ -0,0 +1,197 @@
use std::sync::Arc;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window,
prelude::*,
};
use jj::{Bookmark, JujutsuStore};
use picker::{Picker, PickerDelegate};
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
pub fn register(workspace: &mut Workspace) {
workspace.register_action(open);
}
fn open(
workspace: &mut Workspace,
_: &zed_actions::jj::BookmarkList,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(jj_store) = JujutsuStore::try_global(cx) else {
return;
};
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = BookmarkPickerDelegate::new(cx.entity().downgrade(), jj_store, cx);
BookmarkPicker::new(delegate, window, cx)
});
}
pub struct BookmarkPicker {
picker: Entity<Picker<BookmarkPickerDelegate>>,
}
impl BookmarkPicker {
pub fn new(
delegate: BookmarkPickerDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl ModalView for BookmarkPicker {}
impl EventEmitter<DismissEvent> for BookmarkPicker {}
impl Focusable for BookmarkPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for BookmarkPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
#[derive(Debug, Clone)]
struct BookmarkEntry {
bookmark: Bookmark,
positions: Vec<usize>,
}
pub struct BookmarkPickerDelegate {
picker: WeakEntity<BookmarkPicker>,
matches: Vec<BookmarkEntry>,
all_bookmarks: Vec<Bookmark>,
selected_index: usize,
}
impl BookmarkPickerDelegate {
fn new(
picker: WeakEntity<BookmarkPicker>,
jj_store: Entity<JujutsuStore>,
cx: &mut Context<BookmarkPicker>,
) -> Self {
let bookmarks = jj_store.read(cx).repository().list_bookmarks();
Self {
picker,
matches: Vec::new(),
all_bookmarks: bookmarks,
selected_index: 0,
}
}
}
impl PickerDelegate for BookmarkPickerDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select Bookmark…".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let all_bookmarks = self.all_bookmarks.clone();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
all_bookmarks
.into_iter()
.map(|bookmark| BookmarkEntry {
bookmark,
positions: Vec::new(),
})
.collect()
} else {
let candidates = all_bookmarks
.iter()
.enumerate()
.map(|(ix, bookmark)| StringMatchCandidate::new(ix, &bookmark.ref_name))
.collect::<Vec<_>>();
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background,
)
.await
.into_iter()
.map(|mat| BookmarkEntry {
bookmark: all_bookmarks[mat.candidate_id].clone(),
positions: mat.positions,
})
.collect()
};
this.update(cx, |this, _cx| {
this.delegate.matches = matches;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {
//
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.picker
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
entry.bookmark.ref_name.clone(),
entry.positions.clone(),
)),
)
}
}

39
crates/jj_ui/src/jj_ui.rs Normal file
View File

@@ -0,0 +1,39 @@
mod bookmark_picker;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use gpui::App;
use jj::JujutsuStore;
use workspace::Workspace;
pub fn init(cx: &mut App) {
JujutsuStore::init_global(cx);
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
bookmark_picker::register(workspace);
})
.detach();
feature_gate_jj_ui_actions(cx);
}
fn feature_gate_jj_ui_actions(cx: &mut App) {
const JJ_ACTION_NAMESPACE: &str = "jj";
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(JJ_ACTION_NAMESPACE);
});
cx.observe_flag::<feature_flags::JjUiFeatureFlag, _>({
move |is_enabled, cx| {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if is_enabled {
filter.show_namespace(JJ_ACTION_NAMESPACE);
} else {
filter.hide_namespace(JJ_ACTION_NAMESPACE);
}
});
}
})
.detach();
}

View File

@@ -144,7 +144,7 @@ struct BufferBranchState {
/// state of a buffer.
pub struct BufferSnapshot {
pub text: text::BufferSnapshot,
pub(crate) syntax: SyntaxSnapshot,
pub syntax: SyntaxSnapshot,
file: Option<Arc<dyn File>>,
diagnostics: SmallVec<[(LanguageServerId, DiagnosticSet); 2]>,
remote_selections: TreeMap<ReplicaId, SelectionSet>,

View File

@@ -80,7 +80,9 @@ pub use language_registry::{
};
pub use lsp::{LanguageServerId, LanguageServerName};
pub use outline::*;
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer, ToTreeSitterPoint, TreeSitterOptions};
pub use syntax_map::{
OwnedSyntaxLayer, SyntaxLayer, SyntaxMapMatch, ToTreeSitterPoint, TreeSitterOptions,
};
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
@@ -755,6 +757,10 @@ pub struct LanguageConfig {
/// A list of preferred debuggers for this language.
#[serde(default)]
pub debuggers: IndexSet<SharedString>,
/// Whether to treat documentation comment of this language differently by
/// auto adding prefix on new line, adjusting the indenting , etc.
#[serde(default)]
pub documentation: Option<DocumentationConfig>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@@ -805,6 +811,19 @@ pub struct JsxTagAutoCloseConfig {
pub erroneous_close_tag_name_node_name: Option<String>,
}
/// The configuration for documentation block for this language.
#[derive(Clone, Deserialize, JsonSchema)]
pub struct DocumentationConfig {
/// A start tag of documentation block.
pub start: Arc<str>,
/// A end tag of documentation block.
pub end: Arc<str>,
/// A character to add as a prefix when a new line is added to a documentation block.
pub prefix: Arc<str>,
/// A indent to add for prefix and end line upon new line.
pub tab_size: NonZeroU32,
}
/// Represents a language for the given range. Some languages (e.g. HTML)
/// interleave several languages together, thus a single buffer might actually contain
/// several nested scopes.
@@ -883,6 +902,7 @@ impl Default for LanguageConfig {
completion_query_characters: Default::default(),
debuggers: Default::default(),
significant_indentation: Default::default(),
documentation: None,
}
}
}
@@ -1802,6 +1822,14 @@ impl LanguageScope {
.unwrap_or(false)
}
/// Returns config to documentation block for this language.
///
/// Used for documentation styles that require a leading character on each line,
/// such as the asterisk in JSDoc, Javadoc, etc.
pub fn documentation(&self) -> Option<&DocumentationConfig> {
self.language.config.documentation.as_ref()
}
/// Returns a list of bracket pairs for a given language with an additional
/// piece of information about whether the particular bracket pair is currently active for a given language.
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
@@ -1833,9 +1861,9 @@ impl LanguageScope {
pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
let config = &self.language.config;
let opt_in_servers = &config.scope_opt_in_language_servers;
if opt_in_servers.iter().any(|o| *o == *name) {
if opt_in_servers.contains(name) {
if let Some(over) = self.config_override() {
over.opt_into_language_servers.iter().any(|o| *o == *name)
over.opt_into_language_servers.contains(name)
} else {
false
}

View File

@@ -4,7 +4,7 @@ use crate::{
};
use collections::BTreeMap;
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
use std::sync::Arc;
use std::{str::FromStr, sync::Arc};
use util::maybe;
pub fn init(cx: &mut App) {
@@ -27,11 +27,36 @@ pub struct LanguageModelRegistry {
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
}
#[derive(Debug)]
pub struct SelectedModel {
pub provider: LanguageModelProviderId,
pub model: LanguageModelId,
}
impl FromStr for SelectedModel {
type Err = String;
/// Parse string identifiers like `provider_id/model_id` into a `SelectedModel`
fn from_str(id: &str) -> Result<SelectedModel, Self::Err> {
let parts: Vec<&str> = id.split('/').collect();
let [provider_id, model_id] = parts.as_slice() else {
return Err(format!(
"Invalid model identifier format: `{}`. Expected `provider_id/model_id`",
id
));
};
if provider_id.is_empty() || model_id.is_empty() {
return Err(format!("Provider and model ids can't be empty: `{}`", id));
}
Ok(SelectedModel {
provider: LanguageModelProviderId(provider_id.to_string().into()),
model: LanguageModelId(model_id.to_string().into()),
})
}
}
#[derive(Clone)]
pub struct ConfiguredModel {
pub provider: Arc<dyn LanguageModelProvider>,

View File

@@ -60,10 +60,10 @@ impl State {
impl CopilotChatLanguageModelProvider {
pub fn new(cx: &mut App) -> Self {
let state = cx.new(|cx| {
let _copilot_chat_subscription = CopilotChat::global(cx)
let copilot_chat_subscription = CopilotChat::global(cx)
.map(|copilot_chat| cx.observe(&copilot_chat, |_, _, cx| cx.notify()));
State {
_copilot_chat_subscription,
_copilot_chat_subscription: copilot_chat_subscription,
_settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
cx.notify();
}),

View File

@@ -12,3 +12,4 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
debuggers = ["CodeLLDB", "GDB"]
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }

View File

@@ -12,3 +12,4 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
debuggers = ["CodeLLDB", "GDB"]
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }

View File

@@ -15,3 +15,4 @@ brackets = [
tab_size = 4
hard_tabs = true
debuggers = ["Delve"]
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }

View File

@@ -20,6 +20,7 @@ tab_size = 2
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
prettier_parser_name = "babel"
debuggers = ["JavaScript"]
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element"

View File

@@ -2,6 +2,7 @@ use anyhow::Context as _;
use gpui::{App, UpdateGlobal};
use json::json_task_context;
use node_runtime::NodeRuntime;
use python::PyprojectTomlManifestProvider;
use rust::CargoManifestProvider;
use rust_embed::RustEmbed;
use settings::SettingsStore;
@@ -302,7 +303,13 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
anyhow::Ok(())
})
.detach();
project::ManifestProviders::global(cx).register(Arc::from(CargoManifestProvider));
let manifest_providers: [Arc<dyn ManifestProvider>; 2] = [
Arc::from(CargoManifestProvider),
Arc::from(PyprojectTomlManifestProvider),
];
for provider in manifest_providers {
project::ManifestProviders::global(cx).register(provider);
}
}
#[derive(Default)]

View File

@@ -4,13 +4,13 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use language::LanguageToolchainStore;
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
@@ -38,6 +38,32 @@ use std::{
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::ResultExt;
pub(crate) struct PyprojectTomlManifestProvider;
impl ManifestProvider for PyprojectTomlManifestProvider {
fn name(&self) -> ManifestName {
SharedString::new_static("pyproject.toml").into()
}
fn search(
&self,
ManifestQuery {
path,
depth,
delegate,
}: ManifestQuery,
) -> Option<Arc<Path>> {
for path in path.ancestors().take(depth) {
let p = path.join("pyproject.toml");
if delegate.exists(&p, Some(false)) {
return Some(path.into());
}
}
None
}
}
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
@@ -301,6 +327,9 @@ impl LspAdapter for PythonLspAdapter {
user_settings
})
}
fn manifest_name(&self) -> Option<ManifestName> {
Some(SharedString::new_static("pyproject.toml").into())
}
}
async fn get_cached_server_binary(
@@ -1142,6 +1171,9 @@ impl LspAdapter for PyLspAdapter {
user_settings
})
}
fn manifest_name(&self) -> Option<ManifestName> {
Some(SharedString::new_static("pyproject.toml").into())
}
}
#[cfg(test)]

View File

@@ -52,6 +52,20 @@
(function_definition
name: (identifier) @function.definition)
((call
function: (identifier) @_isinstance
arguments: (argument_list
(_)
(identifier) @type))
(#eq? @_isinstance "isinstance"))
((call
function: (identifier) @_issubclass
arguments: (argument_list
(identifier) @type
(identifier) @type))
(#eq? @_issubclass "issubclass"))
; Function arguments
(function_definition
parameters: (parameters

View File

@@ -1,3 +1,6 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(function_definition
":" @start
body: (block) @indent

View File

@@ -1,11 +1,52 @@
; Capture decorators for standalone annotation highlighting
(decorator) @annotation
(class_definition
"class" @context
name: (identifier) @name
) @item
; class definitions
[
(module
(class_definition
"class" @context
name: (identifier) @name
) @item)
(function_definition
"async"? @context
"def" @context
name: (_) @name) @item
(block
(class_definition
"class" @context
name: (identifier) @name
) @item)
(decorated_definition
(decorator)+ @context.extra
definition:
(class_definition
"class" @context
name: (identifier) @name
) )@item
]
; function definitions
[
(module
(function_definition
"async"? @context
"def" @context
name: (_) @name
) @item
)
(block
(function_definition
"async"? @context
"def" @context
name: (_) @name
) @item
)
(decorated_definition
(decorator)+ @context.extra
definition: (function_definition
"async"? @context
"def" @context
name: (_) @name )
) @item
]

View File

@@ -686,6 +686,7 @@ impl ContextProvider for RustContextProvider {
RUST_PACKAGE_TASK_VARIABLE.template_value(),
"--".into(),
"--nocapture".into(),
"--include-ignored".into(),
RUST_TEST_NAME_TASK_VARIABLE.template_value(),
],
tags: vec!["rust-test".to_owned()],
@@ -706,6 +707,7 @@ impl ContextProvider for RustContextProvider {
RUST_PACKAGE_TASK_VARIABLE.template_value(),
"--".into(),
"--nocapture".into(),
"--include-ignored".into(),
RUST_DOC_TEST_NAME_TASK_VARIABLE.template_value(),
],
tags: vec!["rust-doc-test".to_owned()],

View File

@@ -16,3 +16,4 @@ brackets = [
]
collapsed_placeholder = " /* ... */ "
debuggers = ["CodeLLDB", "GDB"]
documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 }

View File

@@ -18,6 +18,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-
prettier_parser_name = "typescript"
tab_size = 2
debuggers = ["JavaScript"]
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[jsx_tag_auto_close]
open_tag_node_name = "jsx_opening_element"

View File

@@ -18,6 +18,7 @@ word_characters = ["#", "$"]
prettier_parser_name = "typescript"
tab_size = 2
debuggers = ["JavaScript"]
documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 }
[overrides.string]
completion_query_characters = ["."]

View File

@@ -29,11 +29,11 @@ use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context,
DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy,
SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task,
UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size,
uniform_list,
InteractiveElement, IntoElement, KeyContext, ListAlignment, ListHorizontalSizingBehavior,
ListSizingBehavior, ListState, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
Render, ScrollStrategy, SharedString, Stateful, StatefulInteractiveElement as _, Styled,
Subscription, Task, UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred,
div, list, point, px, size, uniform_list,
};
use itertools::Itertools;
use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
@@ -1620,7 +1620,7 @@ impl OutlinePanel {
.get(&external_file.buffer_id)
.into_iter()
.flat_map(|excerpts| {
excerpts.iter().map(|(excerpt_id, _)| {
excerpts.keys().map(|excerpt_id| {
CollapsedEntry::Excerpt(
external_file.buffer_id,
*excerpt_id,
@@ -1641,7 +1641,7 @@ impl OutlinePanel {
entries.extend(
self.excerpts.get(&file.buffer_id).into_iter().flat_map(
|excerpts| {
excerpts.iter().map(|(excerpt_id, _)| {
excerpts.keys().map(|excerpt_id| {
CollapsedEntry::Excerpt(file.buffer_id, *excerpt_id)
})
},
@@ -2302,7 +2302,7 @@ impl OutlinePanel {
depth,
Some(icon),
is_active,
label_element,
label_element.into_any_element(),
window,
cx,
)
@@ -2511,7 +2511,7 @@ impl OutlinePanel {
.when_some(icon_element, |list_item, icon_element| {
list_item.child(h_flex().child(icon_element))
})
.child(h_flex().h_6().child(label_element).ml_1())
.child(h_flex().child(label_element).ml_1())
.on_secondary_mouse_down(cx.listener(
move |outline_panel, event: &MouseDownEvent, window, cx| {
// Stop propagation to prevent the catch-all context menu for the project
@@ -3259,13 +3259,13 @@ impl OutlinePanel {
cx.spawn_in(window, async move |outline_panel, cx| {
let fetched_outlines = cx
.background_spawn(async move {
buffer_snapshot
.outline_items_containing(
excerpt_range.context,
false,
Some(&syntax_theme),
)
.unwrap_or_default()
outline_items_containing(
&buffer_snapshot,
excerpt_range.context,
true,
Some(&syntax_theme),
)
.unwrap_or_default()
})
.await;
outline_panel
@@ -4098,49 +4098,50 @@ impl OutlinePanel {
query: Option<&str>,
cx: &mut Context<Self>,
) {
if let Some(excerpts) = self.excerpts.get(&buffer_id) {
for &excerpt_id in entries_to_add {
let Some(excerpt) = excerpts.get(&excerpt_id) else {
continue;
};
let excerpt_depth = parent_depth + 1;
let Some(excerpts) = self.excerpts.get(&buffer_id) else {
return;
};
for &excerpt_id in entries_to_add {
let Some(excerpt) = excerpts.get(&excerpt_id) else {
continue;
};
let excerpt_depth = parent_depth + 1;
self.push_entry(
state,
track_matches,
PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
buffer_id,
id: excerpt_id,
range: excerpt.range.clone(),
})),
excerpt_depth,
cx,
);
let mut outline_base_depth = excerpt_depth + 1;
if is_singleton {
outline_base_depth = 0;
state.clear();
} else if query.is_none()
&& self
.collapsed_entries
.contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
{
continue;
}
for outline in excerpt.iter_outlines() {
self.push_entry(
state,
track_matches,
PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
buffer_id,
id: excerpt_id,
range: excerpt.range.clone(),
excerpt_id,
outline: outline.clone(),
})),
excerpt_depth,
outline_base_depth + outline.depth,
cx,
);
let mut outline_base_depth = excerpt_depth + 1;
if is_singleton {
outline_base_depth = 0;
state.clear();
} else if query.is_none()
&& self
.collapsed_entries
.contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
{
continue;
}
for outline in excerpt.iter_outlines() {
self.push_entry(
state,
track_matches,
PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
buffer_id,
excerpt_id,
outline: outline.clone(),
})),
outline_base_depth + outline.depth,
cx,
);
}
}
}
}
@@ -4537,121 +4538,130 @@ impl OutlinePanel {
let multi_buffer_snapshot = self
.active_editor()
.map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
uniform_list(cx.entity().clone(), "entries", items_len, {
move |outline_panel, range, window, cx| {
let entries = outline_panel.cached_entries.get(range);
entries
.map(|entries| entries.to_vec())
.unwrap_or_default()
.into_iter()
.filter_map(|cached_entry| match cached_entry.entry {
PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
&entry,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
)),
PanelEntry::FoldedDirs(folded_dirs_entry) => {
Some(outline_panel.render_folded_dirs(
&folded_dirs_entry,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
))
}
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
outline_panel.render_excerpt(
&excerpt,
cached_entry.depth,
window,
cx,
)
}
PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
Some(outline_panel.render_outline(
let outline_panel = cx.entity();
let render_item = {
move |i: usize, window: &mut Window, cx: &mut App| {
outline_panel.update(cx, |outline_panel, cx| {
let entry = outline_panel.cached_entries.get(i).cloned();
let item = entry
.and_then(|cached_entry| match cached_entry.entry {
PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
&entry,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
))
}
PanelEntry::Search(SearchEntry {
match_range,
render_data,
kind,
..
}) => outline_panel.render_search_match(
multi_buffer_snapshot.as_ref(),
&match_range,
&render_data,
kind,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
),
})
.collect()
)),
PanelEntry::FoldedDirs(folded_dirs_entry) => {
Some(outline_panel.render_folded_dirs(
&folded_dirs_entry,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
))
}
PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
outline_panel.render_excerpt(
&excerpt,
cached_entry.depth,
window,
cx,
)
}
PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
Some(outline_panel.render_outline(
&entry,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
))
}
PanelEntry::Search(SearchEntry {
match_range,
render_data,
kind,
..
}) => outline_panel.render_search_match(
multi_buffer_snapshot.as_ref(),
&match_range,
&render_data,
kind,
cached_entry.depth,
cached_entry.string_match.as_ref(),
window,
cx,
),
})
.map(|div| div.into_any_element())
.unwrap_or_else(|| div().into_any_element());
item
})
}
})
};
list(ListState::new(
items_len,
ListAlignment::Top,
px(50.0),
render_item,
))
.with_sizing_behavior(ListSizingBehavior::Infer)
.with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
.with_width_from_item(self.max_width_item_index)
.track_scroll(self.scroll_handle.clone())
.when(show_indent_guides, |list| {
list.with_decoration(
ui::indent_guides(
cx.entity().clone(),
px(indent_size),
IndentGuideColors::panel(cx),
|outline_panel, range, _, _| {
let entries = outline_panel.cached_entries.get(range);
if let Some(entries) = entries {
entries.into_iter().map(|item| item.depth).collect()
} else {
smallvec::SmallVec::new()
}
},
)
.with_render_fn(
cx.entity().clone(),
move |outline_panel, params, _, _| {
const LEFT_OFFSET: Pixels = px(14.);
// .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
// .with_width_from_item(self.max_width_item_index)
// .track_scroll(self.scroll_handle.clone())
// .when(show_indent_guides, |list| {
// list.with_decoration(
// ui::indent_guides(
// cx.entity().clone(),
// px(indent_size),
// IndentGuideColors::panel(cx),
// |outline_panel, range, _, _| {
// let entries = outline_panel.cached_entries.get(range);
// if let Some(entries) = entries {
// entries.into_iter().map(|item| item.depth).collect()
// } else {
// smallvec::SmallVec::new()
// }
// },
// )
// .with_render_fn(
// cx.entity().clone(),
// move |outline_panel, params, _, _| {
// const LEFT_OFFSET: Pixels = px(14.);
let indent_size = params.indent_size;
let item_height = params.item_height;
let active_indent_guide_ix = find_active_indent_guide_ix(
outline_panel,
&params.indent_guides,
);
// let indent_size = params.indent_size;
// let item_height = params.item_height;
// let active_indent_guide_ix = find_active_indent_guide_ix(
// outline_panel,
// &params.indent_guides,
// );
params
.indent_guides
.into_iter()
.enumerate()
.map(|(ix, layout)| {
let bounds = Bounds::new(
point(
layout.offset.x * indent_size + LEFT_OFFSET,
layout.offset.y * item_height,
),
size(px(1.), layout.length * item_height),
);
ui::RenderedIndentGuide {
bounds,
layout,
is_active: active_indent_guide_ix == Some(ix),
hitbox: None,
}
})
.collect()
},
),
)
})
// params
// .indent_guides
// .into_iter()
// .enumerate()
// .map(|(ix, layout)| {
// let bounds = Bounds::new(
// point(
// layout.offset.x * indent_size + LEFT_OFFSET,
// layout.offset.y * item_height,
// ),
// size(px(1.), layout.length * item_height),
// );
// ui::RenderedIndentGuide {
// bounds,
// layout,
// is_active: active_indent_guide_ix == Some(ix),
// hitbox: None,
// }
// })
// .collect()
// },
// ),
// )
// })
};
v_flex()
@@ -4795,6 +4805,248 @@ fn file_name(path: &Path) -> String {
}
}
fn outline_items_containing<T: language::ToOffset>(
buffer_snapshot: &BufferSnapshot,
range: Range<T>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<language::Anchor>>> {
fn next_outline_item_linewise(
buffer_snapshot: &BufferSnapshot,
config: &language::OutlineConfig,
mat: &language::SyntaxMapMatch,
range: &Range<usize>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
) -> Option<OutlineItem<language::Point>> {
use language::ToTreeSitterPoint;
let item_node = mat.captures.iter().find_map(|cap| {
if cap.index == config.item_capture_ix {
Some(cap.node)
} else {
None
}
})?;
let item_byte_range = item_node.byte_range();
if item_byte_range.end < range.start || item_byte_range.start > range.end {
return None;
}
let beg = language::Point::from_ts_point(item_node.start_position());
let end = language::Point::from_ts_point(item_node.end_position());
let item_point_range = beg..end;
let mut open_point = None;
let mut close_point = None;
let mut buffer_ranges = Vec::new();
let mut prev_row = None;
for capture in mat.captures {
let node_is_name;
if capture.index == config.name_capture_ix {
node_is_name = true;
} 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 {
if Some(capture.index) == config.open_capture_ix {
open_point = Some(language::Point::from_ts_point(capture.node.end_position()));
} else if Some(capture.index) == config.close_capture_ix {
close_point = Some(language::Point::from_ts_point(
capture.node.start_position(),
));
}
continue;
}
let mut range = capture.node.start_byte()..capture.node.end_byte();
let start = capture.node.start_position();
if capture.node.end_position().row > start.row {
range.end = range.start + buffer_snapshot.line_len(start.row as u32) as usize
- start.column;
}
let is_new_line = prev_row.is_some_and(|prev| prev != start.row);
prev_row.replace(capture.node.end_position().row);
if !range.is_empty() {
buffer_ranges.push((range, node_is_name, is_new_line));
}
}
if buffer_ranges.is_empty() {
return None;
}
let mut text = String::new();
let mut highlight_ranges = Vec::new();
let mut name_ranges = Vec::new();
let mut chunks = buffer_snapshot.chunks(
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, is_new_line) in buffer_ranges {
let space_added = if !is_new_line {
let space_added = !text.is_empty() && buffer_range.start > last_buffer_range_end;
if space_added {
text.push(' ');
}
space_added
} else {
text.push('\n');
true
};
let before_append_len = text.len();
let mut offset = buffer_range.start;
chunks.seek(buffer_range.clone());
for mut chunk in chunks.by_ref() {
if chunk.text.len() > buffer_range.end - offset {
chunk.text = &chunk.text[0..(buffer_range.end - offset)];
offset = buffer_range.end;
} else {
offset += chunk.text.len();
}
let style = chunk
.syntax_highlight_id
.zip(theme)
.and_then(|(highlight, theme)| highlight.style(theme));
if let Some(style) = style {
let start = text.len();
let end = start + chunk.text.len();
highlight_ranges.push((start..end, style));
}
text.push_str(chunk.text);
if offset >= buffer_range.end {
break;
}
}
if is_name {
let after_append_len = text.len();
let start = if space_added && !name_ranges.is_empty() {
before_append_len - 1
} else {
before_append_len
};
name_ranges.push(start..after_append_len);
}
last_buffer_range_end = buffer_range.end;
}
Some(OutlineItem {
depth: 0, // We'll calculate the depth later
range: item_point_range,
text,
highlight_ranges,
name_ranges,
body_range: open_point.zip(close_point).map(|(start, end)| start..end),
annotation_range: None,
})
}
let range = range.to_offset(buffer_snapshot);
let mut matches =
buffer_snapshot
.syntax
.matches(range.clone(), &buffer_snapshot.text, |grammar| {
grammar.outline_config.as_ref().map(|c| &c.query)
});
let configs = matches
.grammars()
.iter()
.map(|g| g.outline_config.as_ref().unwrap())
.collect::<Vec<_>>();
let mut items = Vec::new();
let mut annotation_row_ranges: Vec<Range<u32>> = Vec::new();
while let Some(mat) = matches.peek() {
let config = &configs[mat.grammar_index];
if let Some(item) = next_outline_item_linewise(
&buffer_snapshot,
config,
&mat,
&range,
include_extra_context,
theme,
) {
items.push(item);
} else if let Some(capture) = mat
.captures
.iter()
.find(|capture| Some(capture.index) == config.annotation_capture_ix)
{
let capture_range = capture.node.start_position()..capture.node.end_position();
let mut capture_row_range =
capture_range.start.row as u32..capture_range.end.row as u32;
if capture_range.end.row > capture_range.start.row && capture_range.end.column == 0 {
capture_row_range.end -= 1;
}
if let Some(last_row_range) = annotation_row_ranges.last_mut() {
if last_row_range.end >= capture_row_range.start.saturating_sub(1) {
last_row_range.end = capture_row_range.end;
} else {
annotation_row_ranges.push(capture_row_range);
}
} else {
annotation_row_ranges.push(capture_row_range);
}
}
matches.advance();
}
items.sort_by_key(|item| (item.range.start, std::cmp::Reverse(item.range.end)));
// Assign depths based on containment relationships and convert to anchors.
let mut item_ends_stack = Vec::<language::Point>::new();
let mut anchor_items = Vec::new();
let mut annotation_row_ranges = annotation_row_ranges.into_iter().peekable();
for item in items {
while let Some(last_end) = item_ends_stack.last().copied() {
if last_end < item.range.end {
item_ends_stack.pop();
} else {
break;
}
}
let mut annotation_row_range = None;
while let Some(next_annotation_row_range) = annotation_row_ranges.peek() {
let row_preceding_item = item.range.start.row.saturating_sub(1);
if next_annotation_row_range.end < row_preceding_item {
annotation_row_ranges.next();
} else {
if next_annotation_row_range.end == row_preceding_item {
annotation_row_range = Some(next_annotation_row_range.clone());
annotation_row_ranges.next();
}
break;
}
}
anchor_items.push(OutlineItem {
depth: item_ends_stack.len(),
range: buffer_snapshot.anchor_after(item.range.start)
..buffer_snapshot.anchor_before(item.range.end),
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
body_range: item.body_range.map(|body_range| {
buffer_snapshot.anchor_after(body_range.start)
..buffer_snapshot.anchor_before(body_range.end)
}),
annotation_range: annotation_row_range.map(|annotation_range| {
buffer_snapshot.anchor_after(language::Point::new(annotation_range.start, 0))
..buffer_snapshot.anchor_before(language::Point::new(
annotation_range.end,
buffer_snapshot.line_len(annotation_range.end),
))
}),
});
item_ends_stack.push(item.range.end);
}
Some(anchor_items)
}
impl Panel for OutlinePanel {
fn persistent_name() -> &'static str {
"Outline Panel"
@@ -4991,7 +5243,7 @@ impl Render for OutlinePanel {
.border_color(cx.theme().colors().border)
.gap_0p5()
.child(Label::new("Searching:").color(Color::Muted))
.child(Label::new(search_state.query.to_string())),
.child(Label::new(format!("'{}'", search_state.query))),
)
})
.child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))

View File

@@ -417,6 +417,14 @@ pub fn local_vscode_launch_file_relative_path() -> &'static Path {
Path::new(".vscode/launch.json")
}
pub fn user_ssh_config_file() -> PathBuf {
home_dir().join(".ssh/config")
}
pub fn global_ssh_config_file() -> &'static Path {
Path::new("/etc/ssh/ssh_config")
}
/// Returns the path to the vscode user settings file
pub fn vscode_settings_file() -> &'static PathBuf {
static LOGS_DIR: OnceLock<PathBuf> = OnceLock::new();

View File

@@ -64,7 +64,6 @@ pub enum DapStoreEvent {
RemoteHasInitialized,
}
#[allow(clippy::large_enum_variant)]
enum DapStoreMode {
Local(LocalDapStore),
Ssh(SshDapStore),

View File

@@ -163,7 +163,7 @@ struct LocalDownstreamState {
_task: Task<Result<()>>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct GitStoreCheckpoint {
checkpoints_by_work_dir_abs_path: HashMap<Arc<Path>, GitRepositoryCheckpoint>,
}

View File

@@ -3415,7 +3415,6 @@ pub struct RemoteLspStore {
upstream_project_id: u64,
}
#[allow(clippy::large_enum_variant)]
pub(crate) enum LspStoreMode {
Local(LocalLspStore), // ssh host and collab host
Remote(RemoteLspStore), // collab guest
@@ -8806,9 +8805,10 @@ impl LspStore {
})
});
let is_unnecessary = diagnostic.tags.as_ref().map_or(false, |tags| {
tags.iter().any(|tag| *tag == DiagnosticTag::UNNECESSARY)
});
let is_unnecessary = diagnostic
.tags
.as_ref()
.map_or(false, |tags| tags.contains(&DiagnosticTag::UNNECESSARY));
if is_supporting {
supporting_diagnostics.insert(

View File

@@ -98,7 +98,7 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
};
}
if !current.labels.is_empty() {
(callback)(&current.worktree_relative_path, &current.labels);
let _ = (callback)(&current.worktree_relative_path, &current.labels);
}
}

View File

@@ -3653,7 +3653,7 @@ impl Project {
let mut buffer_count = 0;
let mut limit_reached = false;
let query = Arc::new(query);
let mut chunks = matching_buffers_rx.ready_chunks(64);
let chunks = matching_buffers_rx.ready_chunks(64);
// Now that we know what paths match the query, we will load at most
// 64 buffers at a time to avoid overwhelming the main thread. For each
@@ -4392,7 +4392,7 @@ impl Project {
envelope: TypedEnvelope<proto::LanguageServerPromptRequest>,
mut cx: AsyncApp,
) -> Result<proto::LanguageServerPromptResponse> {
let (tx, mut rx) = smol::channel::bounded(1);
let (tx, rx) = smol::channel::bounded(1);
let actions: Vec<_> = envelope
.payload
.actions

View File

@@ -21,7 +21,7 @@ use crate::{
worktree_store::WorktreeStore,
};
#[allow(clippy::large_enum_variant)] // platform-dependent warning
// platform-dependent warning
pub enum TaskStore {
Functional(StoreState),
Noop,

View File

@@ -24,7 +24,7 @@ pub struct Terminals {
}
/// Terminals are opened either for the users shell, or to run a task.
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum TerminalKind {
/// Run a shell at the given path (or $HOME if None)

View File

@@ -876,7 +876,7 @@ impl WorktreeStore {
async fn filter_paths(
fs: &Arc<dyn Fs>,
mut input: Receiver<MatchingEntry>,
input: Receiver<MatchingEntry>,
query: &SearchQuery,
) -> Result<()> {
let mut input = pin!(input);

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