Compare commits

..

32 Commits

Author SHA1 Message Date
Cole Miller
54dc913a79 wip 2025-12-19 10:48:20 -05:00
Cole Miller
ae44c3c881 Fix extra terminal being created when a task replaces a terminal in the center pane (#45317)
Closes https://github.com/zed-industries/zed/issues/21144

Release Notes:

- Fixed spawned tasks creating an extra terminal in the dock in some
cases.
2025-12-19 09:39:58 -05:00
ozzy
4e0471cf66 git panel: Truncate file paths from the left (#43462)
https://github.com/user-attachments/assets/758e1ec9-6c34-4e13-b605-cf00c18ca16f

Release Notes:

- Improved: Git panel now truncates long file paths from the left,
showing "…path/filename" when space is limited, keeping filenames always
visible.

@cole-miller @mattermill

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-19 13:50:35 +00:00
Danilo Leal
62d36b22fd gpui: Add text_ellipsis_start method (#45122)
This PR is an additive change introducing the `truncate_start` method to
labels, which gives us the ability to add an ellipsis at the beginning
of the text as opposed to the regular `truncate`. This will be generally
used for truncating file paths, where the end is typically more relevant
than the beginning, but given it's a general method, there's the
possibility to be used anywhere else, too.

<img width="500" height="690" alt="Screenshot 2025-12-17 at 12  35@2x"
src="https://github.com/user-attachments/assets/f853f5a3-60b3-4380-a11c-bb47868a4470"
/>

Release Notes:

- N/A

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-19 10:25:19 -03:00
Piotr Osiewicz
69f6eeaa3a toolchains: Fix persistence by not relying on unstable worktree id (#45357)
Closes #42268
We've migrated user selections when a given workspace has a single
worktree (as then we could determine what the target worktree is).

Release Notes:

- python: Fixed selected virtual environments not being
persisted/deserialized correctly within long-running Zed sessions (where
multiple different projects might've been opened). This is a breaking
change for users of multi-worktree projects - your selected toolchain
for those projects will be reset.

Co-authored-by: Dino <dino@zed.dev>
2025-12-19 14:06:15 +01:00
Jakub Konka
1dc5de4592 workspace: Auto-switch git context when focus changed (#45354)
Closes #44955 

Release Notes:

- Fixed workspace incorrectly automatically switching Git
repository/branch context in multi-repository projects when repo/branch
switched manually from the Git panel.
2025-12-19 13:54:30 +01:00
Lena
b9aef75f2d Turn on the fixed stalebot (#45355)
It will run weekly and it promised not to touch issues of the wrong
types anymore.

Release Notes:

- N/A
2025-12-19 12:41:03 +00:00
Agus Zubiaga
95ae388c0c Fix title bar spacing when building on the macOS Tahoe SDK (#45351)
The size and spacing around the traffic light buttons changes after
macOS SDK 26. Our official builds aren't using this SDK yet, but dev
builds sometimes are and the official will in the future.

<table>
<tr>
<th>Before</th>
<th>After</th>
</tr>
<tr>
<td>
<img width="582" height="146" alt="CleanShot 2025-12-19 at 08 58 53@2x"
src="https://github.com/user-attachments/assets/1a28d74a-98a3-49d0-98d6-ab05b0580665"
/>
</td>
<td>
<img width="610" height="156" alt="CleanShot 2025-12-19 at 08 57 02@2x"
src="https://github.com/user-attachments/assets/7b7693b3-baa1-4d7e-9fc1-bd7a7bfacd36"
/>
</td>
</tr>
<tr>
<td>
<img width="532" height="154" alt="CleanShot 2025-12-19 at 08 59 40@2x"
src="https://github.com/user-attachments/assets/df7f40e7-7576-44f2-9cf3-047a5d00bb4e"
/>
</td>
<td>
<img width="520" height="150" alt="CleanShot 2025-12-19 at 09 01 17@2x"
src="https://github.com/user-attachments/assets/b0fbdeb6-1b1d-4e7a-95d0-3c78f0569df1"
/>
</td>
</tr>
</table>

Release Notes:

- N/A
2025-12-19 12:19:04 +00:00
Lena
1ac170e663 Upgrade stalebot and make testing it easier (#45350)
- adjust wording for the upcoming simplified process
- upgrade to the github action version that has a fix for configuring issue types the bot should look at
- add two inputs for the manual runs of stalebot that help testing it in a safe and controlled manner 

Release Notes:

- N/A
2025-12-19 12:46:20 +01:00
Angelo Verlain
3104482c6c languages: Detect .bst files as YAML (#45015)
These files are used by the BuildStream build project:
https://buildstream.build/index.html


Release Notes:

- Added recognition for .bst files as yaml.
2025-12-19 10:34:40 +00:00
Piotr Osiewicz
7ee56e1a18 chore: Add worktree_benchmarks to cargo workspace (#45344)
Idk why it was missing, but

Release Notes:

- N/A
2025-12-19 11:18:36 +01:00
Korbin de Man
f2495a6f98 Add Restore File action in project_panel for git modified files (#42490)
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-12-19 10:12:01 +00:00
prayansh_chhablani
6d776c3157 project: Sanitize single-line completions from trailing newlines (#44965)
Closes #43991
trim documentation string to prevent completion overlap


previous
[Screencast from 2025-12-16
14-55-58.webm](https://github.com/user-attachments/assets/d7674d82-63b0-4a85-a90f-b5c5091e4a82)
after change
[Screencast from 2025-12-16
14-50-05.webm](https://github.com/user-attachments/assets/109c22b5-3fff-49c8-a2ec-b1af467d6320)
Release Notes:

- Fixed an issue where completions in the completion menu would span
multiple lines.
2025-12-19 10:11:36 +01:00
Mayank Verma
596826f741 editor: Strip trailing newlines from completion documentation (#45342)
Closes #45337

Release Notes:

- Fixed broken completion menu layout caused by trailing newlines in ty
documentation

<table>
  <tr>
    <td>Before</td>
    <td>After</td>
  </tr>
  <tr>
    <td>
<img width="756" height="875" alt="before"
src="https://github.com/user-attachments/assets/1d9da7d8-437a-4f03-8158-32ff1af9a428"
/>
    </td>
    <td>  
<img width="755" height="875" alt="after"
src="https://github.com/user-attachments/assets/dca31af3-e571-445a-b4a9-c300bb4c63fa"
/>
    </td>
  </tr>
</table>
2025-12-19 08:43:35 +00:00
Mustaque Ahmed
e44529ed7b Hide inline overlays when context menu is open (#45266)
Closes #23367 

**Summary**
- Prevents inline diagnostics, code actions, blame annotations, and
hover popovers from overlapping with the right-click context menu by
checking for `mouse_context_menu` presence before rendering these UI
elements.

PS: Same behaviour is present in other editors like VS Code.


**Screen recording**


https://github.com/user-attachments/assets/8290412b-0f86-4985-8c70-13440686e530



Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-19 08:24:30 +00:00
rabsef-bicrym
e052127e1c terminal: Prevent scrollbar arithmetic underflow panic (#45282)
## Summary

Fixes arithmetic underflow panics in `terminal_scrollbar.rs` by
converting unsafe subtractions to `saturating_sub`.

Closes #45281

## Problem

Two locations perform raw subtraction on `usize` values that panic when
underflow occurs:

- `offset()`: `state.total_lines - state.viewport_lines -
state.display_offset`
- `set_offset()`: `state.total_lines - state.viewport_lines`

This happens when `total_lines < viewport_lines + display_offset`, which
can occur during terminal creation, with small window sizes, or when
display state becomes stale.

## Solution

Replace the two unsafe subtractions with `saturating_sub`, which returns
0 on underflow instead of panicking.

Also standardizes the existing `checked_sub().unwrap_or(0)` in
`max_offset()` to `saturating_sub` for consistency across the file.

## Changes

- N/A
2025-12-19 06:33:59 +00:00
Ryan Steil
0531035b86 docs: Fix link to Anthropic prompt engineering resource (#45329) 2025-12-19 01:47:40 -03:00
Ben Kunkle
05ce34eea4 ci: Fix docs build post #45130 (#45330)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-19 03:40:27 +00:00
Alvaro Parker
63c4406137 git: Add git clone open listener (#41669) 2025-12-19 00:21:46 -03:00
Ben Kunkle
3f67c5220d Remove zed dependency from docs_preprocessor (#45130)
Closes #ISSUE

Uses the existing `--dump-all-actions` arg on the Zed binary to generate
an asset of all of our actions so that the `docs_preprocessor` can
injest it, rather than depending on the Zed crate itself to collect all
action names

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-18 21:59:05 -05:00
Ben Kunkle
435d4c5f24 vim: Make vaf include const for arrow functions in JS/TS/TSX (#45327)
Closes #24264

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 21:56:47 -05:00
Danilo Leal
e0ff995e2d agent ui: Make some UI elements more consistent (#45319)
- Both the mode, profile, and model selectors have the option to cycle
through its options with a keybinding. In the tooltip that shows it, in
some of them the "Cycle Through..." label was at the top, and in others
at the bottom. Now it's all at the bottom.
- We used different language in different places for "going to a file".
The tool call edit card's header said "_Jump_ to File" while the edit
files list said "_Go_ to File". Now it's both "Go to File".

Release Notes:

- N/A
2025-12-19 01:18:41 +00:00
Conrad Irwin
6976208e21 Move autofix stuff to zippy (#45304)
Although I wanted to avoid the dependency, it's hard to get github to do
what we want.

Release Notes:

- N/A
2025-12-18 15:23:09 -07:00
Richard Feldman
6055b45ee1 Add support for provider extensions (but no extensions yet) (#45277)
This adds support for provider extensions but doesn't actually add any
yet.

Release Notes:

- N/A
2025-12-18 17:05:04 -05:00
Joseph T. Lyons
88f90c12ed Add language server version in a tooltip on language server hover (#45302)
I wanted a way to make it easy to figure out which version of a language
server Zed is running. Now, you get a tooltip when hovering on a
language server in the Language Servers popover.

<img width="498" height="168" alt="SCR-20251218-ovln"
src="https://github.com/user-attachments/assets/1ced4214-b868-4405-8881-eb7c0b75a53e"
/>

This PR also fixes a bug. We had existing code to open a tooltip on
these language server entrees and display the language server message,
which was never fully wired up for `CustomEntry`s. Now, in this PR, we
will show show either version, message, or both, in the documentation
aside, depending on what the server has given us.

Mostly done with Droid (using GPT-5.2), with manual review and multiple
follow ups to guide it into using existing patterns in the codebase,
when it did something abnormal.

Release Notes:

- Added language server version in a tooltip on language server hover

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-18 21:59:21 +00:00
Marshall Bowers
0d74f982a5 danger: Upgrade danger-plugin-pr-hygiene to v0.7.1 (#45303)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.1.

Release Notes:

- N/A
2025-12-18 21:52:34 +00:00
Marshall Bowers
ca90b8555d docs: Remove local collaboration docs (#45301)
This PR removes the docs for running Collab locally, as they are
outdated and don't reflect the current state of affairs.

Release Notes:

- N/A
2025-12-18 21:42:28 +00:00
Richard Feldman
8516d81e13 Fix display name for Ollama models (#45287)
Closes #43646

Release Notes:

- Fixed display name for Ollama models
2025-12-18 16:32:59 -05:00
Danilo Leal
af589ff25f agent_ui: Simplify timestamp display (#45296)
This PR simplifies how we display thread timestamps in the agent panel's
history view. For threads that are older-than-yesterday, we just show
how many days ago that thread was had in. Hovering over the thread item
shows you both the title and the full date, if needed (time and date).

<img width="450" height="786" alt="Screenshot 2025-12-18 at 5  24@2x"
src="https://github.com/user-attachments/assets/11416e9b-f1b0-4307-9db0-988a95a316a1"
/>


Release Notes:

- N/A
2025-12-18 17:49:17 -03:00
Julia Ryan
d2bbfbb3bf lsp: Broadcast our capability for MessageActionItems (#45047)
Closes #37902

Release Notes:

- Enable LSP Message action items for more language servers. These are interactive prompts, often for things like downloading build inputs for a project.
2025-12-18 11:09:40 -08:00
Peter Tripp
413f4ea49c Redact environment variables from language server spawn errors (#44783)
Redact environment variables from zed logs when lsp fails to spawn.

Release Notes:

- N/A
2025-12-18 21:05:14 +02:00
Marshall Bowers
1b6d588413 danger: Deny conventional commits in PR titles (#45283)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.0 so that we can
have Danger deny conventional commits in PR titles.

Release Notes:

- N/A
2025-12-18 18:42:28 +00:00
120 changed files with 2786 additions and 972 deletions

View File

@@ -19,6 +19,18 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Install mold linker
shell: bash -euxo pipefail {0}
run: ./script/install-mold
- name: Download WASI SDK
shell: bash -euxo pipefail {0}
run: ./script/download-wasi-sdk
- name: Generate action metadata
shell: bash -euxo pipefail {0}
run: ./script/generate-action-metadata
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:

View File

@@ -1,29 +1,40 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 8 31 DEC *"
- cron: "0 2 * * 5"
workflow_dispatch:
inputs:
debug-only:
description: "Run in dry-run mode (no changes made)"
type: boolean
default: false
operations-per-run:
description: "Max number of issues to process (default: 1000)"
type: number
default: 1000
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
Hi there! 👋
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
Hi there!
Zed development moves fast and a significant number of bugs become outdated.
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
days-before-stale: 60
days-before-close: 14
only-issue-types: "Bug,Crash"
operations-per-run: 1000
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
ascending: true
enable-statistics: true
debug-only: ${{ inputs.debug-only }}
stale-issue-label: "stale"
exempt-issue-labels: "never stale"

View File

@@ -61,8 +61,7 @@ jobs:
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy

View File

@@ -26,8 +26,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -72,15 +71,9 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -94,8 +87,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -114,8 +105,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -20,8 +20,7 @@ jobs:
with:
clean: false
fetch-depth: 0
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: ./script/clippy
@@ -45,8 +44,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -74,19 +74,12 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- id: prettier
name: steps::prettier
- name: steps::prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- id: record_style_failure
name: steps::record_style_failure
if: always()
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -97,8 +90,6 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
outputs:
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -119,8 +110,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large
@@ -167,15 +157,9 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -189,8 +173,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_mac:
needs:
@@ -211,8 +193,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -372,6 +353,9 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/generate-action-metadata
run: ./script/generate-action-metadata
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:
@@ -592,24 +576,6 @@ jobs:
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
call_autofix:
needs:
- check_style
- run_tests_linux
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-app-token
name: steps::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: run_tests::call_autofix::dispatch_autofix
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@
DerivedData/
Packages
xcuserdata/
crates/docs_preprocessor/actions.json
# Don't commit any secrets to the repo.
.env

16
Cargo.lock generated
View File

@@ -5021,8 +5021,6 @@ name = "docs_preprocessor"
version = "0.1.0"
dependencies = [
"anyhow",
"command_palette",
"gpui",
"mdbook",
"regex",
"serde",
@@ -5031,7 +5029,6 @@ dependencies = [
"task",
"theme",
"util",
"zed",
"zlog",
]
@@ -8932,6 +8929,8 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"extension",
"extension_host",
"fs",
"futures 0.3.31",
"google_ai",
@@ -12571,6 +12570,7 @@ dependencies = [
"gpui",
"language",
"menu",
"notifications",
"pretty_assertions",
"project",
"rayon",
@@ -20265,6 +20265,16 @@ dependencies = [
"zlog",
]
[[package]]
name = "worktree_benchmarks"
version = "0.1.0"
dependencies = [
"fs",
"gpui",
"settings",
"worktree",
]
[[package]]
name = "writeable"
version = "0.6.1"

View File

@@ -198,6 +198,7 @@ members = [
"crates/web_search_providers",
"crates/workspace",
"crates/worktree",
"crates/worktree_benchmarks",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",

View File

@@ -20,7 +20,6 @@ Other platforms are not yet available:
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
### Contributing

View File

@@ -1319,17 +1319,15 @@
// Globs to match files that will be considered "hidden". These files can be hidden from the
// project panel by toggling the "hide_hidden" setting.
"hidden_files": ["**/.*"],
// Git integration settings.
// Git gutter behavior configuration.
"git": {
// Master switch to disable all git integration features.
// When true, all git features are disabled regardless of other settings.
// When false (default), individual features are controlled by their respective settings.
// Global switch to enable or disable all git integration features.
// If set to true, disables all git integration features.
// If set to false, individual git integration features below will be independently enabled or disabled.
"disable_git": false,
// Whether to show git status indicators (modified, added, deleted) in the
// project panel, outline panel, and tabs.
// Whether to enable git status tracking.
"enable_status": true,
// Whether to show git diff information, including gutter diff indicators
// and scrollbar diff markers.
// Whether to enable git diff display.
"enable_diff": true,
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter

View File

@@ -210,12 +210,21 @@ pub trait AgentModelSelector: 'static {
}
}
/// Icon for a model in the model selector.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgentModelIcon {
/// A built-in icon from Zed's icon set.
Named(IconName),
/// Path to a custom SVG icon file.
Path(SharedString),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentModelInfo {
pub id: acp::ModelId,
pub name: SharedString,
pub description: Option<SharedString>,
pub icon: Option<IconName>,
pub icon: Option<AgentModelIcon>,
}
impl From<acp::ModelInfo> for AgentModelInfo {

View File

@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
@@ -93,7 +93,7 @@ impl LanguageModels {
fn refresh_list(&mut self, cx: &App) {
let providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -153,7 +153,10 @@ impl LanguageModels {
id: Self::model_id(model),
name: model.name().0,
description: None,
icon: Some(provider.icon()),
icon: Some(match provider.icon() {
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
}),
}
}
@@ -164,7 +167,7 @@ impl LanguageModels {
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -1630,7 +1633,9 @@ mod internal_tests {
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
icon: Some(ui::IconName::ZedAssistant),
icon: Some(acp_thread::AgentModelIcon::Named(
ui::IconName::ZedAssistant
)),
}]
)])
);

View File

@@ -186,6 +186,17 @@ impl Render for ModeSelector {
move |_window, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.child(
h_flex()
.pb_1()
@@ -200,17 +211,6 @@ impl Render for ModeSelector {
cx,
)),
)
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.into_any()
}
}),

View File

@@ -1,6 +1,6 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol::ModelId;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
@@ -350,7 +350,11 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
.child(
ModelSelectorListItem::new(ix, model_info.name.clone())
.when_some(model_info.icon, |this, icon| this.icon(icon))
.map(|this| match &model_info.icon {
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
None => this,
})
.is_selected(is_selected)
.is_focused(selected)
.when(supports_favorites, |this| {

View File

@@ -1,7 +1,7 @@
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
@@ -70,7 +70,7 @@ impl Render for AcpModelSelectorPopover {
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("Select a Model"));
let model_icon = model.as_ref().and_then(|model| model.icon);
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
let focus_handle = self.focus_handle.clone();
@@ -125,7 +125,14 @@ impl Render for AcpModelSelectorPopover {
ButtonLike::new("active-model")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.when_some(model_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.child(
Label::new(model_name)

View File

@@ -1,7 +1,7 @@
use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -2718,7 +2718,7 @@ impl AcpThreadView {
..default_markdown_style(false, true, window, cx)
},
))
.tooltip(Tooltip::text("Jump to File"))
.tooltip(Tooltip::text("Go to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))

View File

@@ -22,7 +22,8 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -117,7 +118,7 @@ impl AgentConfiguration {
}
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
for provider in providers {
self.add_provider_configuration_view(&provider, window, cx);
}
@@ -261,9 +262,12 @@ impl AgentConfiguration {
.w_full()
.gap_1p5()
.child(
Icon::new(provider.icon())
.size(IconSize::Small)
.color(Color::Muted),
match provider.icon() {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
@@ -416,7 +420,7 @@ impl AgentConfiguration {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(

View File

@@ -4,6 +4,7 @@ use crate::{
};
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::IconOrSvg;
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
@@ -103,7 +104,14 @@ impl Render for AgentModelSelector {
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(provider_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
@@ -115,7 +123,7 @@ impl Render for AgentModelSelector {
.child(
Icon::new(IconName::ChevronDown)
.color(color)
.size(IconSize::Small),
.size(IconSize::XSmall),
),
move |_window, cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)

View File

@@ -2428,7 +2428,7 @@ impl AgentPanel {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)

View File

@@ -348,7 +348,8 @@ fn init_language_model_settings(cx: &mut App) {
|_, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
update_active_language_model_from_settings(cx);
}
_ => {}

View File

@@ -7,8 +7,8 @@ use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
LanguageModelProviderId, LanguageModelRegistry,
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -55,7 +55,7 @@ pub fn language_model_selector(
fn all_models(cx: &App) -> GroupedModels {
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
let providers = lm_registry.providers();
let providers = lm_registry.visible_providers();
let mut favorites_index = FavoritesIndex::default();
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
icon: IconOrSvg,
is_favorite: bool,
}
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -474,7 +474,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let configured_providers = language_model_registry
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -566,7 +566,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
Some(
ModelSelectorListItem::new(ix, model_info.model.name().0)
.icon(model_info.icon)
.map(|this| match &model_info.icon {
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
})
.is_selected(is_selected)
.is_focused(selected)
.is_favorite(is_favorite)
@@ -702,7 +705,7 @@ mod tests {
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
ModelInfo {
model: Arc::new(TestLanguageModel::new(name, provider)),
icon: IconName::Ai,
icon: IconOrSvg::Icon(IconName::Ai),
is_favorite,
}
})

View File

@@ -191,6 +191,9 @@ impl Render for ProfileSelector {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.child(
container()
.pb_1()
@@ -203,9 +206,6 @@ impl Render for ProfileSelector {
cx,
)),
)
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.into_any()
}
}),

View File

@@ -33,7 +33,8 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -2231,10 +2232,10 @@ impl TextThreadEditor {
.default_model()
.map(|default| default.provider);
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let provider_icon = active_provider
.as_ref()
.map(|p| p.icon())
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
let focus_handle = self.editor().focus_handle(cx);
@@ -2244,6 +2245,13 @@ impl TextThreadEditor {
(Color::Muted, IconName::ChevronDown)
};
let provider_icon_element = match provider_icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall);
let tooltip = Tooltip::element({
move |_, cx| {
let focus_handle = focus_handle.clone();
@@ -2291,7 +2299,7 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
.child(provider_icon_element)
.child(
Label::new(model_name)
.color(color)

View File

@@ -1,6 +1,11 @@
use gpui::{Action, FocusHandle, prelude::*};
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
enum ModelIcon {
Name(IconName),
Path(SharedString),
}
#[derive(IntoElement)]
pub struct ModelSelectorHeader {
title: SharedString,
@@ -39,7 +44,7 @@ impl RenderOnce for ModelSelectorHeader {
pub struct ModelSelectorListItem {
index: usize,
title: SharedString,
icon: Option<IconName>,
icon: Option<ModelIcon>,
is_selected: bool,
is_focused: bool,
is_favorite: bool,
@@ -60,7 +65,12 @@ impl ModelSelectorListItem {
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self.icon = Some(ModelIcon::Name(icon));
self
}
pub fn icon_path(mut self, path: SharedString) -> Self {
self.icon = Some(ModelIcon::Path(path));
self
}
@@ -105,9 +115,12 @@ impl RenderOnce for ModelSelectorListItem {
.gap_1p5()
.when_some(self.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small),
match icon {
ModelIcon::Name(icon_name) => Icon::new(icon_name),
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
}
.color(model_icon_color)
.size(IconSize::Small),
)
})
.child(Label::new(self.title).truncate()),

View File

@@ -1,5 +1,5 @@
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -1,9 +1,9 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconName, SharedString)>,
configured_providers: Vec<(IconOrSvg, SharedString)>,
}
impl ApiKeysWithProviders {
@@ -13,7 +13,8 @@ impl ApiKeysWithProviders {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
match icon {
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
}
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(name))
});
div()

View File

@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
configured_providers: Vec<(IconName, SharedString)>,
has_configured_providers: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
@@ -27,8 +27,9 @@ impl AgentPanelOnboarding {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_available_providers(cx)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.has_configured_providers = Self::has_configured_providers(cx)
}
_ => {}
},
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
Self {
user_store,
client,
configured_providers: Self::compute_available_providers(cx),
has_configured_providers: Self::has_configured_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn has_configured_providers(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0))
.collect()
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
}
}
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
command_palette.workspace = true
gpui.workspace = true
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
# Ask @maxdeviant about this before bumping.
mdbook = "= 0.4.40"
@@ -17,7 +15,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
util.workspace = true
zed.workspace = true
zlog.workspace = true
task.workspace = true
theme.workspace = true
@@ -27,4 +24,4 @@ workspace = true
[[bin]]
name = "docs_preprocessor"
path = "src/main.rs"
path = "src/main.rs"

View File

@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
fn main() -> Result<()> {
zlog::init();
zlog::init_output_stderr();
// call a zed:: function so everything in `zed` crate is linked and
// all actions in the actual app are registered
zed::stdout_is_a_pty();
let args = std::env::args().skip(1).collect::<Vec<_>>();
match args.get(0).map(String::as_str) {
@@ -72,8 +69,8 @@ enum PreprocessorError {
impl PreprocessorError {
fn new_for_not_found_action(action_name: String) -> Self {
for action in &*ALL_ACTIONS {
for alias in action.deprecated_aliases {
if alias == &action_name {
for alias in &action.deprecated_aliases {
if alias == action_name.as_str() {
return PreprocessorError::DeprecatedActionUsed {
used: action_name,
should_be: action.name.to_string(),
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
chapter.content = regex
.replace_all(&chapter.content, |caps: &regex::Captures| {
let action = caps[1].trim();
if find_action_by_name(action).is_none() {
if is_missing_action(action) {
errors.insert(PreprocessorError::new_for_not_found_action(
action.to_string(),
));
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
.replace_all(&chapter.content, |caps: &regex::Captures| {
let name = caps[1].trim();
let Some(action) = find_action_by_name(name) else {
errors.insert(PreprocessorError::new_for_not_found_action(
name.to_string(),
));
return String::new();
if actions_available() {
errors.insert(PreprocessorError::new_for_not_found_action(
name.to_string(),
));
}
return format!("<code class=\"hljs\">{}</code>", name);
};
format!("<code class=\"hljs\">{}</code>", &action.human_name)
})
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
ALL_ACTIONS
.binary_search_by(|action| action.name.cmp(name))
.binary_search_by(|action| action.name.as_str().cmp(name))
.ok()
.map(|index| &ALL_ACTIONS[index])
}
fn actions_available() -> bool {
!ALL_ACTIONS.is_empty()
}
fn is_missing_action(name: &str) -> bool {
actions_available() && find_action_by_name(name).is_none()
}
fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
.context("Failed to parse keymap JSON")?;
for section in keymap.sections() {
for (keystrokes, action) in section.bindings() {
keystrokes
.split_whitespace()
.map(|source| gpui::Keystroke::parse(source))
.collect::<std::result::Result<Vec<_>, _>>()
.context("Failed to parse keystroke")?;
for (_keystrokes, action) in section.bindings() {
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
.map_err(|err| anyhow::format_err!(err))
.context("Failed to parse action")?
{
anyhow::ensure!(
find_action_by_name(action_name).is_some(),
!is_missing_action(action_name),
"Action not found: {}",
action_name
);
@@ -491,27 +493,35 @@ where
});
}
#[derive(Debug, serde::Serialize)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct ActionDef {
name: &'static str,
name: String,
human_name: String,
deprecated_aliases: &'static [&'static str],
docs: Option<&'static str>,
deprecated_aliases: Vec<String>,
#[serde(rename = "documentation")]
docs: Option<String>,
}
fn dump_all_gpui_actions() -> Vec<ActionDef> {
let mut actions = gpui::generate_list_of_all_registered_actions()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
deprecated_aliases: action.deprecated_aliases,
docs: action.documentation,
})
.collect::<Vec<ActionDef>>();
actions.sort_by_key(|a| a.name);
actions
fn load_all_actions() -> Vec<ActionDef> {
let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
match std::fs::read_to_string(asset_path) {
Ok(content) => {
let mut actions: Vec<ActionDef> =
serde_json::from_str(&content).expect("Failed to parse actions.json");
actions.sort_by(|a, b| a.name.cmp(&b.name));
actions
}
Err(err) => {
if std::env::var("CI").is_ok() {
panic!("actions.json not found at {}: {}", asset_path, err);
}
eprintln!(
"Warning: actions.json not found, action validation will be skipped: {}",
err
);
Vec::new()
}
}
}
fn handle_postprocessing() -> Result<()> {
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
let mut output = String::new();
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
actions_sorted.sort_by_key(|a| a.name);
actions_sorted.sort_by_key(|a| a.name.as_str());
// Start the definition list with custom styling for better spacing
output.push_str("<dl style=\"line-height: 1.8;\">\n");
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
// Add the description, escaping HTML if needed
if let Some(description) = action.docs {
if let Some(description) = action.docs.as_ref() {
output.push_str(
&description
.replace("&", "&amp;")
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<br>\n");
}
output.push_str("Keymap Name: <code>");
output.push_str(action.name);
output.push_str(&action.name);
output.push_str("</code><br>\n");
if !action.deprecated_aliases.is_empty() {
output.push_str("Deprecated Alias(es): ");

View File

@@ -893,7 +893,7 @@ impl CompletionsMenu {
None
} else {
Some(
Label::new(text.clone())
Label::new(text.trim().to_string())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
@@ -1615,8 +1615,12 @@ impl CodeActionsMenu {
window.text_style().font(),
window.text_style().font_size.to_pixels(window.rem_size()),
);
let is_truncated =
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "");
let is_truncated = line_wrapper.should_truncate_line(
&label,
CODE_ACTION_MENU_MAX_WIDTH,
"",
gpui::TruncateFrom::End,
);
if is_truncated.is_none() {
return None;

View File

@@ -189,7 +189,7 @@ use std::{
time::{Duration, Instant},
};
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _};
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _, ToPoint as _};
use theme::{
AccentColors, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
observe_buffer_font_size_adjustment,
@@ -11413,12 +11413,25 @@ impl Editor {
let diff = buffer.diff_for(hunk.buffer_id)?;
let buffer = buffer.buffer(hunk.buffer_id)?;
let buffer = buffer.read(cx);
let original_text = diff
.read(cx)
.base_text()
.as_rope()
.slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0);
let base_text = diff.read(cx).base_text();
let mut base_text_start = hunk.diff_base_byte_range.start.0.to_point(base_text);
let buffer_snapshot = buffer.snapshot();
let mut buffer_start = hunk.buffer_range.start.to_point(&buffer_snapshot);
if base_text_start.row > 0
&& base_text_start.column == 0
&& buffer_start.row > 0
&& buffer_start.column == 0
{
base_text_start.row -= 1;
base_text_start.column = base_text.line_len(base_text_start.row);
buffer_start.row -= 1;
buffer_start.column = buffer.line_len(buffer_start.row);
}
let original_text = diff.read(cx).base_text().as_rope().slice(
text::ToOffset::to_offset(&base_text_start, base_text)..hunk.diff_base_byte_range.end.0,
);
let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default();
if let Err(i) = buffer_revert_changes.binary_search_by(|probe| {
probe
@@ -11427,7 +11440,13 @@ impl Editor {
.cmp(&hunk.buffer_range.start, &buffer_snapshot)
.then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot))
}) {
buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text));
buffer_revert_changes.insert(
i,
(
buffer_snapshot.anchor_before(buffer_start)..hunk.buffer_range.end,
original_text,
),
);
Some(())
} else {
None

View File

@@ -64,7 +64,10 @@ use project::{
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
project_settings::ProjectSettings,
};
use settings::{GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, Settings};
use settings::{
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
Settings,
};
use smallvec::{SmallVec, smallvec};
use std::{
any::TypeId,
@@ -2201,8 +2204,8 @@ impl EditorElement {
.display_diff_hunks_for_rows(display_rows, folded_buffers)
.map(|hunk| (hunk, None))
.collect::<Vec<_>>();
let git_settings = &ProjectSettings::get_global(cx).git;
if git_settings.is_gutter_enabled() {
let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
for (hunk, hitbox) in &mut display_hunks {
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
let hunk_bounds =
@@ -5414,6 +5417,12 @@ impl EditorElement {
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
);
// Don't show hover popovers when context menu is open to avoid overlap
let has_context_menu = self.editor.read(cx).mouse_context_menu.is_some();
if has_context_menu {
return;
}
let hover_popovers = self.editor.update(cx, |editor, cx| {
editor.hover_state.render(
snapshot,
@@ -6466,7 +6475,12 @@ impl EditorElement {
.position_map
.snapshot
.show_git_diff_gutter
.unwrap_or_else(|| ProjectSettings::get_global(cx).git.is_gutter_enabled());
.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
GitGutterSetting::TrackedFiles
)
});
if show_git_gutter {
Self::paint_gutter_diff_hunks(layout, window, cx)
}

View File

@@ -205,6 +205,49 @@ impl EditorLspTestContext {
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
"#})),
text_objects: Some(Cow::from(indoc! {r#"
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
; Arrow function in variable declaration - capture the full declaration
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
]) @function.around
([
(lexical_declaration
(variable_declarator
value: (arrow_function)))
(variable_declaration
(variable_declarator
value: (arrow_function)))
]) @function.around
; Catch-all for arrow functions in other contexts (callbacks, etc.)
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
"#})),
..Default::default()
})
.expect("Could not parse queries");
@@ -276,6 +319,49 @@ impl EditorLspTestContext {
(jsx_opening_element) @start
(jsx_closing_element)? @end) @indent
"#})),
text_objects: Some(Cow::from(indoc! {r#"
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(method_definition
body: (_
"{"
(_)* @function.inside
"}")) @function.around
; Arrow function in variable declaration - capture the full declaration
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
]) @function.around
([
(lexical_declaration
(variable_declarator
value: (arrow_function)))
(variable_declaration
(variable_declarator
value: (arrow_function)))
]) @function.around
; Catch-all for arrow functions in other contexts (callbacks, etc.)
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
"#})),
..Default::default()
})
.expect("Could not parse queries");

View File

@@ -19,6 +19,9 @@ impl Global for GlobalExtensionHostProxy {}
///
/// This object implements each of the individual proxy types so that their
/// methods can be called directly on it.
/// Registration function for language model providers.
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send>;
#[derive(Default)]
pub struct ExtensionHostProxy {
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
@@ -29,6 +32,7 @@ pub struct ExtensionHostProxy {
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
}
impl ExtensionHostProxy {
@@ -54,6 +58,7 @@ impl ExtensionHostProxy {
slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
debug_adapter_provider_proxy: RwLock::default(),
language_model_provider_proxy: RwLock::default(),
}
}
@@ -90,6 +95,15 @@ impl ExtensionHostProxy {
.write()
.replace(Arc::new(proxy));
}
pub fn register_language_model_provider_proxy(
&self,
proxy: impl ExtensionLanguageModelProviderProxy,
) {
self.language_model_provider_proxy
.write()
.replace(Arc::new(proxy));
}
}
pub trait ExtensionThemeProxy: Send + Sync + 'static {
@@ -446,3 +460,37 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
proxy.unregister_debug_locator(locator_name)
}
}
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
fn register_language_model_provider(
&self,
provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
);
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App);
}
impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy {
fn register_language_model_provider(
&self,
provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
) {
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
return;
};
proxy.register_language_model_provider(provider_id, register_fn, cx)
}
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
return;
};
proxy.unregister_language_model_provider(provider_id, cx)
}
}

View File

@@ -93,6 +93,8 @@ pub struct ExtensionManifest {
pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub language_model_providers: BTreeMap<Arc<str>, LanguageModelProviderManifestEntry>,
}
impl ExtensionManifest {
@@ -288,6 +290,16 @@ pub struct DebugAdapterManifestEntry {
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct DebugLocatorManifestEntry {}
/// Manifest entry for a language model provider.
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct LanguageModelProviderManifestEntry {
/// Display name for the provider.
pub name: String,
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
#[serde(default)]
pub icon: Option<String>,
}
impl ExtensionManifest {
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
let extension_name = extension_dir
@@ -358,6 +370,7 @@ fn manifest_from_old_manifest(
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: Default::default(),
}
}
@@ -391,6 +404,7 @@ mod tests {
capabilities: vec![],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -148,6 +148,7 @@ fn manifest() -> ExtensionManifest {
)],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -113,6 +113,7 @@ mod tests {
capabilities: vec![],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -165,6 +165,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},
@@ -196,6 +197,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},
@@ -376,6 +378,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},

155
crates/git_ui/src/clone.rs Normal file
View File

@@ -0,0 +1,155 @@
use gpui::{App, Context, WeakEntity, Window};
use notifications::status_toast::{StatusToast, ToastIcon};
use std::sync::Arc;
use ui::{Color, IconName, SharedString};
use util::ResultExt;
use workspace::{self, Workspace};
pub fn clone_and_open(
repo_url: SharedString,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
on_success: Arc<
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
>,
) {
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select as Repository Destination".into()),
});
window
.spawn(cx, async move |cx| {
let mut paths = destination_prompt.await.ok()?.ok()??;
let mut destination_dir = paths.pop()?;
let repo_name = repo_url
.split('/')
.next_back()
.map(|name| name.strip_suffix(".git").unwrap_or(name))
.unwrap_or("repository")
.to_owned();
let clone_task = workspace
.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
let destination_dir = destination_dir.clone();
let repo_url = repo_url.clone();
cx.spawn(async move |_workspace, _cx| {
fs.git_clone(&repo_url, destination_dir.as_path()).await
})
})
.ok()?;
if let Err(error) = clone_task.await {
workspace
.update(cx, |workspace, cx| {
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.dismiss_button(true)
});
workspace.toggle_status_toast(toast, cx);
})
.log_err();
return None;
}
let has_worktrees = workspace
.read_with(cx, |workspace, cx| {
workspace.project().read(cx).worktrees(cx).next().is_some()
})
.ok()?;
let prompt_answer = if has_worktrees {
cx.update(|window, cx| {
window.prompt(
gpui::PromptLevel::Info,
&format!("Git Clone: {}", repo_name),
None,
&["Add repo to project", "Open repo in new project"],
cx,
)
})
.ok()?
.await
.ok()?
} else {
// Don't ask if project is empty
0
};
destination_dir.push(&repo_name);
match prompt_answer {
0 => {
workspace
.update_in(cx, |workspace, window, cx| {
let create_task = workspace.project().update(cx, |project, cx| {
project.create_worktree(destination_dir.as_path(), true, cx)
});
let workspace_weak = cx.weak_entity();
let on_success = on_success.clone();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
})
.ok()?;
}
1 => {
workspace
.update(cx, move |workspace, cx| {
let app_state = workspace.app_state().clone();
let destination_path = destination_dir.clone();
let on_success = on_success.clone();
workspace::open_new(
Default::default(),
app_state,
cx,
move |workspace, window, cx| {
cx.activate(true);
let create_task =
workspace.project().update(cx, |project, cx| {
project.create_worktree(
destination_path.as_path(),
true,
cx,
)
});
let workspace_weak = cx.weak_entity();
cx.spawn_in(window, async move |_window, cx| {
if create_task.await.log_err().is_some() {
workspace_weak
.update_in(cx, |workspace, window, cx| {
(on_success)(workspace, window, cx);
})
.ok();
}
})
.detach();
},
)
.detach();
})
.ok();
}
_ => {}
}
Some(())
})
.detach();
}

View File

@@ -2849,93 +2849,15 @@ impl GitPanel {
}
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
files: false,
directories: true,
multiple: false,
prompt: Some("Select as Repository Destination".into()),
});
let workspace = self.workspace.clone();
cx.spawn_in(window, async move |this, cx| {
let mut paths = path.await.ok()?.ok()??;
let mut path = paths.pop()?;
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
Ok(_) => cx.update(|window, cx| {
window.prompt(
PromptLevel::Info,
&format!("Git Clone: {}", repo_name),
None,
&["Add repo to project", "Open repo in new project"],
cx,
)
}),
Err(e) => {
this.update(cx, |this: &mut GitPanel, cx| {
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.dismiss_button(true)
});
this.workspace
.update(cx, |workspace, cx| {
workspace.toggle_status_toast(toast, cx);
})
.ok();
})
.ok()?;
return None;
}
}
.ok()?;
path.push(repo_name);
match prompt_answer.await.ok()? {
0 => {
workspace
.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| {
project.create_worktree(path.as_path(), true, cx)
})
.detach();
})
.ok();
}
1 => {
workspace
.update(cx, move |workspace, cx| {
workspace::open_new(
Default::default(),
workspace.app_state().clone(),
cx,
move |workspace, _, cx| {
cx.activate(true);
workspace
.project()
.update(cx, |project, cx| {
project.create_worktree(&path, true, cx)
})
.detach();
},
)
.detach();
})
.ok();
}
_ => {}
}
Some(())
})
.detach();
crate::clone::clone_and_open(
repo.into(),
workspace,
window,
cx,
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
);
}
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -5281,7 +5203,7 @@ impl GitPanel {
this.child(
self.entry_label(path_name, path_color)
.truncate()
.truncate_start()
.when(strikethrough, Label::strikethrough),
)
})

View File

@@ -10,6 +10,7 @@ use ui::{
};
mod blame_ui;
pub mod clone;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},

View File

@@ -2,8 +2,8 @@ use crate::{
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
register_tooltip_mouse_handlers, set_tooltip_on_window,
TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
};
use anyhow::Context as _;
use itertools::Itertools;
@@ -354,7 +354,7 @@ impl TextLayout {
None
};
let (truncate_width, truncation_suffix) =
let (truncate_width, truncation_affix, truncate_from) =
if let Some(text_overflow) = text_style.text_overflow.clone() {
let width = known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
@@ -365,10 +365,11 @@ impl TextLayout {
});
match text_overflow {
TextOverflow::Truncate(s) => (width, s),
TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
}
} else {
(None, "".into())
(None, "".into(), TruncateFrom::End)
};
if let Some(text_layout) = element_state.0.borrow().as_ref()
@@ -383,8 +384,9 @@ impl TextLayout {
line_wrapper.truncate_line(
text.clone(),
truncate_width,
&truncation_suffix,
&truncation_affix,
&runs,
truncate_from,
)
} else {
(text.clone(), Cow::Borrowed(&*runs))

View File

@@ -334,9 +334,13 @@ pub enum WhiteSpace {
/// How to truncate text that overflows the width of the element
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum TextOverflow {
/// Truncate the text when it doesn't fit, and represent this truncation by displaying the
/// provided string.
/// Truncate the text at the end when it doesn't fit, and represent this truncation by
/// displaying the provided string (e.g., "very long te…").
Truncate(SharedString),
/// Truncate the text at the start when it doesn't fit, and represent this truncation by
/// displaying the provided string at the beginning (e.g., "…ong text here").
/// Typically more adequate for file paths where the end is more important than the beginning.
TruncateStart(SharedString),
}
/// How to align text within the element

View File

@@ -75,13 +75,21 @@ pub trait Styled: Sized {
self
}
/// Sets the truncate overflowing text with an ellipsis (…) if needed.
/// Sets the truncate overflowing text with an ellipsis (…) at the end if needed.
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
fn text_ellipsis(mut self) -> Self {
self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS));
self
}
/// Sets the truncate overflowing text with an ellipsis (…) at the start if needed.
/// Typically more adequate for file paths where the end is more important than the beginning.
/// Note: This doesn't exist in Tailwind CSS.
fn text_ellipsis_start(mut self) -> Self {
self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS));
self
}
/// Sets the text overflow behavior of the element.
fn text_overflow(mut self, overflow: TextOverflow) -> Self {
self.text_style().text_overflow = Some(overflow);

View File

@@ -2,6 +2,15 @@ use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun,
use collections::HashMap;
use std::{borrow::Cow, iter, sync::Arc};
/// Determines whether to truncate text from the start or end.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TruncateFrom {
/// Truncate text from the start.
Start,
/// Truncate text from the end.
End,
}
/// The GPUI line wrapper, used to wrap lines of text to a given width.
pub struct LineWrapper {
platform_text_system: Arc<dyn PlatformTextSystem>,
@@ -129,29 +138,50 @@ impl LineWrapper {
}
/// Determines if a line should be truncated based on its width.
///
/// Returns the truncation index in `line`.
pub fn should_truncate_line(
&mut self,
line: &str,
truncate_width: Pixels,
truncation_suffix: &str,
truncation_affix: &str,
truncate_from: TruncateFrom,
) -> Option<usize> {
let mut width = px(0.);
let suffix_width = truncation_suffix
let suffix_width = truncation_affix
.chars()
.map(|c| self.width_for_char(c))
.fold(px(0.0), |a, x| a + x);
let mut truncate_ix = 0;
for (ix, c) in line.char_indices() {
if width + suffix_width < truncate_width {
truncate_ix = ix;
match truncate_from {
TruncateFrom::Start => {
for (ix, c) in line.char_indices().rev() {
if width + suffix_width < truncate_width {
truncate_ix = ix;
}
let char_width = self.width_for_char(c);
width += char_width;
if width.floor() > truncate_width {
return Some(truncate_ix);
}
}
}
TruncateFrom::End => {
for (ix, c) in line.char_indices() {
if width + suffix_width < truncate_width {
truncate_ix = ix;
}
let char_width = self.width_for_char(c);
width += char_width;
let char_width = self.width_for_char(c);
width += char_width;
if width.floor() > truncate_width {
return Some(truncate_ix);
if width.floor() > truncate_width {
return Some(truncate_ix);
}
}
}
}
@@ -163,16 +193,23 @@ impl LineWrapper {
&mut self,
line: SharedString,
truncate_width: Pixels,
truncation_suffix: &str,
truncation_affix: &str,
runs: &'a [TextRun],
truncate_from: TruncateFrom,
) -> (SharedString, Cow<'a, [TextRun]>) {
if let Some(truncate_ix) =
self.should_truncate_line(&line, truncate_width, truncation_suffix)
self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
{
let result =
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
let result = match truncate_from {
TruncateFrom::Start => {
SharedString::from(format!("{truncation_affix}{}", &line[truncate_ix + 1..]))
}
TruncateFrom::End => {
SharedString::from(format!("{}{truncation_affix}", &line[..truncate_ix]))
}
};
let mut runs = runs.to_vec();
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
(result, Cow::Owned(runs))
} else {
(line, Cow::Borrowed(runs))
@@ -245,15 +282,35 @@ impl LineWrapper {
}
}
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
fn update_runs_after_truncation(
result: &str,
ellipsis: &str,
runs: &mut Vec<TextRun>,
truncate_from: TruncateFrom,
) {
let mut truncate_at = result.len() - ellipsis.len();
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
runs.truncate(run_index + 1);
break;
match truncate_from {
TruncateFrom::Start => {
for (run_index, run) in runs.iter_mut().enumerate().rev() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
runs.splice(..run_index, std::iter::empty());
break;
}
}
}
TruncateFrom::End => {
for (run_index, run) in runs.iter_mut().enumerate() {
if run.len <= truncate_at {
truncate_at -= run.len;
} else {
run.len = truncate_at + ellipsis.len();
runs.truncate(run_index + 1);
break;
}
}
}
}
}
@@ -503,7 +560,7 @@ mod tests {
}
#[test]
fn test_truncate_line() {
fn test_truncate_line_end() {
let mut wrapper = build_wrapper();
fn perform_test(
@@ -514,8 +571,13 @@ mod tests {
) {
let dummy_run_lens = vec![text.len()];
let dummy_runs = generate_test_runs(&dummy_run_lens);
let (result, dummy_runs) =
wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
let (result, dummy_runs) = wrapper.truncate_line(
text.into(),
px(220.),
ellipsis,
&dummy_runs,
TruncateFrom::End,
);
assert_eq!(result, expected);
assert_eq!(dummy_runs.first().unwrap().len, result.len());
}
@@ -541,7 +603,50 @@ mod tests {
}
#[test]
fn test_truncate_multiple_runs() {
fn test_truncate_line_start() {
let mut wrapper = build_wrapper();
fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
expected: &'static str,
ellipsis: &str,
) {
let dummy_run_lens = vec![text.len()];
let dummy_runs = generate_test_runs(&dummy_run_lens);
let (result, dummy_runs) = wrapper.truncate_line(
text.into(),
px(220.),
ellipsis,
&dummy_runs,
TruncateFrom::Start,
);
assert_eq!(result, expected);
assert_eq!(dummy_runs.first().unwrap().len, result.len());
}
perform_test(
&mut wrapper,
"aaaa bbbb cccc ddddd eeee fff gg",
"cccc ddddd eeee fff gg",
"",
);
perform_test(
&mut wrapper,
"aaaa bbbb cccc ddddd eeee fff gg",
"…ccc ddddd eeee fff gg",
"",
);
perform_test(
&mut wrapper,
"aaaa bbbb cccc ddddd eeee fff gg",
"......dddd eeee fff gg",
"......",
);
}
#[test]
fn test_truncate_multiple_runs_end() {
let mut wrapper = build_wrapper();
fn perform_test(
@@ -554,7 +659,7 @@ mod tests {
) {
let dummy_runs = generate_test_runs(run_lens);
let (result, dummy_runs) =
wrapper.truncate_line(text.into(), line_width, "", &dummy_runs);
wrapper.truncate_line(text.into(), line_width, "", &dummy_runs, TruncateFrom::End);
assert_eq!(result, expected);
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
assert_eq!(run.len, *result_len);
@@ -600,10 +705,75 @@ mod tests {
}
#[test]
fn test_update_run_after_truncation() {
fn test_truncate_multiple_runs_start() {
let mut wrapper = build_wrapper();
#[track_caller]
fn perform_test(
wrapper: &mut LineWrapper,
text: &'static str,
expected: &str,
run_lens: &[usize],
result_run_len: &[usize],
line_width: Pixels,
) {
let dummy_runs = generate_test_runs(run_lens);
let (result, dummy_runs) = wrapper.truncate_line(
text.into(),
line_width,
"",
&dummy_runs,
TruncateFrom::Start,
);
assert_eq!(result, expected);
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
assert_eq!(run.len, *result_len);
}
}
// Case 0: Normal
// Text: abcdefghijkl
// Runs: Run0 { len: 12, ... }
//
// Truncate res: …ijkl (truncate_at = 9)
// Run res: Run0 { string: …ijkl, len: 7, ... }
perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
// Case 1: Drop some runs
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: …ghijkl (truncate_at = 7)
// Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
// 4, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"…ghijkl",
&[4, 4, 4],
&[5, 4],
px(70.),
);
// Case 2: Truncate at start of some run
// Text: abcdefghijkl
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
//
// Truncate res: abcdefgh… (truncate_at = 3)
// Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
// 4, ... }, Run2 { string: ijkl, len: 4, ... }
perform_test(
&mut wrapper,
"abcdefghijkl",
"…efghijkl",
&[4, 4, 4],
&[3, 4, 4],
px(90.),
);
}
#[test]
fn test_update_run_after_truncation_end() {
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
let mut dummy_runs = generate_test_runs(run_lens);
update_runs_after_truncation(result, "", &mut dummy_runs);
update_runs_after_truncation(result, "", &mut dummy_runs, TruncateFrom::End);
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
assert_eq!(run.len, *result_len);
}

View File

@@ -1141,6 +1141,104 @@ fn test_text_objects(cx: &mut App) {
)
}
#[gpui::test]
fn test_text_objects_with_has_parent_predicate(cx: &mut App) {
use std::borrow::Cow;
// Create a language with a custom text_objects query that uses #has-parent?
// This query only matches closure_expression when it's inside a call_expression
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
text_objects: Some(Cow::from(indoc! {r#"
; Only match closures that are arguments to function calls
(closure_expression) @function.around
(#has-parent? @function.around arguments)
"#})),
..Default::default()
})
.expect("Could not parse queries");
let (text, ranges) = marked_text_ranges(
indoc! {r#"
fn main() {
let standalone = |x| x + 1;
let result = foo(|y| y * ˇ2);
}"#
},
false,
);
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
.map(|(range, text_object)| (&text[range], text_object))
.collect::<Vec<_>>();
// Should only match the closure inside foo(), not the standalone closure
assert_eq!(matches, &[("|y| y * 2", TextObject::AroundFunction),]);
}
#[gpui::test]
fn test_text_objects_with_not_has_parent_predicate(cx: &mut App) {
use std::borrow::Cow;
// Create a language with a custom text_objects query that uses #not-has-parent?
// This query only matches closure_expression when it's NOT inside a call_expression
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_queries(LanguageQueries {
text_objects: Some(Cow::from(indoc! {r#"
; Only match closures that are NOT arguments to function calls
(closure_expression) @function.around
(#not-has-parent? @function.around arguments)
"#})),
..Default::default()
})
.expect("Could not parse queries");
let (text, ranges) = marked_text_ranges(
indoc! {r#"
fn main() {
let standalone = |x| x +ˇ 1;
let result = foo(|y| y * 2);
}"#
},
false,
);
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
.text_object_ranges(ranges[0].clone(), TreeSitterOptions::default())
.map(|(range, text_object)| (&text[range], text_object))
.collect::<Vec<_>>();
// Should only match the standalone closure, not the one inside foo()
assert_eq!(matches, &[("|x| x + 1", TextObject::AroundFunction),]);
}
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
#[track_caller]

View File

@@ -19,7 +19,10 @@ use std::{
use streaming_iterator::StreamingIterator;
use sum_tree::{Bias, Dimensions, SeekTarget, SumTree};
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint};
use tree_sitter::{Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree};
use tree_sitter::{
Node, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatch, QueryMatches,
QueryPredicateArg, Tree,
};
pub const MAX_BYTES_TO_QUERY: usize = 16 * 1024;
@@ -82,6 +85,7 @@ struct SyntaxMapMatchesLayer<'a> {
next_captures: Vec<QueryCapture<'a>>,
has_next: bool,
matches: QueryMatches<'a, 'a, TextProvider<'a>, &'a [u8]>,
query: &'a Query,
grammar_index: usize,
_query_cursor: QueryCursorHandle,
}
@@ -1163,6 +1167,7 @@ impl<'a> SyntaxMapMatches<'a> {
depth: layer.depth,
grammar_index,
matches,
query,
next_pattern_index: 0,
next_captures: Vec::new(),
has_next: false,
@@ -1260,13 +1265,20 @@ impl SyntaxMapCapturesLayer<'_> {
impl SyntaxMapMatchesLayer<'_> {
fn advance(&mut self) {
if let Some(mat) = self.matches.next() {
self.next_captures.clear();
self.next_captures.extend_from_slice(mat.captures);
self.next_pattern_index = mat.pattern_index;
self.has_next = true;
} else {
self.has_next = false;
loop {
if let Some(mat) = self.matches.next() {
if !satisfies_custom_predicates(self.query, mat) {
continue;
}
self.next_captures.clear();
self.next_captures.extend_from_slice(mat.captures);
self.next_pattern_index = mat.pattern_index;
self.has_next = true;
return;
} else {
self.has_next = false;
return;
}
}
}
@@ -1295,6 +1307,39 @@ impl<'a> Iterator for SyntaxMapCaptures<'a> {
}
}
fn satisfies_custom_predicates(query: &Query, mat: &QueryMatch) -> bool {
for predicate in query.general_predicates(mat.pattern_index) {
let satisfied = match predicate.operator.as_ref() {
"has-parent?" => has_parent(&predicate.args, mat),
"not-has-parent?" => !has_parent(&predicate.args, mat),
_ => true,
};
if !satisfied {
return false;
}
}
true
}
fn has_parent(args: &[QueryPredicateArg], mat: &QueryMatch) -> bool {
let (
Some(QueryPredicateArg::Capture(capture_ix)),
Some(QueryPredicateArg::String(parent_kind)),
) = (args.first(), args.get(1))
else {
return false;
};
let Some(capture) = mat.captures.iter().find(|c| c.index == *capture_ix) else {
return false;
};
capture
.node
.parent()
.is_some_and(|p| p.kind() == parent_kind.as_ref())
}
fn join_ranges(
a: impl Iterator<Item = Range<usize>>,
b: impl Iterator<Item = Range<usize>>,

View File

@@ -4,7 +4,10 @@
//! which is a set of tools used to interact with the projects written in said language.
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
use std::{path::PathBuf, sync::Arc};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use async_trait::async_trait;
use collections::HashMap;
@@ -36,7 +39,7 @@ pub struct Toolchain {
/// - Only in the subproject they're currently in.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ToolchainScope {
Subproject(WorktreeId, Arc<RelPath>),
Subproject(Arc<Path>, Arc<RelPath>),
Project,
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
Global,

View File

@@ -797,11 +797,26 @@ pub enum AuthenticateError {
Other(#[from] anyhow::Error),
}
/// Either a built-in icon name or a path to an external SVG.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IconOrSvg {
/// A built-in icon from Zed's icon set.
Icon(IconName),
/// Path to a custom SVG icon file.
Svg(SharedString),
}
impl Default for IconOrSvg {
fn default() -> Self {
Self::Icon(IconName::ZedAssistant)
}
}
pub trait LanguageModelProvider: 'static {
fn id(&self) -> LanguageModelProviderId;
fn name(&self) -> LanguageModelProviderName;
fn icon(&self) -> IconName {
IconName::ZedAssistant
fn icon(&self) -> IconOrSvg {
IconOrSvg::default()
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
@@ -820,7 +835,7 @@ pub trait LanguageModelProvider: 'static {
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
}
#[derive(Default, Clone)]
#[derive(Default, Clone, PartialEq, Eq)]
pub enum ConfigurationViewTargetAgent {
#[default]
ZedAgent,

View File

@@ -2,12 +2,16 @@ use crate::{
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderState,
};
use collections::BTreeMap;
use collections::{BTreeMap, HashSet};
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
use std::{str::FromStr, sync::Arc};
use thiserror::Error;
use util::maybe;
/// Function type for checking if a built-in provider should be hidden.
/// Returns Some(extension_id) if the provider should be hidden when that extension is installed.
pub type BuiltinProviderHidingFn = Box<dyn Fn(&str) -> Option<&'static str> + Send + Sync>;
pub fn init(cx: &mut App) {
let registry = cx.new(|_cx| LanguageModelRegistry::default());
cx.set_global(GlobalLanguageModelRegistry(registry));
@@ -48,6 +52,11 @@ pub struct LanguageModelRegistry {
thread_summary_model: Option<ConfiguredModel>,
providers: BTreeMap<LanguageModelProviderId, Arc<dyn LanguageModelProvider>>,
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
/// Set of installed extension IDs that provide language models.
/// Used to determine which built-in providers should be hidden.
installed_llm_extension_ids: HashSet<Arc<str>>,
/// Function to check if a built-in provider should be hidden by an extension.
builtin_provider_hiding_fn: Option<BuiltinProviderHidingFn>,
}
#[derive(Debug)]
@@ -104,6 +113,8 @@ pub enum Event {
ProviderStateChanged(LanguageModelProviderId),
AddedProvider(LanguageModelProviderId),
RemovedProvider(LanguageModelProviderId),
/// Emitted when provider visibility changes due to extension install/uninstall.
ProvidersChanged,
}
impl EventEmitter<Event> for LanguageModelRegistry {}
@@ -183,6 +194,60 @@ impl LanguageModelRegistry {
providers
}
/// Returns providers, filtering out hidden built-in providers.
pub fn visible_providers(&self) -> Vec<Arc<dyn LanguageModelProvider>> {
self.providers()
.into_iter()
.filter(|p| !self.should_hide_provider(&p.id()))
.collect()
}
/// Sets the function used to check if a built-in provider should be hidden.
pub fn set_builtin_provider_hiding_fn(&mut self, hiding_fn: BuiltinProviderHidingFn) {
self.builtin_provider_hiding_fn = Some(hiding_fn);
}
/// Called when an extension is installed/loaded.
/// If the extension provides language models, track it so we can hide the corresponding built-in.
pub fn extension_installed(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
if self.installed_llm_extension_ids.insert(extension_id) {
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Called when an extension is uninstalled/unloaded.
pub fn extension_uninstalled(&mut self, extension_id: &str, cx: &mut Context<Self>) {
if self.installed_llm_extension_ids.remove(extension_id) {
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Sync the set of installed LLM extension IDs.
pub fn sync_installed_llm_extensions(
&mut self,
extension_ids: HashSet<Arc<str>>,
cx: &mut Context<Self>,
) {
if extension_ids != self.installed_llm_extension_ids {
self.installed_llm_extension_ids = extension_ids;
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Returns true if a provider should be hidden from the UI.
/// Built-in providers are hidden when their corresponding extension is installed.
pub fn should_hide_provider(&self, provider_id: &LanguageModelProviderId) -> bool {
if let Some(ref hiding_fn) = self.builtin_provider_hiding_fn {
if let Some(extension_id) = hiding_fn(&provider_id.0) {
return self.installed_llm_extension_ids.contains(extension_id);
}
}
false
}
pub fn configuration_error(
&self,
model: Option<ConfiguredModel>,
@@ -416,4 +481,132 @@ mod tests {
let providers = registry.read(cx).providers();
assert!(providers.is_empty());
}
#[gpui::test]
fn test_provider_hiding_on_extension_install(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
let provider_id = provider.id();
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
});
let visible = registry.read(cx).visible_providers();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].id(), provider_id);
registry.update(cx, |registry, cx| {
registry.extension_installed("fake-extension".into(), cx);
});
let visible = registry.read(cx).visible_providers();
assert!(visible.is_empty());
let all = registry.read(cx).providers();
assert_eq!(all.len(), 1);
}
#[gpui::test]
fn test_provider_unhiding_on_extension_uninstall(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
let provider_id = provider.id();
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
registry.extension_installed("fake-extension".into(), cx);
});
let visible = registry.read(cx).visible_providers();
assert!(visible.is_empty());
registry.update(cx, |registry, cx| {
registry.extension_uninstalled("fake-extension", cx);
});
let visible = registry.read(cx).visible_providers();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].id(), provider_id);
}
#[gpui::test]
fn test_should_hide_provider(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
registry.update(cx, |registry, cx| {
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "anthropic" {
Some("anthropic")
} else if id == "openai" {
Some("openai")
} else {
None
}
}));
registry.extension_installed("anthropic".into(), cx);
});
let registry_read = registry.read(cx);
assert!(registry_read.should_hide_provider(&LanguageModelProviderId("anthropic".into())));
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("openai".into())));
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("unknown".into())));
}
#[gpui::test]
fn test_sync_installed_llm_extensions(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
});
let mut extension_ids = HashSet::default();
extension_ids.insert(Arc::from("fake-extension"));
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(extension_ids, cx);
});
assert!(registry.read(cx).visible_providers().is_empty());
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(HashSet::default(), cx);
});
assert_eq!(registry.read(cx).visible_providers().len(), 1);
}
}

View File

@@ -28,6 +28,8 @@ convert_case.workspace = true
copilot.workspace = true
credentials_provider.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }

View File

@@ -0,0 +1,67 @@
use collections::HashMap;
use extension::{
ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration,
};
use gpui::{App, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use std::sync::{Arc, LazyLock};
/// Maps built-in provider IDs to their corresponding extension IDs.
/// When an extension with this ID is installed, the built-in provider should be hidden.
static BUILTIN_TO_EXTENSION_MAP: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| {
let mut map = HashMap::default();
map.insert("anthropic", "anthropic");
map.insert("openai", "openai");
map.insert("google", "google-ai");
map.insert("openrouter", "openrouter");
map.insert("copilot_chat", "copilot-chat");
map
});
/// Returns the extension ID that should hide the given built-in provider.
pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> {
BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied()
}
/// Proxy that registers extension language model providers with the LanguageModelRegistry.
pub struct LanguageModelProviderRegistryProxy {
registry: Entity<LanguageModelRegistry>,
}
impl LanguageModelProviderRegistryProxy {
pub fn new(registry: Entity<LanguageModelRegistry>) -> Self {
Self { registry }
}
}
impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy {
fn register_language_model_provider(
&self,
_provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
) {
register_fn(cx);
}
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
self.registry.update(cx, |registry, cx| {
registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx);
});
}
}
/// Initialize the extension language model provider proxy.
/// This must be called BEFORE extension_host::init to ensure the proxy is available
/// when extensions try to register their language model providers.
pub fn init_proxy(cx: &mut App) {
let proxy = ExtensionHostProxy::default_global(cx);
let registry = LanguageModelRegistry::global(cx);
registry.update(cx, |registry, _cx| {
registry.set_builtin_provider_hiding_fn(Box::new(extension_for_builtin_provider));
});
proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry));
}

View File

@@ -7,9 +7,12 @@ use gpui::{App, Context, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use provider::deepseek::DeepSeekLanguageModelProvider;
pub mod extension;
pub mod provider;
mod settings;
pub use crate::extension::init_proxy as init_extension_proxy;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::bedrock::BedrockLanguageModelProvider;
use crate::provider::cloud::CloudLanguageModelProvider;
@@ -31,6 +34,56 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
register_language_model_providers(registry, user_store, client.clone(), cx);
});
// Subscribe to extension store events to track LLM extension installations
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
cx.subscribe(&extension_store, {
let registry = registry.clone();
move |extension_store, event, cx| match event {
extension_host::Event::ExtensionInstalled(extension_id) => {
if let Some(manifest) = extension_store
.read(cx)
.extension_manifest_for_id(extension_id)
{
if !manifest.language_model_providers.is_empty() {
registry.update(cx, |registry, cx| {
registry.extension_installed(extension_id.clone(), cx);
});
}
}
}
extension_host::Event::ExtensionUninstalled(extension_id) => {
registry.update(cx, |registry, cx| {
registry.extension_uninstalled(extension_id, cx);
});
}
extension_host::Event::ExtensionsUpdated => {
let mut new_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
if !entry.manifest.language_model_providers.is_empty() {
new_ids.insert(extension_id.clone());
}
}
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(new_ids, cx);
});
}
_ => {}
}
})
.detach();
// Initialize with currently installed extensions
registry.update(cx, |registry, cx| {
let mut initial_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
if !entry.manifest.language_model_providers.is_empty() {
initial_ids.insert(extension_id.clone());
}
}
registry.sync_installed_llm_extensions(initial_ids, cx);
});
}
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
.openai_compatible
.keys()

View File

@@ -8,7 +8,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
@@ -125,8 +125,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiAnthropic
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiAnthropic)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -30,7 +30,7 @@ use gpui::{
use gpui_tokio::Tokio;
use http_client::HttpClient;
use language_model::{
AuthenticateError, EnvVar, LanguageModel, LanguageModelCacheConfiguration,
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
@@ -426,8 +426,8 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiBedrock
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiBedrock)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -19,7 +19,7 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Ta
use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
@@ -304,8 +304,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiZed
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiZed)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -18,12 +18,12 @@ use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
use http_client::StatusCode;
use language::language_settings::all_language_settings;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason, TokenUsage,
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use settings::SettingsStore;
use ui::prelude::*;
@@ -104,8 +104,8 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::Copilot
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::Copilot)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -7,7 +7,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -127,8 +127,8 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiDeepSeek
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiDeepSeek)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -14,7 +14,7 @@ use language_model::{
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
@@ -164,8 +164,8 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiGoogle
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiGoogle)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -10,7 +10,7 @@ use language_model::{
StopReason, TokenUsage,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
@@ -175,8 +175,8 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiLmStudio
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiLmStudio)
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -5,7 +5,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -176,8 +176,8 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiMistral
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiMistral)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -5,7 +5,7 @@ use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
@@ -221,8 +221,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOllama
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOllama)
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
@@ -249,33 +249,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
}
// Override with available models from settings
for setting_model in &OllamaLanguageModelProvider::settings(cx).available_models {
let setting_base = setting_model.name.split(':').next().unwrap();
if let Some(model) = models
.values_mut()
.find(|m| m.name.split(':').next().unwrap() == setting_base)
{
model.max_tokens = setting_model.max_tokens;
model.display_name = setting_model.display_name.clone();
model.keep_alive = setting_model.keep_alive.clone();
model.supports_tools = setting_model.supports_tools;
model.supports_vision = setting_model.supports_images;
model.supports_thinking = setting_model.supports_thinking;
} else {
models.insert(
setting_model.name.clone(),
ollama::Model {
name: setting_model.name.clone(),
display_name: setting_model.display_name.clone(),
max_tokens: setting_model.max_tokens,
keep_alive: setting_model.keep_alive.clone(),
supports_tools: setting_model.supports_tools,
supports_vision: setting_model.supports_images,
supports_thinking: setting_model.supports_thinking,
},
);
}
}
merge_settings_into_models(&mut models, &settings.available_models);
let mut models = models
.into_values()
@@ -921,6 +895,35 @@ impl Render for ConfigurationView {
}
}
fn merge_settings_into_models(
models: &mut HashMap<String, ollama::Model>,
available_models: &[AvailableModel],
) {
for setting_model in available_models {
if let Some(model) = models.get_mut(&setting_model.name) {
model.max_tokens = setting_model.max_tokens;
model.display_name = setting_model.display_name.clone();
model.keep_alive = setting_model.keep_alive.clone();
model.supports_tools = setting_model.supports_tools;
model.supports_vision = setting_model.supports_images;
model.supports_thinking = setting_model.supports_thinking;
} else {
models.insert(
setting_model.name.clone(),
ollama::Model {
name: setting_model.name.clone(),
display_name: setting_model.display_name.clone(),
max_tokens: setting_model.max_tokens,
keep_alive: setting_model.keep_alive.clone(),
supports_tools: setting_model.supports_tools,
supports_vision: setting_model.supports_images,
supports_thinking: setting_model.supports_thinking,
},
);
}
}
}
fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
ollama::OllamaTool::Function {
function: OllamaFunctionTool {
@@ -930,3 +933,83 @@ fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_settings_preserves_display_names_for_similar_models() {
// Regression test for https://github.com/zed-industries/zed/issues/43646
// When multiple models share the same base name (e.g., qwen2.5-coder:1.5b and qwen2.5-coder:3b),
// each model should get its own display_name from settings, not a random one.
let mut models: HashMap<String, ollama::Model> = HashMap::new();
models.insert(
"qwen2.5-coder:1.5b".to_string(),
ollama::Model {
name: "qwen2.5-coder:1.5b".to_string(),
display_name: None,
max_tokens: 4096,
keep_alive: None,
supports_tools: None,
supports_vision: None,
supports_thinking: None,
},
);
models.insert(
"qwen2.5-coder:3b".to_string(),
ollama::Model {
name: "qwen2.5-coder:3b".to_string(),
display_name: None,
max_tokens: 4096,
keep_alive: None,
supports_tools: None,
supports_vision: None,
supports_thinking: None,
},
);
let available_models = vec![
AvailableModel {
name: "qwen2.5-coder:1.5b".to_string(),
display_name: Some("QWEN2.5 Coder 1.5B".to_string()),
max_tokens: 5000,
keep_alive: None,
supports_tools: Some(true),
supports_images: None,
supports_thinking: None,
},
AvailableModel {
name: "qwen2.5-coder:3b".to_string(),
display_name: Some("QWEN2.5 Coder 3B".to_string()),
max_tokens: 6000,
keep_alive: None,
supports_tools: Some(true),
supports_images: None,
supports_thinking: None,
},
];
merge_settings_into_models(&mut models, &available_models);
let model_1_5b = models
.get("qwen2.5-coder:1.5b")
.expect("1.5b model missing");
let model_3b = models.get("qwen2.5-coder:3b").expect("3b model missing");
assert_eq!(
model_1_5b.display_name,
Some("QWEN2.5 Coder 1.5B".to_string()),
"1.5b model should have its own display_name"
);
assert_eq!(model_1_5b.max_tokens, 5000);
assert_eq!(
model_3b.display_name,
Some("QWEN2.5 Coder 3B".to_string()),
"3b model should have its own display_name"
);
assert_eq!(model_3b.max_tokens, 6000);
}
}

View File

@@ -5,7 +5,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -122,8 +122,8 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOpenAi
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
@@ -133,8 +133,8 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
self.name.clone()
}
fn icon(&self) -> IconName {
IconName::AiOpenAiCompat
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAiCompat)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -180,8 +180,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOpenRouter
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenRouter)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
@@ -117,8 +117,8 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiVZero
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiVZero)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
@@ -118,8 +118,8 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiXAi
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiXAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -127,6 +127,16 @@ impl LanguageServerState {
return menu;
};
let server_versions = self
.lsp_store
.update(cx, |lsp_store, _| {
lsp_store
.language_server_statuses()
.map(|(server_id, status)| (server_id, status.server_version.clone()))
.collect::<HashMap<_, _>>()
})
.unwrap_or_default();
let mut first_button_encountered = false;
for item in &self.items {
if let LspMenuItem::ToggleServersButton { restart } = item {
@@ -254,6 +264,22 @@ impl LanguageServerState {
};
let server_name = server_info.name.clone();
let server_version = server_versions
.get(&server_info.id)
.and_then(|version| version.clone());
let tooltip_text = match (&server_version, &message) {
(None, None) => None,
(Some(version), None) => {
Some(SharedString::from(format!("Version: {}", version.as_ref())))
}
(None, Some(message)) => Some(message.clone()),
(Some(version), Some(message)) => Some(SharedString::from(format!(
"Version: {}\n\n{}",
version.as_ref(),
message.as_ref()
))),
};
menu = menu.item(ContextMenuItem::custom_entry(
move |_, _| {
h_flex()
@@ -355,11 +381,11 @@ impl LanguageServerState {
}
}
},
message.map(|server_message| {
tooltip_text.map(|tooltip_text| {
DocumentationAside::new(
DocumentationSide::Right,
DocumentationEdge::Bottom,
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
DocumentationEdge::Top,
Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
)
}),
));

View File

@@ -330,6 +330,8 @@ impl LspLogView {
let server_info = format!(
"* Server: {NAME} (id {ID})
* Version: {VERSION}
* Binary: {BINARY}
* Registered workspace folders:
@@ -340,6 +342,12 @@ impl LspLogView {
* Configuration: {CONFIGURATION}",
NAME = info.status.name,
ID = info.id,
VERSION = info
.status
.server_version
.as_ref()
.map(|version| version.as_ref())
.unwrap_or("Unknown"),
BINARY = info
.status
.binary
@@ -1334,6 +1342,7 @@ impl ServerInfo {
capabilities: server.capabilities(),
status: LanguageServerStatus {
name: server.name(),
server_version: server.version(),
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),

View File

@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
(arrow_function
((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(#not-has-parent? @function.around variable_declarator))
(arrow_function) @function.around
; Arrow function in variable declaration - capture the full declaration
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
]) @function.around
; Arrow function in variable declaration (captures body for expression-bodied arrows)
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
]) @function.around
; Catch-all for arrow functions in other contexts (callbacks, etc.)
((arrow_function
body: (_) @function.inside) @function.around
(#not-has-parent? @function.around variable_declarator))
(generator_function
body: (_

View File

@@ -18,13 +18,47 @@
(_)* @function.inside
"}")) @function.around
(arrow_function
((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(#not-has-parent? @function.around variable_declarator))
(arrow_function) @function.around
; Arrow function in variable declaration - capture the full declaration
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
]) @function.around
; Arrow function in variable declaration (expression body fallback)
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
]) @function.around
; Catch-all for arrow functions in other contexts (callbacks, etc.)
((arrow_function
body: (_) @function.inside) @function.around
(#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function

View File

@@ -18,13 +18,48 @@
(_)* @function.inside
"}")) @function.around
(arrow_function
((arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}")) @function.around
(#not-has-parent? @function.around variable_declarator))
(arrow_function) @function.around
; Arrow function in variable declaration - capture the full declaration
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (statement_block
"{"
(_)* @function.inside
"}"))))
]) @function.around
; Arrow function in variable declaration - capture body as @function.inside
; (for statement blocks, the more specific pattern above captures just the contents)
([
(lexical_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
(variable_declaration
(variable_declarator
value: (arrow_function
body: (_) @function.inside)))
]) @function.around
; Catch-all for arrow functions in other contexts (callbacks, etc.)
((arrow_function
body: (_) @function.inside) @function.around
(#not-has-parent? @function.around variable_declarator))
(function_signature) @function.around
(generator_function

View File

@@ -1,6 +1,6 @@
name = "YAML"
grammar = "yaml"
path_suffixes = ["yml", "yaml", "pixi.lock", "clang-format", "clangd"]
path_suffixes = ["yml", "yaml", "pixi.lock", "clang-format", "clangd", "bst"]
line_comments = ["# "]
autoclose_before = ",]}"
brackets = [

View File

@@ -89,6 +89,7 @@ pub struct LanguageServer {
outbound_tx: channel::Sender<String>,
notification_tx: channel::Sender<NotificationSerializer>,
name: LanguageServerName,
version: Option<SharedString>,
process_name: Arc<str>,
binary: LanguageServerBinary,
capabilities: RwLock<ServerCapabilities>,
@@ -501,6 +502,7 @@ impl LanguageServer {
response_handlers,
io_handlers,
name: server_name,
version: None,
process_name: binary
.path
.file_name()
@@ -882,7 +884,9 @@ impl LanguageServer {
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),
show_message: Some(ShowMessageRequestClientCapabilities {
message_action_item: None,
message_action_item: Some(MessageActionItemCapabilities {
additional_properties_support: Some(true),
}),
}),
..WindowClientCapabilities::default()
}),
@@ -923,6 +927,7 @@ impl LanguageServer {
)
})?;
if let Some(info) = response.server_info {
self.version = info.version.map(SharedString::from);
self.process_name = info.name.into();
}
self.capabilities = RwLock::new(response.capabilities);
@@ -1153,6 +1158,11 @@ impl LanguageServer {
self.name.clone()
}
/// Get the version of the running language server.
pub fn version(&self) -> Option<SharedString> {
self.version.clone()
}
pub fn process_name(&self) -> &str {
&self.process_name
}

View File

@@ -128,6 +128,7 @@ use util::{
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
paths::{PathStyle, SanitizedPath},
post_inc,
redact::redact_command,
rel_path::RelPath,
};
@@ -577,9 +578,12 @@ impl LocalLspStore {
},
},
);
log::error!("Failed to start language server {server_name:?}: {err:?}");
log::error!(
"Failed to start language server {server_name:?}: {}",
redact_command(&format!("{err:?}"))
);
if !log.is_empty() {
log::error!("server stderr: {log}");
log::error!("server stderr: {}", redact_command(&log));
}
None
}
@@ -3860,6 +3864,7 @@ pub enum LspStoreEvent {
#[derive(Clone, Debug, Serialize)]
pub struct LanguageServerStatus {
pub name: LanguageServerName,
pub server_version: Option<SharedString>,
pub pending_work: BTreeMap<ProgressToken, LanguageServerProgress>,
pub has_pending_diagnostic_updates: bool,
pub progress_tokens: HashSet<ProgressToken>,
@@ -8350,6 +8355,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name,
server_version: None,
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),
@@ -9385,6 +9391,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name: server_name.clone(),
server_version: None,
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),
@@ -11415,6 +11422,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name: language_server.name(),
server_version: language_server.version(),
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),
@@ -13768,7 +13776,7 @@ impl From<lsp::Documentation> for CompletionDocumentation {
match docs {
lsp::Documentation::String(text) => {
if text.lines().count() <= 1 {
CompletionDocumentation::SingleLine(text.into())
CompletionDocumentation::SingleLine(text.trim().to_string().into())
} else {
CompletionDocumentation::MultiLinePlainText(text.into())
}
@@ -14360,4 +14368,22 @@ mod tests {
)
);
}
#[test]
fn test_trailing_newline_in_completion_documentation() {
let doc = lsp::Documentation::String(
"Inappropriate argument value (of correct type).\n".to_string(),
);
let completion_doc: CompletionDocumentation = doc.into();
assert!(
matches!(completion_doc, CompletionDocumentation::SingleLine(s) if s == "Inappropriate argument value (of correct type).")
);
let doc = lsp::Documentation::String(" some value \n".to_string());
let completion_doc: CompletionDocumentation = doc.into();
assert!(matches!(
completion_doc,
CompletionDocumentation::SingleLine(s) if s == "some value"
));
}
}

View File

@@ -1330,7 +1330,12 @@ impl Project {
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
let toolchain_store = cx.new(|cx| {
ToolchainStore::remote(REMOTE_SERVER_PROJECT_ID, remote.read(cx).proto_client(), cx)
ToolchainStore::remote(
REMOTE_SERVER_PROJECT_ID,
worktree_store.clone(),
remote.read(cx).proto_client(),
cx,
)
});
let task_store = cx.new(|cx| {
TaskStore::remote(

View File

@@ -365,27 +365,6 @@ pub struct GitSettings {
pub path_style: GitPathStyle,
}
impl GitSettings {
/// Returns whether git status indicators should be shown.
/// This includes status in the project panel, outline panel, and tabs.
pub fn is_status_enabled(&self) -> bool {
self.enabled.status
}
/// Returns whether git diff features should be shown.
/// This includes gutter diff indicators and scrollbar diff markers.
pub fn is_diff_enabled(&self) -> bool {
self.enabled.diff
}
/// Returns whether the git gutter should be shown.
/// This checks both the global diff setting and the gutter-specific setting.
pub fn is_gutter_enabled(&self) -> bool {
self.is_diff_enabled()
&& matches!(self.git_gutter, settings::GitGutterSetting::TrackedFiles)
}
}
#[derive(Clone, Copy, Debug)]
pub struct GitEnabledSettings {
/// Whether git integration is enabled for showing git status.

View File

@@ -32,6 +32,7 @@ use crate::{
pub struct ToolchainStore {
mode: ToolchainStoreInner,
user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
worktree_store: Entity<WorktreeStore>,
_sub: Subscription,
}
@@ -66,7 +67,7 @@ impl ToolchainStore {
) -> Self {
let entity = cx.new(|_| LocalToolchainStore {
languages,
worktree_store,
worktree_store: worktree_store.clone(),
project_environment,
active_toolchains: Default::default(),
manifest_tree,
@@ -77,12 +78,18 @@ impl ToolchainStore {
});
Self {
mode: ToolchainStoreInner::Local(entity),
worktree_store,
user_toolchains: Default::default(),
_sub,
}
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) -> Self {
pub(super) fn remote(
project_id: u64,
worktree_store: Entity<WorktreeStore>,
client: AnyProtoClient,
cx: &mut Context<Self>,
) -> Self {
let entity = cx.new(|_| RemoteToolchainStore { client, project_id });
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
@@ -90,6 +97,7 @@ impl ToolchainStore {
Self {
mode: ToolchainStoreInner::Remote(entity),
user_toolchains: Default::default(),
worktree_store,
_sub,
}
}
@@ -165,12 +173,22 @@ impl ToolchainStore {
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Option<Toolchains>> {
let Some(worktree) = self
.worktree_store
.read(cx)
.worktree_for_id(path.worktree_id, cx)
else {
return Task::ready(None);
};
let target_root_path = worktree.read_with(cx, |this, _| this.abs_path());
let user_toolchains = self
.user_toolchains
.iter()
.filter(|(scope, _)| {
if let ToolchainScope::Subproject(worktree_id, relative_path) = scope {
path.worktree_id == *worktree_id && relative_path.starts_with(&path.path)
if let ToolchainScope::Subproject(subproject_root_path, relative_path) = scope {
target_root_path == *subproject_root_path
&& relative_path.starts_with(&path.path)
} else {
true
}

1
crates/project/src/x.py Normal file
View File

@@ -0,0 +1 @@
Gliwice makerspace

View File

@@ -45,6 +45,7 @@ workspace.workspace = true
language.workspace = true
zed_actions.workspace = true
telemetry.workspace = true
notifications.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -29,6 +29,7 @@ use gpui::{
};
use language::DiagnosticSeverity;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId,
ProjectPath, Worktree, WorktreeId,
@@ -1140,6 +1141,12 @@ impl ProjectPanel {
"Copy Relative Path",
Box::new(zed_actions::workspace::CopyRelativePath),
)
.when(!is_dir && self.has_git_changes(entry_id), |menu| {
menu.separator().action(
"Restore File",
Box::new(git::RestoreFile { skip_prompt: false }),
)
})
.when(has_git_repo, |menu| {
menu.separator()
.action("View File History", Box::new(git::FileHistory))
@@ -1180,6 +1187,19 @@ impl ProjectPanel {
cx.notify();
}
fn has_git_changes(&self, entry_id: ProjectEntryId) -> bool {
for visible in &self.state.visible_entries {
if let Some(git_entry) = visible.entries.iter().find(|e| e.id == entry_id) {
let total_modified =
git_entry.git_summary.index.modified + git_entry.git_summary.worktree.modified;
let total_deleted =
git_entry.git_summary.index.deleted + git_entry.git_summary.worktree.deleted;
return total_modified > 0 || total_deleted > 0;
}
}
false
}
fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
if !entry.is_dir() || self.state.unfolded_dir_ids.contains(&entry.id) {
return false;
@@ -2041,6 +2061,100 @@ impl ProjectPanel {
self.remove(false, action.skip_prompt, window, cx);
}
fn restore_file(
&mut self,
action: &git::RestoreFile,
window: &mut Window,
cx: &mut Context<Self>,
) {
maybe!({
let selection = self.state.selection?;
let project = self.project.read(cx);
let (_worktree, entry) = self.selected_sub_entry(cx)?;
if entry.is_dir() {
return None;
}
let project_path = project.path_for_entry(selection.entry_id, cx)?;
let git_store = project.git_store();
let (repository, repo_path) = git_store
.read(cx)
.repository_and_path_for_project_path(&project_path, cx)?;
let snapshot = repository.read(cx).snapshot();
let status = snapshot.status_for_path(&repo_path)?;
if !status.status.is_modified() && !status.status.is_deleted() {
return None;
}
let file_name = entry.path.file_name()?.to_string();
let answer = if !action.skip_prompt {
let prompt = format!("Discard changes to {}?", file_name);
Some(window.prompt(PromptLevel::Info, &prompt, None, &["Restore", "Cancel"], cx))
} else {
None
};
cx.spawn_in(window, async move |panel, cx| {
if let Some(answer) = answer
&& answer.await != Ok(0)
{
return anyhow::Ok(());
}
let task = panel.update(cx, |_panel, cx| {
repository.update(cx, |repo, cx| {
repo.checkout_files("HEAD", vec![repo_path], cx)
})
})?;
if let Err(e) = task.await {
panel
.update(cx, |panel, cx| {
let message = format!("Failed to restore {}: {}", file_name, e);
let toast = StatusToast::new(message, cx, |this, _| {
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
.dismiss_button(true)
});
panel
.workspace
.update(cx, |workspace, cx| {
workspace.toggle_status_toast(toast, cx);
})
.ok();
})
.ok();
}
panel
.update(cx, |panel, cx| {
panel.project.update(cx, |project, cx| {
if let Some(buffer_id) = project
.buffer_store()
.read(cx)
.buffer_id_for_project_path(&project_path)
{
if let Some(buffer) = project.buffer_for_id(*buffer_id, cx) {
buffer.update(cx, |buffer, cx| {
let _ = buffer.reload(cx);
});
}
}
})
})
.ok();
anyhow::Ok(())
})
.detach_and_log_err(cx);
Some(())
});
}
fn remove(
&mut self,
trash: bool,
@@ -5631,6 +5745,7 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::copy))
.on_action(cx.listener(Self::paste))
.on_action(cx.listener(Self::duplicate))
.on_action(cx.listener(Self::restore_file))
.when(!project.is_remote(), |el| {
el.on_action(cx.listener(Self::trash))
})

View File

@@ -322,28 +322,12 @@ pub struct GitSettings {
pub path_style: Option<GitPathStyle>,
}
/// Global controls for enabling or disabling git integration features.
///
/// These settings provide a centralized way to control git integration,
/// rather than having to configure each feature individually.
#[with_fallible_options]
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
#[serde(rename_all = "snake_case")]
pub struct GitEnabledSettings {
/// Master switch to disable all git integration features.
/// When true, all git features are disabled regardless of other settings.
///
/// Default: false
pub disable_git: Option<bool>,
/// Whether to show git status indicators (modified, added, deleted)
/// in the project panel, outline panel, and tabs.
///
/// Default: true
pub enable_status: Option<bool>,
/// Whether to show git diff information, including gutter diff
/// indicators and scrollbar diff markers.
///
/// Default: true
pub enable_diff: Option<bool>,
}

View File

@@ -939,7 +939,6 @@ impl TerminalPanel {
cx: &mut Context<Self>,
) -> Task<Result<WeakEntity<Terminal>>> {
let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target;
let task_workspace = self.workspace.clone();
cx.spawn_in(window, async move |terminal_panel, cx| {
let project = terminal_panel.update(cx, |this, cx| {
@@ -955,6 +954,14 @@ impl TerminalPanel {
terminal_to_replace.set_terminal(new_terminal.clone(), window, cx);
})?;
let reveal_target = terminal_panel.update(cx, |panel, _| {
if panel.center.panes().iter().any(|p| **p == task_pane) {
RevealTarget::Dock
} else {
RevealTarget::Center
}
})?;
match reveal {
RevealStrategy::Always => match reveal_target {
RevealTarget::Center => {

View File

@@ -50,28 +50,24 @@ impl ScrollableHandle for TerminalScrollHandle {
let state = self.state.borrow();
size(
Pixels::ZERO,
state
.total_lines
.checked_sub(state.viewport_lines)
.unwrap_or(0) as f32
* state.line_height,
state.total_lines.saturating_sub(state.viewport_lines) as f32 * state.line_height,
)
}
fn offset(&self) -> Point<Pixels> {
let state = self.state.borrow();
let scroll_offset = state.total_lines - state.viewport_lines - state.display_offset;
Point::new(
Pixels::ZERO,
-(scroll_offset as f32 * self.state.borrow().line_height),
)
let scroll_offset = state
.total_lines
.saturating_sub(state.viewport_lines)
.saturating_sub(state.display_offset);
Point::new(Pixels::ZERO, -(scroll_offset as f32 * state.line_height))
}
fn set_offset(&self, point: Point<Pixels>) {
let state = self.state.borrow();
let offset_delta = (point.y / state.line_height).round() as i32;
let max_offset = state.total_lines - state.viewport_lines;
let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
self.future_display_offset

28
crates/title_bar/build.rs Normal file
View File

@@ -0,0 +1,28 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
fn main() {
println!("cargo::rustc-check-cfg=cfg(macos_sdk_26)");
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-version"])
.output()
.unwrap();
let sdk_version = String::from_utf8(output.stdout).unwrap();
let major_version: Option<u32> = sdk_version
.trim()
.split('.')
.next()
.and_then(|v| v.parse().ok());
if let Some(major) = major_version
&& major >= 26
{
println!("cargo:rustc-cfg=macos_sdk_26");
}
}
}

View File

@@ -1,12 +1,7 @@
use gpui::{Entity, OwnedMenu, OwnedMenuItem};
use gpui::{Action, Entity, OwnedMenu, OwnedMenuItem, actions};
use settings::Settings;
#[cfg(not(target_os = "macos"))]
use gpui::{Action, actions};
#[cfg(not(target_os = "macos"))]
use schemars::JsonSchema;
#[cfg(not(target_os = "macos"))]
use serde::Deserialize;
use smallvec::SmallVec;
@@ -14,18 +9,23 @@ use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use crate::title_bar_settings::TitleBarSettings;
#[cfg(not(target_os = "macos"))]
actions!(
app_menu,
[
/// Navigates to the menu item on the right.
/// Activates the menu on the right in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuRight,
/// Navigates to the menu item on the left.
/// Activates the menu on the left in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
ActivateMenuLeft
]
);
#[cfg(not(target_os = "macos"))]
/// Opens the named menu in the client-side application menu.
///
/// Does not apply to platform menu bars (e.g. on macOS).
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)]
#[action(namespace = app_menu)]
pub struct OpenApplicationMenu(String);

View File

@@ -1,6 +1,10 @@
/// Use pixels here instead of a rem-based size because the macOS traffic
/// lights are a static size, and don't scale with the rest of the UI.
///
/// Magic number: There is one extra pixel of padding on the left side due to
/// the 1px border around the window on macOS apps.
// Use pixels here instead of a rem-based size because the macOS traffic
// lights are a static size, and don't scale with the rest of the UI.
//
// Magic number: There is one extra pixel of padding on the left side due to
// the 1px border around the window on macOS apps.
#[cfg(macos_sdk_26)]
pub const TRAFFIC_LIGHT_PADDING: f32 = 78.;
#[cfg(not(macos_sdk_26))]
pub const TRAFFIC_LIGHT_PADDING: f32 = 71.;

View File

@@ -447,34 +447,38 @@ impl TitleBar {
return None;
}
Some(
Button::new("restricted_mode_trigger", "Restricted Mode")
.style(ButtonStyle::Tinted(TintColor::Warning))
.label_size(LabelSize::Small)
.color(Color::Warning)
.icon(IconName::Warning)
.icon_color(Color::Warning)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.tooltip(|_, cx| {
Tooltip::with_meta(
"You're in Restricted Mode",
Some(&ToggleWorktreeSecurity),
"Mark this project as trusted and unlock all features",
cx,
)
let button = Button::new("restricted_mode_trigger", "Restricted Mode")
.style(ButtonStyle::Tinted(TintColor::Warning))
.label_size(LabelSize::Small)
.color(Color::Warning)
.icon(IconName::Warning)
.icon_color(Color::Warning)
.icon_size(IconSize::Small)
.icon_position(IconPosition::Start)
.tooltip(|_, cx| {
Tooltip::with_meta(
"You're in Restricted Mode",
Some(&ToggleWorktreeSecurity),
"Mark this project as trusted and unlock all features",
cx,
)
})
.on_click({
cx.listener(move |this, _, window, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_worktree_trust_security_modal(true, window, cx)
})
.log_err();
})
.on_click({
cx.listener(move |this, _, window, cx| {
this.workspace
.update(cx, |workspace, cx| {
workspace.show_worktree_trust_security_modal(true, window, cx)
})
.log_err();
})
})
.into_any_element(),
)
});
if cfg!(macos_sdk_26) {
// Make up for Tahoe's traffic light buttons having less spacing around them
Some(div().child(button).ml_0p5().into_any_element())
} else {
Some(button.into_any_element())
}
}
pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {

View File

@@ -198,10 +198,17 @@ impl ActiveToolchain {
.or_else(|| toolchains.toolchains.first())
.cloned();
if let Some(toolchain) = &default_choice {
let worktree_root_path = project
.read_with(cx, |this, cx| {
this.worktree_for_id(worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
})
.ok()
.flatten()?;
workspace::WORKSPACE_DB
.set_toolchain(
workspace_id,
worktree_id,
worktree_root_path,
relative_path.clone(),
toolchain.clone(),
)

View File

@@ -1,6 +1,7 @@
mod active_toolchain;
pub use active_toolchain::ActiveToolchain;
use anyhow::Context as _;
use convert_case::Casing as _;
use editor::Editor;
use file_finder::OpenPathDelegate;
@@ -62,6 +63,7 @@ struct AddToolchainState {
language_name: LanguageName,
root_path: ProjectPath,
weak: WeakEntity<ToolchainSelector>,
worktree_root_path: Arc<Path>,
}
struct ScopePickerState {
@@ -99,12 +101,17 @@ impl AddToolchainState {
root_path: ProjectPath,
window: &mut Window,
cx: &mut Context<ToolchainSelector>,
) -> Entity<Self> {
) -> anyhow::Result<Entity<Self>> {
let weak = cx.weak_entity();
cx.new(|cx| {
let worktree_root_path = project
.read(cx)
.worktree_for_id(root_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
.context("Could not find worktree")?;
Ok(cx.new(|cx| {
let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx);
let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx));
Self {
state: AddState::Path {
_subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
@@ -118,8 +125,9 @@ impl AddToolchainState {
language_name,
root_path,
weak,
worktree_root_path,
}
})
}))
}
fn create_path_browser_delegate(
@@ -237,7 +245,15 @@ impl AddToolchainState {
// Suggest a default scope based on the applicability.
let scope = if let Some(project_path) = resolved_toolchain_path {
if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
ToolchainScope::Subproject(root_path.worktree_id, root_path.path)
let worktree_root_path = project
.read_with(cx, |this, cx| {
this.worktree_for_id(root_path.worktree_id, cx)
.map(|worktree| worktree.read(cx).abs_path())
})
.ok()
.flatten()
.context("Could not find a worktree with a given worktree ID")?;
ToolchainScope::Subproject(worktree_root_path, root_path.path)
} else {
ToolchainScope::Project
}
@@ -400,7 +416,7 @@ impl Render for AddToolchainState {
ToolchainScope::Global,
ToolchainScope::Project,
ToolchainScope::Subproject(
self.root_path.worktree_id,
self.worktree_root_path.clone(),
self.root_path.path.clone(),
),
];
@@ -693,7 +709,7 @@ impl ToolchainSelector {
cx: &mut Context<Self>,
) {
if matches!(self.state, State::Search(_)) {
self.state = State::AddToolchain(AddToolchainState::new(
let Ok(state) = AddToolchainState::new(
self.project.clone(),
self.language_name.clone(),
ProjectPath {
@@ -702,7 +718,10 @@ impl ToolchainSelector {
},
window,
cx,
));
) else {
return;
};
self.state = State::AddToolchain(state);
self.state.focus_handle(cx).focus(window, cx);
cx.notify();
}
@@ -899,11 +918,17 @@ impl PickerDelegate for ToolchainSelectorDelegate {
{
let workspace = self.workspace.clone();
let worktree_id = self.worktree_id;
let worktree_abs_path_root = self.worktree_abs_path_root.clone();
let path = self.relative_path.clone();
let relative_path = self.relative_path.clone();
cx.spawn_in(window, async move |_, cx| {
workspace::WORKSPACE_DB
.set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone())
.set_toolchain(
workspace_id,
worktree_abs_path_root,
relative_path,
toolchain.clone(),
)
.await
.log_err();
workspace

View File

@@ -893,39 +893,57 @@ impl ContextMenu {
entry_render,
handler,
selectable,
documentation_aside,
..
} => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
let keep_open_on_confirm = self.keep_open_on_confirm;
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
if keep_open_on_confirm {
menu.rebuild(window, cx);
} else {
cx.emit(DismissEvent);
div()
.id(("context-menu-child", ix))
.when_some(documentation_aside.clone(), |this, documentation_aside| {
this.occlude()
.on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered {
menu.documentation_aside = Some((ix, documentation_aside.clone()));
} else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
{
menu.documentation_aside = None;
}
cx.notify();
}))
})
.child(
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
let keep_open_on_confirm = self.keep_open_on_confirm;
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
if keep_open_on_confirm {
menu.rebuild(window, cx);
} else {
cx.emit(DismissEvent);
}
})
.ok();
}
})
.ok();
}
})
})
.child(entry_render(window, cx))
})
.child(entry_render(window, cx)),
)
.into_any_element()
}
}

View File

@@ -126,17 +126,6 @@ enum IconSource {
ExternalSvg(SharedString),
}
impl IconSource {
fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
if path.starts_with("icons/") {
Self::Embedded(path)
} else {
Self::External(Arc::from(PathBuf::from(path.as_ref())))
}
}
}
#[derive(IntoElement, RegisterComponent)]
pub struct Icon {
source: IconSource,
@@ -155,9 +144,18 @@ impl Icon {
}
}
/// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
/// - Paths starting with "icons/" are treated as embedded SVGs
/// - Other paths are treated as external raster images (from icon themes)
pub fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
let source = if path.starts_with("icons/") {
IconSource::Embedded(path)
} else {
IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
};
Self {
source: IconSource::from_path(path),
source,
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),

View File

@@ -56,6 +56,12 @@ impl Label {
pub fn set_text(&mut self, text: impl Into<SharedString>) {
self.label = text.into();
}
/// Truncates the label from the start, keeping the end visible.
pub fn truncate_start(mut self) -> Self {
self.base = self.base.truncate_start();
self
}
}
// Style methods.
@@ -256,7 +262,8 @@ impl Component for Label {
"Special Cases",
vec![
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
single_example("Regular Truncation", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
single_example("Start Truncation", div().max_w_24().child(Label::new("zed/crates/ui/src/components/label/truncate/label/label.rs").truncate_start()).into_any_element()),
],
),
])

View File

@@ -56,7 +56,7 @@ pub trait LabelCommon {
/// Sets the alpha property of the label, overwriting the alpha value of the color.
fn alpha(self, alpha: f32) -> Self;
/// Truncates overflowing text with an ellipsis (`…`) if needed.
/// Truncates overflowing text with an ellipsis (`…`) at the end if needed.
fn truncate(self) -> Self;
/// Sets the label to render as a single line.
@@ -88,6 +88,7 @@ pub struct LabelLike {
underline: bool,
single_line: bool,
truncate: bool,
truncate_start: bool,
}
impl Default for LabelLike {
@@ -113,6 +114,7 @@ impl LabelLike {
underline: false,
single_line: false,
truncate: false,
truncate_start: false,
}
}
}
@@ -126,6 +128,12 @@ impl LabelLike {
gpui::margin_style_methods!({
visibility: pub
});
/// Truncates overflowing text with an ellipsis (`…`) at the start if needed.
pub fn truncate_start(mut self) -> Self {
self.truncate_start = true;
self
}
}
impl LabelCommon for LabelLike {
@@ -169,7 +177,7 @@ impl LabelCommon for LabelLike {
self
}
/// Truncates overflowing text with an ellipsis (`…`) if needed.
/// Truncates overflowing text with an ellipsis (`…`) at the end if needed.
fn truncate(mut self) -> Self {
self.truncate = true;
self
@@ -235,6 +243,9 @@ impl RenderOnce for LabelLike {
.when(self.truncate, |this| {
this.overflow_x_hidden().text_ellipsis()
})
.when(self.truncate_start, |this| {
this.overflow_x_hidden().text_ellipsis_start()
})
.text_color(color)
.font_weight(
self.weight

View File

@@ -1,3 +1,9 @@
use std::sync::LazyLock;
static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
});
/// Whether a given environment variable name should have its value redacted
pub fn should_redact(env_var_name: &str) -> bool {
const REDACTED_SUFFIXES: &[&str] = &[
@@ -13,3 +19,31 @@ pub fn should_redact(env_var_name: &str) -> bool {
.iter()
.any(|suffix| env_var_name.ends_with(suffix))
}
/// Redact a string which could include a command with environment variables
pub fn redact_command(command: &str) -> String {
REDACT_REGEX
.replace_all(command, |caps: &regex::Captures| {
let var_name = &caps[1];
let value = &caps[2];
if should_redact(var_name) {
format!(r#"{}="[REDACTED]""#, var_name)
} else {
format!("{}={}", var_name, value)
}
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_string_with_multiple_env_vars() {
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
let result = redact_command(input);
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
assert_eq!(result, expected);
}
}

View File

@@ -3407,4 +3407,390 @@ mod test {
.assert_eq(" ˇf = (x: unknown) => {");
cx.shared_clipboard().await.assert_eq("const ");
}
#[gpui::test]
async fn test_arrow_function_text_object(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_typescript(cx).await;
cx.set_state(
indoc! {"
const foo = () => {
return ˇ1;
};
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const foo = () => {
return 1;
};ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
arr.map(() => {
return ˇ1;
});
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
arr.map(«() => {
return 1;
}ˇ»);
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const foo = () => {
return ˇ1;
};
"},
Mode::Normal,
);
cx.simulate_keystrokes("v i f");
cx.assert_state(
indoc! {"
const foo = () => {
«return 1;ˇ»
};
"},
Mode::Visual,
);
cx.set_state(
indoc! {"
(() => {
console.log(ˇ1);
})();
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
(«() => {
console.log(1);
}ˇ»)();
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const foo = () => {
return ˇ1;
};
export { foo };
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const foo = () => {
return 1;
};ˇ»
export { foo };
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
let bar = () => {
return ˇ2;
};
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«let bar = () => {
return 2;
};ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
var baz = () => {
return ˇ3;
};
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«var baz = () => {
return 3;
};ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const add = (a, b) => a + ˇb;
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const add = (a, b) => a + b;ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const add = ˇ(a, b) => a + b;
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const add = (a, b) => a + b;ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const add = (a, b) => a + bˇ;
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const add = (a, b) => a + b;ˇ»
"},
Mode::VisualLine,
);
cx.set_state(
indoc! {"
const add = (a, b) =ˇ> a + b;
"},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {"
«const add = (a, b) => a + b;ˇ»
"},
Mode::VisualLine,
);
}
#[gpui::test]
async fn test_arrow_function_in_jsx(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new_tsx(cx).await;
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() => {
alert("Hello world!");
console.log(ˇ"clicked");
}}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => {
alert("Hello world!");
console.log("clicked");
}ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() => console.log("clickˇed")}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={ˇ() => console.log("clicked")}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() => console.log("clicked"ˇ)}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() =ˇ> console.log("clicked")}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => console.log("clicked")ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() => {
console.log("cliˇcked");
}}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => {
console.log("clicked");
}ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
cx.set_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={() => fˇoo()}>Hello world!</div>
</div>
);
};
"#},
Mode::Normal,
);
cx.simulate_keystrokes("v a f");
cx.assert_state(
indoc! {r#"
export const MyComponent = () => {
return (
<div>
<div onClick={«() => foo()ˇ»}>Hello world!</div>
</div>
);
};
"#},
Mode::VisualLine,
);
}
}

View File

@@ -522,12 +522,16 @@ impl Vim {
selection.start = original_point.to_display_point(map)
}
} else {
selection.end = movement::saturating_right(
map,
original_point.to_display_point(map),
);
if original_point.column > 0 {
selection.reversed = true
let original_display_point =
original_point.to_display_point(map);
if selection.end <= original_display_point {
selection.end = movement::saturating_right(
map,
original_display_point,
);
if original_point.column > 0 {
selection.reversed = true
}
}
}
}

View File

@@ -24,7 +24,6 @@ use project::{
};
use language::{LanguageName, Toolchain, ToolchainScope};
use project::WorktreeId;
use remote::{
DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
};
@@ -845,6 +844,44 @@ impl Domain for WorkspaceDb {
host_name TEXT
) STRICT;
),
sql!(CREATE TABLE toolchains2 (
workspace_id INTEGER,
worktree_root_path TEXT NOT NULL,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
relative_worktree_path TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
INSERT OR REPLACE INTO toolchains2
// The `instr(paths, '\n') = 0` part allows us to find all
// workspaces that have a single worktree, as `\n` is used as a
// separator when serializing the workspace paths, so if no `\n` is
// found, we know we have a single worktree.
SELECT toolchains.workspace_id, paths, language_name, name, path, raw_json, relative_worktree_path FROM toolchains INNER JOIN workspaces ON toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
DROP TABLE toolchains;
ALTER TABLE toolchains2 RENAME TO toolchains;
),
sql!(CREATE TABLE user_toolchains2 (
remote_connection_id INTEGER,
workspace_id INTEGER NOT NULL,
worktree_root_path TEXT NOT NULL,
relative_worktree_path TEXT NOT NULL,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
INSERT OR REPLACE INTO user_toolchains2
// The `instr(paths, '\n') = 0` part allows us to find all
// workspaces that have a single worktree, as `\n` is used as a
// separator when serializing the workspace paths, so if no `\n` is
// found, we know we have a single worktree.
SELECT user_toolchains.remote_connection_id, user_toolchains.workspace_id, paths, relative_worktree_path, language_name, name, path, raw_json FROM user_toolchains INNER JOIN workspaces ON user_toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
DROP TABLE user_toolchains;
ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
),
];
// Allow recovering from bad migration that was initially shipped to nightly
@@ -1030,11 +1067,11 @@ impl WorkspaceDb {
workspace_id: WorkspaceId,
remote_connection_id: Option<RemoteConnectionId>,
) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
type RowKind = (WorkspaceId, u64, String, String, String, String, String);
type RowKind = (WorkspaceId, String, String, String, String, String, String);
let toolchains: Vec<RowKind> = self
.select_bound(sql! {
SELECT workspace_id, worktree_id, relative_worktree_path,
SELECT workspace_id, worktree_root_path, relative_worktree_path,
language_name, name, path, raw_json
FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
workspace_id IN (0, ?2)
@@ -1048,7 +1085,7 @@ impl WorkspaceDb {
for (
_workspace_id,
worktree_id,
worktree_root_path,
relative_worktree_path,
language_name,
name,
@@ -1058,22 +1095,24 @@ impl WorkspaceDb {
{
// INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
let scope = if _workspace_id == WorkspaceId(0) {
debug_assert_eq!(worktree_id, u64::MAX);
debug_assert_eq!(worktree_root_path, String::default());
debug_assert_eq!(relative_worktree_path, String::default());
ToolchainScope::Global
} else {
debug_assert_eq!(workspace_id, _workspace_id);
debug_assert_eq!(
worktree_id == u64::MAX,
worktree_root_path == String::default(),
relative_worktree_path == String::default()
);
let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
continue;
};
if worktree_id != u64::MAX && relative_worktree_path != String::default() {
if worktree_root_path != String::default()
&& relative_worktree_path != String::default()
{
ToolchainScope::Subproject(
WorktreeId::from_usize(worktree_id as usize),
Arc::from(worktree_root_path.as_ref()),
relative_path.into(),
)
} else {
@@ -1159,13 +1198,13 @@ impl WorkspaceDb {
for (scope, toolchains) in workspace.user_toolchains {
for toolchain in toolchains {
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
let (workspace_id, worktree_id, relative_worktree_path) = match scope {
ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())),
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
ToolchainScope::Subproject(ref worktree_root_path, ref path) => (Some(workspace.id), Some(worktree_root_path.to_string_lossy().into_owned()), Some(path.as_unix_str().to_owned())),
ToolchainScope::Project => (Some(workspace.id), None, None),
ToolchainScope::Global => (None, None, None),
};
let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(),
let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
if let Err(err) = conn.exec_bound(query)?(args) {
log::error!("{err}");
@@ -1844,24 +1883,24 @@ impl WorkspaceDb {
pub(crate) async fn toolchains(
&self,
workspace_id: WorkspaceId,
) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
self.write(move |this| {
let mut select = this
.select_bound(sql!(
SELECT
name, path, worktree_id, relative_worktree_path, language_name, raw_json
name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
FROM toolchains
WHERE workspace_id = ?
))
.context("select toolchains")?;
let toolchain: Vec<(String, String, u64, String, String, String)> =
let toolchain: Vec<(String, String, String, String, String, String)> =
select(workspace_id)?;
Ok(toolchain
.into_iter()
.filter_map(
|(name, path, worktree_id, relative_worktree_path, language, json)| {
|(name, path, worktree_root_path, relative_worktree_path, language, json)| {
Some((
Toolchain {
name: name.into(),
@@ -1869,7 +1908,7 @@ impl WorkspaceDb {
language_name: LanguageName::new(&language),
as_json: serde_json::Value::from_str(&json).ok()?,
},
WorktreeId::from_proto(worktree_id),
Arc::from(worktree_root_path.as_ref()),
RelPath::from_proto(&relative_worktree_path).log_err()?,
))
},
@@ -1882,18 +1921,18 @@ impl WorkspaceDb {
pub async fn set_toolchain(
&self,
workspace_id: WorkspaceId,
worktree_id: WorktreeId,
worktree_root_path: Arc<Path>,
relative_worktree_path: Arc<RelPath>,
toolchain: Toolchain,
) -> Result<()> {
log::debug!(
"Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
"Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
toolchain.name
);
self.write(move |conn| {
let mut insert = conn
.exec_bound(sql!(
INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT DO
UPDATE SET
name = ?5,
@@ -1904,7 +1943,7 @@ impl WorkspaceDb {
insert((
workspace_id,
worktree_id.to_usize(),
worktree_root_path.to_string_lossy().into_owned(),
relative_worktree_path.as_unix_str(),
toolchain.language_name.as_ref(),
toolchain.name.as_ref(),

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