Compare commits

..

22 commits

Author SHA1 Message Date
Lukas Wirth
0bfd2d7cf0
language_core: Remove lsp-types dependency (#61041)
Some checks are pending
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-15 10:46:01 +00:00
Smit Barmase
3565c49dad
terminal_view: Fix cursor column for non-ASCII lines when opening path-like targets (#61036)
Found this while looking into a markdown preview issue. Clicking a
`path:row:column` target in the terminal treated the column as a byte
offset, so on lines with non-ASCII characters the cursor landed at mid
codepoint.

The real fix is just 3 lines and the rest is a refactor that extracts
the open and navigate logic so it can be reused later for cases like
markdown preview.

Release Notes:

- Fixed the cursor landing at the wrong column when clicking a
`path:row:column` link in the terminal for lines containing non-ASCII
characters.
2026-07-15 10:05:32 +00:00
Smit Barmase
9eb5fa98f1
settings_ui: Move deleted skills to trash instead of permanently deleting (#61031)
Deleting a skill from the settings UI permanently removed its directory
from `~/.agents/skills`, which is shared with other tools that read the
same unified skills directory. Now we move it to the system trash
instead so it's recoverable.

Release Notes:

- Deleting a skill now moves it to the system trash instead of
permanently deleting it.
2026-07-15 09:04:28 +00:00
Remco Smits
fa2c3794d0
agent_panel: Fix that the awaiting confirmation content does cover the hole agent panel (#60992)
# Objective

Currently the sticky `awaiting confirmation` content can grow as big as
the agent panel can go as you can see inside the before picture, that
shows that the full agent panel is coverd by the cofirmation content.
Which is not something we want, because there is no way to see the
actual thread anymore.

**Note**: There was already a fix for this according to this comment but
I could not end up inside that case where it would limit the content.
```
// Cap the content (e.g. a full plan awaiting
// approval) so the floating row can never
// consume the entire panel and squeeze the
// conversation list to zero height, while the
// permission buttons below stay visible.
```

## Solution

The fix is to actually limit the content to a max height of
160px(10rem), which is the value that it was before, but that didn't
work. So I made sure the tool output is now scrollable and it's height
is limited to the specified limit described earlier.

## Testing

You can ask if the agent wants to executes this command, that is just a
very long non existing url. Note, you need to have not auto accept of
permissions enabled to test this bug.

```
curl -sS -v --max-time 15 "https://this-domain-almost-certainly-does-not-exist-abcdefghijklmnopqrstuvwxyz-0123456789-super-duper-extra-long-subdomain-that-nobody-would-ever-register.another-ridiculously-long-and-completely-fake-nonexistent-host-name-9876543210.invalid/api/v1/resources/collections/items/subitems/segment01/segment02/segment03/segment04/segment05/segment06/segment07/segment08/segment09/segment10/segment11/segment12/segment13/segment14/segment15/segment16/segment17/segment18/segment19/segment20/segment21/segment22/segment23/segment24/segment25/segment26/segment27/segment28/segment29/segment30/segment31/segment32/segment33/segment34/segment35/segment36/segment37/segment38/segment39/segment40/segment41/segment42/segment43/segment44/segment45/segment46/segment47/segment48/segment49/segment50/segment51/segment52/segment53/segment54/segment55/segment56/segment57/segment58/segment59/segment60/segment61/segment62/segment63/segment64/segment65/segment66/segment67/segment68/segment69/segment70/segment71/segment72/segment73/segment74/segment75/segment76/segment77/segment78/segment79/segment80/segment81/segment82/segment83/segment84/segment85/segment86/segment87/segment88/segment89/segment90/segment91/segment92/segment93/segment94/segment95/segment96/segment97/segment98/segment99/segment100?param01=value01&param02=value02&param03=value03&param04=value04&param05=value05&param06=value06&param07=value07&param08=value08&param09=value09&param10=value10&param11=value11&param12=value12&param13=value13&param14=value14&param15=value15&param16=value16&param17=value17&param18=value18&param19=value19&param20=value20&param21=value21&param22=value22&param23=value23&param24=value24&param25=value25&param26=value26&param27=value27&param28=value28&param29=value29&param30=value30&param31=value31&param32=value32&param33=value33&param34=value34&param35=value35&param36=value36&param37=value37&param38=value38&param39=value39&param40=value40&param41=value41&param42=value42&param43=value43&param44=value44&param45=value45&param46=value46&param47=value47&param48=value48&param49=value49&param50=value50&the=end"
```

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

**Before** (**Note** the panel is fully coverd by the confirmation
content)

<img width="528" height="817" alt="Screenshot 2026-07-14 at 19 17 53"
src="https://github.com/user-attachments/assets/db0a6b87-2564-4831-9a26-384896ccaefe"
/>


**After** (**Note** the confirmation text is now scrollable again and it
does not cover the full panel anymore)


https://github.com/user-attachments/assets/6c391509-755b-4727-b473-58c21603c4ec

---

Release Notes:

- Agent Panel: Fixed that the sticky `awaiting confirmation` content
could take up the hole agent panel
2026-07-15 08:47:22 +00:00
Xin Zhao
c7b43b20b4
project: Persist workspace diagnostics when closing buffer (#59875)
# Objective

Closes #50739

Many language servers have settings for their diagnostic mode, typically
supporting either "single file" mode or "workspace" mode.

In single file mode, the language server only generates diagnostics for
files that the user has open. When a file is closed, those diagnostics
are cleared. This is the behavior currently implemented in Zed.

However, when a language server runs in workspace mode, we should retain
all diagnostics regardless of whether the corresponding file is open or
closed. As reported in #50739, Zed currently discards these diagnostics
when a buffer is closed, just like in single file mode.

When a buffer is completely closed, Zed triggers a diagnostics update to
perform cleanup:

8372eb1b13/crates/project/src/lsp_store.rs (L4747-L4773)
and the Pulled diagnostics are all cleaned, that's why when closing a
buffer, the diagnostic summary disappeared. This process aggressively
clears all `Pulled` diagnostics, causing the diagnostic summary for the
closed file to disappear.

## Solution

Filter the target language servers when updating diagnostics on buffer
close. If a language server has workspace diagnostics enabled, we skip
clearing its diagnostics.

## Testing

Added two new integration tests in
`crates/project/tests/integration/project_tests.rs`:
1.
`test_diagnostic_summaries_cleared_on_buffer_close_without_workspace_diagnostics`:
Verifies that diagnostics are cleared on buffer close when workspace
diagnostics is disabled.
2.
`test_diagnostic_summaries_retained_on_buffer_close_with_workspace_diagnostics`:
Verifies that diagnostics are retained when workspace diagnostics are
enabled. (This test fails on the current `main` branch).

I have also verified this functionality locally. Both local and remote
development environments work as expected.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

<details>
  <summary>Click to view showcase</summary>

My super cool demos here

</details>

---

Release Notes:

- Fixed workspace diagnostics being cleared when closing a buffer.
2026-07-15 08:43:47 +00:00
Xin Zhao
c28e37687d
languages: Fix Pyright ignoring toolchain when LSP settings are configured (#59990)
# Objective

Closes #55936
Helps #57288

There is a strange behavior for Python users when using basedpyright or
pyright as the language server. As reported in #55936, when no settings
are configured in `lsp.basedpyright.settings`, the language server
correctly receives toolchain information discovered by Zed:
```json
{
  "venvPath": ".",
  "venv": ".venv",
  "python": {
    "pythonPath": "/home/xin/works/test/test_python/.venv/bin/python",
    "defaultInterpreterPath": "/home/xin/works/test/test_python/.venv/bin/python"
  },
  "basedpyright.analysis": {
    "typeCheckingMode": "standard"
  },
  "basedpyright.disableOrganizeImports": true
}
```
However, when the user configures any other settings in
`lsp.basedpyright.settings`, the toolchain information is silently lost.
For example, with the following `settings.json`:
```jsonc
{
  "lsp": {
    "basedpyright": {
      "settings": {
        "analysis": {
          "diagnosticMode": "workspace",
          "typeCheckingMode": "standard",
          "inlayHints.callArgumentNames": true,
          "inlayHints.variableTypes": true,
        },
      },
    }
  }
}
```
The final configuration sent to the language server becomes:
```json
{
  "analysis": {
    "diagnosticMode": "workspace",
    "typeCheckingMode": "standard",
    "inlayHints.callArgumentNames": true,
    "inlayHints.variableTypes": true
  }
}
```
No toolchain information is passed, so the language server ignores the
user's selected Python environment and falls back to whatever virtual
environment it discovers on its own. To make toolchain selection work,
users currently have to manually configure virtual environment settings
themselves (as noted in
https://github.com/zed-industries/zed/issues/57288#issuecomment-4750935341).


This issue was introduced in #48262, which aimed to make the virtual
environment used by basedpyright/pyright configurable. The core logic is
the `should_insert_toolchain` closure, originally designed to check
whether Zed should inject the detected toolchain into the language
server configuration. The intent was: if the user has already configured
a virtual environment in the language server settings, skip the
injection.


2c346f60a7/crates/languages/src/python.rs (L2115-L2128)

However, the original logic was inverted — it injected the toolchain
when the user had no settings at all or when they had configured virtual
environment keys. This meant that when the user had LSP settings but no
virtual environment configuration, toolchain injection was skipped, as
reported in #55936.


## Solution

Change the logic defined in `should_insert_toolchain` closure, instead
of insert toolchain when detected virtual environment settings, now we
insert toolchain when no virtual environment settings is detected.

## Testing

Tested locally. The logic change is simple, should be easy for review.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed toolchain selection being ignored when Pyright or Basedpyright
settings are configured without specifying a Python interpreter path
2026-07-15 08:39:19 +00:00
Lukas Wirth
f181a2f47b
Split out RelPath into a separate crate (#61029)
This is necessary to remove some `util` dependencies from crates, as
well as better sharing for our projects. This also includes the WIP
AbsPath abstraction as well as some bug fixes from internal tooling.


Release Notes:

- N/A or Added/Fixed/Improved ...
2026-07-15 08:33:25 +00:00
Xin Zhao
febe242c0b
workspace: Fix toolchain not recovered correctly in remote development (#60030)
# Objective

Closes #55925
Closes #59094

When a Python project is opened and a toolchain is selected, Zed
stores/persists this choice in the database. When the project is
reopened later, that saved toolchain should be automatically selected.
However, this recovery mechanism requires the associated worktree to be
loaded first.

In local development, the worktrees are loaded first, after which the
toolchain is restored and activated:

afaef857b9/crates/workspace/src/workspace.rs (L1916-L1955)

But in remote development, the toolchain restoration block runs at the
very beginning of initialization, before any worktrees are loaded:

afaef857b9/crates/workspace/src/workspace.rs (L10381-L10413)

Because the worktree does not exist yet when the toolchain is processed,
`find_worktree` always returns `None`, and the saved toolchain is never
activated. This breaks toolchain persistence for remote workspaces.

## Solution

Move the toolchain activation block in `open_remote_project_inner` so it
runs after the worktrees have been loaded, matching the sequence used in
`new_local`. This ensures the worktree is present in the project when we
perform the ID lookup and activate the toolchain.

## Testing

Tested locally with a Windows client connecting to a remote Linux
server, verifying that the previously selected virtual environment is
correctly restored upon reopening the workspace.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed Python virtual environments not automatically restoring when
reopening a remote development workspace.

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-15 08:28:00 +00:00
Remco Smits
b76e4db15d
git: Add a gpg wrapper script to route GnuPG prompts via the askpass UI (#58791)
This PR adds support for entering your git GPG passphrase through the
askpass UI. As a daily Zed user I'm really missing this feature, this
forces me to switch to a terminal or an external application. which is
not ideal for me and most people that are forced to use GPG signing.

Right now Zed can show a similar error (redacted) when you are trying to
commit with GPG signing enabled:
```
error: gpg failed to sign the data:
[GNUPG:] KEY_CONSIDERED <redacted> 2
[GNUPG:] BEGIN_SIGNING H8
[GNUPG:] PINENTRY_LAUNCHED 4625 curses 1.3.2 - xterm-ghostty - - 501/20 0
gpg: signing failed: Inappropriate ioctl for device
[GNUPG:] FAILURE sign <redacted>
gpg: signing failed: Inappropriate ioctl for device

fatal: failed to write commit object
```

Here is the key configurion parts of my **global** git config:

```
[user]
	name = Remco Smits
	email = <redacted>
	signingkey = <redacted>
[commit]
	gpgsign = true
[tag]
	gpgsign = true
```

**Before**


https://github.com/user-attachments/assets/60a06574-92f1-45df-a29a-8ae11db6751d

**After**


https://github.com/user-attachments/assets/92b648e9-b538-4ce5-af4c-136689d14e1a


**How to test this?**
1. Create a key `gpg --full-generate-key`
2. Run `git config --global user.signingkey <KEY ID>`
3. Run `git config --global commit.gpgsign true`
4. Run & copy result from `gpg --armor --export <KEY ID>` and submit
your public key to github
[https://github.com/settings/gpg/new](https://github.com/settings/gpg/new)
5. Make a change and try committing
6. See that it promts for your passphrase :)

**Note**: I used AI as an assistant to write this PR

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Git: Passphrase prompts from GPG to unlock commit signing keys are now
shown in Zed.

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2026-07-15 08:27:31 +00:00
Philipp Schaffrath
166f044fd0
gpui: Linux wayland set exclusive zone and edge (#60163)
# Objective

The exclusive zone and exclusive edge of a wlr-layer-shell surface could
only be set once, at creation, through `LayerShellOptions`. This adds
runtime setters so a live layer-shell window such as a panel can update
them without being recreated.

## Solution

Add two methods on `Window`:

- `set_exclusive_zone` updates how much screen space the surface
reserves. A positive value reserves that distance from the anchored
edge, 0 lets the surface be moved out of others' exclusive zones, and -1
ignores reserved space and may extend under other surfaces.
- `set_exclusive_edge` chooses which anchored edge the exclusive zone
applies to, which is needed to disambiguate a corner-anchored surface.

Setting an exclusive edge the surface is not anchored to is a fatal
protocol error, so the edge is validated (it must be a single edge that
the surface anchor contains) and an invalid edge is logged and ignored.
The same validation now also guards the creation path. Both setters only
commit the surface when a change actually applies, and are no-ops on
non-layer-shell windows and on other platforms.

This is where these 2 are documented:

https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_surface_v1:request:set_exclusive_zone

https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_surface_v1:request:set_exclusive_edge

## Testing

- Verified with a small local layer-shell window (a top bar anchored
TOP, LEFT, RIGHT) with buttons that call the setters at runtime
- No automated test was added, since this calls through to the
compositor.
- Reviewers without a layer-shell capable compositor (for example
GNOME/Mutter) cannot exercise this, as `zwlr_layer_shell_v1` is
unavailable there, I tested this on the
[Smithay](https://github.com/Smithay/smithay/) based compositor
[niri](https://github.com/niri-wm/niri)

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- N/A
2026-07-15 08:18:46 +00:00
Kirill Bulatov
88ce2d526e
Show download percentage in download button's tooltip (#60990)
Closes https://github.com/zed-industries/zed/discussions/60944

<img width="568" height="143" alt="downloads"
src="https://github.com/user-attachments/assets/e5643106-a0c9-496c-bff2-300faad38839"
/>



Release Notes:

- Started to show download percentage in download button's tooltip
2026-07-15 07:39:33 +00:00
Ben Kunkle
4a3e0af532
Add a bespoke LSP request path for edit prediction context (#60947)
Some checks are pending
Congratsbot / congrats (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Closes EP-193

Edit prediction context collection previously reused the editor
goto-definition / goto-type-definition path, which resolves every LSP
result into a `LocationLink` — opening a buffer per target (worktree
creation, buffer registration, anchor conversion) before edit prediction
gets a chance to filter. On remote projects the host additionally
created a peer-visible buffer per link and the client waited on each
one.

This adds a bespoke request path that returns raw results first and only
does the expensive work for results that survive filtering:

- New `EditPredictionDefinition { path: ProjectPath, range:
Range<Unclipped<PointUtf16>> }` boundary type. LSP wire types never
leave `lsp_command.rs`; the workspace-only filter and URI→`ProjectPath`
resolution happen while normalizing the LSP response, before any buffer
exists, so the type encodes the workspace-only invariant.
- New `GetEditPredictionDefinitions` /
`GetEditPredictionTypeDefinitions` LSP commands and proto messages.
Responses carry only path + UTF-16 range — the host never calls
`create_buffer_for_peer` and the client never waits on remote buffers.
- One public API: `Project::edit_prediction_definitions(buffer,
position, include_type_definitions, cx)` fires both LSP requests
concurrently and returns a merged, deduped list ("not capable" = empty).
The definition/type-definition split was never used downstream, so
`CacheEntry` now holds a single list and the fetch pipeline runs one
task per identifier.
- `edit_prediction_context` dedupes raw results before opening buffers;
survivors open via `project.open_buffer(ProjectPath)` (skipping the
invisible-worktree/yarn machinery of `open_local_buffer_via_lsp`, and
working identically on remote projects), then clip → anchor →
`MAX_TARGET_LEN`.
- Removes the `workspace_only` flag from `GetDefinitions` /
`GetTypeDefinitions`: edit prediction was its only user. The editor path
always sent `false`, which proto3 doesn't encode, so normal
goto-definition requests are wire-identical; old peers still sending
`true` get unfiltered results (graceful degradation). The proto field
numbers are `reserved`.

Tested with a local test proving filtering precedes buffer opening (an
out-of-workspace target never spawns a worktree, an oversized target is
excluded from related files) and a collab test proving the remote
round-trip returns correct paths/ranges without opening buffers on the
client.

Release Notes:

- N/A
2026-07-15 01:54:27 +00:00
Richard Feldman
6b9f448ffc
crashes: Capture glibc's abort diagnostic in crash reports (#60624)
Linux crash reports for runtime-initiated aborts currently carry no
information about why the runtime aborted. A sizable family of Sentry
issues (FR-108 / [ZED-9SC](https://zed-dev.sentry.io/issues/7581925141/)
among them) consists of glibc's malloc integrity checks detecting heap
corruption and aborting from inside `malloc`/`realloc`/`free`, so each
event lands on a random victim stack and the underlying problem stays
invisible and ungroupable.

glibc records the diagnostic it prints before aborting ("free(): invalid
pointer", "double free or corruption", assertion failures,
stack-smashing reports) in the private `__abort_msg` global specifically
so it can be recovered post-mortem. This resolves that symbol's address
at startup and sends it to the crash-handler process; when a crash comes
in, the handler reads the message out of the crashed process's memory
with `process_vm_readv` while the client is still parked in its signal
handler awaiting the dump acknowledgement. The message is then attached
to crash uploads as a searchable `abort_message` tag plus a full-text
context. The crashed process does no work itself, which matters because
in exactly these crashes the crashed thread may hold the allocator's
arena lock, so it cannot safely allocate or format anything. Everything
degrades gracefully to the current behavior when the symbol is
unavailable (musl, future glibc changes).

With the tag in place, Sentry fingerprint rules can fold these
corruption aborts into a single tracked issue instead of minting a new
single-event issue per victim stack, and the message text distinguishes
double-frees from out-of-bounds metadata corruption when hunting the
actual culprit.

Release Notes:

- N/A
2026-07-14 21:47:47 +00:00
Joseph T. Lyons
e3dd7b462e
Focus on the history tabs context menu when deploying (#61000)
Release Notes:

- N/A
2026-07-14 21:06:27 +00:00
Ollie Rutherfurd
fca4016aef
workspace: Add ToggleEditorZoom action to maximize active pane (#53911)
VS Code has a command called "Toggle Maximize Editor Group" (`Cmd+K
Cmd+M`) that expands the active editor pane to fill the entire center
area, hiding editor split panes, but leaving docks and panels visible.
Zed's existing `ToggleZoom` hides all panels and is closer to VS Code's
Zen Mode. VS Code has both — it would be useful for Zed to as well.

This is particularly useful for people working on laptops, and has been
raised before (#32715, #4897, #32860), but the addition of the project
panel alongside the agent panel has made it more relevant. I work with
the editor split vertically, but with the project and agent panels both
visible, on a laptop screen there's really only room for a single editor
pane. This command makes it easy to switch to and from that mode.

**Behavior:**
- New action: `workspace::ToggleEditorZoom`
- When activated, the active editor pane expands to fill 100% of the
center/editor area, hiding sibling split panes
- Docks and panels remain visible and unaffected
- Toggling again restores the previous split layout
- If `ToggleZoom` is active, it unzooms first, then maximizes

Here's a demo:


https://github.com/user-attachments/assets/fcf6748e-6c45-4c4a-8d9f-f106c6cd58ea

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #32715

Release Notes:

- Added `workspace::ToggleEditorZoom` action to maximize the active
editor pane within the center area while keeping panels visible.
2026-07-14 20:36:17 +00:00
Hamza Paracha
2838ea3f59
Show both local and remote checkouts in Recent Projects (#53953)
This fixes #53917.

Recent Projects was treating local and remote workspaces as the same
entry whenever they resolved to the same checkout path. That meant one
could hide the other, and if one of them was already open, the picker
could end up hiding both.

This change keeps the deduplication and picker filtering keyed on both
the workspace location and the path list, so local and remote copies of
the same repo can both appear when they should.

## Test plan
- `rustfmt --edition 2024 crates/recent_projects/src/recent_projects.rs
crates/workspace/src/persistence.rs`
- `git diff --check -- crates/recent_projects/src/recent_projects.rs
crates/workspace/src/persistence.rs`

Release Notes:

- Fixed Recent Projects hiding either the local or remote workspace when
both used the same checkout path.

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2026-07-14 19:17:17 +00:00
Joseph T. Lyons
75ac36d92a
Surface the git graph context menu in the git panel's history tab (#60713)
This PR refactors the Git Graph’s commit context menu into its own file
so it can be reused by both the Git Graph and the Git panel’s History
tab.

<img width="511" height="566" alt="SCR-20260709-qlxt"
src="https://github.com/user-attachments/assets/8f18f22b-21d7-46a0-8ce2-183cf4af86c9"
/>

Also, includes a "Show in Git Graph" option only shown in the history
tab's context menu:


https://github.com/user-attachments/assets/2e30648b-2802-4caa-a99e-2d135e2dc539

Release Notes:

- Surfaced the git graph context menu in the git panel's history tab.
2026-07-14 19:16:37 +00:00
Danilo Leal
48a0cbcfd8
git_ui: Improve the staged & unstaged grouping in the git panel (#60976)
Some checks are pending
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
Follow up to https://github.com/zed-industries/zed/pull/59884

This PR refines how the git panel behaves when grouping changes by
staging state, with a focus on making staging workflows predictable and
conflict resolution safe.

- Renamed the "Group By" menu options to describe what you actually see:
**"Tracked & Untracked"** (was "Status") and **"Staged & Unstaged"**
(was "Staging").
- When grouping by staged & unstaged, both sections now stay visible
even when empty, showing a placeholder message ("No staged changes yet"
/ "No unstaged changes"). Previously an empty section disappeared
entirely, which made the panel layout jump around as you staged and
unstaged files, and made it harder to tell at a glance that nothing was
staged yet.
- Section header controls are now consistent checkboxes everywhere
(previously a mix of checkboxes and +/− icon buttons), with "Stage All"
/ "Unstage All" tooltips. Headers of empty sections render no checkbox
and don't react to clicks or hover. I appreciate the debate that
happened in the PR linked above about this but I was personally having a
hard time understanding what was the difference in interaction given the
action was exactly the same, we were just having different UIs, which
looked inconsistent.
- Arrow-key navigation now skips over section headers and empty-section
placeholder rows in both flat and tree view, instead of getting stuck or
selecting non-interactive rows — including when jumping to the first or
last entry.
- In the staged & unstaged grouping, a partially staged file appears in
both sections. Each row's checkbox and tooltip now follow the section
it's rendered in: rows in Staged always unstage, rows in Unstaged always
stage — including via shift-click range operations, which now also
support bulk *unstaging* within the Staged section (previously ranges
could only stage). The context menu's Stage/Unstage label follows the
same rule and stays in sync with in-flight staging operations.
- Conflicted files are grouped by whether the current merge marked them
conflicted (rather than raw status), so a conflict you've resolved stays
visible under "Conflicts" until the merge concludes. On top of that,
resolution is now one-way in the UI:
- Ticking a conflicted file's checkbox marks it resolved (stages it).
Once resolved, the checkbox is disabled with a "Conflict marked as
resolved" tooltip — unticking it would silently discard git's record of
the unmerged base/ours/theirs versions, a round-trip git can't actually
perform.
- The same lock applies everywhere the file can be reached: the keyboard
toggle, folder checkboxes in tree view, the Conflicts section header
(disabled once all conflicts are resolved), "Stage All"/"Unstage All" on
the Staged/Unstaged headers, and shift-click range sweeps — none of them
will resolve or un-resolve a conflict as a side effect.
- The explicit `git: unstage file` action still works as a deliberate
escape hatch.

---

Here's a video, where you can see I stage and unstage whole files, make
a partial staging, and get into a merge-conflict state, where the
conflicted files are tagged as resolved:


https://github.com/user-attachments/assets/d27ef9c6-5691-45c1-91ab-c3b152aaaa60

---

Release Notes:

- N/A _(given the feature hasn't been released yet_)
2026-07-14 18:05:11 +00:00
Yara 🏳️‍⚧️
3e76813825
pickers: Remove picker::ToMultiBuffer action we did not land (#60979)
Missed this when shipping the pickers. The feature is still planned and
tracked in the tracking issue.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
2026-07-14 17:18:23 +00:00
Smit Barmase
ba0eea3f49
settings_content: Fix globally excluded language servers starting (#60984)
Closes https://github.com/zed-industries/zed/issues/60763

Follow-up to #60000. Global language server exclusions were added to
per-language settings without removing an explicit enable for the same
server, allowing it to still start.

For example, this setting should disable `vtsls` for every language:

```json
{
  "language_servers": ["!vtsls", "..."]
}
```

However, TypeScript’s default configuration explicitly enables `vtsls`,
causing it to still start.

Release Notes:

- Fixed language servers starting despite being disabled globally.
2026-07-14 17:15:05 +00:00
Kirill Bulatov
ef2430c8f3
Properly notify on inline diagnostics toggle (#60978)
Closes https://github.com/zed-industries/zed/issues/60959

Release Notes:

- Fixed inline diagnostics not instantly [dis]appearing after toggling
2026-07-14 16:57:03 +00:00
John Tur
72656afa6d
Use more efficient thread pool API on Windows (#60917)
This interface uses fewer syscalls and has less internal synchronization
(since the `TP_WORK` object is not exposed).

Release Notes:

- N/A
2026-07-14 16:40:22 +00:00
136 changed files with 5192 additions and 2049 deletions

21
Cargo.lock generated
View file

@ -954,6 +954,7 @@ dependencies = [
"smol",
"tempfile",
"util",
"which 6.0.3",
"windows 0.61.3",
"zeroize",
]
@ -4347,6 +4348,7 @@ version = "0.1.0"
dependencies = [
"async-process",
"crash-handler",
"libc",
"log",
"mach2 0.5.0",
"minidumper",
@ -6318,6 +6320,7 @@ dependencies = [
"log",
"lsp",
"parking_lot",
"path",
"pretty_assertions",
"proto",
"semver",
@ -6907,6 +6910,7 @@ dependencies = [
"log",
"notify 9.0.0-rc.4",
"parking_lot",
"path",
"paths",
"proto",
"rope",
@ -9751,15 +9755,14 @@ dependencies = [
"gpui_shared_string",
"gpui_util",
"log",
"lsp-types",
"parking_lot",
"path",
"regex",
"schemars 1.0.4",
"serde",
"serde_json",
"toml 0.8.23",
"tree-sitter",
"util",
]
[[package]]
@ -12704,6 +12707,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "path"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"serde",
"tempfile",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
@ -14039,6 +14052,7 @@ dependencies = [
"markdown",
"node_runtime",
"parking_lot",
"path",
"paths",
"percent-encoding",
"postage",
@ -19696,9 +19710,11 @@ dependencies = [
"num-format",
"schemars 1.0.4",
"serde",
"settings",
"smallvec",
"strum 0.27.2",
"theme",
"theme_settings",
"ui_macros",
"windows 0.61.3",
]
@ -19951,6 +19967,7 @@ dependencies = [
"log",
"mach2 0.5.0",
"nix 0.29.0",
"path",
"percent-encoding",
"pretty_assertions",
"rand 0.9.4",

View file

@ -151,6 +151,7 @@ members = [
"crates/outline",
"crates/outline_panel",
"crates/panel",
"crates/path",
"crates/paths",
"crates/picker",
"crates/picker_preview",
@ -414,6 +415,7 @@ outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
panel = { path = "crates/panel" }
paths = { path = "crates/paths" }
path = { path = "crates/path" }
perf = { path = "tooling/perf" }
picker = { path = "crates/picker" }
picker_preview = { path = "crates/picker_preview" }

View file

@ -1095,8 +1095,7 @@ mod tests {
#[test]
fn test_posix_paths_are_not_rewritten_as_windows_drives() {
let parsed =
MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Unix).unwrap();
match parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, PathBuf::from("/c/Projects/AGENTS.md"));
@ -1107,7 +1106,7 @@ mod tests {
#[test]
fn test_hyperlink_percent_escapes_are_decoded() {
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1126,15 +1125,14 @@ mod tests {
);
// Separator escapes stay encoded (no introduced path traversal).
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
abs_path: PathBuf::from("/tmp/a%2Fb.rs")
}
);
let parsed =
MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1145,7 +1143,7 @@ mod tests {
#[test]
fn test_parse_keeps_bare_path_targets_verbatim() {
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
parsed,
MentionUri::File {
@ -1165,7 +1163,7 @@ mod tests {
#[test]
fn test_parse_hyperlink_literal_keeps_percent_escapes() {
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
assert_eq!(
literal,
MentionUri::File {
@ -1175,7 +1173,7 @@ mod tests {
// Line suffixes still parse.
let literal =
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Posix).unwrap();
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Unix).unwrap();
assert_eq!(
literal,
MentionUri::Selection {
@ -1200,27 +1198,27 @@ mod tests {
fn test_parse_hyperlink_literal_returns_none_when_unambiguous() {
// No percent escapes: identical to `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Unix),
None
);
// Invalid escape sequences are also left alone by `parse_hyperlink`.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Unix),
None
);
// Separator escapes are never decoded, so they're not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Unix),
None
);
// URLs are spec-encoded, not ambiguous.
assert_eq!(
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Unix),
None
);
// Relative paths are not bare-path mentions.
assert_eq!(
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Posix),
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Unix),
None
);
}
@ -1476,7 +1474,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_row() {
let file_path = "/path/to/file.rs:42";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1494,7 +1492,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_row_and_column() {
let file_path = "/path/to/file.rs:42:5";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1506,7 +1504,7 @@ mod tests {
assert_eq!(line_range.end(), &41);
assert_eq!(column, &Some(4));
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Posix)
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Unix)
.expect("selection URI with column should parse");
assert_eq!(parsed_again, parsed.clone());
}
@ -1517,7 +1515,7 @@ mod tests {
#[test]
fn test_parse_absolute_file_path_with_fragment_line() {
let file_path = "/path/to/file.rs#L42";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,
@ -1589,7 +1587,7 @@ mod tests {
#[test]
fn test_parse_backticked_absolute_file_path() {
let file_path = "`/path/to/file.rs`";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::File { abs_path } => {
assert_eq!(abs_path, Path::new("/path/to/file.rs"));
@ -1601,7 +1599,7 @@ mod tests {
#[test]
fn test_parse_backticked_absolute_file_path_with_fragment_line() {
let file_path = "`/path/to/file.rs#L42`";
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
match &parsed {
MentionUri::Selection {
abs_path: path,

View file

@ -446,18 +446,22 @@ impl gpui::EventEmitter<SkillLoadingIssuesUpdated> for NativeAgent {}
static RULES_FILE_REL_PATHS: LazyLock<Vec<Arc<RelPath>>> = LazyLock::new(|| {
RULES_FILE_NAMES
.iter()
.filter_map(|name| RelPath::unix(name).ok().map(|path| path.into_arc()))
.filter_map(|name| {
RelPath::from_unix_str(name)
.ok()
.map(|path| path.into_arc())
})
.collect()
});
static AGENTS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
RelPath::unix(AGENTS_DIR_NAME)
RelPath::from_unix_str(AGENTS_DIR_NAME)
.ok()
.map(|path| path.into_arc())
});
static SKILLS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
RelPath::unix(project_skills_relative_path())
RelPath::from_unix_str(project_skills_relative_path())
.ok()
.map(|path| path.into_arc())
});
@ -492,7 +496,7 @@ async fn expand_project_skills_directories(
worktree: &Entity<Worktree>,
cx: &mut AsyncApp,
) -> Result<()> {
let agents_dir = RelPath::unix(AGENTS_DIR_NAME)?;
let agents_dir = RelPath::from_unix_str(AGENTS_DIR_NAME)?;
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
return Ok(());
};
@ -518,7 +522,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
return Vec::new();
};
let Ok(skill_file_name) = RelPath::unix(SKILL_FILE_NAME) else {
let Ok(skill_file_name) = RelPath::from_unix_str(SKILL_FILE_NAME) else {
return Vec::new();
};
@ -538,7 +542,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
skill_files.push(ProjectSkillFile {
display_path: worktree.absolutize(&relative_path),
relative_path,
relative_path: relative_path.into(),
size: skill_file.size,
});
}

View file

@ -122,7 +122,7 @@ mod tests {
root_name: "my-project".to_string(),
abs_path: std::path::Path::new("/tmp/my-project").into(),
rules_file: Some(RulesFileContext {
path_in_worktree: RelPath::unix("AGENTS.md").unwrap().into(),
path_in_worktree: RelPath::from_unix_str("AGENTS.md").unwrap().into(),
text: "project-specific guidance".to_string(),
project_entry_id: 1,
}),

View file

@ -1245,11 +1245,11 @@ fn resolve_path(
let file_name = path
.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(|file_name| RelPath::unix(file_name).ok())
.and_then(|file_name| RelPath::from_unix_str(file_name).ok())
.ok_or_else(|| "Can't create file: invalid filename".to_string())?;
let new_file_path = parent_project_path.map(|parent| ProjectPath {
path: parent.path.join(file_name),
path: parent.path.join(file_name).into(),
..parent
});

View file

@ -238,7 +238,7 @@ fn project_agents_md_path(
require_existing_file: bool,
cx: &App,
) -> Option<PathBuf> {
let rel_path = util::rel_path::RelPath::unix("AGENTS.md").ok()?;
let rel_path = util::rel_path::RelPath::from_unix_str("AGENTS.md").ok()?;
project
.read(cx)
.visible_worktrees(cx)

View file

@ -8057,7 +8057,7 @@ impl ThreadView {
let tool_output_display = if is_open {
match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { options, .. } => {
ToolCallStatus::WaitingForConfirmation { .. } => {
let confirmation_content = v_flex()
.w_full()
.children(tool_call.content.iter().enumerate().map(
@ -8167,37 +8167,7 @@ impl ThreadView {
)
});
v_flex()
.w_full()
.map(|this| {
if layout == ToolCallLayout::Floating {
// Cap the content (e.g. a full plan awaiting
// approval) so the floating row can never
// consume the entire panel and squeeze the
// conversation list to zero height, while the
// permission buttons below stay visible.
this.child(
div()
.id(("floating-confirmation-content", entry_ix))
.max_h_40()
.overflow_y_scroll()
.child(confirmation_content),
)
} else {
this.child(confirmation_content)
}
})
.child(self.render_permission_buttons(
self.thread.read(cx).session_id().clone(),
self.is_first_tool_call(active_session_id, &tool_call.id, cx),
options,
entry_ix,
tool_call.id.clone(),
focus_handle,
self.sandbox_confusables_block_allow(tool_call, cx),
cx,
))
.into_any()
confirmation_content.into_any()
}
ToolCallStatus::Pending | ToolCallStatus::InProgress
if is_edit
@ -8295,35 +8265,23 @@ impl ThreadView {
None
};
v_flex()
.map(|this| {
if matches!(
layout,
ToolCallLayout::Embedded | ToolCallLayout::Floating
) {
this
} else if use_card_layout {
this.my_1p5()
.rounded_md()
.border_1()
.when(failed_or_canceled, |this| this.border_dashed())
.border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background)
.overflow_hidden()
} else {
this.my_1()
}
})
.when(layout == ToolCallLayout::Standalone, |this| {
this.map(|this| {
if has_location && !use_card_layout {
this.ml_4()
} else {
this.ml_5()
}
})
.mr_5()
})
let permission_buttons =
if let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status {
Some(self.render_permission_buttons(
self.thread.read(cx).session_id().clone(),
self.is_first_tool_call(active_session_id, &tool_call.id, cx),
options,
entry_ix,
tool_call.id.clone(),
focus_handle,
self.sandbox_confusables_block_allow(tool_call, cx),
cx,
))
} else {
None
};
let body = v_flex()
.map(|this| {
if is_terminal_tool {
this.child(self.render_collapsible_command(
@ -8498,7 +8456,48 @@ impl ThreadView {
)
}
})
.children(tool_output_display)
.children(tool_output_display);
v_flex()
.map(|this| {
if matches!(layout, ToolCallLayout::Embedded | ToolCallLayout::Floating) {
this
} else if use_card_layout {
this.my_1p5()
.rounded_md()
.border_1()
.when(failed_or_canceled, |this| this.border_dashed())
.border_color(self.tool_card_border_color(cx))
.bg(cx.theme().colors().editor_background)
.overflow_hidden()
} else {
this.my_1()
}
})
.when(layout == ToolCallLayout::Standalone, |this| {
this.map(|this| {
if has_location && !use_card_layout {
this.ml_4()
} else {
this.ml_5()
}
})
.mr_5()
})
.map(|this| {
if layout == ToolCallLayout::Floating {
this.child(
div()
.id(("floating-tool-call-body", entry_ix))
.max_h_40()
.overflow_y_scroll()
.child(body),
)
} else {
this.child(body)
}
})
.children(permission_buttons)
}
/// A small "Learn more" link to the sandboxing docs, deep-linked to

View file

@ -1248,8 +1248,7 @@ fn full_mention_for_directory(
|(worktree_path, full_path): (Arc<RelPath>, String)| {
let rel_path = worktree_path
.strip_prefix(&directory_path)
.log_err()
.map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
.map_or_else(|_| worktree_path.clone(), |rel_path| rel_path.into());
let open_task = project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {

View file

@ -22,6 +22,9 @@ tempfile.workspace = true
util.workspace = true
zeroize.workspace = true
[target.'cfg(not(target_os = "windows"))'.dependencies]
which.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View file

@ -91,6 +91,9 @@ pub struct AskPassSession {
#[cfg(not(target_os = "windows"))]
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
#[cfg(not(target_os = "windows"))]
const GPG_WRAPPER_SCRIPT_NAME: &str = "gpg-wrapper.sh";
impl AskPassSession {
/// This will create a new AskPassSession.
/// You must retain this session until the master process exits.
@ -189,6 +192,15 @@ impl AskPassSession {
self.askpass_task.script_path()
}
/// Path to a script suitable for git's `gpg.program`, routing GnuPG
/// passphrase prompts through Zed's askpass UI. `None` if unavailable.
pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
#[cfg(not(target_os = "windows"))]
return self.askpass_task.gpg_wrapper_path();
#[cfg(target_os = "windows")]
return None;
}
/// Returns the socket path to set as ZED_ASKPASS_SOCKET.
///
/// On Windows, SSH_ASKPASS points directly to cli.exe. SSH passes only
@ -206,6 +218,8 @@ pub struct PasswordProxy {
/// On Unix: path to the generated .sh askpass script (set as SSH_ASKPASS).
/// On Windows: path to cli.exe (set as SSH_ASKPASS directly — no script needed).
askpass_script_path: std::path::PathBuf,
#[cfg(not(target_os = "windows"))]
gpg_wrapper_script_path: Option<std::path::PathBuf>,
/// On Windows only: path to the Unix socket, passed as ZED_ASKPASS_SOCKET
/// so cli.exe can find it without --askpass argument parsing.
#[cfg(target_os = "windows")]
@ -238,6 +252,24 @@ impl PasswordProxy {
let askpass_socket_path = askpass_socket.clone();
// Create a gpg wrapper script that routes GnuPG passphrase prompts through
// the same socket (and thus through Zed's askpass UI). This only works on
// Unix where we control the pinentry via loopback mode. We compute the path
// before the socket task takes ownership of `temp_dir`, and write the file
// afterwards.
#[cfg(not(target_os = "windows"))]
let (gpg_wrapper_script_path, gpg_wrapper_script) =
match generate_gpg_wrapper_script(askpass_program, &askpass_socket_path) {
Ok(script) => (
Some(temp_dir.path().join(GPG_WRAPPER_SCRIPT_NAME)),
Some(script),
),
Err(err) => {
log::warn!("could not create gpg askpass wrapper: {err:#}");
(None, None)
}
};
let _task = executor.spawn(async move {
maybe!(async move {
let listener =
@ -291,9 +323,36 @@ impl PasswordProxy {
})?;
}
// Write the gpg wrapper script (computed above) and mark it executable.
#[cfg(not(target_os = "windows"))]
let gpg_wrapper_script_path =
if let Some((path, script)) = gpg_wrapper_script_path.zip(gpg_wrapper_script) {
match async {
fs::write(&path, script)
.await
.with_context(|| format!("creating gpg wrapper script at {path:?}"))?;
make_file_executable(&path).await.with_context(|| {
format!("marking gpg wrapper script executable at {path:?}")
})?;
anyhow::Ok(())
}
.await
{
Ok(()) => Some(path),
Err(err) => {
log::warn!("could not write gpg askpass wrapper: {err:#}");
None
}
}
} else {
None
};
Ok(Self {
_task,
askpass_script_path,
#[cfg(not(target_os = "windows"))]
gpg_wrapper_script_path,
#[cfg(target_os = "windows")]
askpass_socket_path,
})
@ -307,6 +366,11 @@ impl PasswordProxy {
pub fn socket_path(&self) -> impl AsRef<OsStr> {
&self.askpass_socket_path
}
#[cfg(not(target_os = "windows"))]
pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
self.gpg_wrapper_script_path.as_deref()
}
}
/// Runs Zed in netcat mode for use in askpass.
@ -398,3 +462,75 @@ fn generate_askpass_script(
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
))
}
#[inline]
#[cfg(not(target_os = "windows"))]
fn generate_gpg_wrapper_script(
askpass_program: &std::path::Path,
askpass_socket: &std::path::Path,
) -> Result<String> {
let shell_kind = ShellKind::Posix;
let gpg_program = find_gpg_program().context("could not find a gpg binary on PATH")?;
let gpg_program = gpg_program
.to_str()
.context("gpg program is on a non-utf8 path")?;
let gpg_program = shell_kind
.try_quote_prefix_aware(gpg_program)
.context("Failed to shell-escape gpg program path")?;
let askpass_program = shell_kind.prepend_command_prefix(
askpass_program
.to_str()
.context("Askpass program is on a non-utf8 path")?,
);
let askpass_program = shell_kind
.try_quote_prefix_aware(&askpass_program)
.context("Failed to shell-escape Askpass program path")?;
let askpass_socket = askpass_socket
.try_shell_safe(shell_kind)
.context("Failed to shell-escape Askpass socket path")?;
let prompt = shell_kind
.try_quote_prefix_aware("Enter passphrase for your Git signing key:")
.context("Failed to shell-escape gpg passphrase prompt")?;
// The wrapper inspects gpg's arguments and only prompts for a passphrase when
// git asks it to *sign* (e.g. `gpg -bsau <key>`). Other invocations such as
// signature verification (`--verify`) run gpg unchanged so we never pop a
// spurious modal. When signing, we feed the passphrase to gpg on fd 3 via
// `--passphrase-fd 3` in loopback mode, so no pinentry/terminal is required.
Ok(format!(
r#"#!/bin/sh
for arg in "$@"; do
case "$arg" in
# Long-form signing options.
--sign|--detach-sign|--clearsign|--clear-sign) is_signing=1 ;;
# Skip other long options so flags like `--status-fd` don't match below.
--*) ;;
# Short-flag clusters containing `s`, e.g. git's `-bsau`.
-*s*) is_signing=1 ;;
esac
done
# Not a signing request: run gpg as-is, leaving its prompting untouched.
if [ -z "${{is_signing}}" ]; then
exec {gpg_program} "$@"
fi
# Signing: ask Zed for the passphrase, then hand it to gpg on fd 3.
passphrase=$(printf '%s\0' {prompt} | {askpass_program} --askpass={askpass_socket} 2>/dev/null)
exec {gpg_program} --pinentry-mode loopback --passphrase-fd 3 "$@" 3<<EOF
${{passphrase}}
EOF
"#,
))
}
/// Finds the real `gpg` (or `gpg2`) executable on `PATH`.
#[inline]
#[cfg(not(target_os = "windows"))]
fn find_gpg_program() -> Option<std::path::PathBuf> {
["gpg", "gpg2"]
.into_iter()
.find_map(|candidate| which::which(candidate).ok())
}

View file

@ -1077,7 +1077,7 @@ mod tests {
.enumerate()
.filter_map(|(i, path)| {
Some((
Arc::from(RelPath::unix(path).ok()?),
Arc::from(RelPath::from_unix_str(path).ok()?),
ProjectEntryId::from_proto(i as u64 + 1),
PathChange::Added,
))

View file

@ -961,7 +961,7 @@ impl Database {
let path_style = if project.windows_paths {
PathStyle::Windows
} else {
PathStyle::Posix
PathStyle::Unix
};
let features: Vec<String> = serde_json::from_str(&project.features).unwrap_or_default();

View file

@ -5139,6 +5139,109 @@ async fn test_definition(
});
}
#[gpui::test]
async fn test_edit_prediction_definition(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
let capabilities = lsp::ServerCapabilities {
definition_provider: Some(OneOf::Left(true)),
..lsp::ServerCapabilities::default()
};
client_a.language_registry().add(rust_lang());
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: capabilities.clone(),
..FakeLspAdapter::default()
},
);
client_b.language_registry().add(rust_lang());
client_b.language_registry().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
capabilities,
..FakeLspAdapter::default()
},
);
client_a
.fs()
.insert_tree(
path!("/root"),
json!({
"a.rs": "const ONE: usize = TWO;",
"b.rs": "const TWO: usize = 2;",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let (buffer_b, _handle) = project_b
.update(cx_b, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
})
.await
.unwrap();
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
},
);
cx_a.run_until_parked();
cx_b.run_until_parked();
let definitions = project_b
.update(cx_b, |project, cx| {
project.edit_prediction_definitions(&buffer_b, 19, false, cx)
})
.await
.unwrap();
cx_b.read(|cx| {
assert_eq!(definitions.len(), 1);
assert_eq!(
definitions[0].path,
ProjectPath {
worktree_id,
path: rel_path("b.rs").into(),
}
);
assert_eq!(
definitions[0].range.start.0,
language::PointUtf16::new(0, 6)
);
assert_eq!(definitions[0].range.end.0, language::PointUtf16::new(0, 9));
assert!(
project_b
.read(cx)
.get_open_buffer(&definitions[0].path, cx)
.is_none()
);
});
}
#[gpui::test(iterations = 10)]
async fn test_references(
executor: BackgroundExecutor,

View file

@ -448,7 +448,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.choose(rng)
.unwrap();
if entry.path.as_ref().is_empty() {
worktree.root_name().into()
worktree.root_name().to_rel_path_buf()
} else {
worktree.root_name().join(&entry.path)
}
@ -1524,7 +1524,7 @@ fn buffer_for_full_path(
else {
return false;
};
worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
worktree.read(cx).root_name().join(&file.path()) == *full_path
})
})
.cloned()

View file

@ -16,6 +16,9 @@ serde_json.workspace = true
system_specs.workspace = true
zstd.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
libc.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
mach2.workspace = true

View file

@ -138,6 +138,17 @@ where
info!("crash signal handlers installed");
send_crash_server_message(&client, CrashServerMessage::Init(crash_init));
#[cfg(all(target_os = "linux", target_env = "gnu"))]
if let Some(address) = abort_message_address() {
send_crash_server_message(
&client,
CrashServerMessage::AbortMessageLocation(AbortMessageLocation {
pid: process::id(),
address,
}),
);
}
#[cfg(target_os = "linux")]
handler.set_ptracer(Some(_crash_handler.id()));
@ -170,6 +181,7 @@ pub struct CrashServer {
panic_info: Mutex<Option<CrashPanic>>,
active_gpu: Mutex<Option<system_specs::GpuSpecs>>,
user_info: Mutex<Option<UserInfo>>,
abort_message_location: Mutex<Option<AbortMessageLocation>>,
has_connection: Arc<AtomicBool>,
logs_dir: PathBuf,
}
@ -179,11 +191,27 @@ pub struct CrashInfo {
pub init: InitCrashHandler,
pub panic: Option<CrashPanic>,
pub minidump_error: Option<String>,
/// The diagnostic the C runtime recorded before aborting the process, e.g.
/// glibc's "free(): invalid pointer". Only present when the crash was a
/// runtime-initiated abort rather than a signal like SIGSEGV or a panic.
#[serde(default)]
pub abort_message: Option<String>,
pub gpus: Vec<system_specs::GpuInfo>,
pub active_gpu: Option<system_specs::GpuSpecs>,
pub user_info: Option<UserInfo>,
}
/// Where to find the C runtime's abort diagnostic in the crashed process's
/// memory. Sent by the client at startup so that after a crash the server can
/// recover the message with `process_vm_readv`; the crashed process itself
/// can't safely do this work, since its heap may be corrupt and its allocator
/// locks may be held by the crashed thread.
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
pub struct AbortMessageLocation {
pub pid: u32,
pub address: u64,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct InitCrashHandler {
pub session_id: String,
@ -233,6 +261,110 @@ enum CrashServerMessage {
Panic(CrashPanic),
GPUInfo(GpuSpecs),
UserInfo(UserInfo),
AbortMessageLocation(AbortMessageLocation),
}
/// glibc records the diagnostic it prints just before aborting (malloc integrity
/// failures like "free(): invalid pointer", assertion failures, stack-smashing
/// reports) in the private global `__abort_msg`, specifically so it can be
/// recovered post-mortem. Resolve its address here, in a safe context at startup.
/// The symbol is only exported at the GLIBC_PRIVATE version, which plain `dlsym`
/// won't resolve, and it has no stability guarantee, so a null result (e.g. musl,
/// or a future glibc removing it) just disables this diagnostic.
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn abort_message_address() -> Option<u64> {
let ptr = unsafe {
libc::dlvsym(
libc::RTLD_DEFAULT,
c"__abort_msg".as_ptr(),
c"GLIBC_PRIVATE".as_ptr(),
)
};
std::ptr::NonNull::new(ptr).map(|ptr| ptr.as_ptr() as u64)
}
/// Read the crashed process's abort diagnostic. `__abort_msg` points to a
/// `struct abort_msg_s { unsigned int size; char msg[]; }` that glibc allocates
/// with mmap so that it stays intact even when the heap is corrupt. `size` is
/// the total byte size of that mapping (header included, rounded up to whole
/// pages), not the message length; the message itself is NUL-terminated.
#[cfg(target_os = "linux")]
fn read_abort_message(location: AbortMessageLocation) -> Option<String> {
let pointer_bytes = read_process_memory(location.pid, location.address, size_of::<usize>())?;
let message_address = usize::from_ne_bytes(pointer_bytes.try_into().ok()?) as u64;
if message_address == 0 {
return None;
}
let size_bytes = read_process_memory(location.pid, message_address, size_of::<u32>())?;
let size = u32::from_ne_bytes(size_bytes.try_into().ok()?);
let message_bytes = read_process_memory(
location.pid,
message_address + size_of::<u32>() as u64,
abort_message_read_len(size)?,
)?;
parse_abort_message(&message_bytes)
}
/// How many message bytes to read given the `size` field of glibc's
/// `abort_msg_s`. `size` holds the total size of the mmap'd allocation, so a
/// value that isn't a whole number of pages means the layout has changed and
/// we shouldn't trust it. Reading is capped at (one page minus the header),
/// which both bounds the work and ensures the read never extends past the end
/// of the mapping.
#[cfg(any(target_os = "linux", test))]
fn abort_message_read_len(size: u32) -> Option<usize> {
// Every Linux page size (4 KiB, 16 KiB, 64 KiB, ...) is a multiple of 4 KiB.
const PAGE_MULTIPLE: usize = 4096;
const MAX_READ: usize = 4096;
let size = size as usize;
if size == 0 || !size.is_multiple_of(PAGE_MULTIPLE) {
log::warn!("__abort_msg size field {size} is not page-rounded; layout may have changed");
return None;
}
Some(size.min(MAX_READ) - size_of::<u32>())
}
/// The message is NUL-terminated inside a zero-filled mapping, so truncate at
/// the first NUL; `trim` alone would keep the padding, since NUL is not
/// whitespace.
#[cfg(any(target_os = "linux", test))]
fn parse_abort_message(bytes: &[u8]) -> Option<String> {
let len = bytes
.iter()
.position(|&byte| byte == 0)
.unwrap_or(bytes.len());
let message = String::from_utf8_lossy(&bytes[..len]).trim().to_string();
(!message.is_empty()).then_some(message)
}
#[cfg(target_os = "linux")]
fn read_process_memory(pid: u32, address: u64, len: usize) -> Option<Vec<u8>> {
let mut buffer = vec![0u8; len];
let local = libc::iovec {
iov_base: buffer.as_mut_ptr().cast(),
iov_len: len,
};
let remote = libc::iovec {
iov_base: address as *mut libc::c_void,
iov_len: len,
};
let bytes_read =
unsafe { libc::process_vm_readv(pid as libc::pid_t, &local, 1, &remote, 1, 0) };
if bytes_read < 0 {
log::warn!(
"process_vm_readv of {len} bytes at {address:#x} in pid {pid} failed: {}",
io::Error::last_os_error()
);
return None;
}
if bytes_read as usize != len {
log::warn!(
"process_vm_readv short read at {address:#x} in pid {pid}: {bytes_read} of {len} bytes"
);
return None;
}
Some(buffer)
}
impl minidumper::ServerHandler for CrashServer {
@ -281,6 +413,14 @@ impl minidumper::ServerHandler for CrashServer {
}
};
// The crashed process is still alive at this point: it stays parked in
// its signal handler until the server acknowledges the dump request,
// which happens after this callback returns.
#[cfg(target_os = "linux")]
let abort_message = (*self.abort_message_location.lock()).and_then(read_abort_message);
#[cfg(not(target_os = "linux"))]
let abort_message = None;
let crash_info = CrashInfo {
init: self
.initialization_params
@ -289,6 +429,7 @@ impl minidumper::ServerHandler for CrashServer {
.expect("not initialized"),
panic: self.panic_info.lock().clone(),
minidump_error,
abort_message,
active_gpu: self.active_gpu.lock().clone(),
gpus,
user_info: self.user_info.lock().clone(),
@ -320,6 +461,9 @@ impl minidumper::ServerHandler for CrashServer {
CrashServerMessage::UserInfo(user_info) => {
self.user_info.lock().replace(user_info);
}
CrashServerMessage::AbortMessageLocation(location) => {
self.abort_message_location.lock().replace(location);
}
}
}
@ -523,6 +667,7 @@ pub fn crash_server(socket: &Path, logs_dir: PathBuf) {
initialization_params: Mutex::default(),
panic_info: Mutex::default(),
user_info: Mutex::default(),
abort_message_location: Mutex::default(),
has_connection,
active_gpu: Mutex::default(),
logs_dir,
@ -532,3 +677,99 @@ pub fn crash_server(socket: &Path, logs_dir: PathBuf) {
)
.expect("failed to run server");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn abort_message_read_len_requires_page_rounded_total() {
assert_eq!(abort_message_read_len(0), None);
// A message length rather than a mapping total means the glibc layout
// has changed out from under us.
assert_eq!(abort_message_read_len(23), None);
assert_eq!(abort_message_read_len(4097), None);
// The read must stay within the mapping: one page minus the header.
assert_eq!(abort_message_read_len(4096), Some(4092));
// Larger totals (long messages, larger page sizes) are clamped.
assert_eq!(abort_message_read_len(8192), Some(4092));
assert_eq!(abort_message_read_len(65536), Some(4092));
}
#[test]
fn parse_abort_message_truncates_at_nul() {
let mut buffer = b"free(): invalid pointer\n\0".to_vec();
buffer.resize(4092, 0);
assert_eq!(
parse_abort_message(&buffer),
Some("free(): invalid pointer".to_string())
);
}
#[test]
fn parse_abort_message_handles_missing_nul() {
assert_eq!(
parse_abort_message(b"double free or corruption (out)"),
Some("double free or corruption (out)".to_string())
);
}
#[test]
fn parse_abort_message_rejects_empty() {
assert_eq!(parse_abort_message(&[]), None);
assert_eq!(parse_abort_message(&[0; 16]), None);
assert_eq!(parse_abort_message(b"\n \0garbage after nul"), None);
}
/// End-to-end check of `read_abort_message` against a synthetic
/// `abort_msg_s` in this very process (`process_vm_readv` may always read
/// one's own memory). The message page is followed by a `PROT_NONE` guard
/// page so the test fails if the read ever extends past the mapping glibc
/// would have allocated.
#[cfg(target_os = "linux")]
#[test]
fn read_abort_message_reads_glibc_layout_from_a_live_process() {
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
unsafe {
let mapping = libc::mmap(
std::ptr::null_mut(),
2 * page_size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_ANON | libc::MAP_PRIVATE,
-1,
0,
);
assert_ne!(mapping, libc::MAP_FAILED);
assert_eq!(
libc::mprotect(
mapping.cast::<u8>().add(page_size).cast(),
page_size,
libc::PROT_NONE
),
0
);
mapping.cast::<u32>().write(page_size as u32);
let message = b"free(): invalid pointer\n\0";
std::ptr::copy_nonoverlapping(
message.as_ptr(),
mapping.cast::<u8>().add(size_of::<u32>()),
message.len(),
);
// Stands in for the `__abort_msg` global: a pointer variable whose
// address we hand to the reader.
let abort_msg: *mut libc::c_void = mapping;
let location = AbortMessageLocation {
pid: process::id(),
address: (&raw const abort_msg) as u64,
};
assert_eq!(
read_abort_message(location),
Some("free(): invalid pointer".to_string())
);
libc::munmap(mapping, 2 * page_size);
}
}
}

View file

@ -1090,14 +1090,14 @@ impl DebugPanel {
directory_in_worktree: dir,
..
} => {
let relative_path = if dir.ends_with(RelPath::unix(".vscode").unwrap()) {
dir.join(RelPath::unix("launch.json").unwrap())
let relative_path = if dir.ends_with(RelPath::from_unix_str(".vscode").unwrap()) {
dir.join(RelPath::from_unix_str("launch.json").unwrap())
} else {
dir.join(RelPath::unix("debug.json").unwrap())
dir.join(RelPath::from_unix_str("debug.json").unwrap())
};
ProjectPath {
worktree_id: id,
path: relative_path,
path: relative_path.into(),
}
}
_ => return self.save_scenario(scenario, worktree_id, window, cx),

View file

@ -1069,10 +1069,10 @@ impl DebugDelegate {
match path.components().next_back() {
Some(".zed") => {
path.push(RelPath::unix("debug.json").unwrap());
path.push(RelPath::from_unix_str("debug.json").unwrap());
}
Some(".vscode") => {
path.push(RelPath::unix("launch.json").unwrap());
path.push(RelPath::from_unix_str("launch.json").unwrap());
}
_ => {}
}
@ -1165,7 +1165,7 @@ impl DebugDelegate {
id: _,
directory_in_worktree: dir,
id_base: _,
} => dir.ends_with(RelPath::unix(".zed").unwrap()),
} => dir.ends_with(RelPath::from_unix_str(".zed").unwrap()),
_ => false,
});
@ -1186,7 +1186,8 @@ impl DebugDelegate {
id_base: _,
} => {
!(hide_vscode
&& dir.ends_with(RelPath::unix(".vscode").unwrap()))
&& dir
.ends_with(RelPath::from_unix_str(".vscode").unwrap()))
}
_ => true,
})

View file

@ -115,6 +115,7 @@ impl Render for RunningState {
} else if let Some(active) = active {
self.panes
.render(
None,
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,

View file

@ -524,7 +524,7 @@ impl StackFrameList {
.filter(|path| {
// Since we do not know if we are debugging on the host or (a remote/WSL) target,
// we need to check if either the path is absolute as Posix or Windows.
is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
is_absolute(path, PathStyle::Unix) || is_absolute(path, PathStyle::Windows)
})
.map(|path| Arc::<Path>::from(Path::new(path)))
})

View file

@ -172,13 +172,13 @@ pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<D
pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig> {
let mut configs = Vec::new();
let devcontainer_dir_path = RelPath::unix(".devcontainer").expect("valid path");
let devcontainer_dir_path = RelPath::from_unix_str(".devcontainer").expect("valid path");
if let Some(devcontainer_entry) = snapshot.entry_for_path(devcontainer_dir_path) {
if devcontainer_entry.is_dir() {
log::debug!("find_configs_in_snapshot: Scanning .devcontainer directory");
let devcontainer_json_path =
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
RelPath::from_unix_str(".devcontainer/devcontainer.json").expect("valid path");
for entry in snapshot.child_entries(devcontainer_dir_path) {
log::debug!(
"find_configs_in_snapshot: Found entry: {:?}, is_file: {}, is_dir: {}",
@ -199,7 +199,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
let config_json_path =
format!("{}/devcontainer.json", entry.path.as_unix_str());
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
if let Ok(rel_config_path) = RelPath::from_unix_str(&config_json_path) {
if snapshot.entry_for_path(rel_config_path).is_some() {
log::debug!(
"find_configs_in_snapshot: Found config in subfolder: {}",
@ -223,7 +223,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
// Always include `.devcontainer.json` so the user can pick it from the UI
// even when `.devcontainer/devcontainer.json` also exists.
let root_config_path = RelPath::unix(".devcontainer.json").expect("valid path");
let root_config_path = RelPath::from_unix_str(".devcontainer.json").expect("valid path");
if snapshot
.entry_for_path(root_config_path)
.is_some_and(|entry| entry.is_file())
@ -400,7 +400,7 @@ pub(crate) async fn apply_devcontainer_template(
log::error!("Can't create relative path: {e}");
DevContainerError::FilesystemError
})?;
let rel_path = RelPath::unix(relative_path)
let rel_path = RelPath::from_unix_str(relative_path)
.map_err(|e| {
log::error!("Can't create relative path: {e}");
DevContainerError::FilesystemError

View file

@ -1579,11 +1579,12 @@ fn dispatch_apply_templates(
};
if files.project_files.contains(&Arc::from(
RelPath::unix(".devcontainer/devcontainer.json").unwrap(),
RelPath::from_unix_str(".devcontainer/devcontainer.json").unwrap(),
)) {
let Some(workspace_task) = workspace
.update_in(cx, |workspace, window, cx| {
let Ok(path) = RelPath::unix(".devcontainer/devcontainer.json") else {
let Ok(path) = RelPath::from_unix_str(".devcontainer/devcontainer.json")
else {
return Task::ready(Err(anyhow!(
"Couldn't create path for .devcontainer/devcontainer.json"
)));

View file

@ -309,12 +309,12 @@ pub async fn refresh_worktree_entries(
) -> Result<()> {
let mut rel_paths = Vec::new();
for path in paths {
if let Ok(rel_path) = RelPath::new(path, PathStyle::Posix) {
if let Ok(rel_path) = RelPath::new(path, PathStyle::Unix) {
rel_paths.push(rel_path.into_arc());
}
let path_without_root: PathBuf = path.components().skip(1).collect();
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Posix) {
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Unix) {
rel_paths.push(rel_path.into_arc());
}
}

View file

@ -5,8 +5,8 @@ use futures::{FutureExt, StreamExt as _, channel::mpsc, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, TaskExt, WeakEntity,
};
use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, ToOffset as _};
use project::{LocationLink, Project, ProjectPath};
use language::{Anchor, Bias, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, ToOffset as _};
use project::{EditPredictionDefinition, Project, ProjectPath};
use smallvec::SmallVec;
use std::{
collections::hash_map,
@ -77,20 +77,15 @@ struct Identifier {
enum DefinitionTask {
CacheHit(Arc<CacheEntry>),
CacheMiss(
Task<
Option<(
Task<Result<Option<Vec<LocationLink>>>>,
Task<Result<Option<Vec<LocationLink>>>>,
)>,
>,
),
CacheMiss {
project: WeakEntity<Project>,
task: Task<Result<Vec<EditPredictionDefinition>>>,
},
}
#[derive(Debug)]
struct CacheEntry {
definitions: SmallVec<[CachedDefinition; 1]>,
type_definitions: SmallVec<[CachedDefinition; 1]>,
}
#[derive(Clone, Debug)]
@ -183,7 +178,7 @@ impl RelatedExcerptStore {
.path
.strip_prefix(worktree.root_name().as_unix_str())
.ok()?;
let relative_path = RelPath::new(relative_path, PathStyle::Posix).ok()?;
let relative_path = RelPath::new(relative_path, PathStyle::Unix).ok()?;
let project_path = ProjectPath {
worktree_id: worktree.id(),
path: relative_path.into_owned().into(),
@ -312,34 +307,21 @@ impl RelatedExcerptStore {
DefinitionTask::CacheHit(entry.clone())
} else {
let project = this.project.clone();
let buffer = buffer.downgrade();
DefinitionTask::CacheMiss(cx.spawn(async move |_, cx| {
let buffer = buffer.upgrade()?;
let definitions = project
.update(cx, |project, cx| {
project.workspace_definitions(
&buffer,
identifier.range.start,
cx,
)
})
.ok()?;
let type_definitions = project
.update(cx, |project, cx| {
// tombi LSP for toml will open a scratch buffer with the JSON schema of
// the toml file when a goto type definition is requested
if is_tombi_lsp_in_toml(project, &buffer, cx) {
return Task::ready(Ok(None));
}
project.workspace_type_definitions(
&buffer,
identifier.range.start,
cx,
)
})
.ok()?;
Some((definitions, type_definitions))
}))
let task = project
.update(cx, |project, cx| {
// tombi LSP for toml will open a scratch buffer with the JSON schema of
// the toml file when a goto type definition is requested
let include_type_definitions =
!is_tombi_lsp_in_toml(project, &buffer, cx);
project.edit_prediction_definitions(
&buffer,
identifier.range.start,
include_type_definitions,
cx,
)
})
.unwrap_or_else(|_| Task::ready(Ok(Vec::new())));
DefinitionTask::CacheMiss { project, task }
};
let cx = async_cx.clone();
@ -348,50 +330,29 @@ impl RelatedExcerptStore {
DefinitionTask::CacheHit(cache_entry) => {
Some((identifier, cache_entry, None))
}
DefinitionTask::CacheMiss(task) => {
let (definitions, type_definitions) = task.await?;
let (definition_locations, type_definition_locations) =
futures::join!(definitions, type_definitions);
DefinitionTask::CacheMiss { project, task } => {
let definition_locations = task.await.log_err().unwrap_or_default();
let duration = start_time.elapsed();
let definition_locations =
definition_locations.log_err().flatten().unwrap_or_default();
let type_definition_locations = type_definition_locations
.log_err()
.flatten()
.unwrap_or_default();
let definitions: SmallVec<[CachedDefinition; 1]> =
definition_locations
.into_iter()
.filter_map(|location| {
future::join_all(definition_locations.into_iter().map(
|definition| {
let project = project.clone();
let mut cx = cx.clone();
process_definition(location, &mut cx)
})
.collect();
let type_definitions: SmallVec<[CachedDefinition; 1]> =
type_definition_locations
.into_iter()
.filter_map(|location| {
let mut cx = cx.clone();
process_definition(location, &mut cx)
})
.filter(|type_def| {
!definitions.iter().any(|def| {
def.buffer.entity_id()
== type_def.buffer.entity_id()
&& def.anchor_range == type_def.anchor_range
})
})
.collect();
async move {
process_definition(definition, &project, &mut cx)
.await
}
},
))
.await
.into_iter()
.flatten()
.collect();
Some((
identifier,
Arc::new(CacheEntry {
definitions,
type_definitions,
}),
Arc::new(CacheEntry { definitions }),
Some(duration),
))
}
@ -469,11 +430,7 @@ async fn rebuild_related_files(
let mut snapshots = HashMap::default();
let mut worktree_root_names = HashMap::default();
for entry in new_entries.values() {
for definition in entry
.definitions
.iter()
.chain(entry.type_definitions.iter())
{
for definition in entry.definitions.iter() {
if let hash_map::Entry::Vacant(e) = snapshots.entry(definition.buffer.entity_id()) {
definition
.buffer
@ -510,11 +467,7 @@ async fn rebuild_related_files(
.get(identifier)
.copied()
.unwrap_or(usize::MAX);
for definition in entry
.definitions
.iter()
.chain(entry.type_definitions.iter())
{
for definition in entry.definitions.iter() {
let Some(snapshot) = snapshots.get(&definition.buffer.entity_id()) else {
continue;
};
@ -635,16 +588,24 @@ use language::ToPoint as _;
const MAX_TARGET_LEN: usize = 128;
fn process_definition(location: LocationLink, cx: &mut AsyncApp) -> Option<CachedDefinition> {
async fn process_definition(
definition: EditPredictionDefinition,
project: &WeakEntity<Project>,
cx: &mut AsyncApp,
) -> Option<CachedDefinition> {
let EditPredictionDefinition { path, range } = definition;
let buffer = project
.update(cx, |project, cx| project.open_buffer(path.clone(), cx))
.ok()?
.await
.log_err()?;
cx.update(|cx| {
let buffer = location.target.buffer;
let buffer_snapshot = buffer.read(cx);
let file = buffer_snapshot.file()?;
let path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path().clone(),
};
let anchor_range = location.target.range;
let target_start = buffer_snapshot.clip_point_utf16(range.start, Bias::Left);
let target_end = buffer_snapshot.clip_point_utf16(range.end, Bias::Left);
let anchor_range =
buffer_snapshot.anchor_after(target_start)..buffer_snapshot.anchor_before(target_end);
// If the target range is large, it likely means we requested the definition of an entire module.
// For individual definitions, the target range should be small as it only covers the symbol.

View file

@ -5,10 +5,11 @@ use gpui::TestAppContext;
use indoc::indoc;
use language::{Point, ToPoint as _, rust_lang};
use lsp::FakeLanguageServer;
use project::{FakeFs, LocationLink, Project};
use project::{FakeFs, LocationLink, Project, ProjectPath};
use serde_json::json;
use settings::SettingsStore;
use std::fmt::Write as _;
use util::rel_path::rel_path;
use util::{path, test::marked_text_ranges};
#[gpui::test]
@ -561,9 +562,10 @@ async fn test_type_definition_deduplication(cx: &mut TestAppContext) {
// In this project the only identifier near the cursor whose type definition
// resolves is `TypeA`, and its GotoTypeDefinition returns the exact same
// location as GotoDefinition. After deduplication the CacheEntry for `TypeA`
// should have an empty `type_definitions` vec, meaning the type-definition
// path contributes nothing extra to the related-file output.
// location as GotoDefinition. After the definitions and type definitions are
// merged and deduped, the type-definition location is dropped, so it
// contributes nothing extra to the related-file output (the target location
// appears only once).
fs.insert_tree(
path!("/root"),
json!({
@ -638,6 +640,85 @@ async fn test_type_definition_deduplication(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_edit_prediction_filters_raw_definitions_before_opening_buffers(
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"src": {
"main.rs": indoc! {"
// fake-definition-lsp-extra target /root/src/large.rs 0 0 0 129
// fake-definition-lsp-extra target /root/src/valid.rs 0 3 0 9
// fake-definition-lsp-extra target /outside.rs 0 3 0 10
fn main() {
target();
}
"},
"valid.rs": "fn target() {}\n",
"large.rs": format!("{}\n", "a".repeat(MAX_TARGET_LEN + 16)),
},
}),
)
.await;
fs.insert_file(path!("/outside.rs"), "fn outside() {}\n".into())
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let mut servers = setup_fake_lsp(&project, cx);
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
})
.await
.unwrap();
let _fake_language_server = servers.next().await.unwrap();
cx.run_until_parked();
let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
related_excerpt_store.update(cx, |store, cx| {
let position = {
let buffer = buffer.read(cx);
let offset = buffer
.text()
.find("target();")
.expect("target call not found");
buffer.anchor_before(offset)
};
store.set_identifier_line_count(0);
store.refresh(buffer.clone(), position, cx);
});
cx.executor().advance_clock(DEBOUNCE_DURATION);
related_excerpt_store.update(cx, |store, cx| {
assert_related_files(
&store.related_files(cx),
&[
("root/src/valid.rs", &["fn target() {}"]),
("root/src/main.rs", &["fn main() {\n target();\n}"]),
],
);
});
let worktree_id = buffer.read_with(cx, |buffer, cx| {
buffer.file().expect("buffer has file").worktree_id(cx)
});
let valid_path = ProjectPath {
worktree_id,
path: rel_path("src/valid.rs").into(),
};
project.read_with(cx, |project, cx| {
assert!(project.get_open_buffer(&valid_path, cx).is_some());
assert_eq!(project.worktrees(cx).count(), 1);
});
}
#[gpui::test]
async fn test_definitions_ranked_by_cursor_proximity(cx: &mut TestAppContext) {
init_test(cx);

View file

@ -652,7 +652,7 @@ async fn collect_git_log_context(
.into_iter()
.enumerate()
{
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Posix) else {
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Unix) else {
continue;
};
let project_path = ProjectPath {

View file

@ -243,6 +243,12 @@ impl DefinitionIndex {
.or_insert_with(Vec::new)
.push(location);
}
for (name, location) in extract_extra_definition_locations(content) {
self.definitions
.entry(name)
.or_insert_with(Vec::new)
.push(location);
}
let type_annotations = extract_type_annotations(content)
.into_iter()
@ -303,6 +309,34 @@ impl DefinitionIndex {
}
}
fn extract_extra_definition_locations(content: &str) -> Vec<(String, lsp::Location)> {
content
.lines()
.filter_map(|line| {
let mut parts = line
.trim()
.strip_prefix("// fake-definition-lsp-extra ")?
.split_whitespace();
let name = parts.next()?.to_string();
let path = PathBuf::from(parts.next()?);
let start_row = parts.next()?.parse().ok()?;
let start_column = parts.next()?.parse().ok()?;
let end_row = parts.next()?.parse().ok()?;
let end_column = parts.next()?.parse().ok()?;
Some((
name,
lsp::Location::new(
Uri::from_file_path(path).ok()?,
lsp::Range::new(
lsp::Position::new(start_row, start_column),
lsp::Position::new(end_row, end_column),
),
),
))
})
.collect()
}
/// Extracts `identifier_name -> type_name` mappings from field declarations
/// and function parameters. For example, `owner: Arc<Person>` produces
/// `"owner" -> "Person"` by unwrapping common generic wrappers.

View file

@ -740,9 +740,9 @@ fn unused_image_path(
let mut filename = format!("image.{extension}");
let mut counter = 1u32;
loop {
let candidate = dir_rel_path.join(RelPath::unix(&filename).ok()?);
let candidate = dir_rel_path.join(RelPath::from_unix_str(&filename).ok()?);
if !exists(&candidate) {
return Some((filename, candidate));
return Some((filename, candidate.into()));
}
filename = format!("image_{counter}.{extension}");
counter += 1;

View file

@ -331,13 +331,12 @@ impl Editor {
self.set_max_diagnostics_severity(new_severity, cx);
if self.diagnostics_enabled {
self.active_diagnostics = ActiveDiagnostic::None;
self.refresh_inline_diagnostics(false, window, cx);
} else {
self.inline_diagnostics_update = Task::ready(());
self.inline_diagnostics.clear();
} else {
self.refresh_inline_diagnostics(false, window, cx);
cx.notify();
}
cx.notify();
}
pub(super) fn all_diagnostics_active(&self) -> bool {
@ -464,6 +463,7 @@ impl Editor {
{
self.inline_diagnostics_update = Task::ready(());
self.inline_diagnostics.clear();
cx.notify();
return;
}
@ -596,3 +596,158 @@ impl Editor {
}
}
}
#[cfg(test)]
mod tests {
use crate::{
actions::{ToggleDiagnostics, ToggleInlineDiagnostics},
editor_tests::init_test,
test::editor_test_context::EditorTestContext,
};
use gpui::{TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::DiagnosticSourceKind;
use lsp::LanguageServerId;
use settings::{DelayMs, SettingsStore};
use std::sync::{
Arc,
atomic::{self, AtomicUsize},
};
use util::path;
fn setup_inline_diagnostics(cx: &mut EditorTestContext) {
cx.update(|_, cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings(cx, |settings| {
let inline = settings
.diagnostics
.get_or_insert_default()
.inline
.get_or_insert_default();
inline.enabled = Some(true);
inline.update_debounce_ms = Some(DelayMs(0));
});
});
});
cx.set_state(indoc! {"
fn func(abc dˇef: i32) -> u32 {
}
"});
let lsp_store =
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 12),
lsp::Position::new(0, 15),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "cannot find value `def`".to_string(),
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap()
});
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should appear after the language server publishes them"
);
});
}
#[gpui::test]
async fn test_toggle_diagnostics_refreshes_inline_diagnostics(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
setup_inline_diagnostics(&mut cx);
cx.update_editor(|editor, window, cx| {
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
0,
"inline diagnostics should be cleared after disabling diagnostics"
);
});
cx.update_editor(|editor, window, cx| {
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should reappear after re-enabling diagnostics, without further editor events"
);
});
}
#[gpui::test]
async fn test_toggle_inline_diagnostics_notifies_on_hide(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
setup_inline_diagnostics(&mut cx);
let notify_count = Arc::new(AtomicUsize::new(0));
let editor = cx.editor.clone();
let _subscription = cx.update({
let notify_count = notify_count.clone();
move |_, cx| {
cx.observe(&editor, move |_, _| {
notify_count.fetch_add(1, atomic::Ordering::SeqCst);
})
}
});
cx.update_editor(|editor, window, cx| {
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
});
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
0,
"inline diagnostics should be cleared after toggling them off"
);
});
assert_eq!(
notify_count.load(atomic::Ordering::SeqCst),
1,
"toggling inline diagnostics off should notify to repaint the editor"
);
cx.update_editor(|editor, window, cx| {
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
});
cx.run_until_parked();
cx.update_editor(|editor, _, _| {
assert_eq!(
editor.inline_diagnostics.len(),
1,
"inline diagnostics should reappear after toggling them back on"
);
});
}
}

View file

@ -38855,7 +38855,7 @@ fn test_hunk_key(file_path: &str) -> DiffHunkKey {
file_path: if file_path.is_empty() {
Arc::from(util::rel_path::RelPath::empty())
} else {
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
},
hunk_start_anchor: Anchor::Min,
}
@ -38867,7 +38867,7 @@ fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
file_path: if file_path.is_empty() {
Arc::from(util::rel_path::RelPath::empty())
} else {
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
},
hunk_start_anchor: anchor,
}
@ -39306,11 +39306,11 @@ fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
// Create two different hunk keys (simulating two different files)
let anchor = snapshot.anchor_before(Point::new(0, 0));
let key1 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file1.rs").unwrap()),
hunk_start_anchor: anchor,
};
let key2 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file2.rs").unwrap()),
hunk_start_anchor: anchor,
};
@ -39378,11 +39378,11 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
// Create two keys with the same file path and anchor
let key1 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
hunk_start_anchor: anchor,
};
let key2 = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
hunk_start_anchor: anchor,
};
@ -39399,7 +39399,7 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
// Create a key with different file path
let different_file_key = DiffHunkKey {
file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("other.rs").unwrap()),
hunk_start_anchor: anchor,
};

View file

@ -43,7 +43,7 @@ use std::{
};
use text::{BufferId, BufferSnapshot, OffsetRangeExt, Selection, ToPoint as _};
use ui::{IconDecorationKind, prelude::*};
use util::{ResultExt, TryFutureExt, paths::PathExt, rel_path::RelPath};
use util::{ResultExt, TryFutureExt, debug_panic, paths::PathExt, rel_path::RelPath};
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use workspace::{
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
@ -2100,6 +2100,75 @@ pub fn active_match_index(
}
}
/// Opens a path-like target (e.g. `items.rs:100:5`) in the workspace, moving the cursor
/// to the one-based row/column if present. Returns whether the target was opened.
pub async fn open_resolved_target(
workspace: &WeakEntity<Workspace>,
open_target: &workspace::path_link::OpenTarget,
cx: &mut AsyncWindowContext,
) -> Result<bool> {
let path_to_open = open_target.path();
let mut opened_items = workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_paths(
vec![path_to_open.path.clone()],
workspace::OpenOptions {
visible: Some(workspace::OpenVisible::OnlyDirectories),
..Default::default()
},
None,
window,
cx,
)
})
.context("workspace update")?
.await;
if opened_items.len() != 1 {
debug_panic!(
"Received {} items for one path {path_to_open:?}",
opened_items.len(),
);
}
let Some(opened_item) = opened_items.pop() else {
return Ok(false);
};
if open_target.is_file() {
let Some(opened_item) = opened_item else {
return Ok(false);
};
let opened_item =
opened_item.with_context(|| format!("opening {:?}", path_to_open.path))?;
if let Some(row) = path_to_open.row
&& let Some(editor) = opened_item.downcast::<Editor>()
{
let column = path_to_open.column.unwrap_or(0);
editor
.downgrade()
.update_in(cx, |editor, window, cx| {
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
let point = buffer.read(cx).snapshot().point_from_external_input(
row.saturating_sub(1),
column.saturating_sub(1),
);
editor.go_to_singleton_buffer_point(point, window, cx);
}
})
.log_err();
}
Ok(true)
} else if open_target.is_dir() {
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_, cx| {
cx.emit(project::Event::ActivateProjectPanel);
})
})?;
Ok(true)
} else {
Ok(false)
}
}
pub fn entry_label_color(selected: bool) -> Color {
if selected {
Color::Default
@ -2221,14 +2290,14 @@ fn restore_serialized_buffer_contents(
fn serialize_path_key(path_key: &PathKey) -> proto::PathKey {
proto::PathKey {
sort_prefix: path_key.sort_prefix,
path: path_key.path.to_proto(),
path: path_key.path.as_unix_str().to_owned(),
}
}
fn deserialize_path_key(path_key: proto::PathKey) -> Option<PathKey> {
Some(PathKey {
sort_prefix: path_key.sort_prefix,
path: RelPath::from_proto(&path_key.path).ok()?,
path: RelPath::from_unix_str(&path_key.path).ok()?.into(),
})
}
@ -2396,7 +2465,8 @@ mod tests {
use project::FakeFs;
use serde_json::json;
use std::path::{Path, PathBuf};
use util::{path, rel_path::RelPath};
use util::{path, paths::PathWithPosition, rel_path::RelPath};
use workspace::path_link::{OpenTarget, OpenTargetFoundBy};
#[gpui::test]
fn test_path_for_file(cx: &mut App) {
@ -3038,6 +3108,62 @@ mod tests {
);
}
#[gpui::test]
async fn test_open_resolved_target_at_non_ascii_column(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"src": {
"main.rs": "first\naéøbc\n",
},
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let open_target = OpenTarget::Path(
PathWithPosition {
path: PathBuf::from(path!("/root/src/main.rs")),
row: Some(2),
column: Some(4),
},
false,
OpenTargetFoundBy::BackgroundPathResolution,
);
let opened = workspace
.update_in(cx, |_, window, cx| {
cx.spawn_in(window, async move |workspace, cx| {
open_resolved_target(&workspace, &open_target, cx).await
})
})
.await
.expect("opening the target should succeed");
assert!(opened, "target should open as a file");
let editor = workspace.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
.expect("active item should be an editor")
});
let cursor = editor.update_in(cx, |editor, _, cx| {
editor
.selections
.newest::<language::Point>(&editor.display_snapshot(cx))
.head()
});
// Column 4 is the fourth character of `aéøbc` (the `b`), which starts at byte 5.
assert_eq!(cursor, language::Point::new(1, 5));
}
#[gpui::test]
fn test_compute_modified_ranges_git_diff(cx: &mut gpui::TestAppContext) {
let base_text = "line0\nline1\nline2\nline3\nline4\nline5\nline6\n";

View file

@ -26,6 +26,7 @@ language.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
path.workspace = true
proto.workspace = true
semver.workspace = true
serde.workspace = true

View file

@ -56,7 +56,7 @@ pub trait Extension: Send + Sync + 'static {
/// Returns a path relative to this extension's working directory.
fn path_from_extension(&self, path: &Path) -> PathBuf {
util::normalize_path(&self.work_dir().join(path))
path::normalize_path(&self.work_dir().join(path))
}
async fn language_server_command(

View file

@ -11,6 +11,7 @@ use futures::{
use heck::ToSnakeCase;
use http_client::{self, AsyncBody, HttpClient};
use language::LanguageConfig;
use path::PathExt;
use semver::Version;
use serde::Deserialize;
use std::{
@ -20,7 +21,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, command::Stdio, rel_path::PathExt};
use util::{ResultExt, command::Stdio};
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;

View file

@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
@ -11,7 +12,8 @@ use language::LanguageName;
use lsp::LanguageServerName;
use semver::Version;
use serde::{Deserialize, Serialize};
use util::rel_path::{PathExt, RelPathBuf};
use util::paths::PathStyle;
use util::rel_path::{RelPath, RelPathBuf};
use crate::ExtensionCapability;
@ -211,9 +213,12 @@ pub fn build_debug_adapter_schema_path(
) -> anyhow::Result<RelPathBuf> {
match &meta.schema_path {
Some(path) => Ok(path.clone()),
None => Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
.to_rel_path_buf(),
None => RelPath::new(
&Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json")),
PathStyle::local(),
)
.map(Cow::into_owned),
}
}

View file

@ -57,7 +57,7 @@ use std::{
};
use task::TaskTemplates;
use url::Url;
use util::{ResultExt, paths::RemotePathBuf, rel_path::PathExt};
use util::{PathExt, ResultExt, paths::RemotePathBuf};
use wasm_host::{
WasmExtension, WasmHost,
wit::{is_supported_wasm_api_version, wasm_api_version_range},

View file

@ -432,7 +432,7 @@ impl ExtensionImports for WasmState {
self.on_main_thread(|cx| {
async move {
let path = location.as_ref().and_then(|location| {
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
});
let location = path
.as_ref()

View file

@ -595,7 +595,7 @@ impl HostWorktree for WasmState {
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?)
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Unix)?)
.await
.map_err(|error| error.to_string()))
}
@ -948,7 +948,7 @@ impl ExtensionImports for WasmState {
self.on_main_thread(|cx| {
async move {
let path = location.as_ref().and_then(|location| {
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
});
let location = path
.as_ref()

View file

@ -1272,7 +1272,11 @@ impl FileFinderDelegate {
let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
entry_path.project.path.clone()
} else {
worktree.read(cx).root_name().join(&entry_path.project.path)
worktree
.read(cx)
.root_name()
.join(&entry_path.project.path)
.into()
};
let mut components = full_path.components();
let filename = components.next_back().unwrap_or("");
@ -1841,7 +1845,7 @@ impl PickerDelegate for FileFinderDelegate {
.all(|worktree| {
worktree
.read(cx)
.entry_for_path(RelPath::unix(prefix.split_at(1).0).unwrap())
.entry_for_path(RelPath::from_unix_str(prefix.split_at(1).0).unwrap())
.is_none_or(|entry| !entry.is_dir())
})
{

View file

@ -4584,7 +4584,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
if let Some(path_match) = path_match.as_ref() {
search_entries
.history
.push(path_match.0.path_prefix.join(&path_match.0.path));
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
} else {
// This occurs when the query is empty and we show history matches
// that are outside the project.
@ -4597,7 +4597,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
Match::Search(path_match) => {
search_entries
.search
.push(path_match.0.path_prefix.join(&path_match.0.path));
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
search_entries.search_matches.push(path_match.0.clone());
}
Match::CreateNew(_) => {}

View file

@ -42,6 +42,7 @@ tempfile.workspace = true
text.workspace = true
time.workspace = true
util.workspace = true
path.workspace = true
is_executable = "1.0.5"
notify = "9.0.0-rc.4"
trash = { git = "https://github.com/zed-industries/trash-rs", rev = "47761739192828a66b11a94ba5420b82d63c03c5" }

View file

@ -61,7 +61,7 @@ use git::{
status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
};
#[cfg(feature = "test-support")]
use util::normalize_path;
use path::normalize_path;
#[cfg(feature = "test-support")]
use smol::io::AsyncReadExt;

View file

@ -233,7 +233,11 @@ fn generate_nucleo_path_candidates(
paths
.iter()
.map(|path| {
fuzzy_nucleo::PathMatchCandidate::new(RelPath::unix(path).unwrap(), false, None)
fuzzy_nucleo::PathMatchCandidate::new(
RelPath::from_unix_str(path).unwrap(),
false,
None,
)
})
.collect()
}
@ -245,7 +249,7 @@ fn generate_fuzzy_path_candidates(
.iter()
.map(|path| fuzzy::PathMatchCandidate {
is_dir: false,
path: RelPath::unix(path).unwrap(),
path: RelPath::from_unix_str(path).unwrap(),
char_bag: CharBag::from(path.as_str()),
})
.collect()
@ -302,7 +306,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
query,
case,
size,
PathStyle::Posix,
PathStyle::Unix,
)
},
BatchSize::SmallInput,
@ -325,7 +329,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
query,
false,
size,
PathStyle::Posix,
PathStyle::Unix,
)
},
BatchSize::SmallInput,

View file

@ -1517,7 +1517,7 @@ impl GitRepository for RealGitRepository {
let change = change?;
let path = change.path;
// git-show outputs `/`-delimited paths even on Windows.
let Some(rel_path) = RelPath::unix(path).log_err() else {
let Some(rel_path) = RelPath::from_unix_str(path).log_err() else {
continue;
};
@ -3744,6 +3744,16 @@ async fn run_git_command(
.env("GIT_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS", ask_pass.script_path())
.env("SSH_ASKPASS_REQUIRE", "force");
if !env.contains_key("GIT_CONFIG_COUNT")
&& let Some(gpg_wrapper) = ask_pass.gpg_wrapper_path()
{
command
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "gpg.program")
.env("GIT_CONFIG_VALUE_0", gpg_wrapper);
}
#[cfg(target_os = "windows")]
command.env("ZED_ASKPASS_SOCKET", ask_pass.socket_path());
let git_process = command.spawn()?;
@ -3793,7 +3803,7 @@ impl std::fmt::Debug for RepoPath {
impl RepoPath {
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> Result<Self> {
let rel_path = RelPath::unix(s.as_ref())?;
let rel_path = RelPath::from_unix_str(s.as_ref())?;
Ok(Self::from_rel_path(rel_path))
}
@ -3803,7 +3813,7 @@ impl RepoPath {
}
pub fn from_proto(proto: &str) -> Result<Self> {
let rel_path = RelPath::from_proto(proto)?;
let rel_path = RelPath::from_unix_str(proto)?.into();
Ok(Self(rel_path))
}
@ -3822,7 +3832,7 @@ impl RepoPath {
#[cfg(any(test, feature = "test-support"))]
pub fn repo_path<S: AsRef<str> + ?Sized>(s: &S) -> RepoPath {
RepoPath(RelPath::unix(s.as_ref()).unwrap().into())
RepoPath(RelPath::from_unix_str(s.as_ref()).unwrap().into())
}
impl AsRef<Arc<RelPath>> for RepoPath {

View file

@ -454,7 +454,7 @@ impl FromStr for GitStatus {
let status = entry.as_bytes()[0..2].try_into().unwrap();
let status = FileStatus::from_bytes(status).log_err()?;
// git-status outputs `/`-delimited repo paths, even on Windows.
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path).log_err()?);
Some((path, status))
})
.collect::<Vec<_>>();
@ -544,7 +544,7 @@ impl FromStr for TreeDiff {
let mut fields = s.split('\0');
let mut parsed = HashMap::default();
while let Some((status, path)) = fields.next().zip(fields.next()) {
let path = RepoPath::from_rel_path(RelPath::unix(path)?);
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path)?);
let mut fields = status.split(" ").skip(2);
let old_sha = fields

View file

@ -0,0 +1,243 @@
use crate::commit_view::CommitView;
use git::Oid;
use gpui::{Action, ClipboardItem, Entity, FocusHandle, SharedString, WeakEntity, Window, actions};
use project::{GIT_COMMAND_TASK_TAG, git_store::Repository};
use task::{TaskContext, TaskVariables, VariableName};
use ui::{Color, ContextMenu, ContextMenuEntry, IconName, IconPosition, prelude::*};
use workspace::Workspace;
actions!(
git_graph,
[
/// Copies the SHA of the selected commit to the clipboard.
CopyCommitSha,
/// Copies a tag from the selected commit to the clipboard.
CopyCommitTag,
/// Opens the commit view for the selected commit.
OpenCommitView,
]
);
const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.);
const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands";
pub(crate) struct CommitContextMenuData {
pub(crate) sha: Oid,
pub(crate) tag_names: Vec<SharedString>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum CommitContextMenuSource {
GitGraph,
GitPanel,
}
pub(crate) fn commit_context_menu(
commit: CommitContextMenuData,
source: CommitContextMenuSource,
ref_name: Option<SharedString>,
focus_handle: FocusHandle,
repository: Option<WeakEntity<Repository>>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
let sha = commit.sha;
let sha_short = sha.display_short();
let git_tasks = git_context_menu_tasks(
git_task_context(&repository, sha, ref_name.as_deref(), cx),
&workspace,
cx,
);
let header = match &ref_name {
Some(ref_name) => format!("Ref {ref_name}"),
None => format!("Commit {sha_short}"),
};
ContextMenu::build(window, cx, move |context_menu, _, _| {
context_menu
.context(focus_handle)
.header(header)
.entry("View Commit", Some(OpenCommitView.boxed_clone()), {
let repository = repository.clone();
let workspace = workspace.clone();
move |window, cx| {
let Some(repository) = repository.clone() else {
return;
};
CommitView::open(
sha.to_string(),
repository,
workspace.clone(),
None,
None,
window,
cx,
);
}
})
.entry(
"Copy SHA",
Some(CopyCommitSha.boxed_clone()),
move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(sha.to_string()));
},
)
.when_some(ref_name.clone(), |menu, ref_name| {
menu.entry("Copy Ref Name", None, move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(ref_name.to_string()));
})
})
.when(ref_name.is_none(), |menu| {
menu.map(|menu| {
let tag_names = commit.tag_names.clone();
let copy_tag_label = "Copy Tag";
match tag_names.as_slice() {
[] => menu.item(
ContextMenuEntry::new(copy_tag_label)
.action(CopyCommitTag.boxed_clone())
.disabled(true),
),
[tag_name] => {
let tag_name = tag_name.clone();
let label = format!("{copy_tag_label}: {tag_name}");
menu.entry(
label,
Some(CopyCommitTag.boxed_clone()),
move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
tag_name.to_string(),
));
},
)
}
_ => menu.submenu(copy_tag_label, move |menu, _window, _cx| {
let mut menu = menu.fixed_width(COMMIT_TAG_LIST_WIDTH_IN_REMS.into());
for tag_name in tag_names.clone() {
let tag_name_to_copy = tag_name.clone();
menu = menu.entry(tag_name, None, move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
tag_name_to_copy.to_string(),
));
});
}
menu
}),
}
})
})
.when(source == CommitContextMenuSource::GitPanel, |menu| {
menu.entry("Show in Git Graph", None, move |window, cx| {
window.dispatch_action(
Box::new(crate::git_graph::OpenAtCommit {
sha: sha.to_string(),
}),
cx,
);
})
})
.map(|mut menu| {
menu = menu.separator().header("Custom Commands");
if git_tasks.is_empty() {
return menu.item(
ContextMenuEntry::new("Learn More")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler(|_window, cx| {
let docs_url =
release_channel::docs_url(CUSTOM_GIT_COMMANDS_DOCS_SLUG, cx);
cx.open_url(&docs_url);
}),
);
}
for (task_source_kind, resolved_task) in git_tasks {
let label = resolved_task.display_label().to_string();
let workspace = workspace.clone();
menu = menu.entry(label, None, move |window, cx| {
workspace
.update(cx, |workspace, cx| {
workspace.schedule_resolved_task(
task_source_kind.clone(),
resolved_task.clone(),
false,
window,
cx,
);
})
.ok();
});
}
menu
})
})
}
fn git_task_context(
repository: &Option<WeakEntity<Repository>>,
commit_sha: git::Oid,
ref_name: Option<&str>,
cx: &App,
) -> Option<TaskContext> {
let repository_path = repository
.as_ref()?
.upgrade()?
.read(cx)
.work_directory_abs_path
.to_path_buf();
let repository_name = repository_path
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string);
let mut task_variables = TaskVariables::from_iter([
(VariableName::GitSha, commit_sha.to_string()),
(VariableName::GitShaShort, commit_sha.display_short()),
(
VariableName::GitRepositoryPath,
repository_path.to_string_lossy().into_owned(),
),
]);
if let Some(repository_name) = repository_name {
task_variables.insert(VariableName::GitRepositoryName, repository_name);
}
if let Some(ref_name) = ref_name {
task_variables.insert(VariableName::GitRef, ref_name.to_string());
}
Some(TaskContext {
cwd: Some(repository_path),
task_variables,
..TaskContext::default()
})
}
fn git_context_menu_tasks(
task_context: Option<TaskContext>,
workspace: &WeakEntity<Workspace>,
cx: &App,
) -> Vec<(project::TaskSourceKind, task::ResolvedTask)> {
let Some(task_context) = task_context else {
return Vec::new();
};
let Some(workspace) = workspace.upgrade() else {
return Vec::new();
};
let project = workspace.read(cx).project().clone();
let task_inventory = project.read_with(cx, |project, cx| {
project.task_store().read(cx).task_inventory().cloned()
});
let Some(task_inventory) = task_inventory else {
return Vec::new();
};
task_inventory
.read(cx)
.resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, &task_context)
}

View file

@ -1007,7 +1007,7 @@ fn name_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
return repo_path.into_arc();
};
let synthetic = format!("{}/{}", file_name, repo_path.as_unix_str());
RelPath::unix(&synthetic)
RelPath::from_unix_str(&synthetic)
.map(|path| path.into_arc())
.unwrap_or_else(|_| repo_path.into_arc())
}
@ -1032,7 +1032,7 @@ fn tree_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
}
synthetic.push_str(component);
}
RelPath::unix(&synthetic)
RelPath::from_unix_str(&synthetic)
.map(|path| path.into_arc())
.unwrap_or_else(|_| repo_path.into_arc())
}

View file

@ -1,4 +1,6 @@
pub use crate::commit_context_menu::{CopyCommitSha, CopyCommitTag, OpenCommitView};
use crate::{
commit_context_menu::{CommitContextMenuData, CommitContextMenuSource, commit_context_menu},
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
commit_view::CommitView,
git_status_icon,
@ -17,9 +19,9 @@ use git::{
status::{FileStatus, StatusCode, TrackedStatus},
};
use gpui::{
Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength,
DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy,
Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DismissEvent,
DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy,
ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement,
UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*,
px, uniform_list,
@ -29,7 +31,7 @@ use markdown::{Markdown, MarkdownElement};
use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use picker::{Picker, PickerDelegate};
use project::{
GIT_COMMAND_TASK_TAG, ProjectPath, TaskSourceKind,
ProjectPath,
git_store::{
CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository,
RepositoryEvent, RepositoryId,
@ -47,12 +49,12 @@ use std::{
sync::{Arc, OnceLock},
time::{Duration, Instant},
};
use task::{ResolvedTask, TaskContext, TaskVariables, VariableName};
use theme::AccentColors;
use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};
use ui::{
Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, ContextMenuEntry, DiffStat,
Divider, HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing,
Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider,
HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing,
RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState,
TableRenderContext, TableResizeBehavior, Tooltip, WithScrollbar, bind_redistributable_columns,
prelude::*, redistribute_hidden_fractions, redistribute_hidden_widths,
@ -72,7 +74,6 @@ const LINE_WIDTH: Pixels = px(1.5);
const RESIZE_HANDLE_WIDTH: f32 = 8.0;
const COPIED_STATE_DURATION: Duration = Duration::from_secs(2);
const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.);
const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands";
const TREE_INDENT: f32 = 20.0;
const TABLE_COLUMN_COUNT: usize = 4;
const ROW_VERTICAL_PADDING: Pixels = px(4.0);
@ -580,12 +581,6 @@ actions!(
[
/// Opens the Git Graph Tab.
Open,
/// Copies the SHA of the selected commit to the clipboard.
CopyCommitSha,
/// Copies a tag from the selected commit to the clipboard.
CopyCommitTag,
/// Opens the commit view for the selected commit.
OpenCommitView,
/// Focuses the search field.
FocusSearch,
/// Focuses the next git graph tab stop.
@ -2467,91 +2462,6 @@ impl GitGraph {
self.copy_commit_tag(selected_entry_index, window, cx);
}
fn git_task_context(
&self,
commit_sha: Oid,
ref_name: Option<&str>,
cx: &App,
) -> Option<TaskContext> {
let repository_path = self
.get_repository(cx)?
.read(cx)
.work_directory_abs_path
.to_path_buf();
let repository_name = repository_path
.file_name()
.and_then(|name| name.to_str())
.map(ToString::to_string);
let mut task_variables = TaskVariables::from_iter([
(VariableName::GitSha, commit_sha.to_string()),
(VariableName::GitShaShort, commit_sha.display_short()),
(
VariableName::GitRepositoryPath,
repository_path.to_string_lossy().into_owned(),
),
]);
if let Some(repository_name) = repository_name {
task_variables.insert(VariableName::GitRepositoryName, repository_name);
}
if let Some(ref_name) = ref_name {
task_variables.insert(VariableName::GitRef, ref_name.to_string());
}
Some(TaskContext {
cwd: Some(repository_path),
task_variables,
..TaskContext::default()
})
}
fn git_context_menu_tasks(
&self,
task_context: &TaskContext,
cx: &App,
) -> Vec<(TaskSourceKind, ResolvedTask)> {
let Some(workspace) = self.workspace.upgrade() else {
return Vec::new();
};
let project = workspace.read(cx).project().clone();
let task_inventory = project.read_with(cx, |project, cx| {
project.task_store().read(cx).task_inventory().cloned()
});
let Some(task_inventory) = task_inventory else {
return Vec::new();
};
task_inventory
.read(cx)
.resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, task_context)
}
fn schedule_git_task(
&mut self,
task_source_kind: TaskSourceKind,
resolved_task: ResolvedTask,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.workspace
.update(cx, |workspace, cx| {
workspace.schedule_resolved_task(
task_source_kind,
resolved_task,
false,
window,
cx,
);
})
.ok();
}
fn deploy_entry_context_menu(
&mut self,
position: Point<Pixels>,
@ -2563,129 +2473,27 @@ impl GitGraph {
let Some(commit) = self.graph_data.commits.get(index) else {
return;
};
let sha = commit.data.sha;
let sha_short = sha.display_short();
let git_tasks = self
.git_task_context(sha, ref_name.as_deref(), cx)
.map(|task_context| self.git_context_menu_tasks(&task_context, cx))
.unwrap_or_default();
let header = match &ref_name {
Some(ref_name) => format!("Ref {ref_name}"),
None => format!("Commit {sha_short}"),
};
let focus_handle = self.focus_handle.clone();
let git_graph = cx.entity();
let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
context_menu
.context(focus_handle)
.header(header)
.entry(
"View Commit",
Some(OpenCommitView.boxed_clone()),
window.handler_for(&git_graph, move |this, window, cx| {
this.open_commit_view(index, window, cx);
}),
)
.entry(
"Copy SHA",
Some(CopyCommitSha.boxed_clone()),
window.handler_for(&git_graph, move |this, _window, cx| {
this.copy_commit_sha(index, cx);
}),
)
.when_some(ref_name.clone(), |menu, ref_name| {
menu.entry("Copy Ref Name", None, move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(ref_name.to_string()));
})
})
.when(ref_name.is_none(), |menu| {
menu.map(|menu| {
let tag_names = commit
.data
.tag_names()
.into_iter()
.map(|tag_name| SharedString::from(tag_name.to_string()))
.collect::<Vec<_>>();
let copy_tag_label = "Copy Tag";
match tag_names.as_slice() {
[] => menu.item(
ContextMenuEntry::new(copy_tag_label)
.action(CopyCommitTag.boxed_clone())
.disabled(true),
),
[tag_name] => {
let tag_name = tag_name.clone();
let label = format!("{copy_tag_label}: {tag_name}");
menu.entry(
label,
Some(CopyCommitTag.boxed_clone()),
move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
tag_name.to_string(),
));
},
)
}
_ => menu.submenu(copy_tag_label, move |menu, _window, _cx| {
let mut menu =
menu.fixed_width(COMMIT_TAG_LIST_WIDTH_IN_REMS.into());
for tag_name in tag_names.clone() {
let tag_name_to_copy = tag_name.clone();
menu = menu.entry(tag_name, None, move |_window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(
tag_name_to_copy.to_string(),
));
});
}
menu
}),
}
})
})
.map(|mut menu| {
menu = menu.separator().header("Custom Commands");
if git_tasks.is_empty() {
return menu.item(
ContextMenuEntry::new("Learn More")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler(|_window, cx| {
let docs_url = release_channel::docs_url(
CUSTOM_GIT_COMMANDS_DOCS_SLUG,
cx,
);
cx.open_url(&docs_url);
}),
);
}
for (task_source_kind, resolved_task) in git_tasks {
let label = resolved_task.display_label().to_string();
menu = menu.entry(
label,
None,
window.handler_for(&git_graph, move |this, window, cx| {
this.schedule_git_task(
task_source_kind.clone(),
resolved_task.clone(),
window,
cx,
);
}),
);
}
menu
})
});
let repository = self
.get_repository(cx)
.map(|repository| repository.downgrade());
let context_menu = commit_context_menu(
CommitContextMenuData {
sha: commit.data.sha,
tag_names: commit
.data
.tag_names()
.into_iter()
.map(|tag_name| SharedString::from(tag_name.to_string()))
.collect(),
},
CommitContextMenuSource::GitGraph,
ref_name,
self.focus_handle.clone(),
repository,
self.workspace.clone(),
window,
cx,
);
self.set_context_menu(context_menu, position, Some(index), window, cx);
}
@ -4946,7 +4754,9 @@ mod tests {
use git::repository::{CommitData, InitialGraphCommitData};
use gpui::{TestAppContext, UpdateGlobal};
use project::git_store::{GitStoreEvent, RepositoryEvent};
use project::{Project, TaskSourceKind, task_store::TaskSettingsLocation};
use project::{
GIT_COMMAND_TASK_TAG, Project, TaskSourceKind, task_store::TaskSettingsLocation,
};
use rand::prelude::*;
use serde_json::json;
use settings::{SettingsStore, ThemeSettingsContent};

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@ use crate::{
mod askpass_modal;
pub mod branch_diff;
pub mod branch_picker;
mod commit_context_menu;
mod commit_modal;
pub mod commit_tooltip;
pub mod commit_view;

View file

@ -94,7 +94,7 @@ fn register_entry(
RelPath::new(rel, PathStyle::local())
.map(|r| r.into_owned().into())
.unwrap_or_else(|_| {
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
.unwrap()
.into_owned()
.into()
@ -105,10 +105,10 @@ fn register_entry(
.new_path
.file_name()
.and_then(|n| n.to_str())
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok())
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Unix).ok())
.map(|r| r.into_owned().into())
.unwrap_or_else(|| {
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
.unwrap()
.into_owned()
.into()

View file

@ -800,6 +800,9 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn show_window_menu(&self, _position: Point<Pixels>) {}
fn start_window_move(&self) {}
fn start_window_resize(&self, _edge: ResizeEdge) {}
fn set_exclusive_zone(&self, _zone: Pixels) {}
#[cfg(all(target_os = "linux", feature = "wayland"))]
fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
fn window_decorations(&self) -> Decorations {
Decorations::Server

View file

@ -1997,6 +1997,24 @@ impl Window {
self.platform_window.request_decorations(decorations);
}
/// Set the exclusive zone for a layer-shell surface: how much screen space it
/// reserves so other surfaces avoid occluding it (e.g. a panel reserving space).
/// Positive values reserve that distance from the anchored edge, 0 lets the
/// surface be moved out of others' exclusive zones, and -1 ignores reserved
/// space and may extend under other surfaces. (Wayland layer-shell windows only)
pub fn set_exclusive_zone(&self, zone: Pixels) {
self.platform_window.set_exclusive_zone(zone);
}
/// Set which anchored edge a layer-shell surface's exclusive zone applies to.
/// This is only needed to disambiguate a corner-anchored surface; otherwise the
/// edge is deduced from the anchor. The edge must be a single edge the surface
/// is anchored to, or it is ignored. (Wayland layer-shell windows only)
#[cfg(all(target_os = "linux", feature = "wayland"))]
pub fn set_exclusive_edge(&self, edge: crate::layer_shell::Anchor) {
self.platform_window.set_exclusive_edge(edge);
}
/// Start a window resize operation (Wayland)
pub fn start_window_resize(&self, edge: ResizeEdge) {
self.platform_window.start_window_resize(edge);

View file

@ -36,8 +36,10 @@ use gpui::{
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, Tiling,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls,
WindowDecorations, WindowKind, WindowParams, layer_shell::LayerShellNotSupportedError,
popup::PopupOptions, px, size,
WindowDecorations, WindowKind, WindowParams,
layer_shell::{Anchor, LayerShellNotSupportedError},
popup::PopupOptions,
px, size,
};
use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig, wgpu};
@ -183,12 +185,12 @@ impl WaylandSurfaceState {
}
if let Some(exclusive_edge) = options.exclusive_edge {
layer_surface
.set_exclusive_edge(super::layer_shell::wayland_anchor(exclusive_edge));
Self::apply_exclusive_edge(&layer_surface, options.anchor, exclusive_edge);
}
return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState {
layer_surface,
anchor: options.anchor,
}));
}
@ -298,6 +300,7 @@ pub struct WaylandXdgSurfaceState {
pub struct WaylandLayerSurfaceState {
layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
anchor: Anchor,
}
pub struct WaylandPopupSurfaceState {
@ -452,6 +455,49 @@ impl WaylandSurfaceState {
}
}
fn set_exclusive_zone(&self, zone: i32) -> bool {
if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) =
self
{
layer_surface.set_exclusive_zone(zone);
true
} else {
false
}
}
/// An exclusive edge must be a single edge that the surface is anchored to,
/// otherwise the compositor raises a fatal `invalid_exclusive_edge` protocol
/// error. An invalid edge is logged and ignored. Returns whether it applied.
fn apply_exclusive_edge(
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
anchor: Anchor,
edge: Anchor,
) -> bool {
if edge.bits().count_ones() == 1 && anchor.contains(edge) {
layer_surface.set_exclusive_edge(super::layer_shell::wayland_anchor(edge));
true
} else {
log::warn!(
"ignoring exclusive edge {edge:?}: must be a single edge of the surface anchor {anchor:?}"
);
false
}
}
fn set_exclusive_edge(&self, edge: Anchor) -> bool {
if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState {
layer_surface,
anchor,
..
}) = self
{
Self::apply_exclusive_edge(layer_surface, *anchor, edge)
} else {
false
}
}
fn destroy(&mut self) {
match self {
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
@ -470,7 +516,7 @@ impl WaylandSurfaceState {
toplevel.destroy();
xdg_surface.destroy();
}
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => {
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
layer_surface.destroy();
}
WaylandSurfaceState::Popup(WaylandPopupSurfaceState {
@ -1739,6 +1785,27 @@ impl PlatformWindow for WaylandWindow {
}
}
fn set_exclusive_zone(&self, zone: Pixels) {
let state = self.borrow();
if state
.surface_state
.set_exclusive_zone(f32::from(zone) as i32)
{
// Commit to apply it immediately, otherwise it only takes effect
// on the next frame.
state.surface.commit();
}
}
fn set_exclusive_edge(&self, edge: Anchor) {
let state = self.borrow();
if state.surface_state.set_exclusive_edge(edge) {
// Commit to apply it immediately, otherwise it only takes effect
// on the next frame.
state.surface.commit();
}
}
fn set_input_region(&self, region: Option<&[Bounds<Pixels>]>) {
let state = self.borrow();
match region {

View file

@ -12,11 +12,10 @@ use windows::Win32::{
Foundation::{FILETIME, LPARAM, WPARAM},
Media::{timeBeginPeriod, timeEndPeriod},
System::Threading::{
CloseThreadpoolTimer, CloseThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWork,
GetCurrentThread, PTP_CALLBACK_INSTANCE, PTP_TIMER, PTP_WORK, SetThreadPriority,
SetThreadpoolTimer, SubmitThreadpoolWork, THREAD_PRIORITY_TIME_CRITICAL,
CloseThreadpoolTimer, CreateThreadpoolTimer, GetCurrentThread, PTP_CALLBACK_INSTANCE,
PTP_TIMER, SetThreadPriority, SetThreadpoolTimer, THREAD_PRIORITY_TIME_CRITICAL,
TP_CALLBACK_ENVIRON_V3, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH,
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL,
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL, TrySubmitThreadpoolCallback,
},
UI::WindowsAndMessaging::PostMessageW,
};
@ -66,11 +65,8 @@ impl WindowsDispatcher {
let context = runnable.into_raw().as_ptr() as *mut c_void;
unsafe {
if let Ok(work) =
CreateThreadpoolWork(Some(run_work_callback), Some(context), Some(&environ))
{
SubmitThreadpoolWork(work);
}
TrySubmitThreadpoolCallback(Some(run_work_callback), Some(context), Some(&environ))
.log_err();
}
}
@ -179,11 +175,9 @@ impl PlatformDispatcher for WindowsDispatcher {
unsafe extern "system" fn run_work_callback(
_instance: PTP_CALLBACK_INSTANCE,
context: *mut c_void,
work: PTP_WORK,
) {
let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) };
WindowsDispatcher::execute_runnable(runnable);
unsafe { CloseThreadpoolWork(work) };
}
unsafe extern "system" fn run_timer_callback(

View file

@ -586,7 +586,7 @@ impl Item for ImageView {
}
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
let mut path = image.file.path().clone();
let mut path = image.file.path().to_rel_path_buf();
if project.visible_worktrees(cx).count() > 1
&& let Some(worktree) = project.worktree_for_id(image.project_path(cx).worktree_id, cx)
{

View file

@ -1 +1,76 @@
pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};
use gpui::SharedString;
use lsp::{DiagnosticSeverity, NumberOrString};
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A diagnostic associated with a certain range of a buffer.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
/// The name of the service that produced this diagnostic.
pub source: Option<String>,
/// The ID provided by the dynamic registration that produced this diagnostic.
pub registration_id: Option<SharedString>,
/// A machine-readable code that identifies this diagnostic.
pub code: Option<NumberOrString>,
pub code_description: Option<lsp::Uri>,
/// Whether this diagnostic is a hint, warning, or error.
pub severity: DiagnosticSeverity,
/// The human-readable message associated with this diagnostic.
pub message: String,
/// The human-readable message (in markdown format)
pub markdown: Option<String>,
/// An id that identifies the group to which this diagnostic belongs.
///
/// When a language server produces a diagnostic with
/// one or more associated diagnostics, those diagnostics are all
/// assigned a single group ID.
pub group_id: usize,
/// Whether this diagnostic is the primary diagnostic for its group.
///
/// In a given group, the primary diagnostic is the top-level diagnostic
/// returned by the language server. The non-primary diagnostics are the
/// associated diagnostics.
pub is_primary: bool,
/// Whether this diagnostic is considered to originate from an analysis of
/// files on disk, as opposed to any unsaved buffer contents. This is a
/// property of a given diagnostic source, and is configured for a given
/// language server via the `LspAdapter::disk_based_diagnostic_sources` method
/// for the language server.
pub is_disk_based: bool,
/// Whether this diagnostic marks unnecessary code.
pub is_unnecessary: bool,
/// Quick separation of diagnostics groups based by their source.
pub source_kind: DiagnosticSourceKind,
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
pub data: Option<Value>,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticSourceKind {
Pulled,
Pushed,
Other,
}
impl Default for Diagnostic {
fn default() -> Self {
Self {
source: Default::default(),
source_kind: DiagnosticSourceKind::Other,
code: None,
code_description: None,
severity: DiagnosticSeverity::ERROR,
message: Default::default(),
markdown: None,
group_id: 0,
is_primary: false,
is_disk_based: false,
is_unnecessary: false,
underline: true,
data: None,
registration_id: None,
}
}
}

View file

@ -39,7 +39,10 @@ use futures::lock::OwnedMutexGuard;
use gpui::{App, AsyncApp, Entity};
use http_client::HttpClient;
pub use language_core::highlight_map::{HighlightId, HighlightMap};
pub use language_core::{
SymbolKind,
highlight_map::{HighlightId, HighlightMap},
};
use futures::future::FutureExt as _;
pub use language_core::{
@ -48,10 +51,10 @@ pub use language_core::{
DecreaseIndentConfig, Grammar, GrammarId, HighlightsConfig, IndentConfig, InjectionConfig,
InjectionPatternConfig, JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride,
LanguageId, LanguageMatcher, OrderedListConfig, OutlineConfig, Override, OverrideConfig,
OverrideEntry, PromptResponseContext, RedactionConfig, RunnableCapture, RunnableConfig,
SoftWrap, Symbol, TaskListConfig, TextObject, TextObjectConfig, ToLspPosition,
WrapCharactersConfig, auto_indent_using_last_non_empty_line_default, deserialize_regex,
deserialize_regex_vec, regex_json_schema, regex_vec_json_schema, serialize_regex,
OverrideEntry, RedactionConfig, RunnableCapture, RunnableConfig, SoftWrap, Symbol,
TaskListConfig, TextObject, TextObjectConfig, WrapCharactersConfig, default_true,
deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema,
serialize_regex,
};
pub use language_registry::{
LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,
@ -216,6 +219,69 @@ pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
))
});
pub fn symbol_kind_to_lsp(kind: SymbolKind) -> lsp::SymbolKind {
match kind {
SymbolKind::File => lsp::SymbolKind::FILE,
SymbolKind::Module => lsp::SymbolKind::MODULE,
SymbolKind::Namespace => lsp::SymbolKind::NAMESPACE,
SymbolKind::Package => lsp::SymbolKind::PACKAGE,
SymbolKind::Class => lsp::SymbolKind::CLASS,
SymbolKind::Method => lsp::SymbolKind::METHOD,
SymbolKind::Property => lsp::SymbolKind::PROPERTY,
SymbolKind::Field => lsp::SymbolKind::FIELD,
SymbolKind::Constructor => lsp::SymbolKind::CONSTRUCTOR,
SymbolKind::Enum => lsp::SymbolKind::ENUM,
SymbolKind::Interface => lsp::SymbolKind::INTERFACE,
SymbolKind::Function => lsp::SymbolKind::FUNCTION,
SymbolKind::Variable => lsp::SymbolKind::VARIABLE,
SymbolKind::Constant => lsp::SymbolKind::CONSTANT,
SymbolKind::String => lsp::SymbolKind::STRING,
SymbolKind::Number => lsp::SymbolKind::NUMBER,
SymbolKind::Boolean => lsp::SymbolKind::BOOLEAN,
SymbolKind::Array => lsp::SymbolKind::ARRAY,
SymbolKind::Object => lsp::SymbolKind::OBJECT,
SymbolKind::Key => lsp::SymbolKind::KEY,
SymbolKind::Null => lsp::SymbolKind::NULL,
SymbolKind::EnumMember => lsp::SymbolKind::ENUM_MEMBER,
SymbolKind::Struct => lsp::SymbolKind::STRUCT,
SymbolKind::Event => lsp::SymbolKind::EVENT,
SymbolKind::Operator => lsp::SymbolKind::OPERATOR,
SymbolKind::TypeParameter => lsp::SymbolKind::TYPE_PARAMETER,
}
}
pub fn lsp_to_symbol_kind(kind: lsp::SymbolKind) -> SymbolKind {
match kind {
lsp::SymbolKind::FILE => SymbolKind::File,
lsp::SymbolKind::MODULE => SymbolKind::Module,
lsp::SymbolKind::NAMESPACE => SymbolKind::Namespace,
lsp::SymbolKind::PACKAGE => SymbolKind::Package,
lsp::SymbolKind::CLASS => SymbolKind::Class,
lsp::SymbolKind::METHOD => SymbolKind::Method,
lsp::SymbolKind::PROPERTY => SymbolKind::Property,
lsp::SymbolKind::FIELD => SymbolKind::Field,
lsp::SymbolKind::CONSTRUCTOR => SymbolKind::Constructor,
lsp::SymbolKind::ENUM => SymbolKind::Enum,
lsp::SymbolKind::INTERFACE => SymbolKind::Interface,
lsp::SymbolKind::FUNCTION => SymbolKind::Function,
lsp::SymbolKind::VARIABLE => SymbolKind::Variable,
lsp::SymbolKind::CONSTANT => SymbolKind::Constant,
lsp::SymbolKind::STRING => SymbolKind::String,
lsp::SymbolKind::NUMBER => SymbolKind::Number,
lsp::SymbolKind::BOOLEAN => SymbolKind::Boolean,
lsp::SymbolKind::ARRAY => SymbolKind::Array,
lsp::SymbolKind::OBJECT => SymbolKind::Object,
lsp::SymbolKind::KEY => SymbolKind::Key,
lsp::SymbolKind::NULL => SymbolKind::Null,
lsp::SymbolKind::ENUM_MEMBER => SymbolKind::EnumMember,
lsp::SymbolKind::STRUCT => SymbolKind::Struct,
lsp::SymbolKind::EVENT => SymbolKind::Event,
lsp::SymbolKind::OPERATOR => SymbolKind::Operator,
lsp::SymbolKind::TYPE_PARAMETER => SymbolKind::TypeParameter,
_ => SymbolKind::Null,
}
}
/// Commands that the client (editor) handles locally rather than forwarding
/// to the language server. Servers embed these in code lens and code action
/// responses when they want the editor to perform a well-known UI action.
@ -233,6 +299,17 @@ pub struct Location {
pub range: Range<Anchor>,
}
/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
/// This allows adapters to intercept preference selections (like "Always" or "Never")
/// and potentially persist them to Zed's settings.
#[derive(Debug, Clone)]
pub struct PromptResponseContext {
/// The original message shown to the user
pub message: String,
/// The action (button) the user selected
pub selected_action: lsp::MessageActionItem,
}
type ServerBinaryCache = futures::lock::Mutex<Option<(bool, LanguageServerBinary)>>;
type DownloadableLanguageServerBinary = LocalBoxFuture<'static, Result<LanguageServerBinary>>;
pub type LanguageServerBinaryLocations = LocalBoxFuture<

View file

@ -11,17 +11,16 @@ path = "src/language_core.rs"
anyhow.workspace = true
collections.workspace = true
gpui_shared_string.workspace = true
gpui_util.workspace = true
log.workspace = true
parking_lot.workspace = true
path.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
toml.workspace = true
tree-sitter.workspace = true
util.workspace = true
gpui_util.workspace = true
lsp-types.workspace = true
[features]
test-support = []

View file

@ -1,10 +1,74 @@
use crate::highlight_map::HighlightId;
use std::ops::Range;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum SymbolKind {
File,
Module,
Namespace,
Package,
Class,
Method,
Property,
Field,
Constructor,
Enum,
Interface,
Function,
Variable,
Constant,
String,
Number,
Boolean,
Array,
Object,
Key,
Null,
EnumMember,
Struct,
Event,
Operator,
TypeParameter,
}
impl SymbolKind {
pub fn from_proto(i32: i32) -> Self {
match i32 {
1 => SymbolKind::File,
2 => SymbolKind::Module,
3 => SymbolKind::Namespace,
4 => SymbolKind::Package,
5 => SymbolKind::Class,
6 => SymbolKind::Method,
7 => SymbolKind::Property,
8 => SymbolKind::Field,
9 => SymbolKind::Constructor,
10 => SymbolKind::Enum,
11 => SymbolKind::Interface,
12 => SymbolKind::Function,
13 => SymbolKind::Variable,
14 => SymbolKind::Constant,
15 => SymbolKind::String,
16 => SymbolKind::Number,
17 => SymbolKind::Boolean,
18 => SymbolKind::Array,
19 => SymbolKind::Object,
20 => SymbolKind::Key,
21 => SymbolKind::Null,
22 => SymbolKind::EnumMember,
23 => SymbolKind::Struct,
24 => SymbolKind::Event,
25 => SymbolKind::Operator,
26 => SymbolKind::TypeParameter,
_ => SymbolKind::Null,
}
}
}
#[derive(Debug, Clone)]
pub struct Symbol {
pub name: String,
pub kind: lsp_types::SymbolKind,
pub kind: SymbolKind,
pub container_name: Option<String>,
}

View file

@ -1,76 +0,0 @@
use gpui_shared_string::SharedString;
use lsp_types::{DiagnosticSeverity, NumberOrString};
use serde::{Deserialize, Serialize};
use serde_json::Value;
/// A diagnostic associated with a certain range of a buffer.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
/// The name of the service that produced this diagnostic.
pub source: Option<String>,
/// The ID provided by the dynamic registration that produced this diagnostic.
pub registration_id: Option<SharedString>,
/// A machine-readable code that identifies this diagnostic.
pub code: Option<NumberOrString>,
pub code_description: Option<lsp_types::Uri>,
/// Whether this diagnostic is a hint, warning, or error.
pub severity: DiagnosticSeverity,
/// The human-readable message associated with this diagnostic.
pub message: String,
/// The human-readable message (in markdown format)
pub markdown: Option<String>,
/// An id that identifies the group to which this diagnostic belongs.
///
/// When a language server produces a diagnostic with
/// one or more associated diagnostics, those diagnostics are all
/// assigned a single group ID.
pub group_id: usize,
/// Whether this diagnostic is the primary diagnostic for its group.
///
/// In a given group, the primary diagnostic is the top-level diagnostic
/// returned by the language server. The non-primary diagnostics are the
/// associated diagnostics.
pub is_primary: bool,
/// Whether this diagnostic is considered to originate from an analysis of
/// files on disk, as opposed to any unsaved buffer contents. This is a
/// property of a given diagnostic source, and is configured for a given
/// language server via the `LspAdapter::disk_based_diagnostic_sources` method
/// for the language server.
pub is_disk_based: bool,
/// Whether this diagnostic marks unnecessary code.
pub is_unnecessary: bool,
/// Quick separation of diagnostics groups based by their source.
pub source_kind: DiagnosticSourceKind,
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
pub data: Option<Value>,
/// Whether to underline the corresponding text range in the editor.
pub underline: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticSourceKind {
Pulled,
Pushed,
Other,
}
impl Default for Diagnostic {
fn default() -> Self {
Self {
source: Default::default(),
source_kind: DiagnosticSourceKind::Other,
code: None,
code_description: None,
severity: DiagnosticSeverity::ERROR,
message: Default::default(),
markdown: None,
group_id: 0,
is_primary: false,
is_disk_based: false,
is_unnecessary: false,
underline: true,
data: None,
registration_id: None,
}
}
}

View file

@ -5,7 +5,6 @@ use regex::Regex;
use schemars::{JsonSchema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true;
/// Controls the soft-wrapping behavior in the editor.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -46,7 +45,7 @@ pub struct LanguageConfig {
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
#[serde(default = "default_true")]
pub auto_indent_using_last_non_empty_line: bool,
// Whether indentation of pasted content should be adjusted based on the context.
#[serde(default)]
@ -166,7 +165,7 @@ impl Default for LanguageConfig {
grammar: None,
matcher: LanguageMatcher::default(),
brackets: Default::default(),
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
auto_indent_using_last_non_empty_line: default_true(),
auto_indent_on_paste: None,
increase_indent_pattern: Default::default(),
decrease_indent_pattern: Default::default(),
@ -480,7 +479,7 @@ pub struct WrapCharactersConfig {
pub end_suffix: String,
}
pub fn auto_indent_using_last_non_empty_line_default() -> bool {
pub fn default_true() -> bool {
true
}

View file

@ -1,12 +1,10 @@
// language_core: tree-sitter grammar infrastructure, LSP adapter traits,
// language configuration, and highlight mapping.
pub mod diagnostic;
pub mod grammar;
pub mod highlight_map;
pub mod language_config;
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
pub use grammar::{
BracketsConfig, BracketsPatternConfig, DebugVariablesConfig, DebuggerTextObject, Grammar,
GrammarId, HighlightsConfig, IndentConfig, InjectionConfig, InjectionPatternConfig,
@ -17,9 +15,9 @@ pub use highlight_map::{HighlightId, HighlightMap};
pub use language_config::{
BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, DecreaseIndentConfig,
JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig,
auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec,
regex_json_schema, regex_vec_json_schema, serialize_regex,
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig, default_true,
deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema,
serialize_regex,
};
pub mod code_label;
@ -29,11 +27,9 @@ pub mod manifest;
pub mod queries;
pub mod toolchain;
pub use code_label::{CodeLabel, CodeLabelBuilder, Symbol};
pub use code_label::{CodeLabel, CodeLabelBuilder, Symbol, SymbolKind};
pub use language_name::{LanguageId, LanguageName};
pub use lsp_adapter::{
BinaryStatus, LanguageServerStatusUpdate, PromptResponseContext, ServerHealth, ToLspPosition,
};
pub use lsp_adapter::{BinaryStatus, LanguageServerStatusUpdate, ServerHealth};
pub use manifest::ManifestName;
pub use queries::{LanguageQueries, QUERY_FILENAME_PREFIXES};
pub use toolchain::{Toolchain, ToolchainList, ToolchainMetadata, ToolchainScope};

View file

@ -1,23 +1,6 @@
use gpui_shared_string::SharedString;
use serde::{Deserialize, Serialize};
/// Converts a value into an LSP position.
pub trait ToLspPosition {
/// Converts the value into an LSP position.
fn to_lsp_position(self) -> lsp_types::Position;
}
/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
/// This allows adapters to intercept preference selections (like "Always" or "Never")
/// and potentially persist them to Zed's settings.
#[derive(Debug, Clone)]
pub struct PromptResponseContext {
/// The original message shown to the user
pub message: String,
/// The action (button) the user selected
pub selected_action: lsp_types::MessageActionItem,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LanguageServerStatusUpdate {
Binary(BinaryStatus),

View file

@ -7,7 +7,7 @@
use std::{path::Path, sync::Arc};
use gpui_shared_string::SharedString;
use util::rel_path::RelPath;
use path::rel_path::RelPath;
use crate::{LanguageName, ManifestName};

View file

@ -476,7 +476,7 @@ impl LspAdapter for ExtensionLspAdapter {
container_name,
}| extension::Symbol {
name,
kind: lsp_symbol_kind_to_extension(kind),
kind: symbol_kind_to_extension(kind),
container_name,
},
)
@ -636,35 +636,34 @@ fn lsp_insert_text_format_to_extension(
}
}
fn lsp_symbol_kind_to_extension(value: lsp::SymbolKind) -> extension::SymbolKind {
fn symbol_kind_to_extension(value: language::SymbolKind) -> extension::SymbolKind {
match value {
lsp::SymbolKind::FILE => extension::SymbolKind::File,
lsp::SymbolKind::MODULE => extension::SymbolKind::Module,
lsp::SymbolKind::NAMESPACE => extension::SymbolKind::Namespace,
lsp::SymbolKind::PACKAGE => extension::SymbolKind::Package,
lsp::SymbolKind::CLASS => extension::SymbolKind::Class,
lsp::SymbolKind::METHOD => extension::SymbolKind::Method,
lsp::SymbolKind::PROPERTY => extension::SymbolKind::Property,
lsp::SymbolKind::FIELD => extension::SymbolKind::Field,
lsp::SymbolKind::CONSTRUCTOR => extension::SymbolKind::Constructor,
lsp::SymbolKind::ENUM => extension::SymbolKind::Enum,
lsp::SymbolKind::INTERFACE => extension::SymbolKind::Interface,
lsp::SymbolKind::FUNCTION => extension::SymbolKind::Function,
lsp::SymbolKind::VARIABLE => extension::SymbolKind::Variable,
lsp::SymbolKind::CONSTANT => extension::SymbolKind::Constant,
lsp::SymbolKind::STRING => extension::SymbolKind::String,
lsp::SymbolKind::NUMBER => extension::SymbolKind::Number,
lsp::SymbolKind::BOOLEAN => extension::SymbolKind::Boolean,
lsp::SymbolKind::ARRAY => extension::SymbolKind::Array,
lsp::SymbolKind::OBJECT => extension::SymbolKind::Object,
lsp::SymbolKind::KEY => extension::SymbolKind::Key,
lsp::SymbolKind::NULL => extension::SymbolKind::Null,
lsp::SymbolKind::ENUM_MEMBER => extension::SymbolKind::EnumMember,
lsp::SymbolKind::STRUCT => extension::SymbolKind::Struct,
lsp::SymbolKind::EVENT => extension::SymbolKind::Event,
lsp::SymbolKind::OPERATOR => extension::SymbolKind::Operator,
lsp::SymbolKind::TYPE_PARAMETER => extension::SymbolKind::TypeParameter,
_ => extension::SymbolKind::Other(extract_int(value)),
language::SymbolKind::File => extension::SymbolKind::File,
language::SymbolKind::Module => extension::SymbolKind::Module,
language::SymbolKind::Namespace => extension::SymbolKind::Namespace,
language::SymbolKind::Package => extension::SymbolKind::Package,
language::SymbolKind::Class => extension::SymbolKind::Class,
language::SymbolKind::Method => extension::SymbolKind::Method,
language::SymbolKind::Property => extension::SymbolKind::Property,
language::SymbolKind::Field => extension::SymbolKind::Field,
language::SymbolKind::Constructor => extension::SymbolKind::Constructor,
language::SymbolKind::Enum => extension::SymbolKind::Enum,
language::SymbolKind::Interface => extension::SymbolKind::Interface,
language::SymbolKind::Function => extension::SymbolKind::Function,
language::SymbolKind::Variable => extension::SymbolKind::Variable,
language::SymbolKind::Constant => extension::SymbolKind::Constant,
language::SymbolKind::String => extension::SymbolKind::String,
language::SymbolKind::Number => extension::SymbolKind::Number,
language::SymbolKind::Boolean => extension::SymbolKind::Boolean,
language::SymbolKind::Array => extension::SymbolKind::Array,
language::SymbolKind::Object => extension::SymbolKind::Object,
language::SymbolKind::Key => extension::SymbolKind::Key,
language::SymbolKind::Null => extension::SymbolKind::Null,
language::SymbolKind::EnumMember => extension::SymbolKind::EnumMember,
language::SymbolKind::Struct => extension::SymbolKind::Struct,
language::SymbolKind::Event => extension::SymbolKind::Event,
language::SymbolKind::Operator => extension::SymbolKind::Operator,
language::SymbolKind::TypeParameter => extension::SymbolKind::TypeParameter,
}
}

View file

@ -300,43 +300,43 @@ impl super::LspAdapter for CLspAdapter {
) -> Option<CodeLabel> {
let name = &symbol.name;
let (text, filter_range, display_range) = match symbol.kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
language::SymbolKind::Method | language::SymbolKind::Function => {
let text = format!("void {} () {{}}", name);
let filter_range = 0..name.len();
let display_range = 5..5 + name.len();
(text, filter_range, display_range)
}
lsp::SymbolKind::STRUCT => {
language::SymbolKind::Struct => {
let text = format!("struct {} {{}}", name);
let filter_range = 7..7 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::ENUM => {
language::SymbolKind::Enum => {
let text = format!("enum {} {{}}", name);
let filter_range = 5..5 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
language::SymbolKind::Interface | language::SymbolKind::Class => {
let text = format!("class {} {{}}", name);
let filter_range = 6..6 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::CONSTANT => {
language::SymbolKind::Constant => {
let text = format!("const int {} = 0;", name);
let filter_range = 10..10 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::MODULE => {
language::SymbolKind::Module => {
let text = format!("namespace {} {{}}", name);
let filter_range = 10..10 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::TYPE_PARAMETER => {
language::SymbolKind::TypeParameter => {
let text = format!("typename {} {{}};", name);
let filter_range = 9..9 + name.len();
let display_range = 0..filter_range.end;

View file

@ -286,7 +286,7 @@ impl LspAdapter for EsLintLspAdapter {
let file_path = requested_file_path
.as_ref()
.and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok())
.and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
.and_then(|p| RelPath::from_unix_str(&p).ok().map(ToOwned::to_owned))
.unwrap_or_else(|| RelPath::empty().to_owned());
let override_options = cx.update(|cx| {
language_server_settings_for(

View file

@ -394,43 +394,43 @@ impl LspAdapter for GoLspAdapter {
) -> Option<CodeLabel> {
let name = &symbol.name;
let (text, filter_range, display_range) = match symbol.kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
language::SymbolKind::Method | language::SymbolKind::Function => {
let text = format!("func {} () {{}}", name);
let filter_range = 5..5 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::STRUCT => {
language::SymbolKind::Struct => {
let text = format!("type {} struct {{}}", name);
let filter_range = 5..5 + name.len();
let display_range = 0..text.len();
(text, filter_range, display_range)
}
lsp::SymbolKind::INTERFACE => {
language::SymbolKind::Interface => {
let text = format!("type {} interface {{}}", name);
let filter_range = 5..5 + name.len();
let display_range = 0..text.len();
(text, filter_range, display_range)
}
lsp::SymbolKind::CLASS => {
language::SymbolKind::Class => {
let text = format!("type {} T", name);
let filter_range = 5..5 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::CONSTANT => {
language::SymbolKind::Constant => {
let text = format!("const {} = nil", name);
let filter_range = 6..6 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::VARIABLE => {
language::SymbolKind::Variable => {
let text = format!("var {} = nil", name);
let filter_range = 4..4 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::MODULE => {
language::SymbolKind::Module => {
let text = format!("package {}", name);
let filter_range = 8..8 + name.len();
let display_range = 0..filter_range.end;

View file

@ -52,8 +52,12 @@ impl ContextProvider for JsonTaskProvider {
let Some(file) = project::File::from_dyn(file).cloned() else {
return Task::ready(None);
};
let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap());
let is_composer_json = file.path.ends_with(RelPath::unix("composer.json").unwrap());
let is_package_json = file
.path
.ends_with(RelPath::from_unix_str("package.json").unwrap());
let is_composer_json = file
.path
.ends_with(RelPath::from_unix_str("composer.json").unwrap());
if !is_package_json && !is_composer_json {
return Task::ready(None);
}

View file

@ -90,14 +90,14 @@ impl ManifestProvider for PyprojectTomlManifestProvider {
let mut outermost_workspace_root = None;
for path in path.ancestors().take(depth) {
let pyproject_path = path.join(RelPath::unix("pyproject.toml").unwrap());
let pyproject_path = path.join(RelPath::from_unix_str("pyproject.toml").unwrap());
if delegate.exists(&pyproject_path, Some(false)) {
if innermost_pyproject.is_none() {
innermost_pyproject = Some(Arc::from(path));
}
let has_lockfile = WORKSPACE_LOCKFILES.iter().any(|lockfile| {
let lockfile_path = path.join(RelPath::unix(lockfile).unwrap());
let lockfile_path = path.join(RelPath::from_unix_str(lockfile).unwrap());
delegate.exists(&lockfile_path, Some(false))
});
if has_lockfile {
@ -220,19 +220,19 @@ fn label_for_python_symbol(
) -> Option<language::CodeLabel> {
let name = &symbol.name;
let (text, filter_range, display_range) = match symbol.kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
language::SymbolKind::Method | language::SymbolKind::Function => {
let text = format!("def {}():\n", name);
let filter_range = 4..4 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::CLASS => {
language::SymbolKind::Class => {
let text = format!("class {}:", name);
let filter_range = 6..6 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
lsp::SymbolKind::CONSTANT => {
language::SymbolKind::Constant => {
let text = format!("{} = 0", name);
let filter_range = 0..name.len();
let display_range = 0..filter_range.end;
@ -669,7 +669,7 @@ impl LspAdapter for PyrightLspAdapter {
// If we have a detected toolchain, configure Pyright to use it - unless the user sets it themselves.
let should_insert_toolchain = || {
user_settings.as_object().is_none_or(|object| {
[
![
"venvPath",
"venv",
"python",
@ -1114,7 +1114,7 @@ impl PythonContextProvider {
fn python_module_name_from_relative_path(relative_path: &str) -> Option<String> {
let rel_path = RelPath::new(relative_path.as_ref(), PathStyle::local()).ok()?;
let path_with_dots = rel_path.display(PathStyle::Posix).replace('/', ".");
let path_with_dots = rel_path.display(PathStyle::Unix).replace('/', ".");
Some(
path_with_dots
.strip_suffix(".py")
@ -2112,10 +2112,10 @@ impl LspAdapter for BasedPyrightLspAdapter {
.and_then(|s| s.settings.clone())
.unwrap_or_default();
// If we have a detected toolchain, configure Pyright to use it
// If we have a detected toolchain, configure BasedPyright to use it - unless the user sets it themselves.
let should_insert_toolchain = || {
user_settings.as_object().is_none_or(|object| {
[
![
"venvPath",
"venv",
"python",
@ -3265,7 +3265,7 @@ mod tests {
});
let provider = PyprojectTomlManifestProvider;
provider.search(ManifestQuery {
path: RelPath::unix(query_path).unwrap().into(),
path: RelPath::from_unix_str(query_path).unwrap().into(),
depth: 10,
delegate,
})
@ -3274,7 +3274,7 @@ mod tests {
#[test]
fn test_simple_project_no_lockfile() {
let result = search(&["project/pyproject.toml"], "project/src/main.py");
assert_eq!(result.as_deref(), RelPath::unix("project").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("project").ok());
}
#[test]
@ -3287,7 +3287,7 @@ mod tests {
],
"packages/subproject/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3296,7 +3296,7 @@ mod tests {
&["pyproject.toml", "poetry.lock", "libs/mylib/pyproject.toml"],
"libs/mylib/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3309,7 +3309,7 @@ mod tests {
],
"packages/mypackage/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3318,13 +3318,19 @@ mod tests {
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
"project-a/src/main.py",
);
assert_eq!(result_a.as_deref(), RelPath::unix("project-a").ok());
assert_eq!(
result_a.as_deref(),
RelPath::from_unix_str("project-a").ok()
);
let result_b = search(
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
"project-b/src/main.py",
);
assert_eq!(result_b.as_deref(), RelPath::unix("project-b").ok());
assert_eq!(
result_b.as_deref(),
RelPath::from_unix_str("project-b").ok()
);
}
#[test]
@ -3345,7 +3351,7 @@ mod tests {
],
"packages/sub/src/main.py",
);
assert_eq!(result.as_deref(), RelPath::unix("").ok());
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
}
#[test]
@ -3360,11 +3366,16 @@ mod tests {
// "deep/nested/src/main.py", "deep/nested/src", and "deep/nested"
// It won't reach "deep" or root ""
let result = provider.search(ManifestQuery {
path: RelPath::unix("deep/nested/src/main.py").unwrap().into(),
path: RelPath::from_unix_str("deep/nested/src/main.py")
.unwrap()
.into(),
depth: 3,
delegate,
});
assert_eq!(result.as_deref(), RelPath::unix("deep/nested").ok());
assert_eq!(
result.as_deref(),
RelPath::from_unix_str("deep/nested").ok()
);
}
}
}

View file

@ -292,7 +292,7 @@ impl ManifestProvider for CargoManifestProvider {
) -> Option<Arc<RelPath>> {
let mut outermost_cargo_toml = None;
for path in path.ancestors().take(depth) {
let p = path.join(RelPath::unix("Cargo.toml").unwrap());
let p = path.join(RelPath::from_unix_str("Cargo.toml").unwrap());
if delegate.exists(&p, Some(false)) {
outermost_cargo_toml = Some(Arc::from(path));
}
@ -622,15 +622,15 @@ impl LspAdapter for RustLspAdapter {
) -> Option<CodeLabel> {
let name = &symbol.name;
let (prefix, suffix) = match symbol.kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", "();"),
lsp::SymbolKind::STRUCT => ("struct ", ";"),
lsp::SymbolKind::ENUM => ("enum ", "{}"),
lsp::SymbolKind::INTERFACE => ("trait ", "{}"),
lsp::SymbolKind::CONSTANT => ("const ", ":()=();"),
lsp::SymbolKind::MODULE => ("mod ", ";"),
lsp::SymbolKind::PACKAGE => ("extern crate ", ";"),
lsp::SymbolKind::TYPE_PARAMETER => ("type ", "=();"),
lsp::SymbolKind::ENUM_MEMBER => {
language::SymbolKind::Method | language::SymbolKind::Function => ("fn ", "();"),
language::SymbolKind::Struct => ("struct ", ";"),
language::SymbolKind::Enum => ("enum ", "{}"),
language::SymbolKind::Interface => ("trait ", "{}"),
language::SymbolKind::Constant => ("const ", ":()=();"),
language::SymbolKind::Module => ("mod ", ";"),
language::SymbolKind::Package => ("extern crate ", ";"),
language::SymbolKind::TypeParameter => ("type ", "=();"),
language::SymbolKind::EnumMember => {
let prefix = "enum E {";
return Some(CodeLabel::new(
name.to_string(),
@ -1972,7 +1972,7 @@ mod tests {
.label_for_symbol(
&language::Symbol {
name: "hello".to_string(),
kind: lsp::SymbolKind::FUNCTION,
kind: language::SymbolKind::Function,
container_name: None,
},
&language
@ -1990,7 +1990,7 @@ mod tests {
.label_for_symbol(
&language::Symbol {
name: "World".to_string(),
kind: lsp::SymbolKind::TYPE_PARAMETER,
kind: language::SymbolKind::TypeParameter,
container_name: None,
},
&language
@ -2008,7 +2008,7 @@ mod tests {
.label_for_symbol(
&language::Symbol {
name: "zed".to_string(),
kind: lsp::SymbolKind::PACKAGE,
kind: language::SymbolKind::Package,
container_name: None,
},
&language
@ -2026,7 +2026,7 @@ mod tests {
.label_for_symbol(
&language::Symbol {
name: "Variant".to_string(),
kind: lsp::SymbolKind::ENUM_MEMBER,
kind: language::SymbolKind::EnumMember,
container_name: None,
},
&language

View file

@ -625,7 +625,9 @@ impl TypeScriptLspAdapter {
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
let is_yarn = adapter
.read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
.read_text_file(
RelPath::from_unix_str(".yarn/sdks/typescript/lib/typescript.js").unwrap(),
)
.await
.is_ok();

View file

@ -49,7 +49,7 @@ impl PathKey {
} else {
Self {
sort_prefix: None,
path: RelPath::unix(&buffer.entity_id().to_string())
path: RelPath::from_unix_str(&buffer.entity_id().to_string())
.unwrap()
.into_arc(),
}

View file

@ -68,7 +68,7 @@ impl OpenPathDelegate {
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
prompt_root: match path_style {
PathStyle::Posix => "/".to_string(),
PathStyle::Unix => "/".to_string(),
PathStyle::Windows => "C:\\".to_string(),
},
path_style,
@ -158,7 +158,7 @@ impl OpenPathDelegate {
fn current_dir(&self) -> &'static str {
match self.path_style {
PathStyle::Posix => "./",
PathStyle::Unix => "./",
PathStyle::Windows => ".\\",
}
}
@ -929,7 +929,7 @@ fn path_candidates(
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
match path_style {
PathStyle::Posix => {
PathStyle::Unix => {
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
(query[..index].to_string(), query[index + 1..].to_string())
} else {
@ -1028,39 +1028,39 @@ mod tests {
#[test]
fn test_get_dir_and_suffix_with_posix_style() {
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Unix);
assert_eq!(dir, "/");
assert_eq!(suffix, "Use");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Docum");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/");
assert_eq!(suffix, "Documents");
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Unix);
assert_eq!(dir, "/Users/Junkui/Documents/");
assert_eq!(suffix, "");
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, ".");
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, "..");
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Unix);
assert_eq!(dir, "/root/");
assert_eq!(suffix, ".hidden");
}

23
crates/path/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "path"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lib]
path = "src/path.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
dunce.workspace = true
serde = { workspace = true, optional = true }
[dev-dependencies]
tempfile.workspace = true
[lints]
workspace = true

1
crates/path/LICENSE-APACHE Symbolic link
View file

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

286
crates/path/src/abs_path.rs Normal file
View file

@ -0,0 +1,286 @@
use std::{
borrow::{Borrow, Cow},
fmt, io,
ops::Deref,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use anyhow::Context;
use crate::{PathStyle, rel_path::RelPath};
// An absolute path on the user's local filesystem.
// Requires paths to be valid utf-8
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
#[repr(transparent)]
pub struct AbsPath(Path);
impl AbsPath {
pub fn new(path: &Path) -> anyhow::Result<&Self> {
if !path.is_absolute() {
return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
}
if path.to_str().is_none() {
return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
}
Ok(Self::new_unchecked(path))
}
fn new_unchecked(path: &Path) -> &Self {
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { &*(path as *const Path as *const Self) }
}
pub fn to_abs_path_buf(&self) -> AbsPathBuf {
AbsPathBuf(self.0.to_owned())
}
pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
AbsPathBuf(self.0.join(name.as_ref()))
}
pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
AbsPathBuf(self.0.join(relative_path.as_std_path()))
}
pub fn parent(&self) -> Option<&AbsPath> {
let parent = self.0.parent()?;
Some(AbsPath::new_unchecked(parent))
}
pub fn starts_with(&self, other: &AbsPath) -> bool {
self.0.starts_with(&other.0)
}
pub fn ends_with(&self, other: &RelPath) -> bool {
self.0.ends_with(other.as_std_path())
}
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if self == ancestor {
return false;
}
self.starts_with(ancestor)
}
pub fn file_name(&self) -> Option<&str> {
self.0.file_name()?.to_str()
}
pub fn display(&self) -> impl fmt::Display + '_ {
self.0.display()
}
pub fn as_std_path(&self) -> &Path {
&self.0
}
pub fn as_str(&self) -> &str {
self.0
.to_str()
.expect("valid UTF-8 enforced in constructor")
}
pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
}
pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
let prefix = self.0.strip_prefix(&prefix.0).ok()?;
RelPath::new(prefix, PathStyle::local()).ok()
}
}
impl ToOwned for AbsPath {
type Owned = AbsPathBuf;
fn to_owned(&self) -> Self::Owned {
self.to_abs_path_buf()
}
}
impl AsRef<Path> for AbsPath {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl AsRef<AbsPath> for AbsPath {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<&AbsPath> for Arc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Arc<Path> = Arc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
}
}
impl From<&AbsPath> for Rc<AbsPath> {
fn from(path: &AbsPath) -> Self {
let arc: Rc<Path> = Rc::from(&path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
}
}
// An absolute path on the user's local filesystem.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AbsPathBuf(PathBuf);
impl AbsPathBuf {
pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
let path = path.into();
if let Err(e) = AbsPath::new(&path) {
return Err(e);
}
Ok(Self(path))
}
pub fn home_dir() -> anyhow::Result<Self> {
let home = std::env::home_dir().context("no home dir available")?;
Self::new(home)
}
/// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
/// are resolved, relative input is anchored on the current directory, and
/// on case-insensitive filesystems each component takes the casing stored
/// on disk. Unlike [`std::fs::canonicalize`], the result never uses
/// Windows extended-length (`\\?\`) syntax, which chokes tools the path
/// is later handed to (e.g. `git`).
///
/// Paths act as identity in several places (lock keys, watch-target
/// comparisons, persisted repository records), so canonicalize a path
/// where it enters the system whenever it may have been spelled by a user
/// or an external tool.
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
let canonical = dunce::canonicalize(path.as_ref())?;
Self::new(canonical).map_err(io::Error::other)
}
pub fn push(&mut self, name: &str) {
self.0.push(name);
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_test(path: &'static str) -> Self {
if cfg!(windows) {
Self::new(format!("C:{path}")).unwrap()
} else {
Self::new(path).unwrap()
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn abs_path(path: &str) -> AbsPathBuf {
if cfg!(windows) {
AbsPathBuf::new(format!("C:{path}")).unwrap()
} else {
AbsPathBuf::new(path).unwrap()
}
}
impl Deref for AbsPathBuf {
type Target = AbsPath;
fn deref(&self) -> &Self::Target {
AbsPath::new_unchecked(&self.0)
}
}
impl Borrow<AbsPath> for AbsPathBuf {
fn borrow(&self) -> &AbsPath {
self
}
}
impl AsRef<Path> for AbsPathBuf {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl AsRef<AbsPath> for AbsPathBuf {
fn as_ref(&self) -> &AbsPath {
self
}
}
impl From<AbsPathBuf> for PathBuf {
fn from(path: AbsPathBuf) -> PathBuf {
path.0
}
}
impl fmt::Display for AbsPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}
impl PartialEq<AbsPath> for AbsPathBuf {
fn eq(&self, other: &AbsPath) -> bool {
**self == *other
}
}
impl PartialEq<AbsPathBuf> for AbsPath {
fn eq(&self, other: &AbsPathBuf) -> bool {
*self == **other
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_restores_on_disk_spelling() {
let temp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(temp.path()).unwrap();
let dir = root.join("CamelCase");
std::fs::create_dir(&dir).unwrap();
let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
assert_eq!(canonical.as_std_path(), dir);
assert!(
!canonical.as_str().starts_with(r"\\?\"),
"canonical paths must not use Windows extended-length syntax: {canonical}"
);
// A differently-cased spelling addresses the same directory only on a
// case-insensitive filesystem; when it does, canonicalization must
// restore the stored casing.
let lowercased = root.join("camelcase");
if std::fs::metadata(&lowercased).is_ok() {
assert_eq!(
AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
dir,
"canonicalization should restore the on-disk casing"
);
}
}
#[test]
fn test_new_test_normalizes_rooted_paths() {
if cfg!(windows) {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"C:/test/project"
);
} else {
assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
assert_eq!(
AbsPathBuf::new_test("/test/project").as_str(),
"/test/project"
);
}
}
}

263
crates/path/src/path.rs Normal file
View file

@ -0,0 +1,263 @@
//! Relative path types for deltadb.
//!
//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be
//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)
//! format regardless of host platform.
//!
//! Adapted from Zed's `util::rel_path` module.
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use crate::rel_path::RelPath;
pub mod abs_path;
pub mod rel_path;
pub trait PathExt {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathStyle {
Unix,
Windows,
}
impl PathStyle {
#[cfg(target_os = "windows")]
pub const fn local() -> Self {
PathStyle::Windows
}
#[cfg(not(target_os = "windows"))]
pub const fn local() -> Self {
PathStyle::Unix
}
#[inline]
pub fn primary_separator(&self) -> &'static str {
match self {
PathStyle::Unix => "/",
PathStyle::Windows => "\\",
}
}
pub fn separators(&self) -> &'static [&'static str] {
match self {
PathStyle::Unix => &["/"],
PathStyle::Windows => &["\\", "/"],
}
}
pub fn separators_ch(&self) -> &'static [char] {
match self {
PathStyle::Unix => &['/'],
PathStyle::Windows => &['\\', '/'],
}
}
pub fn is_absolute(&self, path_like: &str) -> bool {
path_like.starts_with('/')
|| *self == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
pub fn is_windows(&self) -> bool {
*self == PathStyle::Windows
}
pub fn is_posix(&self) -> bool {
*self == PathStyle::Unix
}
pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
let right = right.as_ref().to_str()?;
if is_absolute(right, self) {
return None;
}
let left = left.as_ref().to_str()?;
if left.is_empty() {
Some(right.into())
} else {
Some(format!(
"{left}{}{right}",
if left.ends_with(self.primary_separator()) {
""
} else {
self.primary_separator()
}
))
}
}
pub fn join_path(
self,
left: impl AsRef<Path>,
right: impl AsRef<Path>,
) -> anyhow::Result<PathBuf> {
let left = left
.as_ref()
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let right = right.as_ref();
let right_string = right
.to_str()
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
let joined = self
.join(left, right_string)
.ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
Ok(PathBuf::from(self.normalize(&joined)))
}
pub fn normalize(self, path_like: &str) -> String {
match self {
PathStyle::Windows => crate::normalize_path(Path::new(path_like))
.to_string_lossy()
.into_owned(),
PathStyle::Unix => {
let is_absolute = path_like.starts_with('/');
let remainder = if is_absolute {
path_like.trim_start_matches('/')
} else {
path_like
};
let mut components = Vec::new();
for component in remainder.split(self.separators_ch()) {
match component {
"" | "." => {}
".." => {
if components
.last()
.is_some_and(|component| *component != "..")
{
components.pop();
} else if !is_absolute {
components.push(component);
}
}
component => components.push(component),
}
}
let normalized = components.join(self.primary_separator());
if is_absolute && normalized.is_empty() {
"/".to_string()
} else if is_absolute {
format!("/{normalized}")
} else {
normalized
}
}
}
}
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
let Some(pos) = path_like.rfind(self.primary_separator()) else {
return (None, path_like);
};
let filename_start = pos + self.primary_separator().len();
(
Some(&path_like[..filename_start]),
&path_like[filename_start..],
)
}
pub fn strip_prefix<'a>(
&self,
child: &'a Path,
parent: &'a Path,
) -> Option<std::borrow::Cow<'a, RelPath>> {
let parent = parent.to_str()?;
if parent.is_empty() {
return RelPath::new(child, *self).ok();
}
let parent = self
.separators()
.iter()
.find_map(|sep| parent.strip_suffix(sep))
.unwrap_or(parent);
let child = child.to_str()?;
// Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
let stripped = if self.is_windows()
&& child.as_bytes().get(1) == Some(&b':')
&& parent.as_bytes().get(1) == Some(&b':')
&& child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
{
child[2..].strip_prefix(&parent[2..])?
} else {
child.strip_prefix(parent)?
};
if let Some(relative) = self
.separators()
.iter()
.find_map(|sep| stripped.strip_prefix(sep))
{
RelPath::new(relative.as_ref(), *self).ok()
} else if stripped.is_empty() {
Some(Cow::Borrowed(RelPath::empty()))
} else {
None
}
}
}
fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
path_like.starts_with('/')
|| path_style == PathStyle::Windows
&& (path_like.starts_with('\\')
|| path_like
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
&& path_like[1..]
.strip_prefix(':')
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
}
/// Normalizes a path by resolving `.` and `..` components without
/// requiring the path to exist on disk (unlike `canonicalize`).
pub fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

View file

@ -1,6 +1,4 @@
use crate::paths::{PathStyle, is_absolute};
use anyhow::{Context as _, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::{
borrow::{Borrow, Cow},
fmt,
@ -9,6 +7,12 @@ use std::{
sync::Arc,
};
use crate::{
PathStyle,
abs_path::{AbsPath, AbsPathBuf},
is_absolute,
};
/// A file system path that is guaranteed to be relative and normalized.
///
/// This type can be used to represent paths in a uniform way, regardless of
@ -20,20 +24,22 @@ use std::{
///
/// Relative paths are also guaranteed to be valid unicode.
#[repr(transparent)]
#[derive(PartialEq, Eq, Hash, Serialize)]
#[derive(PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RelPath(str);
/// An owned representation of a file system path that is guaranteed to be
/// relative and normalized.
///
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
#[derive(PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RelPathBuf(String);
impl RelPath {
/// Creates an empty [`RelPath`].
pub fn empty() -> &'static Self {
Self::new_unchecked("")
Self::from_str("")
}
/// Creates an empty [`RelPath`].
@ -54,7 +60,7 @@ impl RelPath {
let mut path = path.to_str().context("non utf-8 path")?;
let (prefixes, suffixes): (&[_], &[_]) = match path_style {
PathStyle::Posix => (&["./"], &['/']),
PathStyle::Unix => (&["./"], &['/']),
PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
};
@ -77,7 +83,7 @@ impl RelPath {
}
let mut result = match string {
Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
Cow::Borrowed(string) => Cow::Borrowed(Self::from_str(string)),
Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
};
@ -95,7 +101,7 @@ impl RelPath {
return Err(anyhow!("path is not relative: {result:?}"));
}
}
other => normalized.push(RelPath::new_unchecked(other)),
other => normalized.push(RelPath::from_str(other)),
}
}
result = Cow::Owned(normalized)
@ -104,20 +110,25 @@ impl RelPath {
Ok(result)
}
#[track_caller]
pub fn new_test<'a>(path: &'a str) -> Cow<'a, Self> {
Self::new(Path::new(path), PathStyle::Unix).unwrap()
}
/// Converts a path that is already normalized and uses '/' separators
/// into a [`RelPath`] .
///
/// Returns an error if the path is not already in the correct format.
#[track_caller]
pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
pub fn from_unix_str<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
let path = path.as_ref();
match Self::new(path, PathStyle::Posix)? {
match Self::new(path, PathStyle::Unix)? {
Cow::Borrowed(path) => Ok(path),
Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
}
}
fn new_unchecked(s: &str) -> &Self {
fn from_str(s: &str) -> &Self {
// Safety: `RelPath` is a transparent wrapper around `str`.
unsafe { &*(s as *const str as *const Self) }
}
@ -131,7 +142,12 @@ impl RelPath {
}
pub fn ancestors(&self) -> RelPathAncestors<'_> {
RelPathAncestors(Some(&self.0))
RelPathAncestors {
full: &self.0,
front: self.0.len(),
back: 0,
done: false,
}
}
pub fn file_name(&self) -> Option<&str> {
@ -156,7 +172,22 @@ impl RelPath {
self.strip_prefix(other).is_ok()
}
/// Returns true if this path is a strict descendant of `ancestor`.
///
/// Unlike `starts_with`, this returns false when `self == ancestor`
/// and false when `ancestor` is empty (since every path trivially
/// starts with the empty prefix).
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
if ancestor.is_empty() || self == ancestor {
return false;
}
self.starts_with(ancestor)
}
pub fn ends_with(&self, other: &Self) -> bool {
if other.is_empty() {
return true;
}
if let Some(suffix) = self.0.strip_suffix(&other.0) {
if suffix.ends_with('/') {
return true;
@ -173,7 +204,7 @@ impl RelPath {
}
if let Some(suffix) = self.0.strip_prefix(&other.0) {
if let Some(suffix) = suffix.strip_prefix('/') {
return Ok(Self::new_unchecked(suffix));
return Ok(Self::from_str(suffix));
} else if suffix.is_empty() {
return Ok(Self::empty());
}
@ -182,7 +213,11 @@ impl RelPath {
}
pub fn len(&self) -> usize {
self.0.matches('/').count() + 1
if self.0.is_empty() {
0
} else {
self.0.matches('/').count() + 1
}
}
pub fn last_n_components(&self, count: usize) -> Option<&Self> {
@ -198,15 +233,14 @@ impl RelPath {
}
}
pub fn join(&self, other: &Self) -> Arc<Self> {
let result = if self.0.is_empty() {
Cow::Borrowed(&other.0)
pub fn join(&self, other: &Self) -> RelPathBuf {
if self.0.is_empty() {
other.to_rel_path_buf()
} else if other.0.is_empty() {
Cow::Borrowed(&self.0)
self.to_rel_path_buf()
} else {
Cow::Owned(format!("{}/{}", &self.0, &other.0))
};
Arc::from(Self::new_unchecked(result.as_ref()))
RelPathBuf(format!("{}/{}", &self.0, &other.0))
}
}
pub fn to_rel_path_buf(&self) -> RelPathBuf {
@ -217,23 +251,13 @@ impl RelPath {
Arc::from(self)
}
/// Convert the path into the wire representation.
pub fn to_proto(&self) -> String {
self.as_unix_str().to_owned()
}
/// Load the path from its wire representation.
pub fn from_proto(path: &str) -> Result<Arc<Self>> {
Ok(Arc::from(Self::unix(path)?))
}
/// Convert the path into a string with the given path style.
///
/// Whenever a path is presented to the user, it should be converted to
/// a string via this method.
pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
match style {
PathStyle::Posix => Cow::Borrowed(&self.0),
PathStyle::Unix => Cow::Borrowed(&self.0),
PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
PathStyle::Windows => Cow::Borrowed(&self.0),
}
@ -255,19 +279,16 @@ impl RelPath {
pub fn as_std_path(&self) -> &Path {
Path::new(&self.0)
}
/// Resolves this relative path against an absolute base path.
pub fn absolutize(&self, base: impl AsRef<AbsPath>) -> AbsPathBuf {
base.as_ref().join(self.as_unix_str())
}
}
#[derive(Debug)]
pub struct StripPrefixError;
impl std::fmt::Display for StripPrefixError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("prefix not found")
}
}
impl std::error::Error for StripPrefixError {}
impl ToOwned for RelPath {
type Owned = RelPathBuf;
@ -324,14 +345,33 @@ impl RelPathBuf {
}
pub fn push(&mut self, path: &RelPath) {
if path.is_empty() {
return;
}
if !self.is_empty() {
self.0.push('/');
}
self.0.push_str(&path.0);
}
pub fn push_component(&mut self, component: &str) -> Result<()> {
anyhow::ensure!(
!component.is_empty()
&& !component.contains('/')
&& component != "."
&& component != "..",
"invalid relative path component: {component:?}"
);
if !self.is_empty() {
self.0.push('/');
}
self.0.push_str(component);
Ok(())
}
pub fn as_rel_path(&self) -> &RelPath {
RelPath::new_unchecked(self.0.as_str())
RelPath::from_str(self.0.as_str())
}
pub fn set_extension(&mut self, extension: &str) -> bool {
@ -339,7 +379,7 @@ impl RelPathBuf {
let mut filename = PathBuf::from(filename);
filename.set_extension(extension);
self.pop();
self.0.push_str(filename.to_str().unwrap());
self.push(RelPath::from_str(filename.to_str().unwrap()));
true
} else {
false
@ -347,33 +387,21 @@ impl RelPathBuf {
}
}
impl<'de> Deserialize<'de> for RelPathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
let rel_path =
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
Ok(rel_path.into_owned())
impl PartialOrd for RelPathBuf {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Into<Arc<RelPath>> for RelPathBuf {
fn into(self) -> Arc<RelPath> {
Arc::from(self.as_rel_path())
impl Ord for RelPathBuf {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_rel_path().cmp(other.as_rel_path())
}
}
impl AsRef<Path> for RelPathBuf {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<Path> for RelPath {
fn as_ref(&self) -> &Path {
self.as_std_path()
impl From<RelPathBuf> for Arc<RelPath> {
fn from(value: RelPathBuf) -> Self {
Arc::from(value.as_rel_path())
}
}
@ -383,12 +411,24 @@ impl AsRef<RelPath> for RelPathBuf {
}
}
impl AsRef<std::path::Path> for RelPathBuf {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<RelPath> for RelPath {
fn as_ref(&self) -> &RelPath {
self
}
}
impl AsRef<std::path::Path> for RelPath {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl Deref for RelPathBuf {
type Target = RelPath;
@ -406,20 +446,37 @@ impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
impl From<&RelPath> for Arc<RelPath> {
fn from(rel_path: &RelPath) -> Self {
let bytes: Arc<str> = Arc::from(&rel_path.0);
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
}
}
impl<'a> TryFrom<&'a str> for &'a RelPath {
type Error = anyhow::Error;
fn try_from(s: &'a str) -> Result<Self> {
RelPath::from_unix_str(s)
}
}
impl TryFrom<&str> for RelPathBuf {
type Error = anyhow::Error;
fn try_from(s: &str) -> Result<Self> {
RelPath::new(Path::new(s), PathStyle::Unix).map(|cow| cow.into_owned())
}
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path(path: &str) -> &RelPath {
RelPath::unix(path).unwrap()
RelPath::from_unix_str(path).unwrap()
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path_buf(path: &str) -> RelPathBuf {
RelPath::unix(path).unwrap().to_rel_path_buf()
rel_path(path).to_owned()
}
impl PartialEq<str> for RelPath {
@ -428,26 +485,45 @@ impl PartialEq<str> for RelPath {
}
}
pub trait PathExt {
fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
impl PartialEq<RelPath> for RelPathBuf {
fn eq(&self, other: &RelPath) -> bool {
self.as_rel_path() == other
}
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
impl PartialEq<RelPathBuf> for RelPath {
fn eq(&self, other: &RelPathBuf) -> bool {
other.as_rel_path() == self
}
}
impl fmt::Display for RelPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl fmt::Display for RelPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Default)]
pub struct RelPathComponents<'a>(&'a str);
pub struct RelPathAncestors<'a>(Option<&'a str>);
pub struct RelPathAncestors<'a> {
full: &'a str,
front: usize,
back: usize,
done: bool,
}
const SEPARATOR: char = '/';
impl<'a> RelPathComponents<'a> {
pub fn rest(&self) -> &'a RelPath {
RelPath::new_unchecked(self.0)
RelPath::from_str(self.0)
}
}
@ -473,15 +549,35 @@ impl<'a> Iterator for RelPathAncestors<'a> {
type Item = &'a RelPath;
fn next(&mut self) -> Option<Self::Item> {
let result = self.0?;
if let Some(sep_ix) = result.rfind(SEPARATOR) {
self.0 = Some(&result[..sep_ix]);
} else if !result.is_empty() {
self.0 = Some("");
} else {
self.0 = None;
if self.done {
return None;
}
Some(RelPath::new_unchecked(result))
let result = &self.full[..self.front];
if self.front == self.back {
self.done = true;
} else {
self.front = result.rfind(SEPARATOR).unwrap_or_default();
}
Some(RelPath::from_str(result))
}
}
impl<'a> DoubleEndedIterator for RelPathAncestors<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
let result = &self.full[..self.back];
if self.front == self.back {
self.done = true;
} else {
let search_start = if self.back == 0 { 0 } else { self.back + 1 };
self.back = match self.full[search_start..].find(SEPARATOR) {
Some(sep_ix) => search_start + sep_ix,
None => self.full.len(),
};
}
Some(RelPath::from_str(result))
}
}
@ -501,11 +597,21 @@ impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
}
}
impl<'de> serde::Deserialize<'de> for RelPathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
let rel_path =
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
Ok(rel_path.into_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
use pretty_assertions::assert_matches;
#[test]
fn test_rel_path_new() {
@ -515,11 +621,11 @@ mod tests {
let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
assert_eq!(
RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
@ -528,29 +634,29 @@ mod tests {
rel_path("foo/baz/quux")
);
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Unix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Unix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
assert!(matches!(path, Cow::Owned(_)));
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
assert!(matches!(path, Cow::Borrowed(_)));
let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
assert!(matches!(path, Cow::Owned(_)));
}
#[test]
@ -590,6 +696,41 @@ mod tests {
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(RelPath::empty()));
assert_eq!(ancestors.next(), None);
let path = rel_path("foo/bar/baz");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(rel_path("")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar/baz")));
assert_eq!(ancestors.next_back(), None);
let path = rel_path("foo/bar/baz");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
assert_eq!(ancestors.next_back(), Some(rel_path("")));
assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next(), None);
assert_eq!(ancestors.next_back(), None);
let path = rel_path("foo");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
assert_eq!(ancestors.next_back(), None);
let path = RelPath::empty();
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), None);
let path = rel_path("über/x");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
assert_eq!(ancestors.next_back(), Some(rel_path("über")));
assert_eq!(ancestors.next_back(), Some(rel_path("über/x")));
assert_eq!(ancestors.next_back(), None);
}
#[test]
@ -602,13 +743,18 @@ mod tests {
#[test]
fn test_rel_path_partial_ord_is_compatible_with_std() {
let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
assert_eq!(
Path::new(lhs).cmp(Path::new(rhs)),
RelPath::unix(lhs)
.unwrap()
.cmp(&RelPath::unix(rhs).unwrap())
);
for (i, lhs) in test_cases.iter().enumerate() {
for rhs in &test_cases[i + 1..] {
assert_eq!(
Path::new(lhs).cmp(Path::new(rhs)),
RelPath::from_unix_str(lhs)
.unwrap()
.cmp(RelPath::from_unix_str(rhs).unwrap()),
"ordering mismatch for {:?} vs {:?}",
lhs,
rhs,
);
}
}
}
@ -621,19 +767,28 @@ mod tests {
assert_eq!(child.strip_prefix(parent).unwrap(), child);
}
#[test]
fn test_ends_with() {
assert!(rel_path("foo/bar").ends_with(rel_path("bar")));
assert!(rel_path("foo/bar").ends_with(rel_path("foo/bar")));
assert!(rel_path("foo/bar").ends_with(RelPath::empty()));
assert!(RelPath::empty().ends_with(RelPath::empty()));
assert!(!rel_path("foobar").ends_with(rel_path("bar")));
}
#[test]
fn test_rel_path_constructors_absolute_path() {
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Unix).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Unix).is_ok());
}
#[test]
fn test_pop() {
let mut path = rel_path("a/b").to_rel_path_buf();
let mut path = rel_path_buf("a/b");
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "a");
path.pop();
@ -641,4 +796,27 @@ mod tests {
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "");
}
#[test]
fn test_len() {
assert_eq!(RelPath::empty().len(), 0);
assert_eq!(rel_path("a").len(), 1);
assert_eq!(rel_path("a/b").len(), 2);
assert_eq!(rel_path("a/b/c").len(), 3);
}
#[test]
fn test_set_extension() {
let mut path = rel_path_buf("a/b/c.txt");
assert!(path.set_extension("rs"));
assert_eq!(path.as_rel_path().as_unix_str(), "a/b/c.rs");
let mut single = rel_path_buf("file.txt");
assert!(single.set_extension("md"));
assert_eq!(single.as_rel_path().as_unix_str(), "file.md");
let mut no_ext = rel_path_buf("a/b/c");
assert!(no_ext.set_extension("rs"));
assert_eq!(no_ext.as_rel_path().as_unix_str(), "a/b/c.rs");
}
}

View file

@ -68,7 +68,7 @@ static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Returns the relative path to the zed_server directory on the ssh host.
pub fn remote_server_dir_relative() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_server").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed_server").unwrap());
*CACHED
}
@ -76,7 +76,7 @@ pub fn remote_server_dir_relative() -> &'static RelPath {
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed_wsl_server").unwrap());
*CACHED
}
@ -496,21 +496,21 @@ pub fn local_vscode_folder_name() -> &'static str {
/// Returns the relative path to a `settings.json` file within a project.
pub fn local_settings_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/settings.json").unwrap());
*CACHED
}
/// Returns the relative path to a `tasks.json` file within a project.
pub fn local_tasks_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/tasks.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".vscode/tasks.json").unwrap());
*CACHED
}
@ -526,14 +526,14 @@ pub fn task_file_name() -> &'static str {
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".zed/debug.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/launch.json` file within a project.
pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap());
LazyLock::new(|| RelPath::from_unix_str(".vscode/launch.json").unwrap());
*CACHED
}

View file

@ -76,8 +76,6 @@ actions!(
SetPreviewHidden,
/// Opens the footer's actions menu.
ToggleActionsMenu,
/// Take the picker's content and open it in a multibuffer
ToMultiBuffer,
/// Toggles multi-select mode, in which clicking items adds them to
/// the selection instead of opening them
ToggleMultiSelect,

View file

@ -3,13 +3,11 @@
use std::sync::Arc;
use gpui::{
Action, AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText,
Task, Window, px,
AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText, Task,
Window, px,
};
use language::{Bias, Buffer, HighlightedText, HighlightedTextBuilder, ToPoint};
use picker::{
MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate, ToMultiBuffer,
};
use picker::{MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate};
use project::{Project, Symbol};
use rope::Point;
use settings::Settings;
@ -335,7 +333,7 @@ impl EditorPreview {
div()
.flex_1()
.overflow_hidden()
.child(self.editor_as_giant_button())
.child(self.occluded_editor())
.into_any_element()
}
}
@ -357,7 +355,7 @@ impl EditorPreview {
.child(content)
}
fn editor_as_giant_button(&self) -> impl IntoElement {
fn occluded_editor(&self) -> impl IntoElement {
div()
.relative()
.size_full()
@ -367,10 +365,7 @@ impl EditorPreview {
.id("picker-preview-editor")
.absolute()
.inset_0()
.occlude()
.on_click(|_, window, cx| {
window.dispatch_action(ToMultiBuffer.boxed_clone(), cx);
}),
.occlude(),
)
}
}

View file

@ -37,8 +37,8 @@ test-support = [
aho-corasick.workspace = true
anyhow.workspace = true
askpass.workspace = true
async-trait.workspace = true
async-channel.workspace = true
async-trait.workspace = true
base64.workspace = true
buffer_diff.workspace = true
circular-buffer.workspace = true
@ -60,15 +60,17 @@ globset.workspace = true
gpui.workspace = true
http_client = { workspace = true, features = ["github-download"] }
image.workspace = true
itertools.workspace = true
indexmap.workspace = true
itertools.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
path.workspace = true
paths.workspace = true
percent-encoding.workspace = true
postage.workspace = true
prettier.workspace = true
rand.workspace = true
@ -93,8 +95,8 @@ tempfile.workspace = true
terminal.workspace = true
text.workspace = true
toml.workspace = true
tracing.workspace = true
url.workspace = true
percent-encoding.workspace = true
util.workspace = true
watch.workspace = true
wax.workspace = true
@ -104,7 +106,6 @@ zed_credentials_provider.workspace = true
zeroize.workspace = true
zlog.workspace = true
ztracing.workspace = true
tracing.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View file

@ -312,7 +312,7 @@ impl RemoteBufferStore {
.request(proto::OpenBufferByPath {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
})
.await?;
let buffer_id = BufferId::new(response.buffer_id)?;

View file

@ -254,7 +254,7 @@ impl language::File for IndexTextFile {
rpc::proto::File {
worktree_id: self.worktree_id.to_proto(),
entry_id: None,
path: self.path.as_ref().to_proto(),
path: self.path.as_ref().as_unix_str().to_owned(),
mtime: None,
is_deleted: false,
is_historic: true,
@ -336,7 +336,7 @@ impl StatusEntry {
};
proto::StatusEntry {
repo_path: self.repo_path.to_proto(),
repo_path: self.repo_path.as_unix_str().to_owned(),
simple_status,
status: Some(status_to_proto(self.status)),
diff_stat_added: self.diff_stat.map(|ds| ds.added),
@ -3825,7 +3825,7 @@ impl GitStore {
.files
.into_iter()
.map(|file| proto::CommitFile {
path: file.path.to_proto(),
path: file.path.as_unix_str().to_owned(),
old_text: file.old_text,
new_text: file.new_text,
is_binary: file.is_binary,
@ -4023,7 +4023,7 @@ impl GitStore {
.entries
.into_iter()
.map(|(path, status)| proto::TreeDiffStatus {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
status: match status {
TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
TreeDiffStatus::Modified { .. } => {
@ -5109,7 +5109,7 @@ impl RepositorySnapshot {
.merge
.merge_heads_by_conflicted_path
.iter()
.map(|(repo_path, _)| repo_path.to_proto())
.map(|(repo_path, _)| repo_path.as_unix_str().to_owned())
.collect(),
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
@ -5165,13 +5165,13 @@ impl RepositorySnapshot {
current_new_entry = new_statuses.next();
}
Ordering::Greater => {
removed_statuses.push(old_entry.repo_path.to_proto());
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
current_old_entry = old_statuses.next();
}
}
}
(None, Some(old_entry)) => {
removed_statuses.push(old_entry.repo_path.to_proto());
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
current_old_entry = old_statuses.next();
}
(Some(new_entry), None) => {
@ -5196,7 +5196,7 @@ impl RepositorySnapshot {
.merge
.merge_heads_by_conflicted_path
.iter()
.map(|(path, _)| path.to_proto())
.map(|(path, _)| path.as_unix_str().to_owned())
.collect(),
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
project_id,
@ -6051,7 +6051,7 @@ impl Repository {
commit,
paths: paths
.into_iter()
.map(|p| p.to_proto())
.map(|p| p.as_unix_str().to_owned())
.collect(),
})
.await?;
@ -7013,7 +7013,9 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| {
repo_path.as_unix_str().to_owned()
})
.collect(),
})
.await
@ -7026,7 +7028,9 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| {
repo_path.as_unix_str().to_owned()
})
.collect(),
})
.await
@ -7157,7 +7161,7 @@ impl Repository {
repository_id: id.to_proto(),
paths: entries
.into_iter()
.map(|repo_path| repo_path.to_proto())
.map(|repo_path| repo_path.as_unix_str().to_owned())
.collect(),
})
.await?;
@ -7245,7 +7249,7 @@ impl Repository {
is_dir: bool,
) -> oneshot::Receiver<Result<()>> {
let work_dir = self.snapshot.work_directory_abs_path.clone();
let path_display = repo_path.as_ref().display(PathStyle::Posix);
let path_display = repo_path.as_ref().display(PathStyle::Unix);
let file_path_str = if is_dir {
format!("{}/", path_display)
} else {
@ -7279,7 +7283,7 @@ impl Repository {
is_dir: bool,
) -> oneshot::Receiver<Result<()>> {
let repository_dir = self.snapshot.repository_dir_abs_path.clone();
let path_display = repo_path.as_ref().display(PathStyle::Posix);
let path_display = repo_path.as_ref().display(PathStyle::Unix);
let file_path_str = if is_dir {
format!("{}/", path_display)
} else {
@ -7722,7 +7726,7 @@ impl Repository {
.request(proto::SetIndexText {
project_id: project_id.0,
repository_id: id.to_proto(),
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
text: content,
})
.await?;
@ -8406,7 +8410,7 @@ impl Repository {
};
Some((
RepoPath::from_rel_path(
&RelPath::from_proto(&entry.path).log_err()?,
RelPath::from_unix_str(&entry.path).log_err()?,
),
status,
))
@ -8735,7 +8739,7 @@ impl Repository {
.into_iter()
.filter_map(|path| {
Some(sum_tree::Edit::Remove(PathKey(
RelPath::from_proto(&path).log_err()?,
RelPath::from_unix_str(&path).log_err()?.into(),
)))
})
.chain(
@ -9326,7 +9330,7 @@ fn format_job_key(key: &GitJobKey) -> SharedString {
.iter()
.map(|p| {
let rel: &RelPath = p;
format!("{}", AsRef::<Path>::as_ref(rel).display())
rel.display(PathStyle::local())
})
.collect();
format!("WriteIndex({})", paths_str.join(", ")).into()
@ -9405,7 +9409,7 @@ pub fn worktrees_directory_for_repo(
let resolved = if path_style.is_posix() {
joined
} else {
util::normalize_path(&joined)
path::normalize_path(&joined)
};
let resolved = if resolved.starts_with(repository_anchor_path) {
resolved
@ -9662,7 +9666,9 @@ fn log_source_to_proto(log_source: &LogSource) -> proto::GitLogSource {
LogSource::All => proto::git_log_source::Source::All(proto::GitLogSourceAll {}),
LogSource::Branch(branch) => proto::git_log_source::Source::Branch(branch.to_string()),
LogSource::Sha(sha) => proto::git_log_source::Source::Sha(sha.to_string()),
LogSource::Path(path) => proto::git_log_source::Source::Path(path.to_proto()),
LogSource::Path(path) => {
proto::git_log_source::Source::Path(path.as_unix_str().to_owned())
}
}),
}
}
@ -10066,11 +10072,9 @@ mod tests {
fn test_new_worktree_path_uses_posix_style_for_remote_paths() {
let work_dir = Path::new("/home/user/dev/lsp-tests");
let directory =
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
let directory = PathStyle::Posix
.join_path(&directory, "nimble-sky")
.unwrap();
let path = PathStyle::Posix.join_path(&directory, "lsp-tests").unwrap();
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
let directory = PathStyle::Unix.join_path(&directory, "nimble-sky").unwrap();
let path = PathStyle::Unix.join_path(&directory, "lsp-tests").unwrap();
assert_eq!(
path,

View file

@ -698,7 +698,7 @@ impl ImageStoreImpl for Entity<RemoteImageStore> {
.request(rpc::proto::OpenImageByPath {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
})
.await?;

View file

@ -4,7 +4,7 @@ use crate::{
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor,
DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint,
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectPath,
ProjectTransaction, PulledDiagnostics, ResolveState,
lsp_store::{LocalLspStore, LspDocumentLink, LspFoldingRange, LspStore},
};
@ -19,7 +19,7 @@ use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext,
OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{InlayHintKind, LanguageSettings},
point_from_lsp, point_to_lsp,
lsp_to_symbol_kind, point_from_lsp, point_to_lsp,
proto::{
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
serialize_anchor_range, serialize_version,
@ -35,10 +35,9 @@ use lsp::{
use serde_json::Value;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
use std::{
cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
};
use std::{cmp::Reverse, collections::hash_map, ops::Range, path::Path, str::FromStr, sync::Arc};
use text::{BufferId, LineEnding};
use util::rel_path::RelPath;
use util::{ResultExt as _, debug_panic};
pub use signature_help::SignatureHelp;
@ -189,7 +188,17 @@ pub(crate) struct PerformRename {
#[derive(Debug, Clone, Copy)]
pub struct GetDefinitions {
pub position: PointUtf16,
pub workspace_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EditPredictionDefinition {
pub path: ProjectPath,
pub range: Range<Unclipped<PointUtf16>>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct GetEditPredictionDefinitions {
pub position: PointUtf16,
}
#[derive(Debug, Clone, Copy)]
@ -200,7 +209,11 @@ pub(crate) struct GetDeclarations {
#[derive(Debug, Clone, Copy)]
pub(crate) struct GetTypeDefinitions {
pub position: PointUtf16,
pub workspace_only: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct GetEditPredictionTypeDefinitions {
pub position: PointUtf16,
}
#[derive(Debug, Clone, Copy)]
@ -694,15 +707,7 @@ impl LspCommand for GetDefinitions {
server_id: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<LocationLink>> {
location_links_from_lsp(
message,
lsp_store,
buffer,
server_id,
self.workspace_only,
cx,
)
.await
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
@ -713,7 +718,6 @@ impl LspCommand for GetDefinitions {
&buffer.anchor_before(self.position),
)),
version: serialize_version(&buffer.version()),
workspace_only: self.workspace_only,
}
}
@ -734,7 +738,6 @@ impl LspCommand for GetDefinitions {
.await?;
Ok(Self {
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
workspace_only: message.workspace_only,
})
}
@ -764,6 +767,106 @@ impl LspCommand for GetDefinitions {
}
}
#[async_trait(?Send)]
impl LspCommand for GetEditPredictionDefinitions {
type Response = Vec<EditPredictionDefinition>;
type LspRequest = lsp::request::GotoDefinition;
type ProtoRequest = proto::GetEditPredictionDefinition;
fn display_name(&self) -> &str {
"Get edit prediction definition"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
capabilities
.server_capabilities
.definition_provider
.is_some_and(|capability| match capability {
OneOf::Left(supported) => supported,
OneOf::Right(_options) => true,
})
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::GotoDefinitionParams> {
Ok(lsp::GotoDefinitionParams {
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
}
async fn response_from_lsp(
self,
message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
_: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<EditPredictionDefinition>> {
edit_prediction_definitions_from_lsp(message, lsp_store, cx)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetEditPredictionDefinition {
proto::GetEditPredictionDefinition {
project_id,
buffer_id: buffer.remote_id().into(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetEditPredictionDefinition,
_: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
Ok(Self {
position: edit_prediction_position_from_proto(
message.position,
message.version,
buffer,
&mut cx,
)
.await?,
})
}
fn response_to_proto(
response: Vec<EditPredictionDefinition>,
_: &mut LspStore,
_: PeerId,
_: &clock::Global,
_: &mut App,
) -> proto::GetEditPredictionDefinitionResponse {
proto::GetEditPredictionDefinitionResponse {
definitions: edit_prediction_definitions_to_proto(response),
}
}
async fn response_from_proto(
self,
message: proto::GetEditPredictionDefinitionResponse,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Vec<EditPredictionDefinition>> {
edit_prediction_definitions_from_proto(message.definitions)
}
fn buffer_id_from_proto(message: &proto::GetEditPredictionDefinition) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for GetDeclarations {
type Response = Vec<LocationLink>;
@ -807,7 +910,7 @@ impl LspCommand for GetDeclarations {
server_id: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, lsp_store, buffer, server_id, false, cx).await
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration {
@ -909,7 +1012,7 @@ impl LspCommand for GetImplementations {
server_id: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, lsp_store, buffer, server_id, false, cx).await
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation {
@ -1008,7 +1111,7 @@ impl LspCommand for GetTypeDefinitions {
server_id: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, project, buffer, server_id, self.workspace_only, cx).await
location_links_from_lsp(message, project, buffer, server_id, cx).await
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
@ -1019,7 +1122,6 @@ impl LspCommand for GetTypeDefinitions {
&buffer.anchor_before(self.position),
)),
version: serialize_version(&buffer.version()),
workspace_only: self.workspace_only,
}
}
@ -1040,7 +1142,6 @@ impl LspCommand for GetTypeDefinitions {
.await?;
Ok(Self {
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
workspace_only: message.workspace_only,
})
}
@ -1070,6 +1171,103 @@ impl LspCommand for GetTypeDefinitions {
}
}
#[async_trait(?Send)]
impl LspCommand for GetEditPredictionTypeDefinitions {
type Response = Vec<EditPredictionDefinition>;
type LspRequest = lsp::request::GotoTypeDefinition;
type ProtoRequest = proto::GetEditPredictionTypeDefinition;
fn display_name(&self) -> &str {
"Get edit prediction type definition"
}
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
!matches!(
&capabilities.server_capabilities.type_definition_provider,
None | Some(lsp::TypeDefinitionProviderCapability::Simple(false))
)
}
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::GotoTypeDefinitionParams> {
Ok(lsp::GotoTypeDefinitionParams {
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
}
async fn response_from_lsp(
self,
message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
_: LanguageServerId,
cx: AsyncApp,
) -> Result<Vec<EditPredictionDefinition>> {
edit_prediction_definitions_from_lsp(message, lsp_store, cx)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetEditPredictionTypeDefinition {
proto::GetEditPredictionTypeDefinition {
project_id,
buffer_id: buffer.remote_id().into(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetEditPredictionTypeDefinition,
_: Entity<LspStore>,
buffer: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<Self> {
Ok(Self {
position: edit_prediction_position_from_proto(
message.position,
message.version,
buffer,
&mut cx,
)
.await?,
})
}
fn response_to_proto(
response: Vec<EditPredictionDefinition>,
_: &mut LspStore,
_: PeerId,
_: &clock::Global,
_: &mut App,
) -> proto::GetEditPredictionTypeDefinitionResponse {
proto::GetEditPredictionTypeDefinitionResponse {
definitions: edit_prediction_definitions_to_proto(response),
}
}
async fn response_from_proto(
self,
message: proto::GetEditPredictionTypeDefinitionResponse,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Vec<EditPredictionDefinition>> {
edit_prediction_definitions_from_proto(message.definitions)
}
fn buffer_id_from_proto(message: &proto::GetEditPredictionTypeDefinition) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
fn language_server_for_buffer(
lsp_store: &Entity<LspStore>,
buffer: &Entity<Buffer>,
@ -1165,57 +1363,13 @@ pub async fn location_links_from_lsp(
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
workspace_only: bool,
mut cx: AsyncApp,
) -> Result<Vec<LocationLink>> {
let message = match message {
Some(message) => message,
None => return Ok(Vec::new()),
};
let mut unresolved_links = Vec::new();
match message {
lsp::GotoDefinitionResponse::Scalar(loc) => {
unresolved_links.push((None, loc.uri, loc.range));
}
lsp::GotoDefinitionResponse::Array(locs) => {
unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
}
lsp::GotoDefinitionResponse::Link(links) => {
unresolved_links.extend(links.into_iter().map(|l| {
(
l.origin_selection_range,
l.target_uri,
l.target_selection_range,
)
}));
}
}
let unresolved_links = definition_locations_from_lsp(message);
let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
let mut definitions = Vec::new();
for (origin_range, target_uri, target_range) in unresolved_links {
if workspace_only
&& !lsp_store.update(&mut cx, |this, cx| {
use util::paths::UrlExt as _;
let worktree_store = this.worktree_store().read(cx);
let path_style = worktree_store.path_style();
let Ok(abs_path) = target_uri.clone().to_file_path_ext(path_style) else {
return false;
};
worktree_store
.find_worktree(&abs_path, cx)
.is_some_and(|(worktree, _)| {
let worktree = worktree.read(cx);
worktree.is_visible() && !worktree.is_single_file()
})
})
{
continue;
}
let target_buffer_handle = lsp_store
.update(&mut cx, |this, cx| {
this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
@ -1256,6 +1410,94 @@ pub async fn location_links_from_lsp(
Ok(definitions)
}
fn definition_locations_from_lsp(
message: Option<lsp::GotoDefinitionResponse>,
) -> Vec<(Option<lsp::Range>, lsp::Uri, lsp::Range)> {
let Some(message) = message else {
return Vec::new();
};
let mut locations = Vec::new();
match message {
lsp::GotoDefinitionResponse::Scalar(location) => {
locations.push((None, location.uri, location.range));
}
lsp::GotoDefinitionResponse::Array(locations_from_lsp) => {
locations.extend(
locations_from_lsp
.into_iter()
.map(|location| (None, location.uri, location.range)),
);
}
lsp::GotoDefinitionResponse::Link(links) => {
locations.extend(links.into_iter().map(|link| {
(
link.origin_selection_range,
link.target_uri,
link.target_selection_range,
)
}));
}
}
locations
}
fn edit_prediction_definitions_from_lsp(
message: Option<lsp::GotoDefinitionResponse>,
lsp_store: Entity<LspStore>,
mut cx: AsyncApp,
) -> Result<Vec<EditPredictionDefinition>> {
let unresolved_locations = definition_locations_from_lsp(message);
lsp_store.update(&mut cx, |lsp_store, cx| {
use util::paths::UrlExt as _;
let mut definitions = Vec::new();
let worktree_store = lsp_store.worktree_store().read(cx);
let path_style = worktree_store.path_style();
for (_, uri, range) in unresolved_locations {
let Ok(abs_path) = uri.to_file_path_ext(path_style) else {
continue;
};
let Some((worktree, relative_path)) = worktree_store.find_worktree(&abs_path, cx)
else {
continue;
};
let worktree = worktree.read(cx);
if !worktree.is_visible() || worktree.is_single_file() {
continue;
}
definitions.push(EditPredictionDefinition {
path: ProjectPath {
worktree_id: worktree.id(),
path: relative_path,
},
range: point_from_lsp(range.start)..point_from_lsp(range.end),
});
}
Ok(definitions)
})
}
async fn edit_prediction_position_from_proto(
position: Option<proto::Anchor>,
version: Vec<proto::VectorClockEntry>,
buffer: Entity<Buffer>,
cx: &mut AsyncApp,
) -> Result<PointUtf16> {
let position = position
.and_then(deserialize_anchor)
.context("invalid position")?;
buffer
.update(cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&version))
})
.await?;
Ok(buffer.read_with(cx, |buffer, _| position.to_point_utf16(buffer)))
}
pub async fn location_link_from_lsp(
link: lsp::LocationLink,
lsp_store: &Entity<LspStore>,
@ -1363,6 +1605,48 @@ pub fn location_link_to_proto(
}
}
fn edit_prediction_definitions_to_proto(
definitions: Vec<EditPredictionDefinition>,
) -> Vec<proto::EditPredictionDefinition> {
definitions
.into_iter()
.map(|definition| proto::EditPredictionDefinition {
worktree_id: definition.path.worktree_id.to_proto(),
path: definition.path.path.as_ref().as_unix_str().to_owned(),
start: Some(proto::PointUtf16 {
row: definition.range.start.0.row,
column: definition.range.start.0.column,
}),
end: Some(proto::PointUtf16 {
row: definition.range.end.0.row,
column: definition.range.end.0.column,
}),
})
.collect()
}
fn edit_prediction_definitions_from_proto(
definitions: Vec<proto::EditPredictionDefinition>,
) -> Result<Vec<EditPredictionDefinition>> {
definitions
.into_iter()
.map(|definition| {
let start = definition.start.context("missing definition start")?;
let end = definition.end.context("missing definition end")?;
Ok(EditPredictionDefinition {
path: ProjectPath {
worktree_id: worktree::WorktreeId::from_proto(definition.worktree_id),
path: RelPath::from_unix_str(&definition.path)
.context("invalid path")?
.into(),
},
range: Unclipped(PointUtf16::new(start.row, start.column))
..Unclipped(PointUtf16::new(end.row, end.column)),
})
})
.collect()
}
#[async_trait(?Send)]
impl LspCommand for GetReferences {
type Response = Vec<Location>;
@ -1749,7 +2033,7 @@ impl LspCommand for GetDocumentSymbols {
.into_iter()
.map(|lsp_symbol| DocumentSymbol {
name: lsp_symbol.name,
kind: lsp_symbol.kind,
kind: lsp_to_symbol_kind(lsp_symbol.kind),
range: range_from_lsp(lsp_symbol.location.range),
selection_range: range_from_lsp(lsp_symbol.location.range),
children: Vec::new(),
@ -1759,7 +2043,7 @@ impl LspCommand for GetDocumentSymbols {
fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
DocumentSymbol {
name: lsp_symbol.name,
kind: lsp_symbol.kind,
kind: lsp_to_symbol_kind(lsp_symbol.kind),
range: range_from_lsp(lsp_symbol.range),
selection_range: range_from_lsp(lsp_symbol.selection_range),
children: lsp_symbol
@ -1811,7 +2095,7 @@ impl LspCommand for GetDocumentSymbols {
fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
proto::DocumentSymbol {
name: symbol.name.clone(),
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
kind: symbol.kind as i32,
start: Some(proto::PointUtf16 {
row: symbol.range.start.0.row,
column: symbol.range.start.0.column,
@ -1854,8 +2138,7 @@ impl LspCommand for GetDocumentSymbols {
fn deserialize_symbol_with_children(
serialized_symbol: proto::DocumentSymbol,
) -> Result<DocumentSymbol> {
let kind =
unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
let kind = language::SymbolKind::from_proto(serialized_symbol.kind);
let start = serialized_symbol.start.context("invalid start")?;
let end = serialized_symbol.end.context("invalid end")?;

View file

@ -83,7 +83,7 @@ use language::{
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LineEndingSetting,
all_language_settings,
},
modeline, point_to_lsp,
lsp_to_symbol_kind, modeline, point_to_lsp,
proto::{
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
serialize_anchor_range, serialize_version,
@ -98,7 +98,7 @@ use lsp::{
FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer,
LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName,
LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf,
RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles,
RenameFilesParams, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles,
WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles,
};
use node_runtime::read_package_installed_version;
@ -4273,7 +4273,7 @@ struct CoreSymbol {
pub source_language_server_id: LanguageServerId,
pub path: SymbolLocation,
pub name: String,
pub kind: lsp::SymbolKind,
pub kind: language::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub container_name: Option<String>,
}
@ -4823,8 +4823,21 @@ impl LspStore {
let diagnostic_updates = local
.language_servers
.keys()
.cloned()
.iter()
.filter_map(|(server_id, state)| {
let supports_workspace_diagnostics = match state {
LanguageServerState::Running {
workspace_diagnostics_refresh_tasks,
..
} => !workspace_diagnostics_refresh_tasks.is_empty(),
_ => false,
};
if supports_workspace_diagnostics {
None
} else {
Some(*server_id)
}
})
.map(|server_id| DocumentDiagnosticsUpdate {
diagnostics: DocumentDiagnostics {
document_abs_path: buffer_abs_path.clone(),
@ -6171,31 +6184,9 @@ impl LspStore {
buffer: &Entity<Buffer>,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
self.definitions_with_filter(buffer, position, false, cx)
}
pub fn workspace_definitions(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
self.definitions_with_filter(buffer, position, true, cx)
}
fn definitions_with_filter(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
workspace_only: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = GetDefinitions {
position,
workspace_only,
};
let request = GetDefinitions { position };
if !self.is_capable_for_proto_request(buffer, &request, cx) {
return Task::ready(Ok(None));
}
@ -6220,11 +6211,7 @@ impl LspStore {
return Ok(None);
};
let actions = join_all(responses.payload.into_iter().map(|response| {
GetDefinitions {
position,
workspace_only,
}
.response_from_proto(
GetDefinitions { position }.response_from_proto(
response.response,
lsp_store.clone(),
buffer.clone(),
@ -6247,10 +6234,7 @@ impl LspStore {
let definitions_task = self.request_multiple_lsp_locally(
buffer,
Some(position),
GetDefinitions {
position,
workspace_only,
},
GetDefinitions { position },
cx,
);
cx.background_spawn(async move {
@ -6266,6 +6250,107 @@ impl LspStore {
}
}
fn edit_prediction_definitions_for_command<C>(
&mut self,
buffer: &Entity<Buffer>,
request: C,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Vec<EditPredictionDefinition>>>
where
C: LspCommand<Response = Vec<EditPredictionDefinition>> + Clone,
C::ProtoRequest: proto::LspRequestMessage,
<C::ProtoRequest as proto::LspRequestMessage>::Response:
Into<<C::ProtoRequest as proto::RequestMessage>::Response>,
<C::LspRequest as lsp::request::Request>::Result: Send,
<C::LspRequest as lsp::request::Request>::Params: Send,
{
if let Some((upstream_client, project_id)) = self.upstream_client() {
if !self.is_capable_for_proto_request(buffer, &request, cx) {
return Task::ready(Ok(Vec::new()));
}
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
let request_task = upstream_client.request_lsp(
project_id,
None,
request_timeout,
cx.background_executor().clone(),
request.to_proto(project_id, buffer.read(cx)),
);
let buffer = buffer.clone();
cx.spawn(async move |weak_lsp_store, cx| {
let Some(lsp_store) = weak_lsp_store.upgrade() else {
return Ok(Vec::new());
};
let Some(responses) = request_task.await? else {
return Ok(Vec::new());
};
let actions = join_all(responses.payload.into_iter().map(|response| {
request.clone().response_from_proto(
response.response.into(),
lsp_store.clone(),
buffer.clone(),
cx.clone(),
)
}))
.await;
Ok(actions
.into_iter()
.collect::<Result<Vec<Vec<_>>>>()?
.into_iter()
.flatten()
.collect())
})
} else {
let definitions_task =
self.request_multiple_lsp_locally(buffer, Some(position), request, cx);
cx.background_spawn(async move {
Ok(definitions_task
.await
.into_iter()
.flat_map(|(_, definitions)| definitions)
.collect())
})
}
}
pub fn edit_prediction_definitions(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
include_type_definitions: bool,
cx: &mut Context<Self>,
) -> Task<Result<Vec<EditPredictionDefinition>>> {
let definitions = self.edit_prediction_definitions_for_command(
buffer,
GetEditPredictionDefinitions { position },
position,
cx,
);
let type_definitions = include_type_definitions.then(|| {
self.edit_prediction_definitions_for_command(
buffer,
GetEditPredictionTypeDefinitions { position },
position,
cx,
)
});
cx.background_spawn(async move {
let mut merged = definitions.await?;
if let Some(type_definitions) = type_definitions {
merged.extend(type_definitions.await?);
}
let mut seen = HashSet::default();
merged.retain(|definition| seen.insert(definition.clone()));
Ok(merged)
})
}
pub fn declarations(
&mut self,
buffer: &Entity<Buffer>,
@ -6340,31 +6425,9 @@ impl LspStore {
buffer: &Entity<Buffer>,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
self.type_definitions_with_filter(buffer, position, false, cx)
}
pub fn workspace_type_definitions(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
self.type_definitions_with_filter(buffer, position, true, cx)
}
fn type_definitions_with_filter(
&mut self,
buffer: &Entity<Buffer>,
position: PointUtf16,
workspace_only: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
if let Some((upstream_client, project_id)) = self.upstream_client() {
let request = GetTypeDefinitions {
position,
workspace_only,
};
let request = GetTypeDefinitions { position };
if !self.is_capable_for_proto_request(buffer, &request, cx) {
return Task::ready(Ok(None));
}
@ -6387,11 +6450,7 @@ impl LspStore {
return Ok(None);
};
let actions = join_all(responses.payload.into_iter().map(|response| {
GetTypeDefinitions {
position,
workspace_only,
}
.response_from_proto(
GetTypeDefinitions { position }.response_from_proto(
response.response,
lsp_store.clone(),
buffer.clone(),
@ -6414,10 +6473,7 @@ impl LspStore {
let type_definitions_task = self.request_multiple_lsp_locally(
buffer,
Some(position),
GetTypeDefinitions {
position,
workspace_only,
},
GetTypeDefinitions { position },
cx,
);
cx.background_spawn(async move {
@ -8117,7 +8173,7 @@ impl LspStore {
server_id: LanguageServerId,
lsp_adapter: Arc<CachedLspAdapter>,
worktree: WeakEntity<Worktree>,
lsp_symbols: Vec<(String, SymbolKind, lsp::Location, Option<String>)>,
lsp_symbols: Vec<(String, language::SymbolKind, lsp::Location, Option<String>)>,
}
let mut requests = Vec::new();
@ -8187,7 +8243,7 @@ impl LspStore {
.map(|lsp_symbol| {
(
lsp_symbol.name,
lsp_symbol.kind,
lsp_to_symbol_kind(lsp_symbol.kind),
lsp_symbol.location,
lsp_symbol.container_name,
)
@ -8211,7 +8267,7 @@ impl LspStore {
};
Some((
lsp_symbol.name,
lsp_symbol.kind,
lsp_to_symbol_kind(lsp_symbol.kind),
location,
lsp_symbol.container_name,
))
@ -8712,7 +8768,7 @@ impl LspStore {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
@ -9003,7 +9059,7 @@ impl LspStore {
diagnostics_summary
.more_summaries
.push(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
@ -9014,7 +9070,7 @@ impl LspStore {
project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count,
warning_count: new_summary.warning_count,
@ -9098,7 +9154,7 @@ impl LspStore {
Ok(ControlFlow::Continue(Some((
*project_id,
proto::DiagnosticSummary {
path: path_in_worktree.to_proto(),
path: path_in_worktree.as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: new_summary.error_count as u32,
warning_count: new_summary.warning_count as u32,
@ -9615,6 +9671,22 @@ impl LspStore {
)
.await?;
}
Request::GetEditPredictionDefinition(get_edit_prediction_definition) => {
let position = get_edit_prediction_definition
.position
.clone()
.and_then(deserialize_anchor);
Self::query_lsp_locally::<GetEditPredictionDefinitions>(
lsp_store,
server_id,
sender_id,
lsp_request_id,
get_edit_prediction_definition,
position,
&mut cx,
)
.await?;
}
Request::GetDeclaration(get_declaration) => {
let position = get_declaration
.position
@ -9647,6 +9719,22 @@ impl LspStore {
)
.await?;
}
Request::GetEditPredictionTypeDefinition(get_edit_prediction_type_definition) => {
let position = get_edit_prediction_type_definition
.position
.clone()
.and_then(deserialize_anchor);
Self::query_lsp_locally::<GetEditPredictionTypeDefinitions>(
lsp_store,
server_id,
sender_id,
lsp_request_id,
get_edit_prediction_type_definition,
position,
&mut cx,
)
.await?;
}
Request::GetImplementation(get_implementation) => {
let position = get_implementation
.position
@ -9871,7 +9959,7 @@ impl LspStore {
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_path =
RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?;
RelPath::from_unix_str(&envelope.payload.new_path).context("invalid relative path")?;
let (worktree_store, old_worktree, new_worktree, old_entry) = this
.update(&mut cx, |this, cx| {
@ -9940,7 +10028,9 @@ impl LspStore {
{
let project_path = ProjectPath {
worktree_id,
path: RelPath::from_proto(&message_summary.path).context("invalid path")?,
path: RelPath::from_unix_str(&message_summary.path)
.context("invalid path")?
.into(),
};
let path = project_path.path.clone();
let server_id = LanguageServerId(message_summary.language_server_id as usize);
@ -9975,7 +10065,7 @@ impl LspStore {
diagnostics_summary
.more_summaries
.push(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
@ -9986,7 +10076,7 @@ impl LspStore {
project_id: *project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: project_path.path.as_ref().to_proto(),
path: project_path.path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: summary.error_count as u32,
warning_count: summary.warning_count as u32,
@ -11464,7 +11554,7 @@ impl LspStore {
project_id,
worktree_id: worktree_id.to_proto(),
summary: Some(proto::DiagnosticSummary {
path: path.as_ref().to_proto(),
path: path.as_ref().as_unix_str().to_owned(),
language_server_id: server_id.0 as u64,
error_count: 0,
warning_count: 0,
@ -12479,7 +12569,7 @@ impl LspStore {
source_worktree_id: symbol.source_worktree_id.to_proto(),
language_server_id: symbol.source_language_server_id.to_proto(),
name: symbol.name.clone(),
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
kind: symbol.kind as i32,
start: Some(proto::PointUtf16 {
row: symbol.range.start.0.row,
column: symbol.range.start.0.column,
@ -12496,7 +12586,7 @@ impl LspStore {
match &symbol.path {
SymbolLocation::InProject(path) => {
result.worktree_id = path.worktree_id.to_proto();
result.path = path.path.to_proto();
result.path = path.path.as_unix_str().to_owned();
}
SymbolLocation::OutsideProject {
abs_path,
@ -12512,13 +12602,14 @@ impl LspStore {
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
let kind = language::SymbolKind::from_proto(serialized_symbol.kind);
let path = if serialized_symbol.signature.is_empty() {
SymbolLocation::InProject(ProjectPath {
worktree_id,
path: RelPath::from_proto(&serialized_symbol.path)
.context("invalid symbol path")?,
path: RelPath::from_unix_str(&serialized_symbol.path)
.context("invalid symbol path")?
.into(),
})
} else {
SymbolLocation::OutsideProject {
@ -14765,7 +14856,7 @@ impl DiagnosticSummary {
path: &RelPath,
) -> proto::DiagnosticSummary {
proto::DiagnosticSummary {
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
language_server_id: language_server_id.0 as u64,
error_count: self.error_count as u32,
warning_count: self.warning_count as u32,

View file

@ -328,6 +328,7 @@ fn enriched_symbol_text(
mod tests {
use super::*;
use gpui::TestAppContext;
use language::lsp_to_symbol_kind;
use text::{OffsetRangeExt, Point, Unclipped};
fn make_symbol(
@ -340,7 +341,7 @@ mod tests {
use text::PointUtf16;
DocumentSymbol {
name: name.to_string(),
kind,
kind: lsp_to_symbol_kind(kind),
range: Unclipped(PointUtf16::new(range.start.0, range.start.1))
..Unclipped(PointUtf16::new(range.end.0, range.end.1)),
selection_range: Unclipped(PointUtf16::new(

View file

@ -443,7 +443,6 @@ impl LspCommand for GoToParentModule {
lsp_store,
buffer,
server_id,
false,
cx,
)
.await

View file

@ -4,6 +4,7 @@ use std::{
sync::Arc,
};
use path::rel_path::RelPathBuf;
use util::rel_path::RelPath;
/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path.
@ -59,12 +60,12 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
) -> &mut Self {
let mut current = self;
let mut path_so_far = <Arc<RelPath>>::from(RelPath::empty());
let mut path_so_far = RelPathBuf::new();
for key in path.0.iter() {
path_so_far = path_so_far.join(RelPath::unix(key.as_ref()).unwrap());
path_so_far = path_so_far.join(RelPath::from_unix_str(key.as_ref()).unwrap());
current = match current.children.entry(key.clone()) {
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone()))
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone().into()))
}
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
};

View file

@ -460,7 +460,7 @@ impl PrettierStore {
) {
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
.iter()
.map(|name| RelPath::unix(name).unwrap())
.map(|name| RelPath::from_unix_str(name).unwrap())
.collect::<HashSet<_>>();
let prettier_config_file_changed = changes

View file

@ -99,6 +99,7 @@ use lsp::{
LanguageServerBinary, LanguageServerId, LanguageServerName, LanguageServerSelector,
MessageActionItem,
};
pub use lsp_command::EditPredictionDefinition;
use lsp_command::*;
use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
pub use manifest_tree::ManifestProvidersStore;
@ -439,14 +440,14 @@ impl ProjectPath {
pub fn from_proto(p: proto::ProjectPath) -> Option<Self> {
Some(Self {
worktree_id: WorktreeId::from_proto(p.worktree_id),
path: RelPath::from_proto(&p.path).log_err()?,
path: RelPath::from_unix_str(&p.path).log_err()?.into(),
})
}
pub fn to_proto(&self) -> proto::ProjectPath {
proto::ProjectPath {
worktree_id: self.worktree_id.to_proto(),
path: self.path.as_ref().to_proto(),
path: self.path.as_ref().as_unix_str().to_owned(),
}
}
@ -864,7 +865,7 @@ pub struct Symbol {
pub path: SymbolLocation,
pub label: CodeLabel,
pub name: String,
pub kind: lsp::SymbolKind,
pub kind: language::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub container_name: Option<String>,
}
@ -872,7 +873,7 @@ pub struct Symbol {
#[derive(Clone, Debug)]
pub struct DocumentSymbol {
pub name: String,
pub kind: lsp::SymbolKind,
pub kind: language::SymbolKind,
pub range: Range<Unclipped<PointUtf16>>,
pub selection_range: Range<Unclipped<PointUtf16>>,
pub children: Vec<DocumentSymbol>,
@ -1724,7 +1725,7 @@ impl Project {
let path_style = if response.payload.windows_paths {
PathStyle::Windows
} else {
PathStyle::Posix
PathStyle::Unix
};
let worktree_store = cx.new(|cx| {
@ -2151,7 +2152,7 @@ impl Project {
root_repo_is_linked_worktree: false,
},
client,
PathStyle::Posix,
PathStyle::Unix,
cx,
);
self.worktree_store
@ -3151,7 +3152,7 @@ impl Project {
// because SSH projects have client_state: Local but still need to communicate with remote server
let project_id = self.remote_id().unwrap_or(REMOTE_SERVER_PROJECT_ID);
let downloading_files = self.downloading_files.clone();
let path_str = path.to_proto();
let path_str = path.as_unix_str().to_owned();
static NEXT_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let file_id = NEXT_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
@ -4287,21 +4288,16 @@ impl Project {
})
}
pub fn workspace_definitions<T: ToPointUtf16>(
pub fn edit_prediction_definitions<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
position: T,
include_type_definitions: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
) -> Task<Result<Vec<EditPredictionDefinition>>> {
let position = position.to_point_utf16(buffer.read(cx));
let guard = self.retain_remotely_created_models(cx);
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.workspace_definitions(buffer, position, cx)
});
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.edit_prediction_definitions(buffer, position, include_type_definitions, cx)
})
}
@ -4341,24 +4337,6 @@ impl Project {
})
}
pub fn workspace_type_definitions<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
position: T,
cx: &mut Context<Self>,
) -> Task<Result<Option<Vec<LocationLink>>>> {
let position = position.to_point_utf16(buffer.read(cx));
let guard = self.retain_remotely_created_models(cx);
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.workspace_type_definitions(buffer, position, cx)
});
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
})
}
pub fn implementations<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
@ -5788,7 +5766,7 @@ impl Project {
) -> Result<proto::OpenBufferResponse> {
let peer_id = envelope.original_sender_id()?;
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = RelPath::from_proto(&envelope.payload.path)?;
let path = RelPath::from_unix_str(&envelope.payload.path)?.into();
let open_buffer = this
.update(&mut cx, |this, cx| {
this.open_buffer(ProjectPath { worktree_id, path }, cx)

View file

@ -1023,7 +1023,7 @@ impl SettingsObserver {
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_proto(),
path: path.as_unix_str().to_owned(),
content: Some(content),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
@ -1206,7 +1206,7 @@ impl SettingsObserver {
.unwrap()
.into();
(settings_dir, LocalSettingsKind::Debug)
} else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
} else if path.ends_with(RelPath::from_unix_str(EDITORCONFIG_NAME).unwrap()) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};

View file

@ -263,7 +263,7 @@ impl ToolchainStore {
};
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = if let Some(path) = envelope.payload.path {
RelPath::from_proto(&path)?
RelPath::from_unix_str(&path)?.into()
} else {
RelPath::empty_arc()
};
@ -277,7 +277,7 @@ impl ToolchainStore {
envelope: TypedEnvelope<proto::ActiveToolchain>,
mut cx: AsyncApp,
) -> Result<proto::ActiveToolchainResponse> {
let path = RelPath::unix(envelope.payload.path.as_deref().unwrap_or(""))?;
let path = RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?;
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
@ -314,7 +314,8 @@ impl ToolchainStore {
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?;
let path =
RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?.into();
anyhow::Ok(this.list_toolchains(
ProjectPath { worktree_id, path },
language_name,
@ -364,7 +365,7 @@ impl ToolchainStore {
has_values,
toolchains,
groups,
relative_worktree_path: Some(relative_path.to_proto()),
relative_worktree_path: Some(relative_path.as_unix_str().to_owned()),
})
}
@ -637,7 +638,7 @@ impl RemoteToolchainStore {
path: path.to_string_lossy().into_owned(),
raw_json: toolchain.as_json.to_string(),
}),
path: Some(project_path.path.to_proto()),
path: Some(project_path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;
@ -667,7 +668,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_proto()),
path: Some(path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;
@ -693,7 +694,7 @@ impl RemoteToolchainStore {
Some((usize::try_from(group.start_index).ok()?, group.name.into()))
})
.collect();
let relative_path = RelPath::from_proto(
let relative_path = RelPath::from_unix_str(
response
.relative_worktree_path
.as_deref()
@ -706,7 +707,7 @@ impl RemoteToolchainStore {
default: None,
groups,
},
relative_path,
relative_path.into(),
))
})
}
@ -724,7 +725,7 @@ impl RemoteToolchainStore {
project_id,
worktree_id: path.worktree_id.to_proto(),
language_name: language_name.clone().into(),
path: Some(path.path.to_proto()),
path: Some(path.path.as_unix_str().to_owned()),
})
.await
.log_err()?;

View file

@ -513,7 +513,7 @@ impl WorktreeStore {
let response = upstream_client.request(proto::CopyProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_path: new_project_path.path.as_unix_str().to_owned(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
@ -668,7 +668,7 @@ impl WorktreeStore {
let response = upstream_client.request(proto::RenameProjectEntry {
project_id: *upstream_project_id,
entry_id: entry_id.to_proto(),
new_path: new_project_path.path.to_proto(),
new_path: new_project_path.path.as_unix_str().to_owned(),
new_worktree_id: new_project_path.worktree_id.to_proto(),
});
cx.spawn(async move |_, cx| {
@ -1231,7 +1231,7 @@ impl WorktreeStore {
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
let new_project_path = (
new_worktree_id,
RelPath::from_proto(&envelope.payload.new_path)?,
RelPath::from_unix_str(&envelope.payload.new_path)?,
);
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
let Some((_, project_id)) = this.downstream_client else {
@ -1289,7 +1289,7 @@ impl WorktreeStore {
) -> Result<proto::ProjectEntryResponse> {
let entry_id = ProjectEntryId::from_proto(request.entry_id);
let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
let rel_path = RelPath::from_proto(&request.new_path)
let rel_path = RelPath::from_unix_str(&request.new_path)
.with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
let (scan_id, task) = this.update(&mut cx, |this, cx| {

View file

@ -721,7 +721,12 @@ mod git_traversal {
let traversal = GitTraversal::new(
&repo_snapshots,
worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()),
worktree_snapshot.traverse_from_path(
true,
false,
true,
RelPath::from_unix_str("x").unwrap(),
),
);
let entries = traversal
.map(|entry| (entry.path.clone(), entry.git_summary))
@ -1204,45 +1209,44 @@ mod git_worktrees {
let work_dir = Path::new("/code/my-project");
// Valid: sibling
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).is_ok());
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).is_ok());
// Valid: subdirectory
assert!(
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Posix).is_ok()
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Unix).is_ok()
);
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Posix).is_ok());
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Unix).is_ok());
// Invalid: just ".." would resolve back to the working directory itself
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
// Invalid: ".." with trailing separators
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be \"..\""));
// Invalid: empty string would resolve to the working directory itself
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("must not be empty"));
// Invalid: absolute path
let err =
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Posix).unwrap_err();
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: "/" is absolute on Unix
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: "///" is absolute
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Posix).unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Unix).unwrap_err();
assert!(err.to_string().contains("relative path"));
// Invalid: escapes too far up
let err =
worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Posix)
.unwrap_err();
let err = worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Unix)
.unwrap_err();
assert!(err.to_string().contains("outside"));
}
@ -1251,7 +1255,7 @@ mod git_worktrees {
let work_dir = Path::new("/home/user/dev/lsp-tests");
let directory =
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
assert_eq!(
directory,

View file

@ -48,7 +48,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
let entry = Entry {
id: ProjectEntryId::from_proto(1),
kind: EntryKind::UnloadedDir,
path: Arc::from(RelPath::unix(Path::new("src/data")).unwrap()),
path: Arc::from(RelPath::from_unix_str(Path::new("src/data")).unwrap()),
inode: 0,
mtime: None,
canonical_path: None,
@ -90,7 +90,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
// 2. Test searching for `field`, including ignored files but updating
// `files_to_include` to only include files under `src/lib`.
let include_ignored = true;
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Posix).unwrap();
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Unix).unwrap();
let files_to_exclude = PathMatcher::default();
let match_full_paths = false;
let search_query = SearchQuery::text(

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