Compare commits

...

39 Commits

Author SHA1 Message Date
Michael Sloan
c69e7499af Navigate to multibuffer headers (WIP)
Issue #5129
2025-02-25 13:26:56 -07:00
Conrad Irwin
a78f3cfea2 Notarize with a team key (#25479)
Should make it less likely that notorization fails when nathan changes
his passwords.

(though probably no less likly to fail beacuse apple forces us to resign
new agreements on the regular)

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-02-24 14:43:59 -07:00
Marshall Bowers
0acd556106 language_model: Remove dependencies on individual model provider crates (#25503)
This PR removes the dependencies on the individual model provider crates
from the `language_model` crate.

The various conversion methods for converting a `LanguageModelRequest`
into its provider-specific request type have been inlined into the
various provider modules in the `language_models` crate.

The model providers we provide via Zed's cloud offering get to stay, for
now.

Release Notes:

- N/A
2025-02-24 16:41:35 -05:00
Agus Zubiaga
2f7a62780a edit predictions: Refine leading whitespace behavior (#25491)
Closes https://github.com/zed-industries/zed/issues/25406

### Problem

Users have been confused about requiring `alt-tab` instead of just `tab`
in cases where they don't have a completions menu open (see issue
above). When they insert a newline and are in leading whitespace, they
expect to be able to accept a prediction with just `tab`, but doing so
increasing the indentation instead.

This PR changes the behavior in so a modifier is only required if the
cursor isn't already at the right indentation level based on the
surrounding block. In this case, `tab` would increase the indentation
and the prediction would get interpolated, allowing the user to press
`tab` again to accept it.

We also updated the docs to break down this behavior:
https://github.com/zed-industries/zed/pull/25493

### Before


https://github.com/user-attachments/assets/91fe6193-dddd-43c1-8c26-0f4648bdc3fa

### After


https://github.com/user-attachments/assets/671041bf-bf22-46a3-8466-b19b3e7dd6a0


Release Notes:

- edit predictions: Do not require a modifier key when indentation is
correct according to its surrounding block

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-02-24 18:37:18 -03:00
João Marcos
f1e6b144e8 Git: Add hotkey to open file from changes list (#25500)
Release Notes:

- N/A
2025-02-24 21:22:25 +00:00
Peter Tripp
10a4760f90 Add Anthropic Claude 3.7 support (#25497) 2025-02-24 16:10:26 -05:00
Marshall Bowers
cdd07fdf29 Add aws_http_client and bedrock crates (#25490)
This PR adds new `aws_http_client` and `bedrock` crates for supporting
AWS Bedrock.

Pulling out of https://github.com/zed-industries/zed/pull/21092 to make
it easier to land.

Release Notes:

- N/A

---------

Co-authored-by: Shardul Vaidya <cam.v737@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-02-24 20:28:20 +00:00
Michael Sloan
8a3fb890b0 Document why ForegroundExecutor is !Send (#25492)
Release Notes:

- N/A
2025-02-24 19:48:59 +00:00
Nate Butler
30af8d0a81 git_ui: Commit modal refinement (#25484)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-02-24 19:19:06 +00:00
Agus Zubiaga
ceb7fc2cb2 edit predictions: Split layout_edit_prediction popover (#25463)
This function has grown a lot and it was getting really hard to
navigate. This PR splits it into smaller methods and moves it into
`Editor` with the rest of the edit prediction popovers' code.

I think there are opportunities to consolidate the many popovers we
have, but we'll do that separately.

Release Notes:

- N/A
2025-02-24 16:16:19 -03:00
Peter Tripp
d8694510b5 emacs: Add support for paragraph navigation (#25284)
- emacs: Added support for `alt-{` and `alt-}` paragraph navigation
2025-02-24 19:02:07 +00:00
João Marcos
ec7ce41324 Git: Fix Linux bindings (#25486)
- Tooltip with binding wasn't showing up
- Missing Linux bindings
- Commit modal wasn't opening when binding was pressed

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-02-24 18:49:17 +00:00
Peter Tripp
b2921bd3fd Disable edit predictions in diff/patch files by default (#25291) 2025-02-24 13:47:11 -05:00
Peter Tripp
64756fa96f Fix tmux being broken by default on Linux (#25476)
Tmux uses `ctrl-b` as default prefix.
Prior to this tmux was basically useless in the default zed configuration.
(ctrl-b would toggle the left dock).
2025-02-24 13:43:15 -05:00
Mikayla Maki
ff6844300e Git push/pull/fetch (#25445)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-02-24 18:29:52 +00:00
Jason Lee
b1b6401ce7 gpui: Don’t hide Window in docs (#25449)
Release Notes:

- N/A

This side effect is only appearing in outside project.

## Before

<img width="414" alt="image"
src="https://github.com/user-attachments/assets/73d215d2-20e4-4337-82f4-74daa88a1bae"
/>

## After

<img width="453" alt="image"
src="https://github.com/user-attachments/assets/030d34d7-c425-44f6-9cc5-0f2f6fd0a1ac"
/>

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-02-24 18:11:46 +00:00
Jason Lee
07ba7c8c44 gpui: Add underline style method (#24784)
Release Notes:

- N/A

Add a shorter method to apply underline style.

https://tailwindcss.com/docs/text-decoration-line#underling-text

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-02-24 12:58:20 -05:00
Conrad Irwin
6516249302 Fix conflict state (was broken by merge conflict) (#25354)
Closes #ISSUE

Release Notes:

- N/A
2025-02-24 10:52:42 -07:00
Danilo Leal
fc8218d728 project panel: Change marked entries behavior and improve state colors (#25457)
Follow up to @0xtimsb's PR
https://github.com/zed-industries/zed/pull/22658.

- We're now changing the marked entry as we change the active buffer via
the pane tabs. If all tabs are closed, we clear all marked entries, too.
That means: if we have no open buffer, we don't have any highlighted
entry (i.e., background color) in the project panel.
- Also, now only marked entries have a different, more distinct
background color. The `is_active` state doesn't change an item's
background color anymore.
- This improves an edge case where you could have multiple entries
marked—where all of them would have a background color—and upon
unmarking one of them, that entry would continue to have a bg color.
Now, once you click or move your focus to unmark that entry, the bg
color goes away.

We discovered some new problems by doing these changes that we want to
fix:
1. If you open a project without any open buffer, focus on the project
panel, navigate with arrows to a given entry, and hit space, you will
mark and open the file in the buffer. This is all correct. If you then
hit `escape` to clear the marked entries, nothing happens to the open
buffer, and the marked styled in the project panel entry go away. This
is all correct. The wrong behavior happens if you now hit space _again_
on the active entry. That should mark it, and thus change its styles,
but it doesn't happen. You just see it upon moving to a different entry
with arrow up/down.
2. If you mark multiple entries on the project panel and then click on
an open buffer, we still see all the multiple entries marked. This feels
incorrect. We should only allow one marked entry at a time.

These fixes should happen in follow up PRs, though.

Release Notes:

- Improved the scenario where there'd be a project panel entry
highlighted/marked even if there is no open buffer.

---------

Co-authored-by: smit <0xtimsb@gmail.com>
2025-02-24 22:44:09 +05:30
Eli Kaplan
a8d56877ee copilot: Support HTTP/HTTPS proxy for Copilot language server (#24364)
Closes #6701 (one of the top ranking issues as of writing)

Adds the ability to specify an HTTP/HTTPS proxy to route Copilot code
completion API requests through. This should fix copilot functionality
in restricted network environments (where such a proxy is required) but
also opens up the ability to point copilot code completion requests at
your own local LLM, using e.g.:
- https://github.com/jjleng/copilot-proxy
- https://github.com/bernardo-bruning/ollama-copilot/tree/master

External MITM-proxy tools permitting, this can serve as a stop-gap to
allow local LLM code completion in Zed until a proper OpenAI-compatible
local code completions provider is implemented. With this in mind, in
this PR I've added separate `settings.json` variables to configure a
proxy server _specific to the code completions provider_ instead of
using the global `proxy` setting, to allow for cases like this where we
_only_ want to proxy e.g. the Copilot requests, but not all outgoing
traffic from the application.

Currently, two new settings are added:
- `inline_completions.copilot.proxy`: Proxy server URL (HTTP and HTTPS
schemes supported)
- `inline_completions.copilot.proxy_no_verify`: Whether to disable
certificate verification through the proxy

Example:
```js
"features": {
  "inline_completion_provider": "copilot"
},
"show_completions_on_input": true,
// New:
"inline_completions": {
  "copilot": {
    "proxy": "http://example.com:15432",
    "proxy_no_verify": true
  }
}
```


Release Notes:

- Added the ability to specify an HTTP/HTTPS proxy for Copilot.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-02-24 12:11:00 -05:00
João Marcos
dd0de3cfa9 Git panel: Fix commit binding tooltip not showing up (#25472)
Co-authored by: Conrad <conrad@zed.dev>

Release Notes:

- N/A
2025-02-24 16:41:37 +00:00
Marshall Bowers
198c36811e Change @variable color in Gruvbox themes to be less intense (#25464)
This PR changes the color used for `@variable` syntax highlights in the
Gruvbox themes to be less intense.

We now use the same color as `editor.foreground`.

| Language | Before | After |
| -------- |
-----------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------
|
| Rust | <img width="1410" alt="Screenshot 2025-02-24 at 10 08 41 AM"
src="https://github.com/user-attachments/assets/9a34964d-9fdc-4deb-ac30-4a1c9e6fb531"
/> | <img width="1410" alt="Screenshot 2025-02-24 at 10 55 18 AM"
src="https://github.com/user-attachments/assets/c245d0fd-28af-42b8-93f6-48cb14671d94"
/> |
| Python | <img width="1410" alt="Screenshot 2025-02-24 at 10 08 38 AM"
src="https://github.com/user-attachments/assets/8f8d111e-1d50-4229-a333-eb29b6ce9f4f"
/> | <img width="1410" alt="Screenshot 2025-02-24 at 10 55 20 AM"
src="https://github.com/user-attachments/assets/010b661e-dc9e-4ccb-8e52-ee10c8eb8342"
/> |

In #25333 and #25331 the highlight used for identifiers in Rust and
Python, respectively, was changed to `@variable`, which resulted in the
intense colors you see in the "Before" screenshots above.

We considered reverting the highlight query changes to those languages,
but after taking a look at our other languages, they already use similar
queries. Instead we're adjusting the theme to make these cases less
visually intense.

Release Notes:

- Gruvbox themes: Changed the color used for `@variable` syntax
highlights to be less intense.
2025-02-24 16:12:15 +00:00
Marshall Bowers
133704a419 Adjust @variable color in One Dark theme (#25468)
This PR adjusts the color used for `@variable`s in One Dark to use the
`editor.foreground` color.

| Language | Before | After |
| -------- |
-----------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------
|
| Rust | <img width="1410" alt="Screenshot 2025-02-24 at 10 46 04 AM"
src="https://github.com/user-attachments/assets/3e1de7d2-03f6-45cc-87bb-93b86b5b1cb2"
/> | <img width="1410" alt="Screenshot 2025-02-24 at 10 46 15 AM"
src="https://github.com/user-attachments/assets/da6129aa-6886-4655-b305-c283e23bfd1e"
/> |
| Python | <img width="1410" alt="Screenshot 2025-02-24 at 10 46 10 AM"
src="https://github.com/user-attachments/assets/f60833f9-d306-44b6-a0b0-42b447e60498"
/> | <img width="1410" alt="Screenshot 2025-02-24 at 10 46 19 AM"
src="https://github.com/user-attachments/assets/256aa6b3-b798-46e4-9943-f21469e7d8bb"
/> |

Release Notes:

- One Dark theme: Adjusted the color used for `@variable` syntax
highlights.
2025-02-24 16:08:22 +00:00
João Marcos
d1302a7a08 Diff view: Fold excerpts of deleted files by default (#25436)
Release Notes:

- N/A
2025-02-24 12:52:13 -03:00
João Marcos
2b28b5969f Remove unused variable distinguish_unstaged_diff_hunks (#25462)
Release Notes:

- N/A
2025-02-24 15:25:13 +00:00
Nikita Pivkin
bc941bfc97 assistant_tools: Rename FileToolInput to NowToolInput (#25456)
Renamed the `FileToolInput` structure to `NowToolInput` to better
reflect its purpose, as the tool is related to time-based operations.

Release Notes:

- N/A

Signed-off-by: Nikita Pivkin <nikita.pivkin@smartforce.io>
2025-02-24 09:37:07 -05:00
Agus Zubiaga
72a9429ef6 edit predictions: Always position jump/accept popovers inside viewport (#25348)
https://github.com/user-attachments/assets/345961c5-9bcb-4ee5-80f2-03d5fd0741d3

Release Notes:

- edit prediction: Fixed jump/accept popover position for long lines

---------

Co-authored-by: Danilo <danilo@zed.dev>
2025-02-24 10:26:29 -03:00
Danilo Leal
f020291039 assistant: Rename action from New Context to New Chat (#25455)
If you looked that up via the Command Palette, we were showing an
outdated action name ("new context") which causes confusion given the
panel says "New Chat".

Release Notes:

- N/A
2025-02-24 09:19:38 -03:00
smit
4b3a2a33a8 vim: Fix auto_indent_on_paste not being respected (#25447)
Closes #12236

This PR fixes an issue where the `auto_indent_on_paste` setting was not
being applied for pasting in Vim mode. It was correctly used for normal
paste behavior.

Also includes tests.  


Release Notes:

- Fixed yank + paste indenting incorrectly when `auto_indent_on_paste`
is set to `false` in certain languages.
2025-02-24 14:59:05 +05:30
张小白
1f257f4704 fs: Fix copy_recursive (#25317)
Closes #24746

This PR modifies the implementation of `copy_recursive`. Previously, we
were copying and pasting simultaneously, which caused an issue when a
user copied a folder into one of its subfolders. This resulted in new
content being created in the folder while copying, and subsequent
recursive calls to `copy_recursive` would continue this process, leading
to an infinite loop.

In this PR, the approach has been changed: we now first collect the
paths of the files to be copied, and only then perform the copy
operation.

Additionally, I have added corresponding tests. On the main branch, this
test would previously run indefinitely.

Release Notes:

- Fixed `copy_recursive` runs infinitely when copying a folder into its
subfolder.
2025-02-24 11:02:14 +02:00
Antonio Scandurra
f517050548 Partially fix assistant onboarding (#25313)
While investigating #24896, I noticed two issues:

1. The default configuration for the `zed.dev` provider was using the
wrong string for Claude 3.5 Sonnet. This meant the provider would always
result as not configured until the user selected it from the model
picker, because we couldn't deserialize that string to a valid
`anthropic::Model` enum variant.
2. When clicking on `Open New Chat`/`Start New Thread` in the provider
configuration, we would select `Claude 3.5 Haiku` by default instead of
Claude 3.5 Sonnet.

Release Notes:

- Fixed some issues that caused AI providers to sometimes be
misconfigured.
2025-02-24 07:29:55 +00:00
Joseph T. Lyons
535ba75bc7 Do not indent on enter in python comments ending in colon (#25437)
Closes https://github.com/zed-industries/zed/issues/25416

Release Notes:

- Fixed a bug where indentation was applied when adding a newline to a
Python comment ending in `:`.
2025-02-24 04:11:22 +00:00
Joseph T. Lyons
ee280b0d05 Resurrect top-ranking issues script (#25433)
Release Notes:

- N/A
2025-02-23 14:24:56 -05:00
Marshall Bowers
a43793493a docs: Remove empty Tree-sitter grammar link in the Tailwind docs (#25426)
This PR removes the empty Tree-sitter grammar link in the Tailwind docs.

Tailwind does not use a Tree-sitter grammar.

Release Notes:

- N/A
2025-02-23 10:33:03 -05:00
Marshall Bowers
3b3c379852 docs: Fix casing of "Tree-sitter" (#25427)
This PR fixes the casing of "Tree-sitter" in the docs.

It is "Tree-sitter", not "Tree Sitter" or "Tree-Sitter".

Release Notes:

- N/A
2025-02-23 15:30:10 +00:00
Kirill Bulatov
147f407b7a Fix theme selector resetting the buffer size (#25425)
Closes https://github.com/zed-industries/zed/issues/25413

As the issue points out well, themes do not need to alter any in-memory
state on load: that is done via settings file load.
Originally, it was introduced in
https://github.com/zed-industries/zed/pull/4064 and
https://github.com/zed-industries/zed/pull/24857 had restored that
behavior, which seems wrong to do.

Apart from removing that part, removes unnecessary methods and
emphasizes that in-memory state is the Buffer/UI size — no need to add
`Adjusted` there as the settings file presence is already enough.

Release Notes:

- Fixed theme selector resetting the buffer size
2025-02-23 15:24:43 +00:00
Marshall Bowers
822f42e8fe docs: Remove outdated note about pinning @vue/language-server (#25424)
This PR removes the outdated note about pinning `@vue/language-server`
to v1.8.

As of https://github.com/zed-extensions/vue/pull/1 we now use the latest
available version.

Release Notes:

- N/A
2025-02-23 15:21:51 +00:00
Aivaz Latypov
7e097d529a zeta: Add LICENSE.md and LICENCE.md to license detection (#25422)
Maybe it's not a very common, but it has a place to be.

Release Notes:

- Added `LICENSE.md` and `LICENCE.md` files to license detection for
edit prediction.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-02-23 15:08:24 +00:00
Coenen Benjamin
65f76e6f4d Add support for --target-dir for Rust tasks (#24725)
This PR is an attempt to add support for `--target-dir` argument to
`cargo` commands when executing tasks with rust.
When using VSCode I was already using this trick to not block the
current binary compilation when I was trying a specific test. As it's a
different target directory it won't block the `cargo` commands I'm using
in my terminal.

I used the task variables to achieve this but I'm not sure it's the best
option to be honest. I didn't find any examples in your docs to see if
sometimes you had specific configuration for languages and tasks.

Let me know if this solution would be a good fit and if the
implementation is ok.

If so feel free to redirect me to an example I can reproduce to write a
unit test or so... And I will also update the docs.

Example of config:

```
{
  "languages": {
    "Rust": {
      "tasks": {
        "variables": {
          "RUST_TARGET_DIR": ".cargo_check"
        }
      }
    }
  }
}
```

it will run `cargo test -p XXX --target-dir .cargo-check`


Release Notes:

- Added support for `--target-dir` for Rust tasks

---------

Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
2025-02-23 11:05:07 +01:00
156 changed files with 4854 additions and 1412 deletions

View File

@@ -298,8 +298,9 @@ jobs:
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}

View File

@@ -0,0 +1,25 @@
name: Update All Top Ranking Issues
on:
schedule:
- cron: "0 */12 * * *"
workflow_dispatch:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
- name: Install Python 3.13
run: uv python install 3.13
- name: Install dependencies
run: uv sync --project script/update_top_ranking_issues -p 3.13
- name: Run script
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 5393

View File

@@ -0,0 +1,25 @@
name: Update Weekly Top Ranking Issues
on:
schedule:
- cron: "0 15 * * *"
workflow_dispatch:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "script/update_top_ranking_issues/pyproject.toml"
- name: Install Python 3.13
run: uv python install 3.13
- name: Install dependencies
run: uv sync --project script/update_top_ranking_issues -p 3.13
- name: Run script
run: uv run --project script/update_top_ranking_issues script/update_top_ranking_issues/main.py --github-token ${{ secrets.GITHUB_TOKEN }} --issue-reference-number 6952 --query-day-interval 7

View File

@@ -62,8 +62,9 @@ jobs:
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}

59
Cargo.lock generated
View File

@@ -1269,6 +1269,30 @@ dependencies = [
"uuid",
]
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6938541d1948a543bca23303fec4cff9c36bf0e63b8fa3ae1b337bcb9d5b81af"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-eventstream",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes 1.10.0",
"fastrand 2.3.0",
"http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-kinesis"
version = "1.61.0"
@@ -1598,6 +1622,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "aws_http_client"
version = "0.1.0"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"futures 0.3.31",
"http_client",
"tokio",
]
[[package]]
name = "axum"
version = "0.6.20"
@@ -1727,6 +1762,22 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bedrock"
version = "0.1.0"
dependencies = [
"anyhow",
"aws-sdk-bedrockruntime",
"aws-smithy-types",
"futures 0.3.31",
"schemars",
"serde",
"serde_json",
"strum",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "bigdecimal"
version = "0.4.7"
@@ -3112,7 +3163,9 @@ dependencies = [
"clock",
"collections",
"command_palette_hooks",
"ctor",
"editor",
"env_logger 0.11.6",
"fs",
"futures 0.3.31",
"gpui",
@@ -3120,6 +3173,7 @@ dependencies = [
"indoc",
"inline_completion",
"language",
"log",
"lsp",
"menu",
"node_runtime",
@@ -5295,6 +5349,7 @@ dependencies = [
"pretty_assertions",
"regex",
"rope",
"schemars",
"serde",
"serde_json",
"smol",
@@ -6960,16 +7015,12 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"collections",
"deepseek",
"futures 0.3.31",
"google_ai",
"gpui",
"http_client",
"image",
"lmstudio",
"log",
"mistral",
"ollama",
"open_ai",
"parking_lot",
"proto",

View File

@@ -15,6 +15,8 @@ members = [
"crates/audio",
"crates/auto_update",
"crates/auto_update_ui",
"crates/aws_http_client",
"crates/bedrock",
"crates/breadcrumbs",
"crates/buffer_diff",
"crates/call",
@@ -218,6 +220,8 @@ assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
aws_http_client = { path = "crates/aws_http_client" }
bedrock = { path = "crates/bedrock" }
breadcrumbs = { path = "crates/breadcrumbs" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
@@ -382,6 +386,11 @@ async-trait = "0.1"
async-tungstenite = "0.28"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
aws-config = { version = "1.5.16", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
aws-sdk-bedrockruntime = { version = "1.73.0", features = ["behavior-version-latest"] }
aws-smithy-runtime-api = { version = "1.7.3", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.2.13", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }

View File

@@ -184,9 +184,9 @@
"ctrl-alt-/": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary",
"new": "assistant::NewContext",
"ctrl-t": "assistant::NewContext",
"ctrl-n": "assistant::NewContext"
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
}
},
{
@@ -368,7 +368,12 @@
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"ctrl-alt-y": "git::ToggleStaged",
"alt-y": "git::StageAndNext",
"alt-shift-y": "git::UnstageAndNext",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPrevHunk"
}
},
{
@@ -705,12 +710,6 @@
"space": "project_panel::Open"
}
},
{
"context": "GitPanel && !CommitEditor",
"bindings": {
"escape": "git_panel::Close"
}
},
{
"context": "GitPanel && ChangesList",
"bindings": {
@@ -722,19 +721,36 @@
"ctrl-shift-space": "git::UnstageAll",
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus"
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"alt-enter": "menu::SecondaryConfirm"
}
},
{
"context": "GitCommit > Editor",
"bindings": {
"enter": "editor::Newline",
"ctrl-enter": "git::Commit"
}
},
{
"context": "GitPanel > Editor",
"bindings": {
"escape": "git_panel::FocusChanges",
"ctrl-enter": "git::Commit",
"tab": "git_panel::FocusChanges",
"shift-tab": "git_panel::FocusChanges",
"ctrl-enter": "git::Commit",
"alt-up": "git_panel::FocusChanges"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"ctrl-enter": "git::Commit"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {
@@ -813,6 +829,7 @@
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"shift-pageup": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",

View File

@@ -157,7 +157,8 @@
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
"alt-enter": "editor::OpenSelectionsInMultibuffer",
"cmd-g": "git::Commit"
}
},
{
@@ -211,8 +212,8 @@
"cmd-alt-/": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary",
"cmd-t": "assistant::NewContext",
"cmd-n": "assistant::NewContext"
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
},
{
@@ -742,14 +743,6 @@
"escape": "git_panel::ToggleFocus"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "git::Commit"
}
},
{
"context": "GitPanel > Editor",
"use_key_equivalents": true,
@@ -761,6 +754,14 @@
"alt-up": "git_panel::FocusChanges"
}
},
{
"context": "GitCommit > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"cmd-enter": "git::Commit"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,

View File

@@ -48,6 +48,8 @@
"ctrl-_": "editor::Undo", // undo
"ctrl-/": "editor::Undo", // undo
"ctrl-x u": "editor::Undo", // undo
"alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph
"alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph
"ctrl-v": "editor::MovePageDown", // scroll-up
"alt-v": "editor::MovePageUp", // scroll-down
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer

View File

@@ -48,6 +48,8 @@
"ctrl-_": "editor::Undo", // undo
"ctrl-/": "editor::Undo", // undo
"ctrl-x u": "editor::Undo", // undo
"alt-{": "editor::MoveToStartOfParagraph", // backward-paragraph
"alt-}": "editor::MoveToEndOfParagraph", // forward-paragraph
"ctrl-v": "editor::MovePageDown", // scroll-up
"alt-v": "editor::MovePageUp", // scroll-down
"ctrl-x [": "editor::MoveToBeginning", // beginning-of-buffer

View File

@@ -581,7 +581,7 @@
// The provider to use.
"provider": "zed.dev",
// The model to use.
"model": "claude-3-5-sonnet"
"model": "claude-3-5-sonnet-latest"
}
},
// The settings for slash commands.
@@ -1093,6 +1093,7 @@
"tab_size": 2
},
"Diff": {
"show_edit_predictions": false,
"remove_trailing_whitespace_on_save": false,
"ensure_final_newline_on_save": false
},

View File

@@ -379,7 +379,7 @@
"font_weight": null
},
"variable": {
"color": "#83a598ff",
"color": "#ebdbb2ff",
"font_style": null,
"font_weight": null
},
@@ -767,7 +767,7 @@
"font_weight": null
},
"variable": {
"color": "#83a598ff",
"color": "#ebdbb2ff",
"font_style": null,
"font_weight": null
},
@@ -1155,7 +1155,7 @@
"font_weight": null
},
"variable": {
"color": "#83a598ff",
"color": "#ebdbb2ff",
"font_style": null,
"font_weight": null
},
@@ -1543,7 +1543,7 @@
"font_weight": null
},
"variable": {
"color": "#066578ff",
"color": "#282828ff",
"font_style": null,
"font_weight": null
},
@@ -1931,7 +1931,7 @@
"font_weight": null
},
"variable": {
"color": "#066578ff",
"color": "#282828ff",
"font_style": null,
"font_weight": null
},
@@ -2319,7 +2319,7 @@
"font_weight": null
},
"variable": {
"color": "#066578ff",
"color": "#282828ff",
"font_style": null,
"font_weight": null
},

View File

@@ -365,7 +365,7 @@
"font_weight": null
},
"variable": {
"color": "#dce0e5ff",
"color": "#acb2beff",
"font_style": null,
"font_weight": null
},

View File

@@ -30,6 +30,8 @@ pub enum Model {
#[default]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
Claude3_7Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -59,6 +61,8 @@ impl Model {
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-7-sonnet") {
Ok(Self::Claude3_7Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else if id.starts_with("claude-3-opus") {
@@ -75,6 +79,7 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
Model::Claude3Opus => "claude-3-opus-latest",
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
@@ -85,6 +90,7 @@ impl Model {
pub fn display_name(&self) -> &str {
match self {
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::Claude3Opus => "Claude 3 Opus",
@@ -98,13 +104,14 @@ impl Model {
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
match self {
Self::Claude3_5Sonnet | Self::Claude3_5Haiku | Self::Claude3Haiku => {
Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
})
}
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3Haiku => Some(AnthropicModelCacheConfiguration {
min_total_token: 2_048,
should_speculate: true,
max_cache_anchors: 4,
}),
Self::Custom {
cache_configuration,
..
@@ -117,6 +124,7 @@ impl Model {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_5Haiku
| Self::Claude3_7Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3Haiku => 200_000,
@@ -127,7 +135,7 @@ impl Model {
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
Self::Claude3_5Sonnet | Self::Claude3_5Haiku => 8_192,
Self::Claude3_5Sonnet | Self::Claude3_7Sonnet | Self::Claude3_5Haiku => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
@@ -137,6 +145,7 @@ impl Model {
pub fn default_temperature(&self) -> f32 {
match self {
Self::Claude3_5Sonnet
| Self::Claude3_7Sonnet
| Self::Claude3_5Haiku
| Self::Claude3Opus
| Self::Claude3Sonnet

View File

@@ -33,7 +33,7 @@ actions!(
[
InsertActivePrompt,
DeployHistory,
NewContext,
NewChat,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]

View File

@@ -1,6 +1,6 @@
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
use crate::{
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewContext,
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
};
use anyhow::{anyhow, Result};
use assistant_context_editor::{
@@ -129,7 +129,7 @@ impl AssistantPanel {
workspace.project().clone(),
Default::default(),
None,
NewContext.boxed_clone(),
NewChat.boxed_clone(),
window,
cx,
);
@@ -228,12 +228,12 @@ impl AssistantPanel {
IconButton::new("new-chat", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(NewContext.boxed_clone(), cx)
window.dispatch_action(NewChat.boxed_clone(), cx)
}))
.tooltip(move |window, cx| {
Tooltip::for_action_in(
"New Chat",
&NewContext,
&NewChat,
&focus_handle,
window,
cx,
@@ -256,7 +256,7 @@ impl AssistantPanel {
let focus_handle = _pane.focus_handle(cx);
Some(ContextMenu::build(window, cx, move |menu, _, _| {
menu.context(focus_handle.clone())
.action("New Chat", Box::new(NewContext))
.action("New Chat", Box::new(NewChat))
.action("History", Box::new(DeployHistory))
.action("Prompt Library", Box::new(DeployPromptLibrary))
.action("Configure", Box::new(ShowConfiguration))
@@ -760,7 +760,7 @@ impl AssistantPanel {
pub fn create_new_context(
workspace: &mut Workspace,
_: &NewContext,
_: &NewChat,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
@@ -978,7 +978,7 @@ impl AssistantPanel {
.active_provider()
.map_or(true, |p| p.id() != provider.id())
{
if let Some(model) = provider.provided_models(cx).first().cloned() {
if let Some(model) = provider.default_model(cx) {
update_settings_file::<AssistantSettings>(
this.fs.clone(),
cx,
@@ -1206,7 +1206,7 @@ impl Render for AssistantPanel {
v_flex()
.key_context("AssistantPanel")
.size_full()
.on_action(cx.listener(|this, _: &NewContext, window, cx| {
.on_action(cx.listener(|this, _: &NewChat, window, cx| {
this.new_context(window, cx);
}))
.on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| {

View File

@@ -431,7 +431,7 @@ impl AssistantPanel {
active_provider.id() != provider.id()
})
{
if let Some(model) = provider.provided_models(cx).first().cloned() {
if let Some(model) = provider.default_model(cx) {
update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,

View File

@@ -13,7 +13,7 @@ use rope::Point;
use settings::Settings;
use std::time::Duration;
use text::Bias;
use theme::{get_ui_font_size, ThemeSettings};
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
};
@@ -369,7 +369,7 @@ impl Render for MessageEditor {
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: (-get_ui_font_size(cx) * 2) - px(4.0),
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2) - px(4.0),
})
.with_handle(self.inline_context_picker_menu_handle.clone()),
)

View File

@@ -1234,8 +1234,8 @@ impl ContextEditor {
.px_1()
.mr_0p5()
.border_1()
.border_color(theme::color_alpha(colors.border_variant, 0.6))
.bg(theme::color_alpha(colors.element_background, 0.6))
.border_color(colors.border_variant.alpha(0.6))
.bg(colors.element_background.alpha(0.6))
.child("esc"),
)
.child("to cancel")

View File

@@ -512,7 +512,7 @@ mod tests {
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
model: "claude-3-5-sonnet-latest".into(),
}
);
});

View File

@@ -17,7 +17,7 @@ pub enum Timezone {
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FileToolInput {
pub struct NowToolInput {
/// The timezone to use for the datetime.
timezone: Timezone,
}
@@ -34,7 +34,7 @@ impl Tool for NowTool {
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(FileToolInput);
let schema = schemars::schema_for!(NowToolInput);
serde_json::to_value(&schema).unwrap()
}
@@ -45,7 +45,7 @@ impl Tool for NowTool {
_window: &mut Window,
_cx: &mut App,
) -> Task<Result<String>> {
let input: FileToolInput = match serde_json::from_value(input) {
let input: NowToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};

View File

@@ -0,0 +1,22 @@
[package]
name = "aws_http_client"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/aws_http_client.rs"
[features]
default = []
[dependencies]
aws-smithy-runtime-api.workspace = true
aws-smithy-types.workspace = true
futures.workspace = true
http_client.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }

View File

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

View File

@@ -0,0 +1,118 @@
use std::fmt;
use std::sync::Arc;
use aws_smithy_runtime_api::client::http::{
HttpClient as AwsClient, HttpConnector as AwsConnector,
HttpConnectorFuture as AwsConnectorFuture, HttpConnectorFuture, HttpConnectorSettings,
SharedHttpConnector,
};
use aws_smithy_runtime_api::client::orchestrator::{HttpRequest as AwsHttpRequest, HttpResponse};
use aws_smithy_runtime_api::client::result::ConnectorError;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_runtime_api::http::StatusCode;
use aws_smithy_types::body::SdkBody;
use futures::AsyncReadExt;
use http_client::{AsyncBody, Inner};
use http_client::{HttpClient, Request};
use tokio::runtime::Handle;
struct AwsHttpConnector {
client: Arc<dyn HttpClient>,
handle: Handle,
}
impl std::fmt::Debug for AwsHttpConnector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AwsHttpConnector").finish()
}
}
impl AwsConnector for AwsHttpConnector {
fn call(&self, request: AwsHttpRequest) -> AwsConnectorFuture {
let req = match request.try_into_http1x() {
Ok(req) => req,
Err(err) => {
return HttpConnectorFuture::ready(Err(ConnectorError::other(err.into(), None)))
}
};
let (parts, body) = req.into_parts();
let response = self
.client
.send(Request::from_parts(parts, convert_to_async_body(body)));
let handle = self.handle.clone();
HttpConnectorFuture::new(async move {
let response = match response.await {
Ok(response) => response,
Err(err) => return Err(ConnectorError::other(err.into(), None)),
};
let (parts, body) = response.into_parts();
let body = convert_to_sdk_body(body, handle).await;
Ok(HttpResponse::new(
StatusCode::try_from(parts.status.as_u16()).unwrap(),
body,
))
})
}
}
#[derive(Clone)]
pub struct AwsHttpClient {
client: Arc<dyn HttpClient>,
handler: Handle,
}
impl std::fmt::Debug for AwsHttpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AwsHttpClient").finish()
}
}
impl AwsHttpClient {
pub fn new(client: Arc<dyn HttpClient>, handle: Handle) -> Self {
Self {
client,
handler: handle,
}
}
}
impl AwsClient for AwsHttpClient {
fn http_connector(
&self,
_settings: &HttpConnectorSettings,
_components: &RuntimeComponents,
) -> SharedHttpConnector {
SharedHttpConnector::new(AwsHttpConnector {
client: self.client.clone(),
handle: self.handler.clone(),
})
}
}
pub async fn convert_to_sdk_body(body: AsyncBody, handle: Handle) -> SdkBody {
match body.0 {
Inner::Empty => SdkBody::empty(),
Inner::Bytes(bytes) => SdkBody::from(bytes.into_inner()),
Inner::AsyncReader(mut reader) => {
let buffer = handle.spawn(async move {
let mut buffer = Vec::new();
let _ = reader.read_to_end(&mut buffer).await;
buffer
});
SdkBody::from(buffer.await.unwrap_or_default())
}
}
}
pub fn convert_to_async_body(body: SdkBody) -> AsyncBody {
match body.bytes() {
Some(bytes) => AsyncBody::from((*bytes).to_vec()),
None => AsyncBody::empty(),
}
}

28
crates/bedrock/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "bedrock"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/bedrock.rs"
[features]
default = []
schemars = ["dep:schemars"]
[dependencies]
anyhow.workspace = true
aws-sdk-bedrockruntime = { workspace = true, features = ["behavior-version-latest"] }
aws-smithy-types = {workspace = true}
futures.workspace = true
schemars = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }

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

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

View File

@@ -0,0 +1,166 @@
mod models;
use std::pin::Pin;
use anyhow::{anyhow, Context, Error, Result};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
ContentBlock as BedrockInnerContent, SpecificToolChoice as BedrockSpecificTool,
ToolChoice as BedrockToolChoice, ToolInputSchema as BedrockToolInputSchema,
ToolSpecification as BedrockTool,
};
use aws_smithy_types::{Document, Number as AwsNumber};
pub use bedrock::operation::converse_stream::ConverseStreamInput as BedrockStreamingRequest;
pub use bedrock::types::{
ContentBlock as BedrockRequestContent, ConversationRole as BedrockRole,
ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse,
Message as BedrockMessage, ResponseStream as BedrockResponseStream,
};
use futures::stream::{self, BoxStream, Stream};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
use thiserror::Error;
pub use crate::models::*;
pub async fn complete(
client: &bedrock::Client,
request: Request,
) -> Result<BedrockResponse, BedrockError> {
let response = bedrock::Client::converse(client)
.model_id(request.model.clone())
.set_messages(request.messages.into())
.send()
.await
.context("failed to send request to Bedrock");
match response {
Ok(output) => output
.output
.ok_or_else(|| BedrockError::Other(anyhow!("no output"))),
Err(err) => Err(BedrockError::Other(err)),
}
}
pub async fn stream_completion(
client: bedrock::Client,
request: Request,
handle: tokio::runtime::Handle,
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
handle
.spawn(async move {
let response = bedrock::Client::converse_stream(&client)
.model_id(request.model.clone())
.set_messages(request.messages.into())
.send()
.await;
match response {
Ok(output) => {
let stream: Pin<
Box<
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
+ Send,
>,
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
match stream.recv().await {
Ok(Some(output)) => Some((Ok(output), stream)),
Ok(None) => None,
Err(err) => {
Some((
// TODO: Figure out how we can capture Throttling Exceptions
Err(BedrockError::ClientError(anyhow!(
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
))),
stream,
))
}
}
}));
Ok(stream)
}
Err(err) => Err(anyhow!(
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
)),
}
})
.await
.map_err(|err| anyhow!("failed to spawn task: {err:?}"))?
}
pub fn aws_document_to_value(document: &Document) -> Value {
match document {
Document::Null => Value::Null,
Document::Bool(value) => Value::Bool(*value),
Document::Number(value) => match *value {
AwsNumber::PosInt(value) => Value::Number(Number::from(value)),
AwsNumber::NegInt(value) => Value::Number(Number::from(value)),
AwsNumber::Float(value) => Value::Number(Number::from_f64(value).unwrap()),
},
Document::String(value) => Value::String(value.clone()),
Document::Array(array) => Value::Array(array.iter().map(aws_document_to_value).collect()),
Document::Object(map) => Value::Object(
map.iter()
.map(|(key, value)| (key.clone(), aws_document_to_value(value)))
.collect(),
),
}
}
pub fn value_to_aws_document(value: &Value) -> Document {
match value {
Value::Null => Document::Null,
Value::Bool(value) => Document::Bool(*value),
Value::Number(value) => {
if let Some(value) = value.as_u64() {
Document::Number(AwsNumber::PosInt(value))
} else if let Some(value) = value.as_i64() {
Document::Number(AwsNumber::NegInt(value))
} else if let Some(value) = value.as_f64() {
Document::Number(AwsNumber::Float(value))
} else {
Document::Null
}
}
Value::String(value) => Document::String(value.clone()),
Value::Array(array) => Document::Array(array.iter().map(value_to_aws_document).collect()),
Value::Object(map) => Document::Object(
map.iter()
.map(|(key, value)| (key.clone(), value_to_aws_document(value)))
.collect(),
),
}
}
#[derive(Debug)]
pub struct Request {
pub model: String,
pub max_tokens: u32,
pub messages: Vec<BedrockMessage>,
pub tools: Vec<BedrockTool>,
pub tool_choice: Option<BedrockToolChoice>,
pub system: Option<String>,
pub metadata: Option<Metadata>,
pub stop_sequences: Vec<String>,
pub temperature: Option<f32>,
pub top_k: Option<u32>,
pub top_p: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
pub user_id: Option<String>,
}
#[derive(Error, Debug)]
pub enum BedrockError {
#[error("client error: {0}")]
ClientError(anyhow::Error),
#[error("extension error: {0}")]
ExtensionError(anyhow::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,199 @@
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use strum::EnumIter;
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
pub enum Model {
// Anthropic models (already included)
#[default]
#[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
Claude3_5Sonnet,
#[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
Claude3Opus,
#[serde(rename = "claude-3-sonnet", alias = "claude-3-sonnet-latest")]
Claude3Sonnet,
#[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
Claude3_5Haiku,
// Amazon Nova Models
AmazonNovaLite,
AmazonNovaMicro,
AmazonNovaPro,
// AI21 models
AI21J2GrandeInstruct,
AI21J2JumboInstruct,
AI21J2Mid,
AI21J2MidV1,
AI21J2Ultra,
AI21J2UltraV1_8k,
AI21J2UltraV1,
AI21JambaInstructV1,
AI21Jamba15LargeV1,
AI21Jamba15MiniV1,
// Cohere models
CohereCommandTextV14_4k,
CohereCommandRV1,
CohereCommandRPlusV1,
CohereCommandLightTextV14_4k,
// Meta models
MetaLlama38BInstructV1,
MetaLlama370BInstructV1,
MetaLlama318BInstructV1_128k,
MetaLlama318BInstructV1,
MetaLlama3170BInstructV1_128k,
MetaLlama3170BInstructV1,
MetaLlama3211BInstructV1,
MetaLlama3290BInstructV1,
MetaLlama321BInstructV1,
MetaLlama323BInstructV1,
// Mistral models
MistralMistral7BInstructV0,
MistralMixtral8x7BInstructV0,
MistralMistralLarge2402V1,
MistralMistralSmall2402V1,
#[serde(rename = "custom")]
Custom {
name: String,
max_tokens: usize,
/// The name displayed in the UI, such as in the assistant panel model dropdown menu.
display_name: Option<String>,
max_output_tokens: Option<u32>,
default_temperature: Option<f32>,
},
}
impl Model {
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-3-5-sonnet") {
Ok(Self::Claude3_5Sonnet)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
} else if id.starts_with("claude-3-sonnet") {
Ok(Self::Claude3Sonnet)
} else if id.starts_with("claude-3-5-haiku") {
Ok(Self::Claude3_5Haiku)
} else {
Err(anyhow!("invalid model id"))
}
}
pub fn id(&self) -> &str {
match self {
Model::Claude3_5Sonnet => "us.anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3Opus => "us.anthropic.claude-3-opus-20240229-v1:0",
Model::Claude3Sonnet => "us.anthropic.claude-3-sonnet-20240229-v1:0",
Model::Claude3_5Haiku => "us.anthropic.claude-3-5-haiku-20241022-v1:0",
Model::AmazonNovaLite => "us.amazon.nova-lite-v1:0",
Model::AmazonNovaMicro => "us.amazon.nova-micro-v1:0",
Model::AmazonNovaPro => "us.amazon.nova-pro-v1:0",
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
Model::AI21J2Mid => "ai21.j2-mid",
Model::AI21J2MidV1 => "ai21.j2-mid-v1",
Model::AI21J2Ultra => "ai21.j2-ultra",
Model::AI21J2UltraV1_8k => "ai21.j2-ultra-v1:0:8k",
Model::AI21J2UltraV1 => "ai21.j2-ultra-v1",
Model::AI21JambaInstructV1 => "ai21.jamba-instruct-v1:0",
Model::AI21Jamba15LargeV1 => "ai21.jamba-1-5-large-v1:0",
Model::AI21Jamba15MiniV1 => "ai21.jamba-1-5-mini-v1:0",
Model::CohereCommandTextV14_4k => "cohere.command-text-v14:7:4k",
Model::CohereCommandRV1 => "cohere.command-r-v1:0",
Model::CohereCommandRPlusV1 => "cohere.command-r-plus-v1:0",
Model::CohereCommandLightTextV14_4k => "cohere.command-light-text-v14:7:4k",
Model::MetaLlama38BInstructV1 => "meta.llama3-8b-instruct-v1:0",
Model::MetaLlama370BInstructV1 => "meta.llama3-70b-instruct-v1:0",
Model::MetaLlama318BInstructV1_128k => "meta.llama3-1-8b-instruct-v1:0:128k",
Model::MetaLlama318BInstructV1 => "meta.llama3-1-8b-instruct-v1:0",
Model::MetaLlama3170BInstructV1_128k => "meta.llama3-1-70b-instruct-v1:0:128k",
Model::MetaLlama3170BInstructV1 => "meta.llama3-1-70b-instruct-v1:0",
Model::MetaLlama3211BInstructV1 => "meta.llama3-2-11b-instruct-v1:0",
Model::MetaLlama3290BInstructV1 => "meta.llama3-2-90b-instruct-v1:0",
Model::MetaLlama321BInstructV1 => "meta.llama3-2-1b-instruct-v1:0",
Model::MetaLlama323BInstructV1 => "meta.llama3-2-3b-instruct-v1:0",
Model::MistralMistral7BInstructV0 => "mistral.mistral-7b-instruct-v0:2",
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
Self::Custom { name, .. } => name,
}
}
pub fn display_name(&self) -> &str {
match self {
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
Self::Claude3Sonnet => "Claude 3 Sonnet",
Self::Claude3_5Haiku => "Claude 3.5 Haiku",
Self::AmazonNovaLite => "Amazon Nova Lite",
Self::AmazonNovaMicro => "Amazon Nova Micro",
Self::AmazonNovaPro => "Amazon Nova Pro",
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
Self::AI21J2Mid => "AI21 Jurassic2 Mid",
Self::AI21J2MidV1 => "AI21 Jurassic2 Mid V1",
Self::AI21J2Ultra => "AI21 Jurassic2 Ultra",
Self::AI21J2UltraV1_8k => "AI21 Jurassic2 Ultra V1 8K",
Self::AI21J2UltraV1 => "AI21 Jurassic2 Ultra V1",
Self::AI21JambaInstructV1 => "AI21 Jamba Instruct",
Self::AI21Jamba15LargeV1 => "AI21 Jamba 1.5 Large",
Self::AI21Jamba15MiniV1 => "AI21 Jamba 1.5 Mini",
Self::CohereCommandTextV14_4k => "Cohere Command Text V14 4K",
Self::CohereCommandRV1 => "Cohere Command R V1",
Self::CohereCommandRPlusV1 => "Cohere Command R Plus V1",
Self::CohereCommandLightTextV14_4k => "Cohere Command Light Text V14 4K",
Self::MetaLlama38BInstructV1 => "Meta Llama 3 8B Instruct V1",
Self::MetaLlama370BInstructV1 => "Meta Llama 3 70B Instruct V1",
Self::MetaLlama318BInstructV1_128k => "Meta Llama 3 1.8B Instruct V1 128K",
Self::MetaLlama318BInstructV1 => "Meta Llama 3 1.8B Instruct V1",
Self::MetaLlama3170BInstructV1_128k => "Meta Llama 3 1 70B Instruct V1 128K",
Self::MetaLlama3170BInstructV1 => "Meta Llama 3 1 70B Instruct V1",
Self::MetaLlama3211BInstructV1 => "Meta Llama 3 2 11B Instruct V1",
Self::MetaLlama3290BInstructV1 => "Meta Llama 3 2 90B Instruct V1",
Self::MetaLlama321BInstructV1 => "Meta Llama 3 2 1B Instruct V1",
Self::MetaLlama323BInstructV1 => "Meta Llama 3 2 3B Instruct V1",
Self::MistralMistral7BInstructV0 => "Mistral 7B Instruct V0",
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
Self::Custom {
display_name, name, ..
} => display_name.as_deref().unwrap_or(name),
}
}
pub fn max_token_count(&self) -> usize {
match self {
Self::Claude3_5Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku => 200_000,
Self::Custom { max_tokens, .. } => *max_tokens,
_ => 200_000,
}
}
pub fn max_output_tokens(&self) -> u32 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_5Sonnet => 8_192,
Self::Custom {
max_output_tokens, ..
} => max_output_tokens.unwrap_or(4_096),
_ => 4_096,
}
}
pub fn default_temperature(&self) -> f32 {
match self {
Self::Claude3_5Sonnet
| Self::Claude3Opus
| Self::Claude3Sonnet
| Self::Claude3_5Haiku => 1.0,
Self::Custom {
default_temperature,
..
} => default_temperature.unwrap_or(1.0),
_ => 1.0,
}
}
}

View File

@@ -256,6 +256,7 @@ async fn perform_completion(
// so that users can use the new version, without having to update Zed.
request.model = match model.as_str() {
"claude-3-5-sonnet" => anthropic::Model::Claude3_5Sonnet.id().to_string(),
"claude-3-7-sonnet" => anthropic::Model::Claude3_7Sonnet.id().to_string(),
"claude-3-opus" => anthropic::Model::Claude3Opus.id().to_string(),
"claude-3-haiku" => anthropic::Model::Claude3Haiku.id().to_string(),
"claude-3-sonnet" => anthropic::Model::Claude3Sonnet.id().to_string(),

View File

@@ -392,9 +392,13 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
.add_request_handler(forward_mutating_project_request::<proto::Push>)
.add_request_handler(forward_mutating_project_request::<proto::Pull>)
.add_request_handler(forward_mutating_project_request::<proto::Fetch>)
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)

View File

@@ -38,6 +38,7 @@ gpui.workspace = true
http_client.workspace = true
inline_completion.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
menu.workspace = true
node_runtime.workspace = true
@@ -62,7 +63,9 @@ async-std = { version = "1.12.0", features = ["unstable"] }
client = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }

View File

@@ -16,6 +16,7 @@ use gpui::{
};
use http_client::github::get_release_by_tag_name;
use http_client::HttpClient;
use language::language_settings::CopilotSettings;
use language::{
language_settings::{all_language_settings, language_settings, EditPredictionProvider},
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
@@ -367,13 +368,13 @@ impl Copilot {
let server_id = self.server_id;
let http = self.http.clone();
let node_runtime = self.node_runtime.clone();
if all_language_settings(None, cx).edit_predictions.provider
== EditPredictionProvider::Copilot
{
let language_settings = all_language_settings(None, cx);
if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot {
if matches!(self.server, CopilotServer::Disabled) {
let env = self.build_env(&language_settings.edit_predictions.copilot);
let start_task = cx
.spawn(move |this, cx| {
Self::start_language_server(server_id, http, node_runtime, this, cx)
Self::start_language_server(server_id, http, node_runtime, env, this, cx)
})
.shared();
self.server = CopilotServer::Starting { task: start_task };
@@ -385,6 +386,30 @@ impl Copilot {
}
}
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
let proxy_url = copilot_settings.proxy.clone()?;
let no_verify = copilot_settings.proxy_no_verify;
let http_or_https_proxy = if proxy_url.starts_with("http:") {
"HTTP_PROXY"
} else if proxy_url.starts_with("https:") {
"HTTPS_PROXY"
} else {
log::error!(
"Unsupported protocol scheme for language server proxy (must be http or https)"
);
return None;
};
let mut env = HashMap::default();
env.insert(http_or_https_proxy.to_string(), proxy_url);
if let Some(true) = no_verify {
env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
};
Some(env)
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
use lsp::FakeLanguageServer;
@@ -422,6 +447,7 @@ impl Copilot {
new_server_id: LanguageServerId,
http: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
env: Option<HashMap<String, String>>,
this: WeakEntity<Self>,
mut cx: AsyncApp,
) {
@@ -432,8 +458,7 @@ impl Copilot {
let binary = LanguageServerBinary {
path: node_path,
arguments,
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
env: None,
env,
};
let root_path = if cfg!(target_os = "windows") {
@@ -611,6 +636,8 @@ impl Copilot {
}
pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Task<()> {
let language_settings = all_language_settings(None, cx);
let env = self.build_env(&language_settings.edit_predictions.copilot);
let start_task = cx
.spawn({
let http = self.http.clone();
@@ -618,7 +645,7 @@ impl Copilot {
let server_id = self.server_id;
move |this, cx| async move {
clear_copilot_dir().await;
Self::start_language_server(server_id, http, node_runtime, this, cx).await
Self::start_language_server(server_id, http, node_runtime, env, this, cx).await
}
})
.shared();
@@ -1279,3 +1306,11 @@ mod tests {
}
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@@ -63,7 +63,7 @@ pub use editor_settings::{
CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar,
};
pub use editor_settings_controls::*;
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
use element::{layout_line, AcceptEditPredictionBinding, LineWithInvisibles, PositionMap};
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
};
@@ -82,7 +82,7 @@ use git::blame::GitBlame;
use gpui::{
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Entity, EntityInputHandler,
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
@@ -113,6 +113,7 @@ use persistence::DB;
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
use smallvec::smallvec;
use std::iter::Peekable;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
@@ -676,8 +677,8 @@ pub struct Editor {
show_inline_completions_override: Option<bool>,
menu_inline_completions_policy: MenuInlineCompletionsPolicy,
edit_prediction_preview: EditPredictionPreview,
edit_prediction_cursor_on_leading_whitespace: bool,
edit_prediction_requires_modifier_in_leading_space: bool,
edit_prediction_indent_conflict: bool,
edit_prediction_requires_modifier_in_indent_conflict: bool,
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
@@ -695,7 +696,6 @@ pub struct Editor {
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
distinguish_unstaged_diff_hunks: bool,
git_blame_inline_enabled: bool,
serialize_dirty_buffers: bool,
show_selection_menu: Option<bool>,
@@ -1403,12 +1403,11 @@ impl Editor {
show_inline_completions_override: None,
menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider,
edit_prediction_settings: EditPredictionSettings::Disabled,
edit_prediction_cursor_on_leading_whitespace: false,
edit_prediction_requires_modifier_in_leading_space: true,
edit_prediction_indent_conflict: false,
edit_prediction_requires_modifier_in_indent_conflict: true,
custom_context_menu: None,
show_git_blame_gutter: false,
show_git_blame_inline: false,
distinguish_unstaged_diff_hunks: false,
show_selection_menu: None,
show_git_blame_inline_delay_task: None,
git_blame_inline_tooltip: None,
@@ -1579,7 +1578,7 @@ impl Editor {
|| self.edit_prediction_requires_modifier()
// Require modifier key when the cursor is on leading whitespace, to allow `tab`
// bindings to insert tab characters.
|| (self.edit_prediction_requires_modifier_in_leading_space && self.edit_prediction_cursor_on_leading_whitespace)
|| (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict)
}
pub fn accept_edit_prediction_keybind(
@@ -2151,7 +2150,7 @@ impl Editor {
self.refresh_selected_text_highlights(window, cx);
refresh_matching_bracket_highlights(self, window, cx);
self.update_visible_inline_completion(window, cx);
self.edit_prediction_requires_modifier_in_leading_space = true;
self.edit_prediction_requires_modifier_in_indent_conflict = true;
linked_editing_ranges::refresh_linked_ranges(self, window, cx);
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(window, cx);
@@ -5137,7 +5136,7 @@ impl Editor {
}
}
self.edit_prediction_requires_modifier_in_leading_space = false;
self.edit_prediction_requires_modifier_in_indent_conflict = false;
}
pub fn accept_partial_inline_completion(
@@ -5435,8 +5434,19 @@ impl Editor {
self.edit_prediction_settings =
self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx);
self.edit_prediction_cursor_on_leading_whitespace =
multibuffer.is_line_whitespace_upto(cursor);
self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor);
if self.edit_prediction_indent_conflict {
let cursor_point = cursor.to_point(&multibuffer);
let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx);
if let Some((_, indent)) = indents.iter().next() {
if indent.len == cursor_point.column {
self.edit_prediction_indent_conflict = false;
}
}
}
let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
let edits = inline_completion
@@ -5784,6 +5794,524 @@ impl Editor {
.map(|menu| menu.origin())
}
const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.);
const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.);
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let active_inline_completion = self.active_inline_completion.as_ref()?;
if self.edit_prediction_visible_in_cursor_popover(true) {
return None;
}
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target_display_point = target.to_display_point(editor_snapshot);
if self.edit_prediction_requires_modifier() {
if !self.edit_prediction_preview_is_active() {
return None;
}
self.render_edit_prediction_modifier_jump_popover(
text_bounds,
content_origin,
visible_row_range,
line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
target_display_point,
window,
cx,
)
} else {
self.render_edit_prediction_eager_jump_popover(
text_bounds,
content_origin,
editor_snapshot,
visible_row_range,
scroll_top,
scroll_bottom,
line_height,
scroll_pixel_position,
target_display_point,
editor_width,
window,
cx,
)
}
}
InlineCompletion::Edit {
display_mode: EditDisplayMode::Inline,
..
} => None,
InlineCompletion::Edit {
display_mode: EditDisplayMode::TabAccept,
edits,
..
} => {
let range = &edits.first()?.0;
let target_display_point = range.end.to_display_point(editor_snapshot);
self.render_edit_prediction_end_of_line_popover(
"Accept",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
)
}
InlineCompletion::Edit {
edits,
edit_preview,
display_mode: EditDisplayMode::DiffPopover,
snapshot,
} => self.render_edit_prediction_diff_popover(
text_bounds,
content_origin,
editor_snapshot,
visible_row_range,
line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
style,
edits,
edit_preview,
snapshot,
window,
cx,
),
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_modifier_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
target_display_point: DisplayPoint,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let scrolled_content_origin =
content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
const SCROLL_PADDING_Y: Pixels = px(12.);
if target_display_point.row() < visible_row_range.start {
return self.render_edit_prediction_scroll_popover(
|_| SCROLL_PADDING_Y,
IconName::ArrowUp,
visible_row_range,
line_layouts,
newest_selection_head,
scrolled_content_origin,
window,
cx,
);
} else if target_display_point.row() >= visible_row_range.end {
return self.render_edit_prediction_scroll_popover(
|size| text_bounds.size.height - size.height - SCROLL_PADDING_Y,
IconName::ArrowDown,
visible_row_range,
line_layouts,
newest_selection_head,
scrolled_content_origin,
window,
cx,
);
}
const POLE_WIDTH: Pixels = px(2.);
let mut element = v_flex()
.items_end()
.child(
self.render_edit_prediction_line_popover("Jump", None, window, cx)?
.rounded_br(px(0.))
.rounded_tr(px(0.))
.border_r_2(),
)
.child(
div()
.w(POLE_WIDTH)
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
.h(line_height),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_layout =
line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?;
let target_column = target_display_point.column() as usize;
let target_x = line_layout.x_for_index(target_column);
let target_y =
(target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y;
let mut origin = scrolled_content_origin + point(target_x, target_y)
- point(size.width - POLE_WIDTH, size.height - line_height);
origin.x = origin.x.max(content_origin.x);
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_scroll_popover(
&mut self,
to_y: impl Fn(Size<Pixels>) -> Pixels,
scroll_icon: IconName,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
newest_selection_head: Option<DisplayPoint>,
scrolled_content_origin: gpui::Point<Pixels>,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let mut element = self
.render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout =
line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
let origin = scrolled_content_origin + point(cursor_character_x, to_y(size));
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_eager_jump_popover(
&mut self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
target_display_point: DisplayPoint,
editor_width: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
if target_display_point.row().as_f32() < scroll_top {
let mut element = self
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = self
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else {
self.render_edit_prediction_end_of_line_popover(
"Jump to Edit",
editor_snapshot,
visible_row_range,
target_display_point,
line_height,
scroll_pixel_position,
content_origin,
editor_width,
window,
cx,
)
}
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_end_of_line_popover(
self: &mut Editor,
label: &'static str,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
target_display_point: DisplayPoint,
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_width: Pixels,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let mut element = self
.render_edit_prediction_line_popover(label, None, window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?;
let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO);
let mut origin = start_point
+ line_origin
+ point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO);
origin.x = origin.x.max(content_origin.x);
let max_x = content_origin.x + editor_width - size.width;
if origin.x > max_x {
let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y;
let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) {
origin.y += offset;
IconName::ArrowUp
} else {
origin.y -= offset;
IconName::ArrowDown
};
element = self
.render_edit_prediction_line_popover(label, Some(icon), window, cx)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
origin.x = content_origin.x + editor_width - size.width - px(2.);
}
element.prepaint_at(origin, window, cx);
Some((element, origin))
}
#[allow(clippy::too_many_arguments)]
fn render_edit_prediction_diff_popover(
self: &Editor,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
edits: &Vec<(Range<Anchor>, String)>,
edit_preview: &Option<language::EditPreview>,
snapshot: &language::BufferSnapshot,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
let highlighted_edits =
crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx);
let styled_text = highlighted_edits.to_styled_text(&style.text);
let line_count = highlighted_edits.text.lines().count();
const BORDER_WIDTH: Pixels = px(1.);
let mut element = h_flex()
.items_start()
.child(
h_flex()
.bg(cx.theme().colors().editor_background)
.border(BORDER_WIDTH)
.shadow_sm()
.border_color(cx.theme().colors().border)
.rounded_l_lg()
.when(line_count > 1, |el| el.rounded_br_lg())
.pr_1()
.child(styled_text),
)
.child(
h_flex()
.h(line_height + BORDER_WIDTH * px(2.))
.px_1p5()
.gap_1()
// Workaround: For some reason, there's a gap if we don't do this
.ml(-BORDER_WIDTH)
.shadow(smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.05),
offset: point(px(1.), px(1.)),
blur_radius: px(2.),
spread_radius: px(0.),
}])
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(self.render_edit_prediction_accept_keybind(window, cx)),
)
.into_any();
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
window,
cx,
)
.width
};
let viewport_bounds =
Bounds::new(Default::default(), window.viewport_size()).extend(Edges {
right: -EditorElement::SCROLLBAR_WIDTH,
..Default::default()
});
let x_after_longest =
text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X
- scroll_pixel_position.x;
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
// Fully visible if it can be displayed within the window (allow overlapping other
// panes). However, this is only allowed if the popover starts within text_bounds.
let can_position_to_the_right = x_after_longest < text_bounds.right()
&& x_after_longest + element_bounds.width < viewport_bounds.right();
let mut origin = if can_position_to_the_right {
point(
x_after_longest,
text_bounds.origin.y + edit_start.row().as_f32() * line_height
- scroll_pixel_position.y,
)
} else {
let cursor_row = newest_selection_head.map(|head| head.row());
let above_edit = edit_start
.row()
.0
.checked_sub(line_count as u32)
.map(DisplayRow);
let below_edit = Some(edit_end.row() + 1);
let above_cursor =
cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
// Place the edit popover adjacent to the edit if there is a location
// available that is onscreen and does not obscure the cursor. Otherwise,
// place it adjacent to the cursor.
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
.into_iter()
.flatten()
.find(|&start_row| {
let end_row = start_row + line_count as u32;
visible_row_range.contains(&start_row)
&& visible_row_range.contains(&end_row)
&& cursor_row.map_or(true, |cursor_row| {
!((start_row..end_row).contains(&cursor_row))
})
})?;
content_origin
+ point(
-scroll_pixel_position.x,
row_target.as_f32() * line_height - scroll_pixel_position.y,
)
};
origin.x -= BORDER_WIDTH;
window.defer_draw(element, origin, 1);
// Do not return an element, since it will already be drawn due to defer_draw.
None
}
fn edit_prediction_cursor_popover_height(&self) -> Pixels {
px(30.)
}
@@ -9005,6 +9533,56 @@ impl Editor {
})
}
pub fn move_to_previous_multibuffer_header(
&mut self,
_: &MoveToStartOfExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::multibuffer_header(
map,
selection.head(),
workspace::searchable::Direction::Prev,
),
SelectionGoal::None,
)
});
})
}
pub fn move_to_next_multibuffer_header(
&mut self,
_: &MoveToEndOfExcerpt,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
s.move_with(|map, selection| {
selection.collapse_to(
movement::multibuffer_header(
map,
selection.head(),
workspace::searchable::Direction::Next,
),
SelectionGoal::None,
)
});
})
}
pub fn select_to_end_of_paragraph(
&mut self,
_: &SelectToEndOfParagraph,
@@ -12736,10 +13314,6 @@ impl Editor {
});
}
pub fn set_distinguish_unstaged_diff_hunks(&mut self) {
self.distinguish_unstaged_diff_hunks = true;
}
pub fn expand_all_diff_hunks(
&mut self,
_: &ExpandAllDiffHunks,
@@ -12985,7 +13559,12 @@ impl Editor {
.update(cx, |buffer_store, cx| buffer_store.save_buffer(buffer, cx))
.detach_and_log_err(cx);
let _ = repo.read(cx).set_index_text(&path, new_index_text);
cx.background_spawn(
repo.read(cx)
.set_index_text(&path, new_index_text)
.log_err(),
)
.detach();
}
pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context<Self>) {

View File

@@ -3711,391 +3711,6 @@ impl EditorElement {
}
}
#[allow(clippy::too_many_arguments)]
fn layout_edit_prediction_popover(
&self,
text_bounds: &Bounds<Pixels>,
content_origin: gpui::Point<Pixels>,
editor_snapshot: &EditorSnapshot,
visible_row_range: Range<DisplayRow>,
scroll_top: f32,
scroll_bottom: f32,
line_layouts: &[LineWithInvisibles],
line_height: Pixels,
scroll_pixel_position: gpui::Point<Pixels>,
newest_selection_head: Option<DisplayPoint>,
editor_width: Pixels,
style: &EditorStyle,
window: &mut Window,
cx: &mut App,
) -> Option<(AnyElement, gpui::Point<Pixels>)> {
const PADDING_X: Pixels = Pixels(24.);
const PADDING_Y: Pixels = Pixels(2.);
let editor = self.editor.read(cx);
let active_inline_completion = editor.active_inline_completion.as_ref()?;
if editor.edit_prediction_visible_in_cursor_popover(true) {
return None;
}
// Adjust text origin for horizontal scrolling (in some cases here)
let start_point = content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0));
// Clamp left offset after extreme scrollings
let clamp_start = |point: gpui::Point<Pixels>| gpui::Point {
x: point.x.max(content_origin.x),
y: point.y,
};
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target_display_point = target.to_display_point(editor_snapshot);
if editor.edit_prediction_requires_modifier() {
if !editor.edit_prediction_preview_is_active() {
return None;
}
if target_display_point.row() < visible_row_range.start {
let mut element = editor
.render_edit_prediction_line_popover(
"Scroll",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout = line_layouts
.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
const PADDING_Y: Pixels = px(12.);
let origin = start_point + point(cursor_character_x, PADDING_Y);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
} else if target_display_point.row() >= visible_row_range.end {
let mut element = editor
.render_edit_prediction_line_popover(
"Scroll",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let cursor = newest_selection_head?;
let cursor_row_layout = line_layouts
.get(cursor.row().minus(visible_row_range.start) as usize)?;
let cursor_column = cursor.column() as usize;
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
const PADDING_Y: Pixels = px(12.);
let origin = start_point
+ point(
cursor_character_x,
text_bounds.size.height - size.height - PADDING_Y,
);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
} else {
const POLE_WIDTH: Pixels = px(2.);
let mut element = v_flex()
.items_end()
.child(
editor
.render_edit_prediction_line_popover("Jump", None, window, cx)?
.rounded_br(px(0.))
.rounded_tr(px(0.))
.border_r_2(),
)
.child(
div()
.w(POLE_WIDTH)
.bg(Editor::edit_prediction_callout_popover_border_color(cx))
.h(line_height),
)
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let line_layout =
line_layouts
.get(target_display_point.row().minus(visible_row_range.start)
as usize)?;
let target_column = target_display_point.column() as usize;
let target_x = line_layout.x_for_index(target_column);
let target_y = (target_display_point.row().as_f32() * line_height)
- scroll_pixel_position.y;
let origin = clamp_start(
start_point + point(target_x, target_y)
- point(size.width - POLE_WIDTH, size.height - line_height),
);
element.prepaint_at(origin, window, cx);
return Some((element, origin));
}
}
if target_display_point.row().as_f32() < scroll_top {
let mut element = editor
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowUp),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
let mut element = editor
.render_edit_prediction_line_popover(
"Jump to Edit",
Some(IconName::ArrowDown),
window,
cx,
)?
.into_any();
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let offset = point(
(text_bounds.size.width - size.width) / 2.,
text_bounds.size.height - size.height - PADDING_Y,
);
let origin = text_bounds.origin + offset;
element.prepaint_at(origin, window, cx);
Some((element, origin))
} else {
let mut element = editor
.render_edit_prediction_line_popover("Jump to Edit", None, window, cx)?
.into_any();
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let origin = self.editor.update(cx, |editor, _cx| {
editor.display_to_pixel_point(target_line_end, editor_snapshot, window)
})?;
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
Some((element, origin))
}
}
InlineCompletion::Edit {
edits,
edit_preview,
display_mode,
snapshot,
} => {
if self.editor.read(cx).has_visible_completions_menu() {
return None;
}
let edit_start = edits
.first()
.unwrap()
.0
.start
.to_display_point(editor_snapshot);
let edit_end = edits
.last()
.unwrap()
.0
.end
.to_display_point(editor_snapshot);
let is_visible = visible_row_range.contains(&edit_start.row())
|| visible_row_range.contains(&edit_end.row());
if !is_visible {
return None;
}
match display_mode {
EditDisplayMode::TabAccept => {
let range = &edits.first()?.0;
let target_display_point = range.end.to_display_point(editor_snapshot);
let target_line_end = DisplayPoint::new(
target_display_point.row(),
editor_snapshot.line_len(target_display_point.row()),
);
let (mut element, origin) = self.editor.update(cx, |editor, cx| {
Some((
editor
.render_edit_prediction_line_popover(
"Accept", None, window, cx,
)?
.into_any(),
editor.display_to_pixel_point(
target_line_end,
editor_snapshot,
window,
)?,
))
})?;
let origin = clamp_start(start_point + origin + point(PADDING_X, px(0.)));
element.prepaint_as_root(origin, AvailableSpace::min_size(), window, cx);
return Some((element, origin));
}
EditDisplayMode::Inline => return None,
EditDisplayMode::DiffPopover => {}
}
let highlighted_edits = crate::inline_completion_edit_text(
&snapshot,
edits,
edit_preview.as_ref()?,
false,
cx,
);
let styled_text = highlighted_edits.to_styled_text(&style.text);
let line_count = highlighted_edits.text.lines().count();
const BORDER_WIDTH: Pixels = px(1.);
let mut element = h_flex()
.items_start()
.child(
h_flex()
.bg(cx.theme().colors().editor_background)
.border(BORDER_WIDTH)
.shadow_sm()
.border_color(cx.theme().colors().border)
.rounded_l_lg()
.when(line_count > 1, |el| el.rounded_br_lg())
.pr_1()
.child(styled_text),
)
.child(
h_flex()
.h(line_height + BORDER_WIDTH * px(2.))
.px_1p5()
.gap_1()
// Workaround: For some reason, there's a gap if we don't do this
.ml(-BORDER_WIDTH)
.shadow(smallvec![gpui::BoxShadow {
color: gpui::black().opacity(0.05),
offset: point(px(1.), px(1.)),
blur_radius: px(2.),
spread_radius: px(0.),
}])
.bg(Editor::edit_prediction_line_popover_bg_color(cx))
.border(BORDER_WIDTH)
.border_color(cx.theme().colors().border)
.rounded_r_lg()
.children(editor.render_edit_prediction_accept_keybind(window, cx)),
)
.into_any();
let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
let longest_line_width = if visible_row_range.contains(&longest_row) {
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
} else {
layout_line(
longest_row,
editor_snapshot,
style,
editor_width,
|_| false,
window,
cx,
)
.width
};
let viewport_bounds = Bounds::new(Default::default(), window.viewport_size())
.extend(Edges {
right: -Self::SCROLLBAR_WIDTH,
..Default::default()
});
let x_after_longest =
text_bounds.origin.x + longest_line_width + PADDING_X - scroll_pixel_position.x;
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx);
// Fully visible if it can be displayed within the window (allow overlapping other
// panes). However, this is only allowed if the popover starts within text_bounds.
let can_position_to_the_right = x_after_longest < text_bounds.right()
&& x_after_longest + element_bounds.width < viewport_bounds.right();
let mut origin = if can_position_to_the_right {
point(
x_after_longest,
text_bounds.origin.y + edit_start.row().as_f32() * line_height
- scroll_pixel_position.y,
)
} else {
let cursor_row = newest_selection_head.map(|head| head.row());
let above_edit = edit_start
.row()
.0
.checked_sub(line_count as u32)
.map(DisplayRow);
let below_edit = Some(edit_end.row() + 1);
let above_cursor = cursor_row
.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow));
let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1);
// Place the edit popover adjacent to the edit if there is a location
// available that is onscreen and does not obscure the cursor. Otherwise,
// place it adjacent to the cursor.
let row_target = [above_edit, below_edit, above_cursor, below_cursor]
.into_iter()
.flatten()
.find(|&start_row| {
let end_row = start_row + line_count as u32;
visible_row_range.contains(&start_row)
&& visible_row_range.contains(&end_row)
&& cursor_row.map_or(true, |cursor_row| {
!((start_row..end_row).contains(&cursor_row))
})
})?;
content_origin
+ point(
-scroll_pixel_position.x,
row_target.as_f32() * line_height - scroll_pixel_position.y,
)
};
origin.x -= BORDER_WIDTH;
window.defer_draw(element, origin, 1);
// Do not return an element, since it will already be drawn due to defer_draw.
None
}
}
}
fn layout_mouse_context_menu(
&self,
editor_snapshot: &EditorSnapshot,
@@ -6238,7 +5853,7 @@ pub(crate) struct LineWithInvisibles {
fragments: SmallVec<[LineFragment; 1]>,
invisibles: Vec<Invisible>,
len: usize,
width: Pixels,
pub(crate) width: Pixels,
font_size: Pixels,
}
@@ -7386,22 +7001,25 @@ impl Element for EditorElement {
});
let (inline_completion_popover, inline_completion_popover_origin) = self
.layout_edit_prediction_popover(
&text_hitbox.bounds,
content_origin,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
&style,
window,
cx,
)
.editor
.update(cx, |editor, cx| {
editor.render_edit_prediction_popover(
&text_hitbox.bounds,
content_origin,
&snapshot,
start_row..end_row,
scroll_position.y,
scroll_position.y + height_in_lines,
&line_layouts,
line_height,
scroll_pixel_position,
newest_selection_head,
editor_width,
&style,
window,
cx,
)
})
.unzip();
let mut inline_diagnostics = self.layout_inline_diagnostics(
@@ -8207,7 +7825,7 @@ struct BlockLayout {
style: BlockStyle,
}
fn layout_line(
pub fn layout_line(
row: DisplayRow,
snapshot: &EditorSnapshot,
style: &EditorStyle,

View File

@@ -467,6 +467,31 @@ pub fn end_of_excerpt(
}
}
pub fn multibuffer_header(
map: &DisplaySnapshot,
display_point: DisplayPoint,
direction: Direction,
) -> DisplayPoint {
let point = map.display_point_to_point(display_point, Bias::Left);
// It seems likely that the better way to implement this is to reuse block logic via
// `map.blocks_in_range` and add support for `reversed_blocks_in_range`. I haven't evaluated in
// depth whether this will work.
//
// Before thinking of that, implementation plan was to:
//
// * For `Direction::Prev`, use `reversed_excerpts_at` to iterate over the excerpts in reverse
// order.
//
// * For `Direction::Next`, use `excerpts_at` to iterate over the excerpts.
//
// * Find boundaries by checking when `buffer_id` changes similar to the block_map logic
// [here](https://github.com/zed-industries/zed/blob/e5b61949148cd87e08ae38e80949bce9b4ede9f7/crates/editor/src/display_map/block_map.rs#L845).
//
// Another alternative might be to use `excerpt_before` and `excerpt_after` methods to walk the
// excerpts.
todo!();
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found,
/// indicated by the given predicate returning true.
/// The predicate is called with the character to the left and right of the candidate boundary location.

View File

@@ -298,6 +298,7 @@ impl EditorTestContext {
self.cx.run_until_parked();
}
#[track_caller]
pub fn assert_index_text(&mut self, expected: Option<&str>) {
let fs = self.update_editor(|editor, _, cx| {
editor.project.as_ref().unwrap().read(cx).fs().as_fake()

View File

@@ -53,10 +53,7 @@ impl RenderOnce for ExtensionCard {
.size_full()
.items_center()
.justify_center()
.bg(theme::color_alpha(
cx.theme().colors().elevated_surface_background,
0.8,
))
.bg(cx.theme().colors().elevated_surface_background.alpha(0.8))
.child(Label::new("Overridden by dev extension.")),
)
}),

View File

@@ -1379,7 +1379,10 @@ impl FakeFs {
pub fn files(&self) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
queue.push_back((PathBuf::from("/"), self.state.lock().root.clone()));
queue.push_back((
PathBuf::from(util::path!("/")),
self.state.lock().root.clone(),
));
while let Some((path, entry)) = queue.pop_front() {
let e = entry.lock();
match &*e {
@@ -2007,11 +2010,52 @@ pub fn normalize_path(path: &Path) -> PathBuf {
ret
}
pub fn copy_recursive<'a>(
pub async fn copy_recursive<'a>(
fs: &'a dyn Fs,
source: &'a Path,
target: &'a Path,
options: CopyOptions,
) -> Result<()> {
for (is_dir, item) in read_dir_items(fs, source).await? {
let Ok(item_relative_path) = item.strip_prefix(source) else {
continue;
};
let target_item = target.join(item_relative_path);
if is_dir {
if !options.overwrite && fs.metadata(&target_item).await.is_ok_and(|m| m.is_some()) {
if options.ignore_if_exists {
continue;
} else {
return Err(anyhow!("{target_item:?} already exists"));
}
}
let _ = fs
.remove_dir(
&target_item,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
fs.create_dir(&target_item).await?;
} else {
fs.copy_file(&item, &target_item, options).await?;
}
}
Ok(())
}
async fn read_dir_items<'a>(fs: &'a dyn Fs, source: &'a Path) -> Result<Vec<(bool, PathBuf)>> {
let mut items = Vec::new();
read_recursive(fs, source, &mut items).await?;
Ok(items)
}
fn read_recursive<'a>(
fs: &'a dyn Fs,
source: &'a Path,
output: &'a mut Vec<(bool, PathBuf)>,
) -> BoxFuture<'a, Result<()>> {
use futures::future::FutureExt;
@@ -2020,39 +2064,19 @@ pub fn copy_recursive<'a>(
.metadata(source)
.await?
.ok_or_else(|| anyhow!("path does not exist: {}", source.display()))?;
if metadata.is_dir {
if !options.overwrite && fs.metadata(target).await.is_ok_and(|m| m.is_some()) {
if options.ignore_if_exists {
return Ok(());
} else {
return Err(anyhow!("{target:?} already exists"));
}
}
let _ = fs
.remove_dir(
target,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await;
fs.create_dir(target).await?;
if metadata.is_dir {
output.push((true, source.to_path_buf()));
let mut children = fs.read_dir(source).await?;
while let Some(child_path) = children.next().await {
if let Ok(child_path) = child_path {
if let Some(file_name) = child_path.file_name() {
let child_target_path = target.join(file_name);
copy_recursive(fs, &child_path, &child_target_path, options).await?;
}
read_recursive(fs, &child_path, output).await?;
}
}
Ok(())
} else {
fs.copy_file(source, target, options).await
output.push((false, source.to_path_buf()));
}
Ok(())
}
.boxed()
}
@@ -2094,12 +2118,13 @@ mod tests {
use super::*;
use gpui::BackgroundExecutor;
use serde_json::json;
use util::path;
#[gpui::test]
async fn test_fake_fs(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/root",
path!("/root"),
json!({
"dir1": {
"a": "A",
@@ -2118,32 +2143,229 @@ mod tests {
assert_eq!(
fs.files(),
vec![
PathBuf::from("/root/dir1/a"),
PathBuf::from("/root/dir1/b"),
PathBuf::from("/root/dir2/c"),
PathBuf::from("/root/dir2/dir3/d"),
PathBuf::from(path!("/root/dir1/a")),
PathBuf::from(path!("/root/dir1/b")),
PathBuf::from(path!("/root/dir2/c")),
PathBuf::from(path!("/root/dir2/dir3/d")),
]
);
fs.create_symlink("/root/dir2/link-to-dir3".as_ref(), "./dir3".into())
fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into())
.await
.unwrap();
assert_eq!(
fs.canonicalize("/root/dir2/link-to-dir3".as_ref())
fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref())
.await
.unwrap(),
PathBuf::from("/root/dir2/dir3"),
PathBuf::from(path!("/root/dir2/dir3")),
);
assert_eq!(
fs.canonicalize("/root/dir2/link-to-dir3/d".as_ref())
fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref())
.await
.unwrap(),
PathBuf::from("/root/dir2/dir3/d"),
PathBuf::from(path!("/root/dir2/dir3/d")),
);
assert_eq!(
fs.load("/root/dir2/link-to-dir3/d".as_ref()).await.unwrap(),
fs.load(path!("/root/dir2/link-to-dir3/d").as_ref())
.await
.unwrap(),
"D",
);
}
#[gpui::test]
async fn test_copy_recursive(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/outer"),
json!({
"inner1": {
"a": "A",
"b": "B",
"inner3": {
"d": "D",
}
},
"inner2": {
"c": "C",
}
}),
)
.await;
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/inner3/d")),
]
);
let source = Path::new(path!("/outer"));
let target = Path::new(path!("/outer/inner1/outer"));
copy_recursive(fs.as_ref(), source, target, Default::default())
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/inner3/d")),
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")),
]
);
}
#[gpui::test]
async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/outer"),
json!({
"inner1": {
"a": "A",
"b": "B",
"outer": {
"inner1": {
"a": "B"
}
}
},
"inner2": {
"c": "C",
}
}),
)
.await;
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
]
);
assert_eq!(
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
.await
.unwrap(),
"B",
);
let source = Path::new(path!("/outer"));
let target = Path::new(path!("/outer/inner1/outer"));
copy_recursive(
fs.as_ref(),
source,
target,
CopyOptions {
overwrite: true,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
]
);
assert_eq!(
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
.await
.unwrap(),
"A"
);
}
#[gpui::test]
async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) {
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/outer"),
json!({
"inner1": {
"a": "A",
"b": "B",
"outer": {
"inner1": {
"a": "B"
}
}
},
"inner2": {
"c": "C",
}
}),
)
.await;
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
]
);
assert_eq!(
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
.await
.unwrap(),
"B",
);
let source = Path::new(path!("/outer"));
let target = Path::new(path!("/outer/inner1/outer"));
copy_recursive(
fs.as_ref(),
source,
target,
CopyOptions {
ignore_if_exists: true,
..Default::default()
},
)
.await
.unwrap();
assert_eq!(
fs.files(),
vec![
PathBuf::from(path!("/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/b")),
PathBuf::from(path!("/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/a")),
PathBuf::from(path!("/outer/inner1/outer/inner1/b")),
PathBuf::from(path!("/outer/inner1/outer/inner2/c")),
PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")),
]
);
assert_eq!(
fs.load(path!("/outer/inner1/outer/inner1/a").as_ref())
.await
.unwrap(),
"B"
);
}
}

View File

@@ -26,6 +26,7 @@ log.workspace = true
parking_lot.workspace = true
regex.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
smol.workspace = true
sum_tree.workspace = true

View File

@@ -8,6 +8,9 @@ pub mod status;
use anyhow::{anyhow, Context as _, Result};
use gpui::action_with_deprecated_aliases;
use gpui::actions;
use gpui::impl_actions;
use repository::PushOptions;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fmt;
@@ -27,6 +30,13 @@ pub static COMMIT_MESSAGE: LazyLock<&'static OsStr> =
LazyLock::new(|| OsStr::new("COMMIT_EDITMSG"));
pub static INDEX_LOCK: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("index.lock"));
#[derive(Debug, Copy, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct Push {
pub options: Option<PushOptions>,
}
impl_actions!(git, [Push]);
actions!(
git,
[
@@ -43,6 +53,8 @@ actions!(
RestoreTrackedFiles,
TrashUntrackedFiles,
Uncommit,
Pull,
Fetch,
Commit,
]
);

View File

@@ -7,6 +7,8 @@ use git2::BranchType;
use gpui::SharedString;
use parking_lot::Mutex;
use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::Borrow;
use std::io::Write as _;
use std::process::Stdio;
@@ -29,6 +31,12 @@ pub struct Branch {
}
impl Branch {
pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
self.upstream
.as_ref()
.and_then(|upstream| upstream.tracking.status())
}
pub fn priority_key(&self) -> (bool, Option<i64>) {
(
self.is_head,
@@ -42,11 +50,32 @@ impl Branch {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Upstream {
pub ref_name: SharedString,
pub tracking: Option<UpstreamTracking>,
pub tracking: UpstreamTracking,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct UpstreamTracking {
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum UpstreamTracking {
/// Remote ref not present in local repository.
Gone,
/// Remote ref present in local repository (fetched from remote).
Tracked(UpstreamTrackingStatus),
}
impl UpstreamTracking {
pub fn is_gone(&self) -> bool {
matches!(self, UpstreamTracking::Gone)
}
pub fn status(&self) -> Option<UpstreamTrackingStatus> {
match self {
UpstreamTracking::Gone => None,
UpstreamTracking::Tracked(status) => Some(*status),
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct UpstreamTrackingStatus {
pub ahead: u32,
pub behind: u32,
}
@@ -68,6 +97,11 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Remote {
pub name: SharedString,
}
pub enum ResetMode {
// reset the branch pointer, leave index and worktree unchanged
// (this will make it look like things that were committed are now
@@ -139,6 +173,22 @@ pub trait GitRepository: Send + Sync {
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
fn push(
&self,
branch_name: &str,
upstream_name: &str,
options: Option<PushOptions>,
) -> Result<()>;
fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
fn fetch(&self) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
pub enum PushOptions {
SetUpstream,
Force,
}
impl std::fmt::Debug for dyn GitRepository {
@@ -165,6 +215,14 @@ impl RealGitRepository {
hosting_provider_registry,
}
}
fn working_directory(&self) -> Result<PathBuf> {
self.repository
.lock()
.workdir()
.context("failed to read git work directory")
.map(Path::to_path_buf)
}
}
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
@@ -209,12 +267,7 @@ impl GitRepository for RealGitRepository {
}
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
let mode_flag = match mode {
ResetMode::Mixed => "--mixed",
@@ -238,12 +291,7 @@ impl GitRepository for RealGitRepository {
if paths.is_empty() {
return Ok(());
}
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -296,12 +344,7 @@ impl GitRepository for RealGitRepository {
}
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if let Some(content) = content {
let mut child = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -485,12 +528,7 @@ impl GitRepository for RealGitRepository {
}
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -498,6 +536,8 @@ impl GitRepository for RealGitRepository {
.args(["update-index", "--add", "--remove", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
// TODO: Get remote response out of this and show it to the user
if !output.status.success() {
return Err(anyhow!(
"Failed to stage paths:\n{}",
@@ -509,12 +549,7 @@ impl GitRepository for RealGitRepository {
}
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -522,6 +557,8 @@ impl GitRepository for RealGitRepository {
.args(["reset", "--quiet", "--"])
.args(paths.iter().map(|p| p.as_ref()))
.output()?;
// TODO: Get remote response out of this and show it to the user
if !output.status.success() {
return Err(anyhow!(
"Failed to unstage:\n{}",
@@ -533,24 +570,21 @@ impl GitRepository for RealGitRepository {
}
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
if let Some(author) = author.as_deref() {
args.push("--author");
args.push(author);
let working_directory = self.working_directory()?;
let mut cmd = new_std_command(&self.git_binary_path);
cmd.current_dir(&working_directory)
.args(["commit", "--quiet", "-m"])
.arg(message)
.arg("--cleanup=strip");
if let Some((name, email)) = name_and_email {
cmd.arg("--author").arg(&format!("{name} <{email}>"));
}
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(args)
.output()?;
let output = cmd.output()?;
// TODO: Get remote response out of this and show it to the user
if !output.status.success() {
return Err(anyhow!(
"Failed to commit:\n{}",
@@ -559,6 +593,118 @@ impl GitRepository for RealGitRepository {
}
Ok(())
}
fn push(
&self,
branch_name: &str,
remote_name: &str,
options: Option<PushOptions>,
) -> Result<()> {
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["push", "--quiet"])
.args(options.map(|option| match option {
PushOptions::SetUpstream => "--set-upstream",
PushOptions::Force => "--force-with-lease",
}))
.arg(remote_name)
.arg(format!("{}:{}", branch_name, branch_name))
.output()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to push:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["pull", "--quiet"])
.arg(remote_name)
.arg(branch_name)
.output()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to pull:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn fetch(&self) -> Result<()> {
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["fetch", "--quiet", "--all"])
.output()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to fetch:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
let working_directory = self.working_directory()?;
if let Some(branch_name) = branch_name {
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["config", "--get"])
.arg(format!("branch.{}.remote", branch_name))
.output()?;
if output.status.success() {
let remote_name = String::from_utf8_lossy(&output.stdout);
return Ok(vec![Remote {
name: remote_name.trim().to_string().into(),
}]);
}
}
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["remote"])
.output()?;
if output.status.success() {
let remote_names = String::from_utf8_lossy(&output.stdout)
.split('\n')
.filter(|name| !name.is_empty())
.map(|name| Remote {
name: name.trim().to_string().into(),
})
.collect();
return Ok(remote_names);
} else {
return Err(anyhow!(
"Failed to get remotes:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
}
}
#[derive(Debug, Clone)]
@@ -743,6 +889,22 @@ impl GitRepository for FakeGitRepository {
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
unimplemented!()
}
fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
unimplemented!()
}
fn fetch(&self) -> Result<()> {
unimplemented!()
}
fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
unimplemented!()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -911,9 +1073,9 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
Ok(branches)
}
fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>> {
fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
if upstream_track == "" {
return Ok(Some(UpstreamTracking {
return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0,
}));
@@ -929,7 +1091,7 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
let mut behind: u32 = 0;
for component in upstream_track.split(", ") {
if component == "gone" {
return Ok(None);
return Ok(UpstreamTracking::Gone);
}
if let Some(ahead_num) = component.strip_prefix("ahead ") {
ahead = ahead_num.parse::<u32>()?;
@@ -938,7 +1100,10 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
behind = behind_num.parse::<u32>()?;
}
}
Ok(Some(UpstreamTracking { ahead, behind }))
Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead,
behind,
}))
}
#[test]
@@ -953,7 +1118,7 @@ fn test_branches_parsing() {
name: "zed-patches".into(),
upstream: Some(Upstream {
ref_name: "refs/remotes/origin/zed-patches".into(),
tracking: Some(UpstreamTracking {
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0
})

View File

@@ -261,7 +261,7 @@ impl PickerDelegate for BranchListDelegate {
.project()
.read(cx)
.active_repository(cx)
.and_then(|repo| repo.read(cx).branch())
.and_then(|repo| repo.read(cx).current_branch())
.map(|branch| branch.name.to_string())
})
.ok()

View File

@@ -4,13 +4,17 @@ use crate::git_panel::{commit_message_editor, GitPanel};
use crate::repository_selector::RepositorySelector;
use anyhow::Result;
use git::Commit;
use language::language_settings::LanguageSettings;
use language::Buffer;
use panel::{panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button};
use panel::{
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button,
};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, Tooltip};
use ui::{prelude::*, KeybindingHint, Tooltip};
use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
use editor::{Direction, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer};
use gpui::*;
use project::git::Repository;
use project::{Fs, Project};
@@ -18,6 +22,8 @@ use std::sync::Arc;
use workspace::dock::{Dock, DockPosition, PanelHandle};
use workspace::{ModalView, Workspace};
// actions!(commit_modal, [NextSuggestion, PrevSuggestion]);
pub fn init(cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
@@ -32,6 +38,8 @@ pub struct CommitModal {
git_panel: Entity<GitPanel>,
commit_editor: Entity<Editor>,
restore_dock: RestoreDock,
current_suggestion: Option<usize>,
suggested_messages: Vec<SharedString>,
}
impl Focusable for CommitModal {
@@ -114,6 +122,7 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
@@ -122,36 +131,276 @@ impl CommitModal {
cx.new(|cx| commit_message_editor(buffer, project.clone(), false, window, cx))
});
let commit_message = commit_editor.read(cx).text(cx);
if let Some(suggested_message) = suggested_message {
if commit_message.is_empty() {
commit_editor.update(cx, |editor, cx| {
editor.set_text(suggested_message, window, cx);
editor.select_all(&Default::default(), window, cx);
});
} else {
if commit_message.as_str().trim() == suggested_message.trim() {
commit_editor.update(cx, |editor, cx| {
// select the message to make it easy to delete
editor.select_all(&Default::default(), window, cx);
});
}
}
}
Self {
git_panel,
commit_editor,
restore_dock,
current_suggestion: None,
suggested_messages: vec![],
}
}
/// Returns container `(width, x padding, border radius)`
fn container_properties(&self, window: &mut Window, cx: &mut Context<Self>) -> (f32, f32, f32) {
// TODO: Let's set the width based on your set wrap guide if possible
// let settings = EditorSettings::get_global(cx);
// let first_wrap_guide = self
// .commit_editor
// .read(cx)
// .wrap_guides(cx)
// .iter()
// .next()
// .map(|(guide, active)| if *active { Some(*guide) } else { None })
// .flatten();
// let preferred_width = if let Some(guide) = first_wrap_guide {
// guide
// } else {
// 80
// };
let border_radius = 16.0;
let preferred_width = 50; // (chars wide)
let mut width = 460.0;
let padding_x = 16.0;
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = style.line_height_in_pixels(window.rem_size());
if let Ok(em_width) = window.text_system().em_width(font_id, font_size) {
width = preferred_width as f32 * em_width.0 + (padding_x * 2.0);
cx.notify();
}
// cx.notify();
(width, padding_x, border_radius)
}
// fn cycle_suggested_messages(&mut self, direction: Direction, cx: &mut Context<Self>) {
// let new_index = match direction {
// Direction::Next => {
// (self.current_suggestion.unwrap_or(0) + 1).rem_euclid(self.suggested_messages.len())
// }
// Direction::Prev => {
// (self.current_suggestion.unwrap_or(0) + self.suggested_messages.len() - 1)
// .rem_euclid(self.suggested_messages.len())
// }
// };
// self.current_suggestion = Some(new_index);
// cx.notify();
// }
// fn next_suggestion(&mut self, _: &NextSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(1);
// self.apply_suggestion(window, cx);
// }
// fn prev_suggestion(&mut self, _: &PrevSuggestion, window: &mut Window, cx: &mut Context<Self>) {
// self.current_suggestion = Some(0);
// self.apply_suggestion(window, cx);
// }
// fn set_commit_message(&mut self, message: &str, window: &mut Window, cx: &mut Context<Self>) {
// self.commit_editor.update(cx, |editor, cx| {
// editor.set_text(message.to_string(), window, cx)
// });
// self.current_suggestion = Some(0);
// cx.notify();
// }
// fn apply_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// let suggested_messages = self.suggested_messages.clone();
// if let Some(suggestion) = self.current_suggestion {
// let suggested_message = &suggested_messages[suggestion];
// self.set_commit_message(suggested_message, window, cx);
// }
// cx.notify();
// }
fn commit_editor_element(&self, window: &mut Window, cx: &mut Context<Self>) -> EditorElement {
let mut editor = self.commit_editor.clone();
let editor_style = panel_editor_style(true, window, cx);
EditorElement::new(&self.commit_editor, editor_style)
}
pub fn render_commit_editor(
&self,
name_and_email: Option<(SharedString, SharedString)>,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let editor = self.commit_editor.clone();
let (width, padding_x, modal_border_radius) = self.container_properties(window, cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let border_radius = modal_border_radius - padding_x / 2.0;
let editor = self.commit_editor.clone();
let editor_focus_handle = editor.focus_handle(cx);
let settings = ThemeSettings::get_global(cx);
let line_height = relative(settings.buffer_line_height.value())
.to_pixels(settings.buffer_font_size(cx).into(), window.rem_size());
let mut snapshot = self
.commit_editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let style = window.text_style().clone();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
let line_height = style.line_height_in_pixels(window.rem_size());
let em_width = window.text_system().em_width(font_id, font_size);
let (branch, tooltip, commit_label, co_authors) =
self.git_panel.update(cx, |git_panel, cx| {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
} else {
"Commit changes to tracked files"
};
let title = if git_panel.has_staged_changes() {
"Commit"
} else {
"Commit Tracked"
};
let co_authors = git_panel.render_co_authors(cx);
(branch, tooltip, title, co_authors)
});
let branch_selector = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Placeholder)
.color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
&zed_actions::git::Branch,
))
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
}))
.style(ButtonStyle::Transparent);
let changes_count = self.git_panel.read(cx).total_staged_count();
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
Some(
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
.suffix("Cancel"),
)
} else {
None
};
let fake_commit_kb =
ui::KeyBinding::new(gpui::KeyBinding::new("cmd-enter", gpui::NoAction, None), cx);
let commit_hint =
KeybindingHint::new(fake_commit_kb, cx.theme().colors().editor_background)
.suffix(commit_label);
let focus_handle = self.focus_handle(cx);
// let next_suggestion_kb =
// ui::KeyBinding::for_action_in(&NextSuggestion, &focus_handle.clone(), window, cx);
// let next_suggestion_hint = next_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background).suffix("Next Suggestion")
// });
// let prev_suggestion_kb =
// ui::KeyBinding::for_action_in(&PrevSuggestion, &focus_handle.clone(), window, cx);
// let prev_suggestion_hint = prev_suggestion_kb.map(|kb| {
// KeybindingHint::new(kb, cx.theme().colors().editor_background)
// .suffix("Previous Suggestion")
// });
v_flex()
.justify_between()
.relative()
.w_full()
.h_full()
.pt_2()
.id("editor-container")
.bg(cx.theme().colors().editor_background)
.child(EditorElement::new(&self.commit_editor, panel_editor_style))
.child(self.render_footer(window, cx))
.flex_1()
.size_full()
.rounded(px(border_radius))
.overflow_hidden()
.border_1()
.border_color(cx.theme().colors().border_variant)
.py_2()
.px_3()
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
}))
.child(
div()
.size_full()
.flex_1()
.child(self.commit_editor_element(window, cx)),
)
.child(
h_flex()
.group("commit_editor_footer")
.flex_none()
.w_full()
.items_center()
.justify_between()
.w_full()
.pt_2()
.pb_0p5()
.gap_1()
.child(h_flex().gap_1().child(branch_selector).children(co_authors))
.child(div().flex_1())
.child(
h_flex()
.opacity(0.7)
.group_hover("commit_editor_footer", |this| this.opacity(1.0))
.items_center()
.justify_end()
.flex_none()
.px_1()
.gap_4()
.children(close_kb_hint)
// .children(next_suggestion_hint)
.child(commit_hint),
),
)
}
pub fn render_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -159,7 +408,12 @@ impl CommitModal {
let branch = git_panel
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
.and_then(|repo| {
repo.read(cx)
.repository_entry
.branch()
.map(|b| b.name.clone())
})
.unwrap_or_else(|| "<no branch>".into());
let tooltip = if git_panel.has_staged_changes() {
"Commit staged changes"
@@ -175,13 +429,10 @@ impl CommitModal {
(branch, tooltip, title, co_authors)
});
let branch_selector = Button::new("branch-selector", branch)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
let branch_selector = panel_button(branch)
.icon(IconName::GitBranch)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
@@ -191,13 +442,29 @@ impl CommitModal {
window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
}))
.style(ButtonStyle::Transparent);
let changes_count = self.git_panel.read(cx).total_staged_count();
let close_kb_hint =
if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
Some(
KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
.suffix("Cancel"),
)
} else {
None
};
h_flex()
.items_center()
.h(px(36.0))
.w_full()
.justify_between()
.child(branch_selector)
.px_3()
.child(h_flex().child(branch_selector))
.child(
h_flex().children(co_authors).child(
panel_filled_button(title)
h_flex().gap_1p5().children(co_authors).child(
Button::new("stage-button", title)
.tooltip(Tooltip::for_action_title(tooltip, &git::Commit))
.on_click(cx.listener(|this, _, window, cx| {
this.commit(&Default::default(), window, cx);
@@ -206,6 +473,10 @@ impl CommitModal {
)
}
fn border_radius(&self) -> f32 {
8.0
}
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
@@ -218,27 +489,33 @@ impl CommitModal {
impl Render for CommitModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
let (width, _, border_radius) = self.container_properties(window, cx);
v_flex()
.id("commit-modal")
.key_context("GitCommit")
.elevation_3(cx)
.overflow_hidden()
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::commit))
// .on_action(cx.listener(Self::next_suggestion))
// .on_action(cx.listener(Self::prev_suggestion))
.relative()
.bg(cx.theme().colors().editor_background)
.rounded(px(16.))
.justify_between()
.bg(cx.theme().colors().elevated_surface_background)
.rounded(px(border_radius))
.border_1()
.border_color(cx.theme().colors().border)
.py_2()
.px_4()
.w(px(480.))
.min_h(rems(18.))
.w(px(width))
.h(px(360.))
.flex_1()
.overflow_hidden()
.child(
v_flex()
.flex_1()
.p_2()
.child(self.render_commit_editor(None, window, cx)),
)
// .child(self.render_footer(window, cx))
}
}

View File

@@ -4,7 +4,7 @@ use crate::repository_selector::RepositorySelectorPopoverMenu;
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
use crate::{project_diff, ProjectDiff};
use crate::{picker_prompt, project_diff, ProjectDiff};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::commit_tooltip::CommitTooltip;
@@ -12,9 +12,9 @@ use editor::{
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
ShowScrollbar,
};
use git::repository::{CommitDetails, ResetMode};
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode, UpstreamTracking};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use git::{Push, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::*;
use itertools::Itertools;
use language::{Buffer, File};
@@ -27,6 +27,9 @@ use project::{
};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::cell::RefCell;
use std::future::Future;
use std::rc::Rc;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
@@ -34,7 +37,7 @@ use ui::{
prelude::*, ButtonLike, Checkbox, ContextMenu, Divider, DividerColor, ElevationIndex, ListItem,
ListItemSpacing, Scrollbar, ScrollbarState, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
use util::{maybe, post_inc, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotificationId},
@@ -174,9 +177,14 @@ struct PendingOperation {
op_id: usize,
}
type RemoteOperations = Rc<RefCell<HashSet<u32>>>;
pub struct GitPanel {
remote_operation_id: u32,
pending_remote_operations: RemoteOperations,
pub(crate) active_repository: Option<Entity<Repository>>,
commit_editor: Entity<Editor>,
suggested_commit_message: Option<String>,
conflicted_count: usize,
conflicted_staged_count: usize,
current_modifiers: Modifiers,
@@ -206,6 +214,17 @@ pub struct GitPanel {
modal_open: bool,
}
struct RemoteOperationGuard {
id: u32,
pending_remote_operations: RemoteOperations,
}
impl Drop for RemoteOperationGuard {
fn drop(&mut self) {
self.pending_remote_operations.borrow_mut().remove(&self.id);
}
}
pub(crate) fn commit_message_editor(
commit_message_buffer: Entity<Buffer>,
project: Entity<Project>,
@@ -286,8 +305,11 @@ impl GitPanel {
cx.new(|cx| RepositorySelector::new(project.clone(), window, cx));
let mut git_panel = Self {
pending_remote_operations: Default::default(),
remote_operation_id: 0,
active_repository,
commit_editor,
suggested_commit_message: None,
conflicted_count: 0,
conflicted_staged_count: 0,
current_modifiers: window.modifiers(),
@@ -341,6 +363,16 @@ impl GitPanel {
cx.notify();
}
fn start_remote_operation(&mut self) -> RemoteOperationGuard {
let id = post_inc(&mut self.remote_operation_id);
self.pending_remote_operations.borrow_mut().insert(id);
RemoteOperationGuard {
id,
pending_remote_operations: self.pending_remote_operations.clone(),
}
}
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
self.pending_serialization = cx.background_spawn(
@@ -1008,6 +1040,10 @@ impl GitPanel {
.detach();
}
pub fn total_staged_count(&self) -> usize {
self.tracked_staged_count + self.new_staged_count + self.conflicted_staged_count
}
pub fn commit_message_buffer(&self, cx: &App) -> Entity<Buffer> {
self.commit_editor
.read(cx)
@@ -1029,17 +1065,15 @@ impl GitPanel {
}
}
/// Commit all staged changes
fn commit(&mut self, _: &git::Commit, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.commit_editor.read(cx);
if editor.is_empty(cx) {
if !editor.focus_handle(cx).contains_focused(window, cx) {
editor.focus_handle(cx).focus(window);
return;
}
if self
.commit_editor
.focus_handle(cx)
.contains_focused(window, cx)
{
self.commit_changes(window, cx)
}
self.commit_changes(window, cx)
cx.propagate();
}
pub(crate) fn commit_changes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1121,23 +1155,44 @@ impl GitPanel {
let Some(repo) = self.active_repository.clone() else {
return;
};
// TODO: Use git merge-base to find the upstream and main branch split
let confirmation = Task::ready(true);
// let confirmation = if self.commit_editor.read(cx).is_empty(cx) {
// Task::ready(true)
// } else {
// let prompt = window.prompt(
// PromptLevel::Warning,
// "Uncomitting will replace the current commit message with the previous commit's message",
// None,
// &["Ok", "Cancel"],
// cx,
// );
// cx.spawn(|_, _| async move { prompt.await.is_ok_and(|i| i == 0) })
// };
let prior_head = self.load_commit_details("HEAD", cx);
let task = cx.spawn(|_, mut cx| async move {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
.await??;
Ok(prior_head)
});
let task = cx.spawn_in(window, |this, mut cx| async move {
let result = task.await;
let result = maybe!(async {
if !confirmation.await {
Ok(None)
} else {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
.await??;
Ok(Some(prior_head))
}
})
.await;
this.update_in(&mut cx, |this, window, cx| {
this.pending_commit.take();
match result {
Ok(prior_commit) => {
Ok(None) => {}
Ok(Some(prior_commit)) => {
this.commit_editor.update(cx, |editor, cx| {
editor.set_text(prior_commit.message, window, cx)
});
@@ -1151,6 +1206,176 @@ impl GitPanel {
self.pending_commit = Some(task);
}
/// Suggests a commit message based on the changed files and their statuses
pub fn suggest_commit_message(&self) -> Option<String> {
let entries = self
.entries
.iter()
.filter_map(|entry| {
if let GitListEntry::GitStatusEntry(status_entry) = entry {
Some(status_entry)
} else {
None
}
})
.collect::<Vec<&GitStatusEntry>>();
if entries.is_empty() {
None
} else if entries.len() == 1 {
let entry = &entries[0];
let file_name = entry
.repo_path
.file_name()
.unwrap_or_default()
.to_string_lossy();
if entry.status.is_deleted() {
Some(format!("Delete {}", file_name))
} else if entry.status.is_created() {
Some(format!("Create {}", file_name))
} else if entry.status.is_modified() {
Some(format!("Update {}", file_name))
} else {
None
}
} else {
None
}
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_commit_message();
self.suggested_commit_message = suggested_commit_message.clone();
if let Some(suggested_commit_message) = suggested_commit_message {
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(suggested_commit_message), cx)
});
}
cx.notify();
}
fn fetch(&mut self, _: &git::Fetch, _window: &mut Window, cx: &mut Context<Self>) {
let Some(repo) = self.active_repository.clone() else {
return;
};
let guard = self.start_remote_operation();
let fetch = repo.read(cx).fetch();
cx.spawn(|_, _| async move {
fetch.await??;
drop(guard);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn pull(&mut self, _: &git::Pull, window: &mut Window, cx: &mut Context<Self>) {
let guard = self.start_remote_operation();
let remote = self.get_current_remote(window, cx);
cx.spawn(move |this, mut cx| async move {
let remote = remote.await?;
this.update(&mut cx, |this, cx| {
let Some(repo) = this.active_repository.clone() else {
return Err(anyhow::anyhow!("No active repository"));
};
let Some(branch) = repo.read(cx).current_branch() else {
return Err(anyhow::anyhow!("No active branch"));
};
Ok(repo.read(cx).pull(branch.name.clone(), remote.name))
})??
.await??;
drop(guard);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn push(&mut self, action: &git::Push, window: &mut Window, cx: &mut Context<Self>) {
let guard = self.start_remote_operation();
let options = action.options;
let remote = self.get_current_remote(window, cx);
cx.spawn(move |this, mut cx| async move {
let remote = remote.await?;
this.update(&mut cx, |this, cx| {
let Some(repo) = this.active_repository.clone() else {
return Err(anyhow::anyhow!("No active repository"));
};
let Some(branch) = repo.read(cx).current_branch() else {
return Err(anyhow::anyhow!("No active branch"));
};
Ok(repo
.read(cx)
.push(branch.name.clone(), remote.name, options))
})??
.await??;
drop(guard);
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn get_current_remote(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl Future<Output = Result<Remote>> {
let repo = self.active_repository.clone();
let workspace = self.workspace.clone();
let mut cx = window.to_async(cx);
async move {
let Some(repo) = repo else {
return Err(anyhow::anyhow!("No active repository"));
};
let mut current_remotes: Vec<Remote> = repo
.update(&mut cx, |repo, cx| {
let Some(current_branch) = repo.current_branch() else {
return Err(anyhow::anyhow!("No active branch"));
};
Ok(repo.get_remotes(Some(current_branch.name.to_string()), cx))
})??
.await?;
if current_remotes.len() == 0 {
return Err(anyhow::anyhow!("No active remote"));
} else if current_remotes.len() == 1 {
return Ok(current_remotes.pop().unwrap());
} else {
let current_remotes: Vec<_> = current_remotes
.into_iter()
.map(|remotes| remotes.name)
.collect();
let selection = cx
.update(|window, cx| {
picker_prompt::prompt(
"Pick which remote to push to",
current_remotes.clone(),
workspace,
window,
cx,
)
})?
.await?;
return Ok(Remote {
name: current_remotes[selection].clone(),
});
}
}
}
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
let mut new_co_authors = Vec::new();
let project = self.project.read(cx);
@@ -1276,6 +1501,7 @@ impl GitPanel {
git_panel.clear_pending();
}
git_panel.update_visible_entries(cx);
git_panel.update_editor_placeholder(cx);
})
.ok();
}
@@ -1591,15 +1817,23 @@ impl GitPanel {
.color(Color::Muted),
)
.child(self.render_repository_selector(cx))
.child(div().flex_grow())
.child(div().flex_grow()) // spacer
.child(
Button::new("diff", "+/-")
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
.on_click(|_, _, cx| {
cx.defer(|cx| {
cx.dispatch_action(&Diff);
})
}),
div()
.h_flex()
.gap_1()
.children(self.render_spinner(cx))
.children(self.render_sync_button(cx))
.children(self.render_pull_button(cx))
.child(
Button::new("diff", "+/-")
.tooltip(Tooltip::for_action_title("Open diff", &Diff))
.on_click(|_, _, cx| {
cx.defer(|cx| {
cx.dispatch_action(&Diff);
})
}),
),
),
)
} else {
@@ -1607,6 +1841,74 @@ impl GitPanel {
}
}
pub fn render_spinner(&self, _cx: &mut Context<Self>) -> Option<impl IntoElement> {
(!self.pending_remote_operations.borrow().is_empty()).then(|| {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
})
}
pub fn render_sync_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.project.read(cx).active_repository(cx);
active_repository.as_ref().map(|_| {
panel_filled_button("Fetch")
.icon(IconName::ArrowCircle)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title("git fetch", &git::Fetch))
.on_click(
cx.listener(move |this, _, window, cx| this.fetch(&git::Fetch, window, cx)),
)
.into_any_element()
})
}
pub fn render_pull_button(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.project.read(cx).active_repository(cx);
active_repository
.as_ref()
.and_then(|repo| repo.read(cx).current_branch())
.and_then(|branch| {
branch.upstream.as_ref().map(|upstream| {
let status = &upstream.tracking;
let disabled = status.is_gone();
panel_filled_button(match status {
git::repository::UpstreamTracking::Tracked(status) if status.behind > 0 => {
format!("Pull ({})", status.behind)
}
_ => "Pull".to_string(),
})
.icon(IconName::ArrowDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.disabled(status.is_gone())
.tooltip(move |window, cx| {
if disabled {
Tooltip::simple("Upstream is gone", cx)
} else {
// TODO: Add <origin> and <branch> argument substitutions to this
Tooltip::for_action("git pull", &git::Pull, window, cx)
}
})
.on_click(
cx.listener(move |this, _, window, cx| this.pull(&git::Pull, window, cx)),
)
.into_any_element()
})
})
}
pub fn render_repository_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
let active_repository = self.project.read(cx).active_repository(cx);
let repository_display_name = active_repository
@@ -1679,27 +1981,25 @@ impl GitPanel {
&& self.pending_commit.is_none()
&& !editor.read(cx).is_empty(cx)
&& self.has_write_access(cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let enable_coauthors = self.render_co_authors(cx);
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let focus_handle_1 = self.focus_handle(cx).clone();
let tooltip = if self.has_staged_changes() {
"Commit staged changes"
"git commit"
} else {
"Commit changes to tracked files"
"git commit --all"
};
let title = if self.has_staged_changes() {
"Commit"
} else {
"Commit All"
"Commit Tracked"
};
let editor_focus_handle = self.commit_editor.focus_handle(cx);
let commit_button = panel_filled_button(title)
.tooltip(move |window, cx| {
let focus_handle = focus_handle_1.clone();
Tooltip::for_action_in(tooltip, &Commit, &focus_handle, window, cx)
Tooltip::for_action_in(tooltip, &Commit, &editor_focus_handle, window, cx)
})
.disabled(!can_commit)
.on_click({
@@ -1709,7 +2009,7 @@ impl GitPanel {
let branch = self
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).branch().map(|b| b.name.clone()))
.and_then(|repo| repo.read(cx).current_branch().map(|b| b.name.clone()))
.unwrap_or_else(|| "<no branch>".into());
let branch_selector = Button::new("branch-selector", branch)
@@ -1743,8 +2043,8 @@ impl GitPanel {
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.cursor_text()
.on_click(cx.listener(move |_, _: &ClickEvent, window, _cx| {
window.focus(&editor_focus_handle);
.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| {
window.focus(&this.commit_editor.focus_handle(cx));
}))
.when(!self.modal_open, |el| {
el.child(EditorElement::new(&self.commit_editor, panel_editor_style))
@@ -1772,24 +2072,9 @@ impl GitPanel {
fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let active_repository = self.active_repository.as_ref()?;
let branch = active_repository.read(cx).branch()?;
let branch = active_repository.read(cx).current_branch()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
if branch.upstream.as_ref().is_some_and(|upstream| {
if let Some(tracking) = &upstream.tracking {
tracking.ahead == 0
} else {
true
}
}) {
return None;
}
let tooltip = if self.has_staged_changes() {
"git reset HEAD^ --soft"
} else {
"git reset HEAD^"
};
let this = cx.entity();
Some(
h_flex()
@@ -1829,9 +2114,17 @@ impl GitPanel {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(tooltip, &git::Uncommit))
.tooltip(Tooltip::for_action_title(
if self.has_staged_changes() {
"git reset HEAD^ --soft"
} else {
"git reset HEAD^"
},
&git::Uncommit,
))
.on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
),
)
.child(self.render_push_button(branch, cx)),
)
}
@@ -2252,6 +2545,69 @@ impl GitPanel {
.into_any_element()
}
fn render_push_button(&self, branch: &Branch, cx: &Context<Self>) -> AnyElement {
let mut disabled = false;
// TODO: Add <origin> and <branch> argument substitutions to this
let button: SharedString;
let tooltip: SharedString;
let action: Option<Push>;
if let Some(upstream) = &branch.upstream {
match upstream.tracking {
UpstreamTracking::Gone => {
button = "Republish".into();
tooltip = "git push --set-upstream".into();
action = Some(git::Push {
options: Some(PushOptions::SetUpstream),
});
}
UpstreamTracking::Tracked(tracking) => {
if tracking.behind > 0 {
disabled = true;
button = "Push".into();
tooltip = "Upstream is ahead of local branch".into();
action = None;
} else if tracking.ahead > 0 {
button = format!("Push ({})", tracking.ahead).into();
tooltip = "git push".into();
action = Some(git::Push { options: None });
} else {
disabled = true;
button = "Push".into();
tooltip = "Upstream matches local branch".into();
action = None;
}
}
}
} else {
button = "Publish".into();
tooltip = "git push --set-upstream".into();
action = Some(git::Push {
options: Some(PushOptions::SetUpstream),
});
};
panel_filled_button(button)
.icon(IconName::ArrowUp)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.disabled(disabled)
.when_some(action, |this, action| {
this.on_click(
cx.listener(move |this, _, window, cx| this.push(&action, window, cx)),
)
})
.tooltip(move |window, cx| {
if let Some(action) = action.as_ref() {
Tooltip::for_action(tooltip.clone(), action, window, cx)
} else {
Tooltip::simple(tooltip.clone(), cx)
}
})
.into_any_element()
}
fn has_write_access(&self, cx: &App) -> bool {
!self.project.read(cx).is_read_only(cx)
}
@@ -2301,6 +2657,9 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::unstage_all))
.on_action(cx.listener(Self::discard_tracked_changes))
.on_action(cx.listener(Self::clean_all))
.on_action(cx.listener(Self::fetch))
.on_action(cx.listener(Self::pull))
.on_action(cx.listener(Self::push))
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
@@ -2317,17 +2676,21 @@ impl Render for GitPanel {
.size_full()
.overflow_hidden()
.bg(ElevationIndex::Surface.bg(cx))
.child(if has_entries {
.child(
v_flex()
.size_full()
.children(self.render_panel_header(window, cx))
.child(self.render_entries(has_write_access, window, cx))
.map(|this| {
if has_entries {
this.child(self.render_entries(has_write_access, window, cx))
} else {
this.child(self.render_empty_state(cx).into_any_element())
}
})
.children(self.render_previous_commit(cx))
.child(self.render_commit_editor(window, cx))
.into_any_element()
} else {
self.render_empty_state(cx).into_any_element()
})
.into_any_element(),
)
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()

View File

@@ -9,6 +9,7 @@ pub mod branch_picker;
mod commit_modal;
pub mod git_panel;
mod git_panel_settings;
pub mod picker_prompt;
pub mod project_diff;
pub mod repository_selector;

View File

@@ -0,0 +1,235 @@
use anyhow::{anyhow, Result};
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate};
use core::cmp;
use gpui::{
rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, WeakEntity, Window,
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
pub struct PickerPrompt {
pub picker: Entity<Picker<PickerPromptDelegate>>,
rem_width: f32,
_subscription: Subscription,
}
pub fn prompt(
prompt: &str,
options: Vec<SharedString>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<usize>> {
if options.is_empty() {
return Task::ready(Err(anyhow!("No options")));
}
let prompt = prompt.to_string().into();
window.spawn(cx, |mut cx| async move {
// Modal branch picker has a longer trailoff than a popover one.
let (tx, rx) = oneshot::channel();
let delegate = PickerPromptDelegate::new(prompt, options, tx, 70);
workspace.update_in(&mut cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
PickerPrompt::new(delegate, 34., window, cx)
})
})?;
rx.await?
})
}
impl PickerPrompt {
fn new(
delegate: PickerPromptDelegate,
rem_width: f32,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
Self {
picker,
rem_width,
_subscription,
}
}
}
impl ModalView for PickerPrompt {}
impl EventEmitter<DismissEvent> for PickerPrompt {}
impl Focusable for PickerPrompt {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for PickerPrompt {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(self.rem_width))
.child(self.picker.clone())
.on_mouse_down_out(cx.listener(|this, _, window, cx| {
this.picker.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
}))
}
}
pub struct PickerPromptDelegate {
prompt: Arc<str>,
matches: Vec<StringMatch>,
all_options: Vec<SharedString>,
selected_index: usize,
max_match_length: usize,
tx: Option<oneshot::Sender<Result<usize>>>,
}
impl PickerPromptDelegate {
pub fn new(
prompt: Arc<str>,
options: Vec<SharedString>,
tx: oneshot::Sender<Result<usize>>,
max_chars: usize,
) -> Self {
Self {
prompt,
all_options: options,
matches: vec![],
selected_index: 0,
max_match_length: max_chars,
tx: Some(tx),
}
}
}
impl PickerDelegate for PickerPromptDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
self.prompt.clone()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
cx.spawn_in(window, move |picker, mut cx| async move {
let candidates = picker.update(&mut cx, |picker, _| {
picker
.delegate
.all_options
.iter()
.enumerate()
.map(|(ix, option)| StringMatchCandidate::new(ix, &option))
.collect::<Vec<StringMatchCandidate>>()
});
let Some(candidates) = candidates.log_err() else {
return;
};
let matches: Vec<StringMatch> = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
10000,
&Default::default(),
cx.background_executor().clone(),
)
.await
};
picker
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_index = 0;
} else {
delegate.selected_index =
cmp::min(delegate.selected_index, delegate.matches.len() - 1);
}
})
.log_err();
})
}
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(option) = self.matches.get(self.selected_index()) else {
return;
};
self.tx.take().map(|tx| tx.send(Ok(option.candidate_id)));
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = &self.matches[ix];
let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
Some(
ListItem::new(SharedString::from(format!("picker-prompt-menu-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.map(|el| {
let highlights: Vec<_> = hit
.positions
.iter()
.filter(|index| index < &&self.max_match_length)
.copied()
.collect();
el.child(HighlightedLabel::new(shortened_option, highlights))
}),
)
}
}

View File

@@ -11,7 +11,7 @@ use editor::{
};
use feature_flags::FeatureFlagViewExt;
use futures::StreamExt;
use git::{Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll};
use git::{status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll};
use gpui::{
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,
@@ -51,6 +51,7 @@ struct DiffBuffer {
path_key: PathKey,
buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
file_status: FileStatus,
}
const CONFLICT_NAMESPACE: &'static str = "0";
@@ -127,7 +128,6 @@ impl ProjectDiff {
window,
cx,
);
diff_display_editor.set_distinguish_unstaged_diff_hunks();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
@@ -352,6 +352,7 @@ impl ProjectDiff {
path_key,
buffer,
diff: changes,
file_status: entry.status,
})
}));
}
@@ -384,15 +385,22 @@ impl ProjectDiff {
.collect::<Vec<_>>()
};
self.multibuffer.update(cx, |multibuffer, cx| {
let is_excerpt_newly_added = self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
path_key.clone(),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
);
)
});
if is_excerpt_newly_added && diff_buffer.file_status.is_deleted() {
self.editor.update(cx, |editor, cx| {
editor.fold_buffer(snapshot.text.remote_id(), cx)
});
}
if self.multibuffer.read(cx).is_empty()
&& self
.editor

View File

@@ -299,7 +299,7 @@ pub struct CountTokensResponse {
}
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, strum::EnumIter)]
#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq, strum::EnumIter)]
pub enum Model {
#[serde(rename = "gemini-1.5-pro")]
Gemini15Pro,
@@ -308,6 +308,7 @@ pub enum Model {
#[serde(rename = "gemini-2.0-pro-exp")]
Gemini20Pro,
#[serde(rename = "gemini-2.0-flash")]
#[default]
Gemini20Flash,
#[serde(rename = "gemini-2.0-flash-thinking-exp")]
Gemini20FlashThinking,

View File

@@ -486,7 +486,31 @@ impl Hsla {
self.a *= 1.0 - factor.clamp(0., 1.);
}
/// Returns a new HSLA color with the same hue, saturation, and lightness, but with a modified alpha value.
/// Multiplies the alpha value of the color by a given factor
/// and returns a new HSLA color.
///
/// Useful for transforming colors with dynamic opacity,
/// like a color from an external source.
///
/// Example:
/// ```
/// let color = gpui::red();
/// let faded_color = color.opacity(0.5);
/// assert_eq!(faded_color.a, 0.5);
/// ```
///
/// This will return a red color with half the opacity.
///
/// Example:
/// ```
/// let color = hlsa(0.7, 1.0, 0.5, 0.7); // A saturated blue
/// let faded_color = color.opacity(0.16);
/// assert_eq!(faded_color.a, 0.112);
/// ```
///
/// This will return a blue color with around ~10% opacity,
/// suitable for an element's hover or selected state.
///
pub fn opacity(&self, factor: f32) -> Self {
Hsla {
h: self.h,
@@ -495,6 +519,35 @@ impl Hsla {
a: self.a * factor.clamp(0., 1.),
}
}
/// Returns a new HSLA color with the same hue, saturation,
/// and lightness, but with a new alpha value.
///
/// Example:
/// ```
/// let color = gpui::red();
/// let red_color = color.alpha(0.25);
/// assert_eq!(red_color.a, 0.25);
/// ```
///
/// This will return a red color with half the opacity.
///
/// Example:
/// ```
/// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
/// let faded_color = color.alpha(0.25);
/// assert_eq!(faded_color.a, 0.25);
/// ```
///
/// This will return a blue color with 25% opacity.
pub fn alpha(&self, a: f32) -> Self {
Hsla {
h: self.h,
s: self.s,
l: self.l,
a: a.clamp(0., 1.),
}
}
}
impl From<Rgba> for Hsla {

View File

@@ -35,6 +35,11 @@ pub struct BackgroundExecutor {
/// A pointer to the executor that is currently running,
/// for spawning tasks on the main thread.
///
/// This is intentionally `!Send` via the `not_send` marker field. This is because
/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is
/// only polled from the same thread it was spawned from. These checks would fail when spawning
/// foreground tasks from from background threads.
#[derive(Clone)]
pub struct ForegroundExecutor {
#[doc(hidden)]

View File

@@ -1,7 +1,7 @@
use crate::{
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, WhiteSpace,
SharedString, StrikethroughStyle, StyleRefinement, TextOverflow, UnderlineStyle, WhiteSpace,
};
use crate::{TextAlign, TextStyleRefinement};
pub use gpui_macros::{
@@ -486,6 +486,17 @@ pub trait Styled: Sized {
self
}
/// Sets the text decoration to underline.
/// [Docs](https://tailwindcss.com/docs/text-decoration-line#underling-text)
fn underline(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
style.underline = Some(UnderlineStyle {
thickness: px(1.),
..Default::default()
});
self
}
/// Sets the decoration of the text to have a line through it.
/// [Docs](https://tailwindcss.com/docs/text-decoration#setting-the-text-decoration)
fn line_through(mut self) -> Self {

View File

@@ -593,8 +593,7 @@ impl Frame {
}
}
// Holds the state for a specific window.
#[doc(hidden)]
/// Holds the state for a specific window.
pub struct Window {
pub(crate) handle: AnyWindowHandle,
pub(crate) invalidator: WindowInvalidator,
@@ -1007,6 +1006,7 @@ impl Window {
subscription
}
/// Replaces the root entity of the window with a new one.
pub fn replace_root<E>(
&mut self,
cx: &mut App,
@@ -1021,6 +1021,7 @@ impl Window {
view
}
/// Returns the root entity of the window, if it has one.
pub fn root<E>(&self) -> Option<Option<Entity<E>>>
where
E: 'static + Render,

View File

@@ -234,6 +234,8 @@ pub struct EditPredictionSettings {
pub disabled_globs: Vec<GlobMatcher>,
/// Configures how edit predictions are displayed in the buffer.
pub mode: EditPredictionsMode,
/// Settings specific to GitHub Copilot.
pub copilot: CopilotSettings,
}
/// The mode in which edit predictions should be displayed.
@@ -248,6 +250,14 @@ pub enum EditPredictionsMode {
EagerPreview,
}
#[derive(Clone, Debug, Default)]
pub struct CopilotSettings {
/// HTTP/HTTPS proxy to use for Copilot.
pub proxy: Option<String>,
/// Disable certificate verification for proxy (not recommended).
pub proxy_no_verify: Option<bool>,
}
/// The settings for all languages.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AllLanguageSettingsContent {
@@ -465,6 +475,23 @@ pub struct EditPredictionSettingsContent {
/// Provider support required.
#[serde(default)]
pub mode: EditPredictionsMode,
/// Settings specific to GitHub Copilot.
#[serde(default)]
pub copilot: CopilotSettingsContent,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CopilotSettingsContent {
/// HTTP/HTTPS proxy to use for Copilot.
///
/// Default: none
#[serde(default)]
pub proxy: Option<String>,
/// Disable certificate verification for the proxy (not recommended).
///
/// Default: false
#[serde(default)]
pub proxy_no_verify: Option<bool>,
}
/// The settings for enabling/disabling features.
@@ -1064,6 +1091,16 @@ impl settings::Settings for AllLanguageSettings {
.map(|globs| globs.iter().collect())
.ok_or_else(Self::missing_default)?;
let mut copilot_settings = default_value
.edit_predictions
.as_ref()
.map(|settings| settings.copilot.clone())
.map(|copilot| CopilotSettings {
proxy: copilot.proxy,
proxy_no_verify: copilot.proxy_no_verify,
})
.unwrap_or_default();
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
for (language, suffixes) in &default_value.file_types {
@@ -1096,6 +1133,22 @@ impl settings::Settings for AllLanguageSettings {
}
}
if let Some(proxy) = user_settings
.edit_predictions
.as_ref()
.and_then(|settings| settings.copilot.proxy.clone())
{
copilot_settings.proxy = Some(proxy);
}
if let Some(proxy_no_verify) = user_settings
.edit_predictions
.as_ref()
.and_then(|settings| settings.copilot.proxy_no_verify)
{
copilot_settings.proxy_no_verify = Some(proxy_no_verify);
}
// A user's global settings override the default global settings and
// all default language-specific settings.
merge_settings(&mut defaults, &user_settings.defaults);
@@ -1147,6 +1200,7 @@ impl settings::Settings for AllLanguageSettings {
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
.collect(),
mode: edit_predictions_mode,
copilot: copilot_settings,
},
defaults,
languages,

View File

@@ -20,16 +20,12 @@ anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
base64.workspace = true
collections.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }
gpui.workspace = true
http_client.workspace = true
image.workspace = true
lmstudio = { workspace = true, features = ["schemars"] }
log.workspace = true
mistral = { workspace = true, features = ["schemars"] }
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
parking_lot.workspace = true
proto.workspace = true

View File

@@ -46,6 +46,10 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
provider_name()
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(Arc::new(FakeLanguageModel::default()))
}
fn provided_models(&self, _: &App) -> Vec<Arc<dyn LanguageModel>> {
vec![Arc::new(FakeLanguageModel::default())]
}

View File

@@ -247,6 +247,7 @@ pub trait LanguageModelProvider: 'static {
fn icon(&self) -> IconName {
IconName::ZedAssistant
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>>;
fn load_model(&self, _model: Arc<dyn LanguageModel>, _cx: &App) {}
fn is_authenticated(&self, cx: &App) -> bool;

View File

@@ -69,6 +69,7 @@ impl CloudModel {
| anthropic::Model::Claude3Sonnet
| anthropic::Model::Claude3Haiku
| anthropic::Model::Claude3_5Haiku
| anthropic::Model::Claude3_7Sonnet
| anthropic::Model::Custom { .. } => {
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
}

View File

@@ -1,7 +1,3 @@
pub mod cloud_model;
pub use anthropic::Model as AnthropicModel;
pub use cloud_model::*;
pub use lmstudio::Model as LmStudioModel;
pub use ollama::Model as OllamaModel;
pub use open_ai::Model as OpenAiModel;

View File

@@ -241,298 +241,6 @@ pub struct LanguageModelRequest {
pub temperature: Option<f32>,
}
impl LanguageModelRequest {
pub fn into_open_ai(self, model: String, max_output_tokens: Option<u32>) -> open_ai::Request {
let stream = !model.starts_with("o1-");
open_ai::Request {
model,
messages: self
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => open_ai::RequestMessage::User {
content: msg.string_contents(),
},
Role::Assistant => open_ai::RequestMessage::Assistant {
content: Some(msg.string_contents()),
tool_calls: Vec::new(),
},
Role::System => open_ai::RequestMessage::System {
content: msg.string_contents(),
},
})
.collect(),
stream,
stop: self.stop,
temperature: self.temperature.unwrap_or(1.0),
max_tokens: max_output_tokens,
tools: Vec::new(),
tool_choice: None,
}
}
pub fn into_mistral(self, model: String, max_output_tokens: Option<u32>) -> mistral::Request {
let len = self.messages.len();
let merged_messages =
self.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
acc.push(match role {
Role::User => mistral::RequestMessage::User { content },
Role::Assistant => mistral::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => mistral::RequestMessage::System { content },
});
acc
});
mistral::Request {
model,
messages: merged_messages,
stream: true,
max_tokens: max_output_tokens,
temperature: self.temperature,
response_format: None,
tools: self
.tools
.into_iter()
.map(|tool| mistral::ToolDefinition::Function {
function: mistral::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
pub fn into_google(self, model: String) -> google_ai::GenerateContentRequest {
google_ai::GenerateContentRequest {
model,
contents: self
.messages
.into_iter()
.map(|msg| google_ai::Content {
parts: vec![google_ai::Part::TextPart(google_ai::TextPart {
text: msg.string_contents(),
})],
role: match msg.role {
Role::User => google_ai::Role::User,
Role::Assistant => google_ai::Role::Model,
Role::System => google_ai::Role::User, // Google AI doesn't have a system role
},
})
.collect(),
generation_config: Some(google_ai::GenerationConfig {
candidate_count: Some(1),
stop_sequences: Some(self.stop),
max_output_tokens: None,
temperature: self.temperature.map(|t| t as f64).or(Some(1.0)),
top_p: None,
top_k: None,
}),
safety_settings: None,
}
}
pub fn into_anthropic(
self,
model: String,
default_temperature: f32,
max_output_tokens: u32,
) -> anthropic::Request {
let mut new_messages: Vec<anthropic::Message> = Vec::new();
let mut system_message = String::new();
for message in self.messages {
if message.contents_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
let cache_control = if message.cache {
Some(anthropic::CacheControl {
cache_type: anthropic::CacheControlType::Ephemeral,
})
} else {
None
};
let anthropic_message_content: Vec<anthropic::RequestContent> = message
.content
.into_iter()
.filter_map(|content| match content {
MessageContent::Text(text) => {
if !text.is_empty() {
Some(anthropic::RequestContent::Text {
text,
cache_control,
})
} else {
None
}
}
MessageContent::Image(image) => {
Some(anthropic::RequestContent::Image {
source: anthropic::ImageSource {
source_type: "base64".to_string(),
media_type: "image/png".to_string(),
data: image.source.to_string(),
},
cache_control,
})
}
MessageContent::ToolUse(tool_use) => {
Some(anthropic::RequestContent::ToolUse {
id: tool_use.id.to_string(),
name: tool_use.name,
input: tool_use.input,
cache_control,
})
}
MessageContent::ToolResult(tool_result) => {
Some(anthropic::RequestContent::ToolResult {
tool_use_id: tool_result.tool_use_id,
is_error: tool_result.is_error,
content: tool_result.content,
cache_control,
})
}
})
.collect();
let anthropic_role = match message.role {
Role::User => anthropic::Role::User,
Role::Assistant => anthropic::Role::Assistant,
Role::System => unreachable!("System role should never occur here"),
};
if let Some(last_message) = new_messages.last_mut() {
if last_message.role == anthropic_role {
last_message.content.extend(anthropic_message_content);
continue;
}
}
new_messages.push(anthropic::Message {
role: anthropic_role,
content: anthropic_message_content,
});
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.string_contents());
}
}
}
anthropic::Request {
model,
messages: new_messages,
max_tokens: max_output_tokens,
system: Some(system_message),
tools: self
.tools
.into_iter()
.map(|tool| anthropic::Tool {
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
})
.collect(),
tool_choice: None,
metadata: None,
stop_sequences: Vec::new(),
temperature: self.temperature.or(Some(default_temperature)),
top_k: None,
top_p: None,
}
}
pub fn into_deepseek(self, model: String, max_output_tokens: Option<u32>) -> deepseek::Request {
let is_reasoner = model == "deepseek-reasoner";
let len = self.messages.len();
let merged_messages =
self.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
if is_reasoner {
if let Some(last_msg) = acc.last_mut() {
match (last_msg, role) {
(deepseek::RequestMessage::User { content: last }, Role::User) => {
last.push(' ');
last.push_str(&content);
return acc;
}
(
deepseek::RequestMessage::Assistant {
content: last_content,
..
},
Role::Assistant,
) => {
*last_content = last_content
.take()
.map(|c| {
let mut s =
String::with_capacity(c.len() + content.len() + 1);
s.push_str(&c);
s.push(' ');
s.push_str(&content);
s
})
.or(Some(content));
return acc;
}
_ => {}
}
}
}
acc.push(match role {
Role::User => deepseek::RequestMessage::User { content },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content },
});
acc
});
deepseek::Request {
model,
messages: merged_messages,
stream: true,
max_tokens: max_output_tokens,
temperature: if is_reasoner { None } else { self.temperature },
response_format: None,
tools: self
.tools
.into_iter()
.map(|tool| deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct LanguageModelResponseMessage {
pub role: Option<Role>,

View File

@@ -45,43 +45,3 @@ impl Display for Role {
}
}
}
impl From<Role> for ollama::Role {
fn from(val: Role) -> Self {
match val {
Role::User => ollama::Role::User,
Role::Assistant => ollama::Role::Assistant,
Role::System => ollama::Role::System,
}
}
}
impl From<Role> for open_ai::Role {
fn from(val: Role) -> Self {
match val {
Role::User => open_ai::Role::User,
Role::Assistant => open_ai::Role::Assistant,
Role::System => open_ai::Role::System,
}
}
}
impl From<Role> for deepseek::Role {
fn from(val: Role) -> Self {
match val {
Role::User => deepseek::Role::User,
Role::Assistant => deepseek::Role::Assistant,
Role::System => deepseek::Role::System,
}
}
}
impl From<Role> for lmstudio::Role {
fn from(val: Role) -> Self {
match val {
Role::User => lmstudio::Role::User,
Role::Assistant => lmstudio::Role::Assistant,
Role::System => lmstudio::Role::System,
}
}
}

View File

@@ -13,7 +13,7 @@ use http_client::HttpClient;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, RateLimiter, Role,
LanguageModelProviderState, LanguageModelRequest, MessageContent, RateLimiter, Role,
};
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
use schemars::JsonSchema;
@@ -183,6 +183,17 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
IconName::AiAnthropic
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = anthropic::Model::default();
Some(Arc::new(AnthropicModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -385,7 +396,8 @@ impl LanguageModel for AnthropicModel {
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
let request = request.into_anthropic(
let request = into_anthropic(
request,
self.model.id().into(),
self.model.default_temperature(),
self.model.max_output_tokens(),
@@ -416,7 +428,8 @@ impl LanguageModel for AnthropicModel {
input_schema: serde_json::Value,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let mut request = request.into_anthropic(
let mut request = into_anthropic(
request,
self.model.tool_model_id().into(),
self.model.default_temperature(),
self.model.max_output_tokens(),
@@ -445,6 +458,117 @@ impl LanguageModel for AnthropicModel {
}
}
pub fn into_anthropic(
request: LanguageModelRequest,
model: String,
default_temperature: f32,
max_output_tokens: u32,
) -> anthropic::Request {
let mut new_messages: Vec<anthropic::Message> = Vec::new();
let mut system_message = String::new();
for message in request.messages {
if message.contents_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
let cache_control = if message.cache {
Some(anthropic::CacheControl {
cache_type: anthropic::CacheControlType::Ephemeral,
})
} else {
None
};
let anthropic_message_content: Vec<anthropic::RequestContent> = message
.content
.into_iter()
.filter_map(|content| match content {
MessageContent::Text(text) => {
if !text.is_empty() {
Some(anthropic::RequestContent::Text {
text,
cache_control,
})
} else {
None
}
}
MessageContent::Image(image) => Some(anthropic::RequestContent::Image {
source: anthropic::ImageSource {
source_type: "base64".to_string(),
media_type: "image/png".to_string(),
data: image.source.to_string(),
},
cache_control,
}),
MessageContent::ToolUse(tool_use) => {
Some(anthropic::RequestContent::ToolUse {
id: tool_use.id.to_string(),
name: tool_use.name,
input: tool_use.input,
cache_control,
})
}
MessageContent::ToolResult(tool_result) => {
Some(anthropic::RequestContent::ToolResult {
tool_use_id: tool_result.tool_use_id,
is_error: tool_result.is_error,
content: tool_result.content,
cache_control,
})
}
})
.collect();
let anthropic_role = match message.role {
Role::User => anthropic::Role::User,
Role::Assistant => anthropic::Role::Assistant,
Role::System => unreachable!("System role should never occur here"),
};
if let Some(last_message) = new_messages.last_mut() {
if last_message.role == anthropic_role {
last_message.content.extend(anthropic_message_content);
continue;
}
}
new_messages.push(anthropic::Message {
role: anthropic_role,
content: anthropic_message_content,
});
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.string_contents());
}
}
}
anthropic::Request {
model,
messages: new_messages,
max_tokens: max_output_tokens,
system: Some(system_message),
tools: request
.tools
.into_iter()
.map(|tool| anthropic::Tool {
name: tool.name,
description: tool.description,
input_schema: tool.input_schema,
})
.collect(),
tool_choice: None,
metadata: None,
stop_sequences: Vec::new(),
temperature: request.temperature.or(Some(default_temperature)),
top_k: None,
top_p: None,
}
}
pub fn map_to_language_model_completion_events(
events: Pin<Box<dyn Send + Stream<Item = Result<Event, AnthropicError>>>>,
) -> impl Stream<Item = Result<LanguageModelCompletionEvent>> {

View File

@@ -1,4 +1,3 @@
use super::open_ai::count_open_ai_tokens;
use anthropic::AnthropicError;
use anyhow::{anyhow, Result};
use client::{
@@ -43,11 +42,13 @@ use strum::IntoEnumIterator;
use thiserror::Error;
use ui::{prelude::*, TintColor};
use crate::provider::anthropic::map_to_language_model_completion_events;
use crate::provider::anthropic::{
count_anthropic_tokens, into_anthropic, map_to_language_model_completion_events,
};
use crate::provider::google::into_google;
use crate::provider::open_ai::{count_open_ai_tokens, into_open_ai};
use crate::AllLanguageModelSettings;
use super::anthropic::count_anthropic_tokens;
pub const PROVIDER_NAME: &str = "Zed";
const ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: Option<&str> =
@@ -272,6 +273,18 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
IconName::AiZed
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
let llm_api_token = self.state.read(cx).llm_api_token.clone();
let model = CloudModel::Anthropic(anthropic::Model::default());
Some(Arc::new(CloudLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
llm_api_token: llm_api_token.clone(),
client: self.client.clone(),
request_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -600,7 +613,7 @@ impl LanguageModel for CloudLanguageModel {
CloudModel::OpenAi(model) => count_open_ai_tokens(request, model, cx),
CloudModel::Google(model) => {
let client = self.client.clone();
let request = request.into_google(model.id().into());
let request = into_google(request, model.id().into());
let request = google_ai::CountTokensRequest {
contents: request.contents,
};
@@ -626,7 +639,8 @@ impl LanguageModel for CloudLanguageModel {
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
match &self.model {
CloudModel::Anthropic(model) => {
let request = request.into_anthropic(
let request = into_anthropic(
request,
model.id().into(),
model.default_temperature(),
model.max_output_tokens(),
@@ -654,7 +668,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::OpenAi(model) => {
let client = self.client.clone();
let request = request.into_open_ai(model.id().into(), model.max_output_tokens());
let request = into_open_ai(request, model.id().into(), model.max_output_tokens());
let llm_api_token = self.llm_api_token.clone();
let future = self.request_limiter.stream(async move {
let response = Self::perform_llm_completion(
@@ -681,7 +695,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::Google(model) => {
let client = self.client.clone();
let request = request.into_google(model.id().into());
let request = into_google(request, model.id().into());
let llm_api_token = self.llm_api_token.clone();
let future = self.request_limiter.stream(async move {
let response = Self::perform_llm_completion(
@@ -724,7 +738,8 @@ impl LanguageModel for CloudLanguageModel {
match &self.model {
CloudModel::Anthropic(model) => {
let mut request = request.into_anthropic(
let mut request = into_anthropic(
request,
model.tool_model_id().into(),
model.default_temperature(),
model.max_output_tokens(),
@@ -764,7 +779,7 @@ impl LanguageModel for CloudLanguageModel {
}
CloudModel::OpenAi(model) => {
let mut request =
request.into_open_ai(model.id().into(), model.max_output_tokens());
into_open_ai(request, model.id().into(), model.max_output_tokens());
request.tool_choice = Some(open_ai::ToolChoice::Other(
open_ai::ToolDefinition::Function {
function: open_ai::FunctionDefinition {

View File

@@ -89,6 +89,14 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
IconName::Copilot
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = CopilotChatModel::default();
Some(Arc::new(CopilotChatLanguageModel {
model,
request_limiter: RateLimiter::new(4),
}) as Arc<dyn LanguageModel>)
}
fn provided_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {
CopilotChatModel::iter()
.map(|model| {

View File

@@ -163,6 +163,17 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
IconName::AiDeepSeek
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = deepseek::Model::Chat;
Some(Arc::new(DeepSeekLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -311,7 +322,11 @@ impl LanguageModel for DeepSeekLanguageModel {
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
let request = request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
let request = into_deepseek(
request,
self.model.id().to_string(),
self.max_output_tokens(),
);
let stream = self.stream_completion(request, cx);
async move {
@@ -346,8 +361,11 @@ impl LanguageModel for DeepSeekLanguageModel {
schema: serde_json::Value,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let mut deepseek_request =
request.into_deepseek(self.model.id().to_string(), self.max_output_tokens());
let mut deepseek_request = into_deepseek(
request,
self.model.id().to_string(),
self.max_output_tokens(),
);
deepseek_request.tools = vec![deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
@@ -391,6 +409,93 @@ impl LanguageModel for DeepSeekLanguageModel {
}
}
pub fn into_deepseek(
request: LanguageModelRequest,
model: String,
max_output_tokens: Option<u32>,
) -> deepseek::Request {
let is_reasoner = model == "deepseek-reasoner";
let len = request.messages.len();
let merged_messages =
request
.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
if is_reasoner {
if let Some(last_msg) = acc.last_mut() {
match (last_msg, role) {
(deepseek::RequestMessage::User { content: last }, Role::User) => {
last.push(' ');
last.push_str(&content);
return acc;
}
(
deepseek::RequestMessage::Assistant {
content: last_content,
..
},
Role::Assistant,
) => {
*last_content = last_content
.take()
.map(|c| {
let mut s =
String::with_capacity(c.len() + content.len() + 1);
s.push_str(&c);
s.push(' ');
s.push_str(&content);
s
})
.or(Some(content));
return acc;
}
_ => {}
}
}
}
acc.push(match role {
Role::User => deepseek::RequestMessage::User { content },
Role::Assistant => deepseek::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => deepseek::RequestMessage::System { content },
});
acc
});
deepseek::Request {
model,
messages: merged_messages,
stream: true,
max_tokens: max_output_tokens,
temperature: if is_reasoner {
None
} else {
request.temperature
},
response_format: None,
tools: request
.tools
.into_iter()
.map(|tool| deepseek::ToolDefinition::Function {
function: deepseek::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
struct ConfigurationView {
api_key_editor: Entity<Editor>,
state: Entity<State>,

View File

@@ -166,6 +166,17 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
IconName::AiGoogle
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = google_ai::Model::default();
Some(Arc::new(GoogleLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
rate_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -261,7 +272,7 @@ impl LanguageModel for GoogleLanguageModel {
request: LanguageModelRequest,
cx: &App,
) -> BoxFuture<'static, Result<usize>> {
let request = request.into_google(self.model.id().to_string());
let request = into_google(request, self.model.id().to_string());
let http_client = self.http_client.clone();
let api_key = self.state.read(cx).api_key.clone();
@@ -292,7 +303,7 @@ impl LanguageModel for GoogleLanguageModel {
'static,
Result<futures::stream::BoxStream<'static, Result<LanguageModelCompletionEvent>>>,
> {
let request = request.into_google(self.model.id().to_string());
let request = into_google(request, self.model.id().to_string());
let http_client = self.http_client.clone();
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
@@ -330,6 +341,38 @@ impl LanguageModel for GoogleLanguageModel {
}
}
pub fn into_google(
request: LanguageModelRequest,
model: String,
) -> google_ai::GenerateContentRequest {
google_ai::GenerateContentRequest {
model,
contents: request
.messages
.into_iter()
.map(|msg| google_ai::Content {
parts: vec![google_ai::Part::TextPart(google_ai::TextPart {
text: msg.string_contents(),
})],
role: match msg.role {
Role::User => google_ai::Role::User,
Role::Assistant => google_ai::Role::Model,
Role::System => google_ai::Role::User, // Google AI doesn't have a system role
},
})
.collect(),
generation_config: Some(google_ai::GenerationConfig {
candidate_count: Some(1),
stop_sequences: Some(request.stop),
max_output_tokens: None,
temperature: request.temperature.map(|t| t as f64).or(Some(1.0)),
top_p: None,
top_k: None,
}),
safety_settings: None,
}
}
pub fn count_google_tokens(
request: LanguageModelRequest,
cx: &App,

View File

@@ -152,6 +152,10 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
IconName::AiLmStudio
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
self.provided_models(cx).into_iter().next()
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models: BTreeMap<String, lmstudio::Model> = BTreeMap::default();

View File

@@ -167,6 +167,17 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
IconName::AiMistral
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = mistral::Model::default();
Some(Arc::new(MistralLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -323,7 +334,11 @@ impl LanguageModel for MistralLanguageModel {
request: LanguageModelRequest,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
let request = request.into_mistral(self.model.id().to_string(), self.max_output_tokens());
let request = into_mistral(
request,
self.model.id().to_string(),
self.max_output_tokens(),
);
let stream = self.stream_completion(request, cx);
async move {
@@ -358,7 +373,7 @@ impl LanguageModel for MistralLanguageModel {
schema: serde_json::Value,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let mut request = request.into_mistral(self.model.id().into(), self.max_output_tokens());
let mut request = into_mistral(request, self.model.id().into(), self.max_output_tokens());
request.tools = vec![mistral::ToolDefinition::Function {
function: mistral::FunctionDefinition {
name: tool_name.clone(),
@@ -400,6 +415,52 @@ impl LanguageModel for MistralLanguageModel {
}
}
pub fn into_mistral(
request: LanguageModelRequest,
model: String,
max_output_tokens: Option<u32>,
) -> mistral::Request {
let len = request.messages.len();
let merged_messages =
request
.messages
.into_iter()
.fold(Vec::with_capacity(len), |mut acc, msg| {
let role = msg.role;
let content = msg.string_contents();
acc.push(match role {
Role::User => mistral::RequestMessage::User { content },
Role::Assistant => mistral::RequestMessage::Assistant {
content: Some(content),
tool_calls: Vec::new(),
},
Role::System => mistral::RequestMessage::System { content },
});
acc
});
mistral::Request {
model,
messages: merged_messages,
stream: true,
max_tokens: max_output_tokens,
temperature: request.temperature,
response_format: None,
tools: request
.tools
.into_iter()
.map(|tool| mistral::ToolDefinition::Function {
function: mistral::FunctionDefinition {
name: tool.name,
description: Some(tool.description),
parameters: Some(tool.input_schema),
},
})
.collect(),
}
}
struct ConfigurationView {
api_key_editor: Entity<Editor>,
state: gpui::Entity<State>,

View File

@@ -157,6 +157,10 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
IconName::AiOllama
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
self.provided_models(cx).into_iter().next()
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models: BTreeMap<String, ollama::Model> = BTreeMap::default();

View File

@@ -169,6 +169,17 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
IconName::AiOpenAi
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
let model = open_ai::Model::default();
Some(Arc::new(OpenAiLanguageModel {
id: LanguageModelId::from(model.id().to_string()),
model,
state: self.state.clone(),
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
let mut models = BTreeMap::default();
@@ -307,7 +318,7 @@ impl LanguageModel for OpenAiLanguageModel {
'static,
Result<futures::stream::BoxStream<'static, Result<LanguageModelCompletionEvent>>>,
> {
let request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
let request = into_open_ai(request, self.model.id().into(), self.max_output_tokens());
let completions = self.stream_completion(request, cx);
async move {
Ok(open_ai::extract_text_from_events(completions.await?)
@@ -325,7 +336,7 @@ impl LanguageModel for OpenAiLanguageModel {
schema: serde_json::Value,
cx: &AsyncApp,
) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<String>>>> {
let mut request = request.into_open_ai(self.model.id().into(), self.max_output_tokens());
let mut request = into_open_ai(request, self.model.id().into(), self.max_output_tokens());
request.tool_choice = Some(ToolChoice::Other(ToolDefinition::Function {
function: FunctionDefinition {
name: tool_name.clone(),
@@ -355,6 +366,39 @@ impl LanguageModel for OpenAiLanguageModel {
}
}
pub fn into_open_ai(
request: LanguageModelRequest,
model: String,
max_output_tokens: Option<u32>,
) -> open_ai::Request {
let stream = !model.starts_with("o1-");
open_ai::Request {
model,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => open_ai::RequestMessage::User {
content: msg.string_contents(),
},
Role::Assistant => open_ai::RequestMessage::Assistant {
content: Some(msg.string_contents()),
tool_calls: Vec::new(),
},
Role::System => open_ai::RequestMessage::System {
content: msg.string_contents(),
},
})
.collect(),
stream,
stop: request.stop,
temperature: request.temperature.unwrap_or(1.0),
max_tokens: max_output_tokens,
tools: Vec::new(),
tool_choice: None,
}
}
pub fn count_open_ai_tokens(
request: LanguageModelRequest,
model: open_ai::Model,

View File

@@ -15,5 +15,5 @@ brackets = [
]
auto_indent_using_last_non_empty_line = false
increase_indent_pattern = ":\\s*$"
increase_indent_pattern = "^[^#].*:\\s*$"
decrease_indent_pattern = "^\\s*(else|elif|except|finally)\\b.*:"

View File

@@ -526,17 +526,25 @@ impl ContextProvider for RustContextProvider {
cx: &App,
) -> Option<TaskTemplates> {
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR";
let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx);
let package_to_run = language_sets
.tasks
.variables
.get(DEFAULT_RUN_NAME_STR)
.cloned();
let custom_target_dir = language_sets
.tasks
.variables
.get(CUSTOM_TARGET_DIR)
.cloned();
let run_task_args = if let Some(package_to_run) = package_to_run {
vec!["run".into(), "-p".into(), package_to_run]
} else {
vec!["run".into()]
};
Some(TaskTemplates(vec![
let mut task_templates = vec![
TaskTemplate {
label: format!(
"Check (package: {})",
@@ -661,7 +669,25 @@ impl ContextProvider for RustContextProvider {
cwd: Some("$ZED_DIRNAME".to_owned()),
..TaskTemplate::default()
},
]))
];
if let Some(custom_target_dir) = custom_target_dir {
task_templates = task_templates
.into_iter()
.map(|mut task_template| {
let mut args = task_template.args.split_off(1);
task_template.args.append(&mut vec![
"--target-dir".to_string(),
custom_target_dir.clone(),
]);
task_template.args.append(&mut args);
task_template
})
.collect();
}
Some(TaskTemplates(task_templates))
}
}

View File

@@ -1448,7 +1448,7 @@ impl MultiBuffer {
excerpt.range.context.start,
))
}
/// Sets excerpts, returns `true` if at least one new excerpt was added.
pub fn set_excerpts_for_path(
&mut self,
path: PathKey,
@@ -1456,7 +1456,7 @@ impl MultiBuffer {
ranges: Vec<Range<Point>>,
context_line_count: u32,
cx: &mut Context<Self>,
) {
) -> bool {
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let mut insert_after = self
@@ -1475,6 +1475,7 @@ impl MultiBuffer {
let mut new_excerpt_ids = Vec::new();
let mut to_remove = Vec::new();
let mut to_insert = Vec::new();
let mut added_a_new_excerpt = false;
let snapshot = self.snapshot(cx);
let mut excerpts_cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
@@ -1489,6 +1490,7 @@ impl MultiBuffer {
continue;
}
(Some(_), None) => {
added_a_new_excerpt = true;
to_insert.push(new_iter.next().unwrap());
continue;
}
@@ -1552,6 +1554,8 @@ impl MultiBuffer {
} else {
self.buffers_by_path.insert(path, new_excerpt_ids);
}
added_a_new_excerpt
}
pub fn paths(&self) -> impl Iterator<Item = PathKey> + '_ {
@@ -4964,6 +4968,23 @@ impl MultiBufferSnapshot {
}
}
pub fn excerpts_at<T>(&self, position: T) -> impl Iterator<Item = MultiBufferExcerpt<'_>>
where
T: ToOffset,
{
todo!()
}
pub fn reversed_excerpts_at<T>(
&self,
position: T,
) -> impl Iterator<Item = MultiBufferExcerpt<'_>>
where
T: ToOffset,
{
todo!()
}
pub fn excerpt_before(&self, id: ExcerptId) -> Option<MultiBufferExcerpt<'_>> {
let start_locator = self.excerpt_locator_for_id(id);
let mut excerpts = self

View File

@@ -15,7 +15,7 @@ use language::{Outline, OutlineItem};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::Settings;
use theme::{color_alpha, ActiveTheme, ThemeSettings};
use theme::{ActiveTheme, ThemeSettings};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{DismissDecision, ModalView};
@@ -332,7 +332,7 @@ pub fn render_item<T>(
cx: &App,
) -> StyledText {
let highlight_style = HighlightStyle {
background_color: Some(color_alpha(cx.theme().colors().text_accent, 0.3)),
background_color: Some(cx.theme().colors().text_accent.alpha(0.3)),
..Default::default()
};
let custom_highlights = match_ranges

View File

@@ -49,6 +49,7 @@ pub fn panel_button(label: impl Into<SharedString>) -> ui::Button {
let id = ElementId::Name(label.clone().to_lowercase().replace(' ', "_").into());
ui::Button::new(id, label)
.label_size(ui::LabelSize::Small)
.icon_size(ui::IconSize::Small)
// TODO: Change this once we use on_surface_bg in button_like
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)

View File

@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
use client::ProjectId;
use futures::channel::{mpsc, oneshot};
use futures::StreamExt as _;
use git::repository::{Branch, CommitDetails, ResetMode};
use git::repository::{Branch, CommitDetails, PushOptions, Remote, ResetMode};
use git::{
repository::{GitRepository, RepoPath},
status::{GitSummary, TrackedSummary},
@@ -74,6 +74,18 @@ pub enum Message {
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
Push {
repo: GitRepo,
branch_name: SharedString,
remote_name: SharedString,
options: Option<PushOptions>,
},
Pull {
repo: GitRepo,
branch_name: SharedString,
remote_name: SharedString,
},
Fetch(GitRepo),
}
pub enum GitEvent {
@@ -107,6 +119,10 @@ impl GitStore {
}
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_get_remotes);
client.add_entity_request_handler(Self::handle_push);
client.add_entity_request_handler(Self::handle_pull);
client.add_entity_request_handler(Self::handle_fetch);
client.add_entity_request_handler(Self::handle_stage);
client.add_entity_request_handler(Self::handle_unstage);
client.add_entity_request_handler(Self::handle_commit);
@@ -242,8 +258,10 @@ impl GitStore {
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, respond)) = update_receiver.next().await {
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
respond.send(result).ok();
if !respond.is_canceled() {
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
respond.send(result).ok();
}
}
})
.detach();
@@ -252,6 +270,94 @@ impl GitStore {
async fn process_git_msg(msg: Message) -> Result<()> {
match msg {
Message::Fetch(repo) => {
match repo {
GitRepo::Local(git_repository) => git_repository.fetch()?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Fetch {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
})
.await
.context("sending fetch request")?;
}
}
Ok(())
}
Message::Pull {
repo,
branch_name,
remote_name,
} => {
match repo {
GitRepo::Local(git_repository) => {
git_repository.pull(&branch_name, &remote_name)?
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Pull {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
branch_name: branch_name.to_string(),
remote_name: remote_name.to_string(),
})
.await
.context("sending pull request")?;
}
}
Ok(())
}
Message::Push {
repo,
branch_name,
remote_name,
options,
} => {
match repo {
GitRepo::Local(git_repository) => {
git_repository.push(&branch_name, &remote_name, options)?
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Push {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
branch_name: branch_name.to_string(),
remote_name: remote_name.to_string(),
options: options.map(|options| match options {
PushOptions::Force => proto::push::PushOptions::Force,
PushOptions::SetUpstream => {
proto::push::PushOptions::SetUpstream
}
}
as i32),
})
.await
.context("sending push request")?;
}
}
Ok(())
}
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
@@ -413,6 +519,73 @@ impl GitStore {
}
}
async fn handle_fetch(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Fetch>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
repository_handle
.update(&mut cx, |repository_handle, _cx| repository_handle.fetch())?
.await??;
Ok(proto::Ack {})
}
async fn handle_push(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Push>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let options = envelope
.payload
.options
.as_ref()
.map(|_| match envelope.payload.options() {
proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
proto::push::PushOptions::Force => git::repository::PushOptions::Force,
});
let branch_name = envelope.payload.branch_name.into();
let remote_name = envelope.payload.remote_name.into();
repository_handle
.update(&mut cx, |repository_handle, _cx| {
repository_handle.push(branch_name, remote_name, options)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_pull(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Pull>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let branch_name = envelope.payload.branch_name.into();
let remote_name = envelope.payload.remote_name.into();
repository_handle
.update(&mut cx, |repository_handle, _cx| {
repository_handle.pull(branch_name, remote_name)
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_stage(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Stage>,
@@ -509,6 +682,34 @@ impl GitStore {
Ok(proto::Ack {})
}
async fn handle_get_remotes(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GetRemotes>,
mut cx: AsyncApp,
) -> Result<proto::GetRemotesResponse> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;
let branch_name = envelope.payload.branch_name;
let remotes = repository_handle
.update(&mut cx, |repository_handle, cx| {
repository_handle.get_remotes(branch_name, cx)
})?
.await?;
Ok(proto::GetRemotesResponse {
remotes: remotes
.into_iter()
.map(|remotes| proto::get_remotes_response::Remote {
name: remotes.name.to_string(),
})
.collect::<Vec<_>>(),
})
}
async fn handle_show(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitShow>,
@@ -648,7 +849,7 @@ impl Repository {
(self.worktree_id, self.repository_entry.work_directory_id())
}
pub fn branch(&self) -> Option<&Branch> {
pub fn current_branch(&self) -> Option<&Branch> {
self.repository_entry.branch()
}
@@ -802,35 +1003,19 @@ impl Repository {
commit: &str,
paths: Vec<RepoPath>,
) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
self.update_sender
.unbounded_send((
Message::CheckoutFiles {
repo: self.git_repo.clone(),
commit,
paths,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::CheckoutFiles {
repo: self.git_repo.clone(),
commit: commit.to_string().into(),
paths,
})
}
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
self.update_sender
.unbounded_send((
Message::Reset {
repo: self.git_repo.clone(),
commit,
reset_mode,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::Reset {
repo: self.git_repo.clone(),
commit: commit.to_string().into(),
reset_mode,
})
}
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
@@ -987,18 +1172,41 @@ impl Repository {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
Message::Commit {
git_repo: self.git_repo.clone(),
message,
name_and_email,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::Commit {
git_repo: self.git_repo.clone(),
message,
name_and_email,
})
}
pub fn fetch(&self) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::Fetch(self.git_repo.clone()))
}
pub fn push(
&self,
branch: SharedString,
remote: SharedString,
options: Option<PushOptions>,
) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::Push {
repo: self.git_repo.clone(),
branch_name: branch,
remote_name: remote,
options,
})
}
pub fn pull(
&self,
branch: SharedString,
remote: SharedString,
) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::Pull {
repo: self.git_repo.clone(),
branch_name: branch,
remote_name: remote,
})
}
pub fn set_index_text(
@@ -1006,13 +1214,49 @@ impl Repository {
path: &RepoPath,
content: Option<String>,
) -> oneshot::Receiver<anyhow::Result<()>> {
self.send_message(Message::SetIndexText(
self.git_repo.clone(),
path.clone(),
content,
))
}
pub fn get_remotes(&self, branch_name: Option<String>, cx: &App) -> Task<Result<Vec<Remote>>> {
match self.git_repo.clone() {
GitRepo::Local(git_repository) => {
cx.background_spawn(
async move { git_repository.get_remotes(branch_name.as_deref()) },
)
}
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => cx.background_spawn(async move {
let response = client
.request(proto::GetRemotes {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
branch_name,
})
.await?;
Ok(response
.remotes
.into_iter()
.map(|remotes| git::repository::Remote {
name: remotes.name.into(),
})
.collect())
}),
}
}
fn send_message(&self, message: Message) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
result_tx,
))
.ok();
self.update_sender.unbounded_send((message, result_tx)).ok();
result_rx
}
}

View File

@@ -946,12 +946,17 @@ impl WorktreeStore {
upstream: proto_branch.upstream.map(|upstream| {
git::repository::Upstream {
ref_name: upstream.ref_name.into(),
tracking: upstream.tracking.map(|tracking| {
git::repository::UpstreamTracking {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
}
}),
tracking: upstream
.tracking
.map(|tracking| {
git::repository::UpstreamTracking::Tracked(
git::repository::UpstreamTrackingStatus {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
},
)
})
.unwrap_or(git::repository::UpstreamTracking::Gone),
}
}),
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {

View File

@@ -265,7 +265,7 @@ struct ItemColors {
default: Hsla,
hover: Hsla,
drag_over: Hsla,
marked_active: Hsla,
marked: Hsla,
focused: Hsla,
}
@@ -274,10 +274,10 @@ fn get_item_color(cx: &App) -> ItemColors {
ItemColors {
default: colors.panel_background,
hover: colors.ghost_element_hover,
drag_over: colors.drop_target_background,
marked_active: colors.element_selected,
hover: colors.element_hover,
marked: colors.element_selected,
focused: colors.panel_focused_border,
drag_over: colors.drop_target_background,
}
}
@@ -302,6 +302,9 @@ impl ProjectPanel {
this.reveal_entry(project.clone(), *entry_id, true, cx);
}
}
project::Event::ActiveEntryChanged(None) => {
this.marked_entries.clear();
}
project::Event::RevealInProjectPanel(entry_id) => {
this.reveal_entry(project.clone(), *entry_id, false, cx);
cx.emit(PanelEvent::Activate);
@@ -3546,18 +3549,16 @@ impl ProjectPanel {
marked_selections: selections,
};
let bg_color = if is_marked || is_active {
item_colors.marked_active
let bg_color = if is_marked {
item_colors.marked
} else {
item_colors.default
};
let bg_hover_color = if self.mouse_down || is_marked || is_active {
item_colors.marked_active
} else if !is_active {
item_colors.hover
let bg_hover_color = if is_marked {
item_colors.marked
} else {
item_colors.default
item_colors.hover
};
let border_color =
@@ -4235,16 +4236,11 @@ impl ProjectPanel {
let worktree_id = worktree.id();
self.expand_entry(worktree_id, entry_id, cx);
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
if self.marked_entries.len() == 1
&& self
.marked_entries
.first()
.filter(|entry| entry.entry_id == entry_id)
.is_none()
{
self.marked_entries.clear();
}
self.marked_entries.clear();
self.marked_entries.insert(SelectedEntry {
worktree_id,
entry_id,
});
self.autoscroll(cx);
cx.notify();
}
@@ -7333,7 +7329,7 @@ mod tests {
select_path(&panel, "root/new", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&["v root", " new <== selected"]
&["v root", " new <== selected <== marked"]
);
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
panel.update_in(cx, |panel, window, cx| {
@@ -7767,7 +7763,7 @@ mod tests {
" > .git",
" v dir_1",
" > gitignored_dir",
" file_1.py <== selected",
" file_1.py <== selected <== marked",
" file_2.py",
" file_3.py",
" > dir_2",
@@ -7793,7 +7789,7 @@ mod tests {
" file_2.py",
" file_3.py",
" v dir_2",
" file_1.py <== selected",
" file_1.py <== selected <== marked",
" file_2.py",
" file_3.py",
" .gitignore",
@@ -7820,7 +7816,7 @@ mod tests {
" file_2.py",
" file_3.py",
" v dir_2",
" file_1.py <== selected",
" file_1.py <== selected <== marked",
" file_2.py",
" file_3.py",
" .gitignore",
@@ -7841,7 +7837,7 @@ mod tests {
" > .git",
" v dir_1",
" v gitignored_dir",
" file_a.py <== selected",
" file_a.py <== selected <== marked",
" file_b.py",
" file_c.py",
" file_1.py",
@@ -7996,7 +7992,7 @@ mod tests {
" > .git",
" v dir_1",
" > gitignored_dir",
" file_1.py <== selected",
" file_1.py <== selected <== marked",
" file_2.py",
" file_3.py",
" > dir_2",
@@ -8022,7 +8018,7 @@ mod tests {
" file_2.py",
" file_3.py",
" v dir_2",
" file_1.py <== selected",
" file_1.py <== selected <== marked",
" file_2.py",
" file_3.py",
" .gitignore",
@@ -8043,7 +8039,7 @@ mod tests {
" > .git",
" v dir_1",
" v gitignored_dir",
" file_a.py <== selected",
" file_a.py <== selected <== marked",
" file_b.py",
" file_c.py",
" file_1.py",

View File

@@ -321,7 +321,13 @@ message Envelope {
GitCommitDetails git_commit_details = 302;
SetIndexText set_index_text = 299;
GitCheckoutFiles git_checkout_files = 303; // current max
GitCheckoutFiles git_checkout_files = 303;
Push push = 304;
Fetch fetch = 305;
GetRemotes get_remotes = 306;
GetRemotesResponse get_remotes_response = 307;
Pull pull = 308; // current max
}
reserved 87 to 88;
@@ -2772,3 +2778,46 @@ message OpenCommitMessageBuffer {
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
}
message Push {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
string remote_name = 4;
string branch_name = 5;
optional PushOptions options = 6;
enum PushOptions {
SET_UPSTREAM = 0;
FORCE = 1;
}
}
message Fetch {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
}
message GetRemotes {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
optional string branch_name = 4;
}
message GetRemotesResponse {
repeated Remote remotes = 1;
message Remote {
string name = 1;
}
}
message Pull {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
string remote_name = 4;
string branch_name = 5;
}

View File

@@ -445,6 +445,11 @@ messages!(
(GitShow, Background),
(GitCommitDetails, Background),
(SetIndexText, Background),
(Push, Background),
(Fetch, Background),
(GetRemotes, Background),
(GetRemotesResponse, Background),
(Pull, Background),
);
request_messages!(
@@ -582,6 +587,10 @@ request_messages!(
(GitReset, Ack),
(GitCheckoutFiles, Ack),
(SetIndexText, Ack),
(Push, Ack),
(Fetch, Ack),
(GetRemotes, GetRemotesResponse),
(Pull, Ack),
);
entity_messages!(
@@ -678,6 +687,10 @@ entity_messages!(
GitReset,
GitCheckoutFiles,
SetIndexText,
Push,
Fetch,
GetRemotes,
Pull,
);
entity_messages!(

View File

@@ -59,6 +59,7 @@ const DEFAULT_NUM_COLUMNS: usize = 128;
pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
let settings = ThemeSettings::get_global(cx).clone();
let font_size = settings.buffer_font_size(cx).into();
let font_family = settings.buffer_font.family;
let font_features = settings.buffer_font.features;
let font_weight = settings.buffer_font.weight;
@@ -71,7 +72,7 @@ pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle {
font_features,
font_weight,
font_fallbacks,
font_size: theme::get_buffer_font_size(cx).into(),
font_size,
font_style: FontStyle::Normal,
line_height: window.line_height().into(),
background_color: Some(theme.colors().terminal_ansi_background),

View File

@@ -95,16 +95,12 @@ pub struct ThemeSettings {
/// as well as the size of a [gpui::Rems] unit.
///
/// Changing this will impact the size of all UI elements.
///
/// Use [ThemeSettings::ui_font_size] to access this.
ui_font_size: Pixels,
/// The font used for UI elements.
pub ui_font: Font,
/// The font size used for buffers, and the terminal.
///
/// The terminal font size can be overridden using it's own setting.
///
/// Use [ThemeSettings::buffer_font_size] to access this.
buffer_font_size: Pixels,
/// The font used for buffers, and the terminal.
///
@@ -246,14 +242,14 @@ impl SystemAppearance {
}
#[derive(Default)]
pub(crate) struct AdjustedBufferFontSize(Pixels);
struct BufferFontSize(Pixels);
impl Global for AdjustedBufferFontSize {}
impl Global for BufferFontSize {}
#[derive(Default)]
pub(crate) struct AdjustedUiFontSize(Pixels);
pub(crate) struct UiFontSize(Pixels);
impl Global for AdjustedUiFontSize {}
impl Global for UiFontSize {}
/// Represents the selection of a theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -568,16 +564,18 @@ impl ThemeSettings {
/// Returns the buffer font size.
pub fn buffer_font_size(&self, cx: &App) -> Pixels {
let font_size = cx
.try_global::<AdjustedBufferFontSize>()
.map_or(self.buffer_font_size, |size| size.0);
.try_global::<BufferFontSize>()
.map(|size| size.0)
.unwrap_or(self.buffer_font_size);
clamp_font_size(font_size)
}
/// Returns the UI font size.
pub fn ui_font_size(&self, cx: &App) -> Pixels {
let font_size = cx
.try_global::<AdjustedUiFontSize>()
.map_or(self.ui_font_size, |size| size.0);
.try_global::<UiFontSize>()
.map(|size| size.0)
.unwrap_or(self.ui_font_size);
clamp_font_size(font_size)
}
@@ -663,51 +661,38 @@ pub fn observe_buffer_font_size_adjustment<V: 'static>(
cx: &mut Context<V>,
f: impl 'static + Fn(&mut V, &mut Context<V>),
) -> Subscription {
cx.observe_global::<AdjustedBufferFontSize>(f)
cx.observe_global::<BufferFontSize>(f)
}
/// Sets the adjusted buffer font size.
/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings.
pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels {
let adjusted_font_size = if let Some(AdjustedBufferFontSize(adjusted_size)) =
cx.try_global::<AdjustedBufferFontSize>()
{
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
let delta = *adjusted_size - buffer_font_size;
size + delta
} else {
size
};
let adjusted_font_size =
if let Some(BufferFontSize(adjusted_size)) = cx.try_global::<BufferFontSize>() {
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
let delta = *adjusted_size - buffer_font_size;
size + delta
} else {
size
};
clamp_font_size(adjusted_font_size)
}
/// Returns the adjusted buffer font size.
pub fn get_buffer_font_size(cx: &App) -> Pixels {
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
cx.try_global::<AdjustedBufferFontSize>()
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0)
}
/// Adjusts the buffer font size.
pub fn adjust_buffer_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
let mut adjusted_size = cx
.try_global::<AdjustedBufferFontSize>()
.try_global::<BufferFontSize>()
.map_or(buffer_font_size, |adjusted_size| adjusted_size.0);
f(&mut adjusted_size);
cx.set_global(AdjustedBufferFontSize(clamp_font_size(adjusted_size)));
cx.set_global(BufferFontSize(clamp_font_size(adjusted_size)));
cx.refresh_windows();
}
/// Returns whether the buffer font size has been adjusted.
pub fn has_adjusted_buffer_font_size(cx: &App) -> bool {
cx.has_global::<AdjustedBufferFontSize>()
}
/// Resets the buffer font size to the default value.
pub fn reset_buffer_font_size(cx: &mut App) {
if cx.has_global::<AdjustedBufferFontSize>() {
cx.remove_global::<AdjustedBufferFontSize>();
if cx.has_global::<BufferFontSize>() {
cx.remove_global::<BufferFontSize>();
cx.refresh_windows();
}
}
@@ -718,41 +703,29 @@ pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font {
let (ui_font, ui_font_size) = {
let theme_settings = ThemeSettings::get_global(cx);
let font = theme_settings.ui_font.clone();
(font, get_ui_font_size(cx))
(font, theme_settings.ui_font_size(cx))
};
window.set_rem_size(ui_font_size);
ui_font
}
/// Gets the adjusted UI font size.
pub fn get_ui_font_size(cx: &App) -> Pixels {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
cx.try_global::<AdjustedUiFontSize>()
.map_or(ui_font_size, |adjusted_size| adjusted_size.0)
}
/// Sets the adjusted UI font size.
pub fn adjust_ui_font_size(cx: &mut App, mut f: impl FnMut(&mut Pixels)) {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let mut adjusted_size = cx
.try_global::<AdjustedUiFontSize>()
.try_global::<UiFontSize>()
.map_or(ui_font_size, |adjusted_size| adjusted_size.0);
f(&mut adjusted_size);
cx.set_global(AdjustedUiFontSize(clamp_font_size(adjusted_size)));
cx.set_global(UiFontSize(clamp_font_size(adjusted_size)));
cx.refresh_windows();
}
/// Returns whether the UI font size has been adjusted.
pub fn has_adjusted_ui_font_size(cx: &App) -> bool {
cx.has_global::<AdjustedUiFontSize>()
}
/// Resets the UI font size to the default value.
pub fn reset_ui_font_size(cx: &mut App) {
if cx.has_global::<AdjustedUiFontSize>() {
cx.remove_global::<AdjustedUiFontSize>();
if cx.has_global::<UiFontSize>() {
cx.remove_global::<UiFontSize>();
cx.refresh_windows();
}
}

View File

@@ -23,7 +23,6 @@ use std::path::Path;
use std::sync::Arc;
use ::settings::Settings;
use ::settings::SettingsStore;
use anyhow::Result;
use fallback_themes::apply_status_color_defaults;
use fs::Fs;
@@ -102,16 +101,6 @@ pub fn init(themes_to_load: LoadThemes, cx: &mut App) {
ThemeSettings::register(cx);
FontFamilyCache::init_global(cx);
let mut prev_buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
cx.observe_global::<SettingsStore>(move |cx| {
let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
if buffer_font_size != prev_buffer_font_size {
prev_buffer_font_size = buffer_font_size;
reset_buffer_font_size(cx);
}
})
.detach();
}
/// Implementing this trait allows accessing the active theme.
@@ -341,14 +330,6 @@ impl Theme {
}
}
/// Compounds a color with an alpha value.
/// TODO: Replace this with a method on Hsla.
pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla {
let mut color = color;
color.a = alpha;
color
}
/// Asynchronously reads the user theme from the specified path.
pub async fn read_user_theme(theme_path: &Path, fs: Arc<dyn Fs>) -> Result<ThemeFamilyContent> {
let reader = fs.open_sync(theme_path).await?;

View File

@@ -1,7 +1,8 @@
use crate::KeyBinding;
use crate::{h_flex, prelude::*};
use crate::{ElevationIndex, KeyBinding};
use gpui::{point, AnyElement, App, BoxShadow, IntoElement, Window};
use gpui::{point, AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window};
use smallvec::smallvec;
use theme::Appearance;
/// Represents a hint for a keybinding, optionally with a prefix and suffix.
///
@@ -23,7 +24,7 @@ pub struct KeybindingHint {
suffix: Option<SharedString>,
keybinding: KeyBinding,
size: Option<Pixels>,
elevation: Option<ElevationIndex>,
background_color: Hsla,
}
impl KeybindingHint {
@@ -37,15 +38,15 @@ impl KeybindingHint {
/// ```
/// use ui::prelude::*;
///
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"));
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
/// ```
pub fn new(keybinding: KeyBinding) -> Self {
pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self {
Self {
prefix: None,
suffix: None,
keybinding,
size: None,
elevation: None,
background_color,
}
}
@@ -59,15 +60,19 @@ impl KeybindingHint {
/// ```
/// use ui::prelude::*;
///
/// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"));
/// let hint = KeybindingHint::with_prefix("Copy:", KeyBinding::from_str("Ctrl+C"), Hsla::new(0.0, 0.0, 0.0, 1.0));
/// ```
pub fn with_prefix(prefix: impl Into<SharedString>, keybinding: KeyBinding) -> Self {
pub fn with_prefix(
prefix: impl Into<SharedString>,
keybinding: KeyBinding,
background_color: Hsla,
) -> Self {
Self {
prefix: Some(prefix.into()),
suffix: None,
keybinding,
size: None,
elevation: None,
background_color,
}
}
@@ -81,15 +86,19 @@ impl KeybindingHint {
/// ```
/// use ui::prelude::*;
///
/// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste");
/// let hint = KeybindingHint::with_suffix(KeyBinding::from_str("Ctrl+V"), "Paste", Hsla::new(0.0, 0.0, 0.0, 1.0));
/// ```
pub fn with_suffix(keybinding: KeyBinding, suffix: impl Into<SharedString>) -> Self {
pub fn with_suffix(
keybinding: KeyBinding,
suffix: impl Into<SharedString>,
background_color: Hsla,
) -> Self {
Self {
prefix: None,
suffix: Some(suffix.into()),
keybinding,
size: None,
elevation: None,
background_color,
}
}
@@ -143,46 +152,37 @@ impl KeybindingHint {
self.size = size.into();
self
}
/// Sets the elevation of the keybinding hint.
///
/// This method allows specifying the elevation index for the keybinding hint,
/// which affects its visual appearance in terms of depth or layering.
///
/// # Examples
///
/// ```
/// use ui::prelude::*;
///
/// let hint = KeybindingHint::new(KeyBinding::from_str("Ctrl+A"))
/// .elevation(ElevationIndex::new(1));
/// ```
pub fn elevation(mut self, elevation: impl Into<Option<ElevationIndex>>) -> Self {
self.elevation = elevation.into();
self
}
}
impl RenderOnce for KeybindingHint {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = cx.theme().colors().clone();
let is_light = cx.theme().appearance() == Appearance::Light;
let border_color =
self.background_color
.blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 }));
let bg_color =
self.background_color
.blend(colors.text.alpha(if is_light { 0.06 } else { 0.12 }));
let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 });
let size = self
.size
.unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size()));
let kb_size = size - px(2.0);
let kb_bg = if let Some(elevation) = self.elevation {
elevation.on_elevation_bg(cx)
} else {
theme::color_alpha(colors.element_background, 0.6)
};
h_flex()
.items_center()
let mut base = h_flex();
base.text_style()
.get_or_insert_with(Default::default)
.font_style = Some(FontStyle::Italic);
base.items_center()
.gap_0p5()
.font_buffer(cx)
.text_size(size)
.text_color(colors.text_muted)
.text_color(colors.text_disabled)
.children(self.prefix)
.child(
h_flex()
@@ -191,10 +191,10 @@ impl RenderOnce for KeybindingHint {
.px_0p5()
.mr_0p5()
.border_1()
.border_color(kb_bg)
.bg(kb_bg.opacity(0.8))
.border_color(border_color)
.bg(bg_color)
.shadow(smallvec![BoxShadow {
color: cx.theme().colors().editor_background.opacity(0.8),
color: shadow_color,
offset: point(px(0.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
@@ -212,6 +212,8 @@ impl ComponentPreview for KeybindingHint {
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
.unwrap_or(KeyBinding::new(enter_fallback, cx));
let bg_color = cx.theme().colors().surface_background;
v_flex()
.gap_6()
.children(vec![
@@ -220,17 +222,17 @@ impl ComponentPreview for KeybindingHint {
vec![
single_example(
"With Prefix",
KeybindingHint::with_prefix("Go to Start:", enter.clone())
KeybindingHint::with_prefix("Go to Start:", enter.clone(), bg_color)
.into_any_element(),
),
single_example(
"With Suffix",
KeybindingHint::with_suffix(enter.clone(), "Go to End")
KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color)
.into_any_element(),
),
single_example(
"With Prefix and Suffix",
KeybindingHint::new(enter.clone())
KeybindingHint::new(enter.clone(), bg_color)
.prefix("Confirm:")
.suffix("Execute selected action")
.into_any_element(),
@@ -242,21 +244,21 @@ impl ComponentPreview for KeybindingHint {
vec![
single_example(
"Small",
KeybindingHint::new(enter.clone())
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(12.0))
.prefix("Small:")
.into_any_element(),
),
single_example(
"Medium",
KeybindingHint::new(enter.clone())
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(16.0))
.suffix("Medium")
.into_any_element(),
),
single_example(
"Large",
KeybindingHint::new(enter.clone())
KeybindingHint::new(enter.clone(), bg_color)
.size(Pixels::from(20.0))
.prefix("Large:")
.suffix("Size")
@@ -264,41 +266,6 @@ impl ComponentPreview for KeybindingHint {
),
],
),
example_group_with_title(
"Elevations",
vec![
single_example(
"Surface",
KeybindingHint::new(enter.clone())
.elevation(ElevationIndex::Surface)
.prefix("Surface:")
.into_any_element(),
),
single_example(
"Elevated Surface",
KeybindingHint::new(enter.clone())
.elevation(ElevationIndex::ElevatedSurface)
.suffix("Elevated")
.into_any_element(),
),
single_example(
"Editor Surface",
KeybindingHint::new(enter.clone())
.elevation(ElevationIndex::EditorSurface)
.prefix("Editor:")
.suffix("Surface")
.into_any_element(),
),
single_example(
"Modal Surface",
KeybindingHint::new(enter.clone())
.elevation(ElevationIndex::ModalSurface)
.prefix("Modal:")
.suffix("Enter")
.into_any_element(),
),
],
),
])
.into_any_element()
}

View File

@@ -2,7 +2,7 @@ use std::fmt::{self, Display, Formatter};
use gpui::{hsla, point, px, App, BoxShadow, Hsla};
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use theme::{ActiveTheme, Appearance};
/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons.
///
@@ -40,19 +40,14 @@ impl Display for ElevationIndex {
impl ElevationIndex {
/// Returns an appropriate shadow for the given elevation index.
pub fn shadow(self) -> SmallVec<[BoxShadow; 2]> {
pub fn shadow(self, cx: &App) -> SmallVec<[BoxShadow; 2]> {
let is_light = cx.theme().appearance() == Appearance::Light;
match self {
ElevationIndex::Surface => smallvec![],
ElevationIndex::EditorSurface => smallvec![],
ElevationIndex::ElevatedSurface => smallvec![BoxShadow {
color: hsla(0., 0., 0., 0.12),
offset: point(px(0.), px(2.)),
blur_radius: px(3.),
spread_radius: px(0.),
}],
ElevationIndex::ModalSurface => smallvec![
ElevationIndex::ElevatedSurface => smallvec![
BoxShadow {
color: hsla(0., 0., 0., 0.12),
offset: point(px(0.), px(2.)),
@@ -60,7 +55,22 @@ impl ElevationIndex {
spread_radius: px(0.),
},
BoxShadow {
color: hsla(0., 0., 0., 0.08),
color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }),
offset: point(px(1.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
}
],
ElevationIndex::ModalSurface => smallvec![
BoxShadow {
color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }),
offset: point(px(0.), px(2.)),
blur_radius: px(3.),
spread_radius: px(0.),
},
BoxShadow {
color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }),
offset: point(px(0.), px(3.)),
blur_radius: px(6.),
spread_radius: px(0.),
@@ -71,6 +81,12 @@ impl ElevationIndex {
blur_radius: px(12.),
spread_radius: px(0.),
},
BoxShadow {
color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }),
offset: point(px(1.), px(1.)),
blur_radius: px(0.),
spread_radius: px(0.),
},
],
_ => smallvec![],

View File

@@ -8,13 +8,13 @@ fn elevated<E: Styled>(this: E, cx: &App, index: ElevationIndex) -> E {
.rounded_lg()
.border_1()
.border_color(cx.theme().colors().border_variant)
.shadow(index.shadow())
.shadow(index.shadow(cx))
}
fn elevated_borderless<E: Styled>(this: E, cx: &mut App, index: ElevationIndex) -> E {
this.bg(cx.theme().colors().elevated_surface_background)
.rounded_lg()
.shadow(index.shadow())
.shadow(index.shadow(cx))
}
/// Extends [`gpui::Styled`] with Zed-specific styling methods.

View File

@@ -155,7 +155,18 @@ impl Vim {
original_start_columns.extend(original_start_column);
}
editor.edit_with_block_indent(edits, original_start_columns, cx);
let cursor_offset = editor.selections.last::<usize>(cx).head();
if editor
.buffer()
.read(cx)
.snapshot(cx)
.settings_at(cursor_offset, cx)
.auto_indent_on_paste
{
editor.edit_with_block_indent(edits, original_start_columns, cx);
} else {
editor.edit(edits, cx);
}
// in line_mode vim will insert the new text on the next (or previous if before) line
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
@@ -278,6 +289,10 @@ mod test {
};
use gpui::ClipboardItem;
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, LanguageSettingsContent},
LanguageName,
};
use settings::SettingsStore;
#[gpui::test]
@@ -614,6 +629,67 @@ mod test {
class A {
a(){}
}
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(
indoc! {"
mod some_module {
ˇfn main() {
}
}
"},
Mode::Normal,
);
// default auto indentation
cx.simulate_keystrokes("y y p");
cx.assert_state(
indoc! {"
mod some_module {
fn main() {
ˇfn main() {
}
}
"},
Mode::Normal,
);
// back to previous state
cx.simulate_keystrokes("u u");
cx.assert_state(
indoc! {"
mod some_module {
ˇfn main() {
}
}
"},
Mode::Normal,
);
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.languages.insert(
LanguageName::new("Rust"),
LanguageSettingsContent {
auto_indent_on_paste: Some(false),
..Default::default()
},
);
});
});
// auto indentation turned off
cx.simulate_keystrokes("y y p");
cx.assert_state(
indoc! {"
mod some_module {
fn main() {
ˇfn main() {
}
}
"},
Mode::Normal,
);

View File

@@ -2265,13 +2265,13 @@ mod test {
(
"c i q",
"This is a \"simple 'qˇuote'\" example.",
"This is a \"ˇ\" example.", // Not supported by tree sitter queries for now
"This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now
Mode::Insert,
),
(
"c a q",
"This is a \"simple 'qˇuote'\" example.",
"This is a ˇ example.", // Not supported by tree sitter queries for now
"This is a ˇ example.", // Not supported by Tree-sitter queries for now
Mode::Insert,
),
(

View File

@@ -20,7 +20,7 @@ use futures::{
};
use fuzzy::CharBag;
use git::{
repository::{Branch, GitRepository, RepoPath},
repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
},
@@ -202,7 +202,7 @@ pub struct RepositoryEntry {
pub(crate) statuses_by_path: SumTree<StatusEntry>,
work_directory_id: ProjectEntryId,
pub work_directory: WorkDirectory,
pub(crate) branch: Option<Branch>,
pub(crate) current_branch: Option<Branch>,
pub current_merge_conflicts: TreeSet<RepoPath>,
}
@@ -216,7 +216,7 @@ impl Deref for RepositoryEntry {
impl RepositoryEntry {
pub fn branch(&self) -> Option<&Branch> {
self.branch.as_ref()
self.current_branch.as_ref()
}
pub fn work_directory_id(&self) -> ProjectEntryId {
@@ -244,8 +244,11 @@ impl RepositoryEntry {
pub fn initial_update(&self) -> proto::RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: self.work_directory_id.to_proto(),
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
branch_summary: self.branch.as_ref().map(branch_to_proto),
branch: self
.current_branch
.as_ref()
.map(|branch| branch.name.to_string()),
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
updated_statuses: self
.statuses_by_path
.iter()
@@ -304,8 +307,11 @@ impl RepositoryEntry {
proto::RepositoryEntry {
work_directory_id: self.work_directory_id.to_proto(),
branch: self.branch.as_ref().map(|branch| branch.name.to_string()),
branch_summary: self.branch.as_ref().map(branch_to_proto),
branch: self
.current_branch
.as_ref()
.map(|branch| branch.name.to_string()),
branch_summary: self.current_branch.as_ref().map(branch_to_proto),
updated_statuses,
removed_statuses,
current_merge_conflicts: self
@@ -329,7 +335,7 @@ pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
ref_name: upstream.ref_name.to_string(),
tracking: upstream
.tracking
.as_ref()
.status()
.map(|upstream| proto::UpstreamTracking {
ahead: upstream.ahead as u64,
behind: upstream.behind as u64,
@@ -355,12 +361,16 @@ pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
.as_ref()
.map(|upstream| git::repository::Upstream {
ref_name: upstream.ref_name.to_string().into(),
tracking: upstream.tracking.as_ref().map(|tracking| {
git::repository::UpstreamTracking {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
}
}),
tracking: upstream
.tracking
.as_ref()
.map(|tracking| {
git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
})
})
.unwrap_or(git::repository::UpstreamTracking::Gone),
}),
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
git::repository::CommitSummary {
@@ -2682,7 +2692,8 @@ impl Snapshot {
self.repositories
.update(&PathKey(work_dir_entry.path.clone()), &(), |repo| {
repo.branch = repository.branch_summary.as_ref().map(proto_to_branch);
repo.current_branch =
repository.branch_summary.as_ref().map(proto_to_branch);
repo.statuses_by_path.edit(edits, &());
repo.current_merge_conflicts = conflicted_paths
});
@@ -2704,7 +2715,7 @@ impl Snapshot {
work_directory: WorkDirectory::InProject {
relative_path: work_dir_entry.path.clone(),
},
branch: repository.branch_summary.as_ref().map(proto_to_branch),
current_branch: repository.branch_summary.as_ref().map(proto_to_branch),
statuses_by_path: statuses,
current_merge_conflicts: conflicted_paths,
},
@@ -3506,7 +3517,7 @@ impl BackgroundScannerState {
RepositoryEntry {
work_directory_id: work_dir_id,
work_directory: work_directory.clone(),
branch: None,
current_branch: None,
statuses_by_path: Default::default(),
current_merge_conflicts: Default::default(),
},
@@ -5472,6 +5483,9 @@ impl BackgroundScanner {
},
&(),
);
if status.is_conflicted() {
repository.current_merge_conflicts.insert(repo_path.clone());
}
if let Some(path) = project_path {
changed_paths.push(path);
@@ -5577,7 +5591,7 @@ fn update_branches(
let mut repository = snapshot
.repository(repository.work_directory.path_key())
.context("Missing repository")?;
repository.branch = branches.into_iter().find(|branch| branch.is_head);
repository.current_branch = branches.into_iter().find(|branch| branch.is_head);
let mut state = state.lock();
state

View File

@@ -563,7 +563,7 @@ fn register_actions(
move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
if action.persist {
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
let ui_font_size = theme::get_ui_font_size(cx) + px(1.0);
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
let _ = settings
.ui_font_size
.insert(theme::clamp_font_size(ui_font_size).0);
@@ -580,7 +580,7 @@ fn register_actions(
move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
if action.persist {
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
let ui_font_size = theme::get_ui_font_size(cx) - px(1.0);
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
let _ = settings
.ui_font_size
.insert(theme::clamp_font_size(ui_font_size).0);
@@ -609,7 +609,8 @@ fn register_actions(
move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
if action.persist {
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
let buffer_font_size = theme::get_buffer_font_size(cx) + px(1.0);
let buffer_font_size =
ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
let _ = settings
.buffer_font_size
.insert(theme::clamp_font_size(buffer_font_size).0);
@@ -626,7 +627,8 @@ fn register_actions(
move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
if action.persist {
update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
let buffer_font_size = theme::get_buffer_font_size(cx) - px(1.0);
let buffer_font_size =
ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
let _ = settings
.buffer_font_size
.insert(theme::clamp_font_size(buffer_font_size).0);

View File

@@ -1,7 +1,14 @@
use regex::Regex;
/// The most common license locations, with US and UK English spelling.
pub const LICENSE_FILES_TO_CHECK: &[&str] = &["LICENSE", "LICENCE", "LICENSE.txt", "LICENCE.txt"];
pub const LICENSE_FILES_TO_CHECK: &[&str] = &[
"LICENSE",
"LICENCE",
"LICENSE.txt",
"LICENCE.txt",
"LICENSE.md",
"LICENCE.md",
];
pub fn is_license_eligible_for_data_collection(license: &str) -> bool {
// TODO: Include more licenses later (namely, Apache)

View File

@@ -3,4 +3,4 @@
AsciiDoc language support in Zed is provided by the community-maintained [AsciiDoc extension](https://github.com/andreicek/zed-asciidoc).
Report issues to: [https://github.com/andreicek/zed-asciidoc/issues](https://github.com/andreicek/zed-asciidoc/issues)
- Tree Sitter: [cathaysia/tree-sitter-asciidoc](https://github.com/cathaysia/tree-sitter-asciidoc)
- Tree-sitter: [cathaysia/tree-sitter-asciidoc](https://github.com/cathaysia/tree-sitter-asciidoc)

View File

@@ -2,7 +2,7 @@
Astro support is available through the [Astro extension](https://github.com/zed-extensions/astro).
- Tree Sitter: [virchau13/tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro)
- Tree-sitter: [virchau13/tree-sitter-astro](https://github.com/virchau13/tree-sitter-astro)
- Language Server: [withastro/language-tools](https://github.com/withastro/language-tools)
<!--

View File

@@ -3,7 +3,7 @@
Bash language support in Zed is provided by the community-maintained [Basher extension](https://github.com/d1y/bash.zed).
Report issues to: [https://github.com/d1y/bash.zed/issues](https://github.com/d1y/bash.zed/issues)
- Tree Sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash)
- Tree-sitter: [tree-sitter/tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash)
- Language Server: [bash-lsp/bash-language-server](https://github.com/bash-lsp/bash-language-server)
## Configuration

View File

@@ -2,7 +2,7 @@
C support is available natively in Zed.
- Tree Sitter: [tree-sitter/tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c)
- Tree-sitter: [tree-sitter/tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c)
- Language Server: [clangd/clangd](https://github.com/clangd/clangd)
## Clangd: Force detect as C

View File

@@ -2,7 +2,7 @@
Clojure support is available through the [Clojure extension](https://github.com/zed-extensions/clojure).
- Tree Sitter: [prcastro/tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure)
- Tree-sitter: [prcastro/tree-sitter-clojure](https://github.com/prcastro/tree-sitter-clojure)
- Language Server: [clojure-lsp/clojure-lsp](https://github.com/clojure-lsp/clojure-lsp)
<!--

View File

@@ -2,7 +2,7 @@
C++ support is available natively in Zed.
- Tree Sitter: [tree-sitter/tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp)
- Tree-sitter: [tree-sitter/tree-sitter-cpp](https://github.com/tree-sitter/tree-sitter-cpp)
- Language Server: [clangd/clangd](https://github.com/clangd/clangd)
## Binary

View File

@@ -4,7 +4,7 @@ Note language name is "CSharp" for settings not "C#'
C# support is available through the [C# extension](https://github.com/zed-industries/zed/tree/main/extensions/csharp).
- Tree Sitter: [tree-sitter/tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp)
- Tree-sitter: [tree-sitter/tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp)
- Language Server: [OmniSharp/omnisharp-roslyn](https://github.com/OmniSharp/omnisharp-roslyn)
## Configuration

View File

@@ -2,7 +2,7 @@
CSS support is available natively in Zed.
- Tree Sitter: [tree-sitter/tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css)
- Tree-sitter: [tree-sitter/tree-sitter-css](https://github.com/tree-sitter/tree-sitter-css)
- Language Servers:
- [microsoft/vscode-html-languageservice](https://github.com/microsoft/vscode-html-languageservice)
- [tailwindcss-language-server](https://github.com/tailwindlabs/tailwindcss-intellisense)

View File

@@ -2,7 +2,7 @@
Dart support is available through the [Dart extension](https://github.com/zed-extensions/dart).
- Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
- Tree-sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
- Language Server: [dart language-server](https://github.com/dart-lang/sdk)
## Pre-requisites

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