Compare commits

..

46 Commits

Author SHA1 Message Date
Antonio Scandurra
414ea980f9 Subtract FREE_TIER_MONTHLY_SPENDING_LIMIT from reported monthly spend 2024-10-17 12:36:28 +02:00
Antonio Scandurra
7b5f236811 Introduce a new /billing/monthly_spend API 2024-10-17 11:55:41 +02:00
Antonio Scandurra
498ecd6404 Fetch more than one page when polling stripe events (#19343)
This fixes a bug that was causing most users to be unable to use the
LLMs via Zed. It was caused by not using pagination and, instead, always
querying the very first page of stripe events.

Note that we're also allowing processing events generated in the last 24
hours (before, this was only 1 hour). I did this so that we can process
the backlog of events that the aforementioned bug was skipping.

Release Notes:

- N/A
2024-10-17 09:47:25 +02:00
Thorsten Ball
3216de7eb5 ssh remoting: Do not print error backtrace on non-zero exit (#19290)
Closes #ISSUE


Release Notes:

- N/A
2024-10-17 09:41:16 +02:00
renovate[bot]
57369b5a54 Update Rust crate tree-sitter-elixir to v0.3.1 (#19335) 2024-10-17 08:44:51 +03:00
Heavysnowjakarta
f9d4272e13 docs: Java extension settings (#19113)
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-10-17 00:04:59 -04:00
Conrad Irwin
378a2cf9d8 Allow passing args to ssh (#19336)
This is useful for passing a custom identity file, jump hosts, etc.

Unlike with the v1 feature, we won't support `gh`/`gcloud` ssh wrappers
(yet?). I think the right way of supporting those would be to let
extensions provide remote projects.

Closes #19118

Release Notes:

- SSH remoting: restored ability to set arguments for SSH
2024-10-16 21:09:31 -06:00
Conrad Irwin
f1d01d59ac Simplify PR template (#19337)
Release Notes:

- N/A
2024-10-16 20:22:08 -06:00
Danilo Leal
78093b8e76 ssh: Clean up title bar indicator icon (#19328)
This PR cleans up the custom icon with indicator implementation in favor
of `IconWithIndicator`, which we already had. It seems like it isn't
super used still, but it's good to try to enforce some consistency
either way. I checked my changes against the REPL stuff (one instance
where its used) and everything's looking good so far. As far as SSH,
nothing has visually changed; we just have less code for this thing now.

<img width="800" alt="Screenshot 2024-10-17 at 2 15 47 AM"
src="https://github.com/user-attachments/assets/5c146757-501e-4242-b145-a576a8f289b5">

---

Release Notes:

- N/A
2024-10-16 22:25:27 -03:00
Danilo Leal
a41e973782 ssh: Remove server count from modal header (#19329)
The server count was something that existed since the remote development
implementation and we just kept it there without a lot of critical
thinking. However, it doesn't feel like it's particularly useful yet,
which means that, at least for now, we could clean it up more and wait
for further feedback to add it back, if ever requested.

Release Notes:

- N/A
2024-10-16 22:25:15 -03:00
Danilo Leal
9a3d8733ce ssh: Use system prompt for the server removal action (#19332)
This PR replaces a toast for the system prompt to confirm the action of
removing a server from the remote list. The alert dialog component is
the right choice here as we want to have a modal action that forces
choice. This should make it easier to convert to a nativa alert dialog
in the future, as well as for other platforms.

<img width="800" alt="Screenshot 2024-10-17 at 3 01 41 AM"
src="https://github.com/user-attachments/assets/7bb1210a-54bf-40da-a85a-f269484825a1">

Release Notes:

- N/A
2024-10-16 22:25:03 -03:00
Conrad Irwin
c888101e4b SSH remoting: Don't panic when opening root, open ~ instead (#19322)
Release Notes:

- Fixed a panic when doing `zed ssh://server/`
2024-10-16 17:17:20 -06:00
Conrad Irwin
0c04fb9862 SSH remoting: better error message for projects (#19320)
Before this, if no project paths were opened you were in a wierd UI
state where
most things didn't work because the project was ssh, but no
files/folders were open.

Release Notes:

- Fixed error handling when no project paths could be opened
2024-10-16 17:16:56 -06:00
Marshall Bowers
f6fad3b09e collab: Remove lifetime spending limit in favor of LLM usage billing (#19321)
This PR removes the lifetime spending limit that was added in #16780.

We had previously added this as a way to prevent runaway usage, but now
that we have a cap on free usage per month with paid access after that,
we don't need this check anymore.

Release Notes:

- N/A
2024-10-16 18:14:07 -04:00
renovate[bot]
6614feff97 Pin astral-sh/setup-uv action to f3bcaeb (#19309)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | pinDigest | -> `f3bcaeb` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 17:20:39 -04:00
Conrad Irwin
08b1545c85 Show a user-visible error message if saving fails (#19311)
Release Notes:

- Added a user-visible error message when a manual save fails.
2024-10-16 15:17:38 -06:00
Marshall Bowers
fedd177b08 collab: Add context to errors syncing billing events to Stripe (#19315)
This PR adds context to errors that occur when trying to sync billing
events to Stripe.

Release Notes:

- N/A
2024-10-16 17:09:26 -04:00
renovate[bot]
4288096ca1 Update Rust crate tree-sitter-cpp to v0.23.1 (#18974)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[tree-sitter-cpp](https://redirect.github.com/tree-sitter/tree-sitter-cpp)
| workspace.dependencies | patch | `0.23.0` -> `0.23.1` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-cpp (tree-sitter-cpp)</summary>

###
[`v0.23.1`](https://redirect.github.com/tree-sitter/tree-sitter-cpp/compare/v0.23.0...v0.23.1)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-cpp/compare/v0.23.0...v0.23.1)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 23:21:23 +03:00
renovate[bot]
256c31a5d9 Update Rust crate tree-sitter-c to v0.23.1 (#18958)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tree-sitter-c](https://redirect.github.com/tree-sitter/tree-sitter-c)
| workspace.dependencies | patch | `0.23.0` -> `0.23.1` |

---

### Release Notes

<details>
<summary>tree-sitter/tree-sitter-c (tree-sitter-c)</summary>

###
[`v0.23.1`](https://redirect.github.com/tree-sitter/tree-sitter-c/compare/v0.23.0...v0.23.1)

[Compare
Source](https://redirect.github.com/tree-sitter/tree-sitter-c/compare/v0.23.0...v0.23.1)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMTQuMCIsInVwZGF0ZWRJblZlciI6IjM4LjExNC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-16 23:19:10 +03:00
David Soria Parra
c8b6ad9666 Context Servers: Protocol fixes and UI improvements (#19087)
This PR does two things. It fixes some minor inconsistencies in the
protocol. This is mostly about handling JSON RPC notifications correctly
and skipping fields when set to None.

Second part is about improving the rendering of context server commands,
by passing on the description
of the command to the slash command UI and showing the name of the
argument as a CodeLabel.

Release Notes:

- N/A
2024-10-16 13:07:15 -07:00
Peter Tripp
0e22c9f275 docs: Add C++ clangd example arguments (#19308) 2024-10-16 16:07:05 -04:00
Kirill Bulatov
56f69be2e7 Do not allow [re]running ssh tasks when not connected to the server (#19306)
Release Notes:

- N/A
2024-10-16 22:57:39 +03:00
Kirill Bulatov
02f63e49ed Resolve proto hints with empty resolve data (#19274)
Fixed ssh remoting not showing a lot of hints


Release Notes:

- N/A
2024-10-16 21:50:51 +03:00
Kirill Bulatov
3dcc638537 Better handle shell for remote ssh projects (#19297)
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-10-16 21:49:54 +03:00
Marshall Bowers
d35b646dbb assistant: Direct user to account page to subscribe for more LLM usage (#19300)
This PR updates the location where we send the user to subscribe for
more LLM usage to the account page.

Release Notes:

- Updated the URL to the account page when subscribing to LLM usage.
2024-10-16 14:03:42 -04:00
张小白
338bf3fd28 windows: Fix window not displaying correctly on launch (#19124)
Closes #18705 (comment)

This PR fixes the issue where the Zed window was not displaying
correctly on launch. Now, when Zed is closed in a maximized state, it
will reopen in a maximized state.

On macOS, when a window is created but not yet visible, calling `zoom`
or `toggle_fullscreen` will still affect the hidden window. However,
this behavior is different on Windows, so special handling is required.

Also, since #18705 hasn't been reviewed yet, I'm not sure if this PR
should be merged now or if it should wait until #18705 is reviewed
first.


Release Notes:

- N/A
2024-10-16 10:29:42 -07:00
Matin Aniss
879a2ea06f gpui: Replace redundant code in animation (#19273)
Just a small change to replace some redundant code in the animation
element.

Release Notes:

- N/A
2024-10-16 10:26:26 -07:00
Piotr Osiewicz
7a5003bea2 ssh: Do not look up dev servers when rendering the default mode (#19295)
This should help with the bug where there's a mismatch between
connection count and the list showing empty state.

Closes #ISSUE

Release Notes:

- N/A
2024-10-16 18:53:05 +02:00
Joseph T. Lyons
f8f3f369f6 v0.159.x dev 2024-10-16 12:47:57 -04:00
Antonio Scandurra
474e670bbd Increase monthly free tier spend from 5 dollars to 10 dollars (#19291)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-16 12:22:24 -04:00
Marshall Bowers
fe0bcc063c collab: Add Stripe API key to Kubernetes template (#19292)
This PR adds the Stripe API key to the Kubernetes template.

It's optional right now, so we can set the API key when we're ready.

Release Notes:

- N/A
2024-10-16 12:10:39 -04:00
Thorsten Ball
69abe71bf7 ssh remoting: Treat closed stderr as error (#19289)
Before this change we had a race condition bug: if stderr was closed
before the other two sockets, we wouldn't properly detect when the
server died, and not report or retry anything.

That's because we treated a closed stderr as a non-error.

Technically, it isn't an error (closing a connection is okay!), but
until we have a proper shutdown ceremony between all three processes, we
can treat it as an error, because that lets us to detect when the server
is gone.

On the client-side, we also always react to these errors by
reconnecting. Except when we shutdown: there we do a proper shutdown and
won't error on the proxy exit code.

So, this works, even if I wish there was a better way for the server to
communicate to the proxy that it shutdown properly. But I don't want a
fourth socket.

Release Notes:

- N/A
2024-10-16 18:05:52 +02:00
Marshall Bowers
9c3d80d6e8 collab: Fetch more meters and prices when initializing StripeBilling (#19288)
This PR makes it so we fetch more meters and prices when initializing
`StripeBilling`, as we have more than 10 meters defined.

Release Notes:

- N/A
2024-10-16 11:40:56 -04:00
Kirill Bulatov
834d50f0db Properly open worktrees when cmd-clicking in terminal or on inlay hints (#19280)
* uses the state that's synced, to fetch the language server name
* uses proper, canonicalized path when creating a remote ssh worktree,
otherwise `~/foo/something` stays unexpanded

Release Notes:

- N/A
2024-10-16 18:12:36 +03:00
Kirill Bulatov
bcdb10b3cb Do not attempt to install prettier if the language change is unrelated (#19283)
Release Notes:

- Fix prettier install being attempted too much
2024-10-16 18:10:05 +03:00
Marshall Bowers
598939d186 collab: Refresh the user's LLM token when their subscription changes (#19281)
This PR makes it so collab will trigger a refresh for a user's LLM token
whenever their subscription changes.

This allows us to proactively push down changes to their subscription.

In order to facilitate this, the Stripe event processing has been moved
from the `api` service to the `collab` service in order to access the
RPC server.

Release Notes:

- N/A
2024-10-16 10:58:28 -04:00
Thorsten Ball
9d944d0662 ssh remote: Restore ControlPersist=no (#19277)
This restores the change from #19193 that I erroneously reverted in
#19234.

I think the bug in #19275 got in my way when testing.

With that bug fixed, the changes in here also work fine.


Release Notes:

- N/A
2024-10-16 16:13:31 +02:00
Tilman Roeder
7d2628e805 Make the divider rule color more muted (#19255)
I've been a bit annoyed by the hover divider rule being extremely bright
compared to other divider rules in the UI. This PR updates their color
to use the regular border color from the current theme instead of the
muted (but still pretty bright) text color.

Apologies for the unsolicited PR (and please feel free to close if it
goes against some other plans / designs you already have in place :).

#### Example screenshot before:
<img width="302" alt="Screenshot 2024-10-15 at 23 29 18"
src="https://github.com/user-attachments/assets/7ea22808-8135-4a46-9457-e670225aebaa">

#### Example screenshot after:
<img width="312" alt="Screenshot 2024-10-15 at 23 28 16"
src="https://github.com/user-attachments/assets/63ac0d02-ae6d-4962-84a2-1fdb95519b15">

***

Release Notes:

- Make the divider rule in LSP hovers more muted
2024-10-16 11:00:22 -03:00
Ihnat Aŭtuška
84df3a0cad Allow formatting selections via LSP (#18752)
Release Notes:

- Added a new `editor: format selections` action that allows formatting
only the currently selected text via the primary language server.

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-10-16 15:58:37 +02:00
Thorsten Ball
eb76065ad3 ssh remoting: Fix hang when activity channel gets dropped (#19275)
When the SSH command dies or the server, the channel gets dropped and
the heartbeat method went into an infinite loop causing a hang.

Oversight from yesterday. Fixed now.

Release Notes:

- N/A
2024-10-16 15:57:58 +02:00
Peter Tripp
84018d7a2d zig: Bump to v0.3.1 (#19252)
Includes:
- https://github.com/zed-industries/zed/pull/18323
- https://github.com/zed-industries/zed/pull/17488
2024-10-16 08:42:45 -04:00
Peter Tripp
57c55b32e1 html: Bump to v0.1.3 (#19251)
Includes:
- https://github.com/zed-industries/zed/pull/18024
2024-10-16 08:42:27 -04:00
Peter Tripp
a4357c429a elixir: Bump to v0.1.0 (#19250)
Includes:
- https://github.com/zed-industries/zed/pull/18024
- https://github.com/zed-industries/zed/pull/17488
- https://github.com/zed-industries/zed/pull/16985
2024-10-16 08:42:07 -04:00
Peter Tripp
103665ee28 astro: Bump to v0.1.1 (#19249)
Includes:
- https://github.com/zed-industries/zed/pull/18024
2024-10-16 08:41:45 -04:00
Thorsten Ball
2f960c4aba project environment: Log when which env is used (#19270)
This adds more logging for debugging purposes.

Release Notes:

- N/A
2024-10-16 14:12:45 +02:00
Piotr Osiewicz
109ebc5f27 ui: Add Scrollbar component (#18927)
Closes #ISSUE

Release Notes:

- N/A
2024-10-16 13:57:28 +02:00
69 changed files with 1916 additions and 1224 deletions

View File

@@ -2,14 +2,4 @@ Closes #ISSUE
Release Notes:
- Added/Fixed/Improved ...
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.
### Or...
Closes #ISSUE
Release Notes:
- N/A
- N/A *or* Added/Fixed/Improved ...

View File

@@ -1,50 +0,0 @@
name: Mark for Nightly
on:
issue_comment:
types: [created]
jobs:
mark-for-nightly:
if: github.event.issue.pull_request && contains(github.event.comment.body, '/nightly')
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Get PR details and check if open
id: pr_details
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
if (pr.data.state !== 'open') {
console.log('PR is not open. Skipping.');
return null;
}
return {
sha: pr.data.head.sha,
ref: pr.data.head.ref
}
- name: Edit comment with SHA
if: steps.pr_details.outputs.result != 'null'
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const sha = '${{ fromJson(steps.pr_details.outputs.result).sha }}';
const originalBody = context.payload.comment.body;
const updatedBody = originalBody.replace('/nightly', `/nightly:${sha}`);
const finalBody = `${updatedBody}\n\nThis PR's current HEAD (${sha}) will be included in the next nightly build. To remove it from the nightly build, comment with \`/nightly:remove\`.`;
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
body: finalBody
});

View File

@@ -14,63 +14,6 @@ env:
RUST_BACKTRACE: 1
jobs:
prepare-nightly-train:
name: Prepare nightly-train branch
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
- name: Create or update nightly-train branch
run: |
git config user.name github-actions
git config user.email github-actions@github.com
git checkout -B nightly-train main
# Find all comments with /nightly:SHA
nightly_comments=$(gh api graphql -f query='
query($owner:String!, $repo:String!) {
repository(owner:$owner, name:$repo) {
pullRequests(last:128, states:OPEN) {
nodes {
number
comments(last:128) {
nodes {
body
}
}
}
}
}
}' -f owner=${{ github.repository_owner }} -f repo=${{ github.event.repository.name }} | jq -r '.data.repository.pullRequests.nodes[] | select(.comments.nodes[].body | contains("/nightly:")) | {number: .number, comments: [.comments.nodes[].body | select(contains("/nightly:"))]} | @json')
# Process each PR
echo "$nightly_comments" | jq -c '.' | while read -r pr; do
pr_number=$(echo "$pr" | jq -r '.number')
last_command=$(echo "$pr" | jq -r '.comments[-1]')
sha=$(echo "$last_command" | grep -oP '/nightly:\K[a-f0-9]+')
if [ -n "$sha" ]; then
if git cherry-pick $sha; then
if cargo check --workspace; then
echo "Successfully cherry-picked and checked $sha from PR #$pr_number"
else
git reset --hard HEAD~1
echo "::warning::Commit $sha from PR #$pr_number failed cargo check, excluding from nightly-train"
fi
else
echo "::warning::Failed to cherry-pick $sha from PR #$pr_number, skipping"
git cherry-pick --abort
fi
elif [[ $last_command == *"/nightly:remove"* ]]; then
echo "Skipping PR #$pr_number due to /nightly:remove command"
fi
done
git push origin nightly-train --force
style:
timeout-minutes: 60
name: Check formatting and Clippy lints
@@ -78,14 +21,12 @@ jobs:
runs-on:
- self-hosted
- test
needs: prepare-nightly-train
steps:
- name: Checkout repo
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
fetch-depth: 0
ref: nightly-train
- name: Run style checks
uses: ./.github/actions/check_style
@@ -106,7 +47,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
ref: nightly-train
- name: Run tests
uses: ./.github/actions/run_tests
@@ -138,7 +78,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
ref: nightly-train
- name: Set release channel to nightly
run: |
@@ -173,7 +112,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
ref: nightly-train
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
@@ -214,7 +152,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
clean: false
ref: nightly-train
- name: Install Linux dependencies
run: ./script/linux
@@ -248,7 +185,6 @@ jobs:
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
with:
fetch-depth: 0
ref: nightly-train
- name: Update nightly tag
run: |

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
with:
version: "latest"
enable-cache: true

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
with:
version: "latest"
enable-cache: true

23
Cargo.lock generated
View File

@@ -9126,6 +9126,7 @@ dependencies = [
"rpc",
"serde",
"serde_json",
"shlex",
"smol",
"tempfile",
"thiserror",
@@ -12285,9 +12286,9 @@ dependencies = [
[[package]]
name = "tree-sitter-c"
version = "0.23.0"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e795ad541f7ae6a80d22975296340a75a12a29afd3a7089f4368021613728e17"
checksum = "c8b3fb515e498e258799a31d78e6603767cd6892770d9e2290ec00af5c3ad80b"
dependencies = [
"cc",
"tree-sitter-language",
@@ -12295,9 +12296,9 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.0"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0a588a816017469b69f2e3544742e34a5a59dddfb4b9457b657a6052e2ea39c"
checksum = "1d67e862242878d6ee50e1e5814f267ee3eea0168aea2cdbd700ccfb4c74b6d3"
dependencies = [
"cc",
"tree-sitter-language",
@@ -12325,9 +12326,9 @@ dependencies = [
[[package]]
name = "tree-sitter-elixir"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174acad8a059851f6f768d7893f4b25eedc80eb6643283d545dd71bbb38222a"
checksum = "97bf0efa4be41120018f23305b105ad4dfd3be1b7f302dc4071d0e6c2dec3a32"
dependencies = [
"cc",
"tree-sitter-language",
@@ -14577,7 +14578,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.158.0"
version = "0.159.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -14698,7 +14699,7 @@ dependencies = [
[[package]]
name = "zed_astro"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"serde",
"zed_extension_api 0.1.0",
@@ -14734,7 +14735,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.9"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -14796,7 +14797,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -14903,7 +14904,7 @@ dependencies = [
[[package]]
name = "zed_zig"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -4347,7 +4347,7 @@ impl ContextEditor {
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
const SUBSCRIBE_URL: &str = "https://zed.dev/api/billing/initiate_checkout";
const ACCOUNT_URL: &str = "https://zed.dev/account";
v_flex()
.gap_0p5()
@@ -4372,7 +4372,7 @@ impl ContextEditor {
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.open_url(SUBSCRIBE_URL);
cx.open_url(ACCOUNT_URL);
cx.notify();
},
)))

View File

@@ -1,3 +1,4 @@
use super::create_label_for_command;
use anyhow::{anyhow, Result};
use assistant_slash_command::{
AfterCompletion, ArgumentCompletion, SlashCommand, SlashCommandOutput,
@@ -6,9 +7,9 @@ use assistant_slash_command::{
use collections::HashMap;
use context_servers::{
manager::{ContextServer, ContextServerManager},
protocol::PromptInfo,
types::Prompt,
};
use gpui::{Task, WeakView, WindowContext};
use gpui::{AppContext, Task, WeakView, WindowContext};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -18,11 +19,11 @@ use workspace::Workspace;
pub struct ContextServerSlashCommand {
server_id: String,
prompt: PromptInfo,
prompt: Prompt,
}
impl ContextServerSlashCommand {
pub fn new(server: &Arc<ContextServer>, prompt: PromptInfo) -> Self {
pub fn new(server: &Arc<ContextServer>, prompt: Prompt) -> Self {
Self {
server_id: server.id.clone(),
prompt,
@@ -35,12 +36,28 @@ impl SlashCommand for ContextServerSlashCommand {
self.prompt.name.clone()
}
fn label(&self, cx: &AppContext) -> language::CodeLabel {
let mut parts = vec![self.prompt.name.as_str()];
if let Some(args) = &self.prompt.arguments {
if let Some(arg) = args.first() {
parts.push(arg.name.as_str());
}
}
create_label_for_command(&parts[0], &parts[1..], cx)
}
fn description(&self) -> String {
format!("Run context server command: {}", self.prompt.name)
match &self.prompt.description {
Some(desc) => desc.clone(),
None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
}
}
fn menu_text(&self) -> String {
format!("Run '{}' from {}", self.prompt.name, self.server_id)
match &self.prompt.description {
Some(desc) => desc.clone(),
None => format!("Run '{}' from {}", self.prompt.name, self.server_id),
}
}
fn requires_argument(&self) -> bool {
@@ -154,7 +171,7 @@ impl SlashCommand for ContextServerSlashCommand {
}
}
fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(String, String)> {
fn completion_argument(prompt: &Prompt, arguments: &[String]) -> Result<(String, String)> {
if arguments.is_empty() {
return Err(anyhow!("No arguments given"));
}
@@ -170,7 +187,7 @@ fn completion_argument(prompt: &PromptInfo, arguments: &[String]) -> Result<(Str
}
}
fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap<String, String>> {
fn prompt_arguments(prompt: &Prompt, arguments: &[String]) -> Result<HashMap<String, String>> {
match &prompt.arguments {
Some(args) if args.len() > 1 => Err(anyhow!(
"Prompt has more than one argument, which is not supported"
@@ -199,7 +216,7 @@ fn prompt_arguments(prompt: &PromptInfo, arguments: &[String]) -> Result<HashMap
/// MCP servers can return prompts with multiple arguments. Since we only
/// support one argument, we ignore all others. This is the necessary predicate
/// for this.
pub fn acceptable_prompt(prompt: &PromptInfo) -> bool {
pub fn acceptable_prompt(prompt: &Prompt) -> bool {
match &prompt.arguments {
None => true,
Some(args) if args.len() <= 1 => true,

View File

@@ -199,6 +199,12 @@ spec:
secretKeyRef:
name: slack
key: panics_webhook
- name: STRIPE_API_KEY
valueFrom:
secretKeyRef:
name: stripe
key: api_key
optional: true
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY

View File

@@ -19,8 +19,8 @@ use stripe::{
};
use util::ResultExt;
use crate::llm::DEFAULT_MAX_MONTHLY_SPEND;
use crate::rpc::ResultExt as _;
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
use crate::rpc::{ResultExt as _, Server};
use crate::{
db::{
billing_customer, BillingSubscriptionId, CreateBillingCustomerParams,
@@ -50,6 +50,7 @@ pub fn router() -> Router {
"/billing/subscriptions/manage",
post(manage_billing_subscription),
)
.route("/billing/monthly_spend", get(get_monthly_spend))
}
#[derive(Debug, Deserialize)]
@@ -404,7 +405,7 @@ const NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP: usize = 4;
/// Polls the Stripe events API periodically to reconcile the records in our
/// database with the data in Stripe.
pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
pub fn poll_stripe_events_periodically(app: Arc<AppState>, rpc_server: Arc<Server>) {
let Some(stripe_client) = app.stripe_client.clone() else {
log::warn!("failed to retrieve Stripe client");
return;
@@ -415,7 +416,9 @@ pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
let executor = executor.clone();
async move {
loop {
poll_stripe_events(&app, &stripe_client).await.log_err();
poll_stripe_events(&app, &rpc_server, &stripe_client)
.await
.log_err();
executor.sleep(POLL_EVENTS_INTERVAL).await;
}
@@ -425,6 +428,7 @@ pub fn poll_stripe_events_periodically(app: Arc<AppState>) {
async fn poll_stripe_events(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
) -> anyhow::Result<()> {
fn event_type_to_string(event_type: EventType) -> String {
@@ -449,29 +453,28 @@ async fn poll_stripe_events(
let mut pages_of_already_processed_events = 0;
let mut unprocessed_events = Vec::new();
log::info!(
"Stripe events: starting retrieval for {}",
event_types.join(", ")
);
let mut params = ListEvents::new();
params.types = Some(event_types.clone());
params.limit = Some(EVENTS_LIMIT_PER_PAGE);
let mut event_pages = stripe::Event::list(&stripe_client, &params)
.await?
.paginate(params);
loop {
if pages_of_already_processed_events >= NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP {
log::info!("saw {pages_of_already_processed_events} pages of already-processed events: stopping event retrieval");
break;
}
log::info!("retrieving events from Stripe: {}", event_types.join(", "));
let mut params = ListEvents::new();
params.types = Some(event_types.clone());
params.limit = Some(EVENTS_LIMIT_PER_PAGE);
let events = stripe::Event::list(stripe_client, &params).await?;
let processed_event_ids = {
let event_ids = &events
let event_ids = event_pages
.page
.data
.iter()
.map(|event| event.id.as_str())
.collect::<Vec<_>>();
app.db
.get_processed_stripe_events_by_event_ids(event_ids)
.get_processed_stripe_events_by_event_ids(&event_ids)
.await?
.into_iter()
.map(|event| event.stripe_event_id)
@@ -479,13 +482,13 @@ async fn poll_stripe_events(
};
let mut processed_events_in_page = 0;
let events_in_page = events.data.len();
for event in events.data {
let events_in_page = event_pages.page.data.len();
for event in &event_pages.page.data {
if processed_event_ids.contains(&event.id.to_string()) {
processed_events_in_page += 1;
log::debug!("Stripe event {} already processed: skipping", event.id);
log::debug!("Stripe events: already processed '{}', skipping", event.id);
} else {
unprocessed_events.push(event);
unprocessed_events.push(event.clone());
}
}
@@ -493,15 +496,21 @@ async fn poll_stripe_events(
pages_of_already_processed_events += 1;
}
if !events.has_more {
if event_pages.page.has_more {
if pages_of_already_processed_events >= NUMBER_OF_ALREADY_PROCESSED_PAGES_BEFORE_WE_STOP
{
log::info!("Stripe events: stopping, saw {pages_of_already_processed_events} pages of already-processed events");
break;
} else {
log::info!("Stripe events: retrieving next page");
event_pages = event_pages.next(&stripe_client).await?;
}
} else {
break;
}
}
log::info!(
"unprocessed events from Stripe: {}",
unprocessed_events.len()
);
log::info!("Stripe events: unprocessed {}", unprocessed_events.len());
// Sort all of the unprocessed events in ascending order, so we can handle them in the order they occurred.
unprocessed_events.sort_by(|a, b| a.created.cmp(&b.created).then_with(|| a.id.cmp(&b.id)));
@@ -517,12 +526,12 @@ async fn poll_stripe_events(
// If the event has happened too far in the past, we don't want to
// process it and risk overwriting other more-recent updates.
//
// 1 hour was chosen arbitrarily. This could be made longer or shorter.
let one_hour = Duration::from_secs(60 * 60);
let an_hour_ago = Utc::now() - one_hour;
if an_hour_ago.timestamp() > event.created {
// 1 day was chosen arbitrarily. This could be made longer or shorter.
let one_day = Duration::from_secs(24 * 60 * 60);
let a_day_ago = Utc::now() - one_day;
if a_day_ago.timestamp() > event.created {
log::info!(
"Stripe event {} is more than {one_hour:?} old, marking as processed",
"Stripe events: event '{}' is more than {one_day:?} old, marking as processed",
event_id
);
app.db
@@ -541,7 +550,7 @@ async fn poll_stripe_events(
| EventType::CustomerSubscriptionPaused
| EventType::CustomerSubscriptionResumed
| EventType::CustomerSubscriptionDeleted => {
handle_customer_subscription_event(app, stripe_client, event).await
handle_customer_subscription_event(app, rpc_server, stripe_client, event).await
}
_ => Ok(()),
};
@@ -609,6 +618,7 @@ async fn handle_customer_event(
async fn handle_customer_subscription_event(
app: &Arc<AppState>,
rpc_server: &Arc<Server>,
stripe_client: &stripe::Client,
event: stripe::Event,
) -> anyhow::Result<()> {
@@ -654,9 +664,52 @@ async fn handle_customer_subscription_event(
.await?;
}
// When the user's subscription changes, we want to refresh their LLM tokens
// to either grant/revoke access.
rpc_server
.refresh_llm_tokens_for_user(billing_customer.user_id)
.await;
Ok(())
}
#[derive(Debug, Deserialize)]
struct GetMonthlySpendParams {
github_user_id: i32,
}
#[derive(Debug, Serialize)]
struct GetMonthlySpendResponse {
monthly_spend_in_cents: i32,
}
async fn get_monthly_spend(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetMonthlySpendParams>,
) -> Result<Json<GetMonthlySpendResponse>> {
let user = app
.db
.get_user_by_github_user_id(params.github_user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let Some(llm_db) = app.llm_db.clone() else {
return Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"LLM database not available".into(),
));
};
let monthly_spend = llm_db
.get_user_spending_for_month(user.id, Utc::now())
.await?
.saturating_sub(FREE_TIER_MONTHLY_SPENDING_LIMIT);
Ok(Json(GetMonthlySpendResponse {
monthly_spend_in_cents: monthly_spend.0 as i32,
}))
}
impl From<SubscriptionStatus> for StripeSubscriptionStatus {
fn from(value: SubscriptionStatus) -> Self {
match value {
@@ -738,6 +791,7 @@ pub fn sync_llm_usage_with_stripe_periodically(app: Arc<AppState>) {
loop {
sync_with_stripe(&app, &llm_db, &stripe_billing)
.await
.context("failed to sync LLM usage to Stripe")
.trace_err();
executor.sleep(SYNC_LLM_USAGE_WITH_STRIPE_INTERVAL).await;
}

View File

@@ -435,7 +435,7 @@ fn normalize_model_name(known_models: Vec<String>, name: String) -> String {
/// The maximum monthly spending an individual user can reach on the free tier
/// before they have to pay.
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(5);
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
/// The default value to use for maximum spend per month if the user did not
/// explicitly set a maximum spend.
@@ -443,9 +443,6 @@ pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(5);
/// Used to prevent surprise bills.
pub const DEFAULT_MAX_MONTHLY_SPEND: Cents = Cents::from_dollars(10);
/// The maximum lifetime spending an individual user can reach before being cut off.
const LIFETIME_SPENDING_LIMIT: Cents = Cents::from_dollars(1_000);
async fn check_usage_limit(
state: &Arc<LlmState>,
provider: LanguageModelProvider,
@@ -487,14 +484,6 @@ async fn check_usage_limit(
}
}
// TODO: Remove this once we've rolled out monthly spending limits.
if usage.lifetime_spending >= LIFETIME_SPENDING_LIMIT {
return Err(Error::http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached.".to_string(),
));
}
let active_users = state.get_active_user_count(provider, model_name).await?;
let users_in_recent_minutes = active_users.users_in_recent_minutes.max(1);

View File

@@ -45,24 +45,17 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
let user_id = UserId::from_proto(123);
let max_monthly_spend = Cents::from_dollars(10);
let max_monthly_spend = Cents::from_dollars(11);
// Record usage that brings us close to the limit but doesn't exceed it
// Let's say we use $9.50 worth of tokens
let tokens_to_use = 190_000_000; // This will cost $9.50 at $0.05 per 1 million tokens
// Let's say we use $10.50 worth of tokens
let tokens_to_use = 210_000_000; // This will cost $10.50 at $0.05 per 1 million tokens
let usage = TokenUsage {
input: tokens_to_use,
input_cache_creation: 0,
input_cache_read: 0,
output: 0,
};
let cost = Cents::new(tokens_to_use as u32 / 1_000_000 * PRICE_PER_MILLION_INPUT_TOKENS as u32);
assert_eq!(
cost,
Cents::new(950),
"expected the cost to be $9.50, based on the inputs, but it wasn't"
);
// Verify that before we record any usage, there are 0 billing events
let billing_events = db.get_billing_events().await.unwrap();
@@ -102,8 +95,8 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(950),
lifetime_spending: Cents::new(950),
spending_this_month: Cents::new(1050),
lifetime_spending: Cents::new(1050),
}
);
@@ -118,7 +111,7 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
assert_eq!(billing_event.input_cache_read_tokens, 0);
assert_eq!(billing_event.output_tokens, 0);
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $9.50 to $10.50, which is over the $10 monthly maximum limit
let tokens_to_exceed = 20_000_000; // This will cost $1.00 more, pushing us from $10.50 to $11.50, which is over the $11 monthly maximum limit
let usage_exceeding = TokenUsage {
input: tokens_to_exceed,
input_cache_creation: 0,
@@ -158,8 +151,8 @@ async fn test_billing_limit_exceeded(db: &mut LlmDatabase) {
input_cache_read: 0,
output: 0,
},
spending_this_month: Cents::new(1050),
lifetime_spending: Cents::new(1050),
spending_this_month: Cents::new(1150),
lifetime_spending: Cents::new(1150),
}
);
}

View File

@@ -132,6 +132,8 @@ async fn main() -> Result<()> {
let rpc_server = collab::rpc::Server::new(epoch, state.clone());
rpc_server.start().await?;
poll_stripe_events_periodically(state.clone(), rpc_server.clone());
app = app
.merge(collab::api::routes(rpc_server.clone()))
.merge(collab::rpc::routes(rpc_server.clone()));
@@ -140,7 +142,6 @@ async fn main() -> Result<()> {
}
if mode.is_api() {
poll_stripe_events_periodically(state.clone());
fetch_extensions_from_blob_store_periodically(state.clone());
spawn_user_backfiller(state.clone());

View File

@@ -45,7 +45,13 @@ impl StripeBilling {
let (meters, prices) = futures::try_join!(
StripeMeter::list(&self.client),
stripe::Price::list(&self.client, &stripe::ListPrices::default())
stripe::Price::list(
&self.client,
&stripe::ListPrices {
limit: Some(100),
..Default::default()
}
)
)?;
for meter in meters.data {
@@ -396,9 +402,12 @@ impl StripeMeter {
pub fn list(client: &stripe::Client) -> stripe::Response<stripe::List<Self>> {
#[derive(Serialize)]
struct Params {}
struct Params {
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
}
client.get_query("/billing/meters", Params {})
client.get_query("/billing/meters", Params { limit: Some(100) })
}
}

View File

@@ -27,6 +27,7 @@ use language::{
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use project::lsp_store::FormatTarget;
use project::{
lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
HoverBlockKind, Project, ProjectPath,
@@ -4417,6 +4418,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4450,6 +4452,7 @@ async fn test_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4555,6 +4558,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
@@ -4574,6 +4578,7 @@ async fn test_prettier_formatting_buffer(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})

View File

@@ -26,7 +26,7 @@ const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type NotificationHandler = Box<dyn Send + FnMut(RequestId, Value, AsyncAppContext)>;
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncAppContext)>;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
@@ -94,7 +94,6 @@ enum CspResult<T> {
#[derive(Serialize, Deserialize)]
struct Notification<'a, T> {
jsonrpc: &'static str,
id: RequestId,
#[serde(borrow)]
method: &'a str,
params: T,
@@ -103,7 +102,6 @@ struct Notification<'a, T> {
#[derive(Debug, Clone, Deserialize)]
struct AnyNotification<'a> {
jsonrpc: &'a str,
id: RequestId,
method: String,
#[serde(default)]
params: Option<Value>,
@@ -246,11 +244,7 @@ impl Client {
if let Some(handler) =
notification_handlers.get_mut(notification.method.as_str())
{
handler(
notification.id,
notification.params.unwrap_or(Value::Null),
cx.clone(),
);
handler(notification.params.unwrap_or(Value::Null), cx.clone());
}
}
}
@@ -378,10 +372,8 @@ impl Client {
/// Sends a notification to the context server without expecting a response.
/// This function serializes the notification and sends it through the outbound channel.
pub fn notify(&self, method: &str, params: impl Serialize) -> Result<()> {
let id = self.next_id.fetch_add(1, SeqCst);
let notification = serde_json::to_string(&Notification {
jsonrpc: JSON_RPC_VERSION,
id: RequestId::Int(id),
method,
params,
})
@@ -390,13 +382,13 @@ impl Client {
Ok(())
}
pub fn on_notification<F>(&self, method: &'static str, mut f: F)
pub fn on_notification<F>(&self, method: &'static str, f: F)
where
F: 'static + Send + FnMut(Value, AsyncAppContext),
{
self.notification_handlers
.lock()
.insert(method, Box::new(move |_, params, cx| f(params, cx)));
.insert(method, Box::new(f));
}
pub fn name(&self) -> &str {

View File

@@ -85,7 +85,7 @@ impl ContextServer {
)?;
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::EntityInfo {
let client_info = types::Implementation {
name: "Zed".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};

View File

@@ -11,8 +11,6 @@ use collections::HashMap;
use crate::client::Client;
use crate::types;
pub use types::PromptInfo;
const PROTOCOL_VERSION: u32 = 1;
pub struct ModelContextProtocol {
@@ -26,7 +24,7 @@ impl ModelContextProtocol {
pub async fn initialize(
self,
client_info: types::EntityInfo,
client_info: types::Implementation,
) -> Result<InitializedContextServerProtocol> {
let params = types::InitializeParams {
protocol_version: PROTOCOL_VERSION,
@@ -96,7 +94,7 @@ impl InitializedContextServerProtocol {
}
/// List the MCP prompts.
pub async fn list_prompts(&self) -> Result<Vec<types::PromptInfo>> {
pub async fn list_prompts(&self) -> Result<Vec<types::Prompt>> {
self.check_capability(ServerCapability::Prompts)?;
let response: types::PromptsListResponse = self
@@ -107,6 +105,18 @@ impl InitializedContextServerProtocol {
Ok(response.prompts)
}
/// List the MCP resources.
pub async fn list_resources(&self) -> Result<types::ResourcesListResponse> {
self.check_capability(ServerCapability::Resources)?;
let response: types::ResourcesListResponse = self
.inner
.request(types::RequestType::ResourcesList.as_str(), ())
.await?;
Ok(response)
}
/// Executes a prompt with the given arguments and returns the result.
pub async fn run_prompt<P: AsRef<str>>(
&self,

View File

@@ -15,6 +15,7 @@ pub enum RequestType {
PromptsGet,
PromptsList,
CompletionComplete,
Ping,
}
impl RequestType {
@@ -30,6 +31,7 @@ impl RequestType {
RequestType::PromptsGet => "prompts/get",
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
RequestType::Ping => "ping",
}
}
}
@@ -39,14 +41,15 @@ impl RequestType {
pub struct InitializeParams {
pub protocol_version: u32,
pub capabilities: ClientCapabilities,
pub client_info: EntityInfo,
pub client_info: Implementation,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolParams {
pub name: String,
pub arguments: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
@@ -77,6 +80,7 @@ pub struct LoggingSetLevelParams {
#[serde(rename_all = "camelCase")]
pub struct PromptsGetParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, String>>,
}
@@ -101,6 +105,13 @@ pub struct PromptReference {
pub name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceReference {
pub r#type: PromptReferenceType,
pub uri: Url,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PromptReferenceType {
@@ -110,13 +121,6 @@ pub enum PromptReferenceType {
Resource,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceReference {
pub r#type: String,
pub uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionArgument {
@@ -129,7 +133,7 @@ pub struct CompletionArgument {
pub struct InitializeResponse {
pub protocol_version: u32,
pub capabilities: ServerCapabilities,
pub server_info: EntityInfo,
pub server_info: Implementation,
}
#[derive(Debug, Deserialize)]
@@ -141,13 +145,39 @@ pub struct ResourcesReadResponse {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesListResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_templates: Option<Vec<ResourceTemplate>>,
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<Vec<Resource>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingMessage {
pub role: SamplingRole,
pub content: SamplingContent,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SamplingRole {
User,
Assistant,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SamplingContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsGetResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub prompt: String,
}
@@ -155,7 +185,7 @@ pub struct PromptsGetResponse {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsListResponse {
pub prompts: Vec<PromptInfo>,
pub prompts: Vec<Prompt>,
}
#[derive(Debug, Deserialize)]
@@ -168,61 +198,91 @@ pub struct CompletionCompleteResponse {
#[serde(rename_all = "camelCase")]
pub struct CompletionResult {
pub values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
}
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptInfo {
pub struct Prompt {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}
#[derive(Debug, Deserialize, Clone)]
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptArgument {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
// Shared Types
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
pub sampling: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
pub logging: Option<HashMap<String, serde_json::Value>>,
pub prompts: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapabilities>,
pub tools: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapabilities>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EntityInfo {
pub struct Implementation {
pub name: String,
pub version: String,
}
@@ -231,6 +291,10 @@ pub struct EntityInfo {
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub uri: Url,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
@@ -238,17 +302,23 @@ pub struct Resource {
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
pub data: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplate {
pub uri_template: String,
pub name: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -260,13 +330,16 @@ pub enum LoggingLevel {
Error,
}
// Client Notifications
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum NotificationType {
Initialized,
Progress,
Message,
ResourcesUpdated,
ResourcesListChanged,
ToolsListChanged,
PromptsListChanged,
}
impl NotificationType {
@@ -274,6 +347,11 @@ impl NotificationType {
match self {
NotificationType::Initialized => "notifications/initialized",
NotificationType::Progress => "notifications/progress",
NotificationType::Message => "notifications/message",
NotificationType::ResourcesUpdated => "notifications/resources/updated",
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
}
}
}
@@ -288,12 +366,13 @@ pub enum ClientNotification {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressParams {
pub progress_token: String,
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
}
// Helper Types that don't map directly to the protocol
pub type ProgressToken = String;
pub enum CompletionTotal {
Exact(u32),

View File

@@ -237,6 +237,7 @@ gpui::actions!(
ToggleFold,
ToggleFoldRecursive,
Format,
FormatSelections,
GoToDeclaration,
GoToDeclarationSplit,
GoToDefinition,

View File

@@ -96,7 +96,9 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
use language::{
point_to_lsp, BufferRow, CharClassifier, LanguageServerName, Runnable, RunnableRange,
};
use linked_editing_ranges::refresh_linked_ranges;
pub use proposed_changes_editor::{
ProposedChangesBuffer, ProposedChangesEditor, ProposedChangesEditorToolbar,
@@ -122,7 +124,7 @@ use multi_buffer::{
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::{
lsp_store::FormatTrigger,
lsp_store::{FormatTarget, FormatTrigger},
project_settings::{GitGutterSetting, ProjectSettings},
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
@@ -9893,21 +9895,19 @@ impl Editor {
&self,
lsp_location: lsp::Location,
server_id: LanguageServerId,
cx: &mut ViewContext<Editor>,
cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Option<Location>>> {
let Some(project) = self.project.clone() else {
return Task::Ready(Some(Ok(None)));
};
cx.spawn(move |editor, mut cx| async move {
let location_task = editor.update(&mut cx, |editor, cx| {
let location_task = editor.update(&mut cx, |_, cx| {
project.update(cx, |project, cx| {
let language_server_name =
editor.buffer.read(cx).as_singleton().and_then(|buffer| {
project
.language_server_for_buffer(buffer.read(cx), server_id, cx)
.map(|(lsp_adapter, _)| lsp_adapter.name.clone())
});
let language_server_name = project
.language_server_statuses(cx)
.find(|(id, _)| server_id == *id)
.map(|(_, status)| LanguageServerName::from(status.name.as_str()));
language_server_name.map(|language_server_name| {
project.open_local_buffer_via_lsp(
lsp_location.uri.clone(),
@@ -10386,13 +10386,39 @@ impl Editor {
None => return None,
};
Some(self.perform_format(project, FormatTrigger::Manual, cx))
Some(self.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx))
}
fn format_selections(
&mut self,
_: &FormatSelections,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let project = match &self.project {
Some(project) => project.clone(),
None => return None,
};
let selections = self
.selections
.all_adjusted(cx)
.into_iter()
.filter(|s| !s.is_empty())
.collect_vec();
Some(self.perform_format(
project,
FormatTrigger::Manual,
FormatTarget::Ranges(selections),
cx,
))
}
fn perform_format(
&mut self,
project: Model<Project>,
trigger: FormatTrigger,
target: FormatTarget,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self.buffer().clone();
@@ -10402,7 +10428,9 @@ impl Editor {
}
let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
let format = project.update(cx, |project, cx| {
project.format(buffers, true, trigger, target, cx)
});
cx.spawn(|_, mut cx| async move {
let transaction = futures::select_biased! {

View File

@@ -7076,7 +7076,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.unwrap();
fake_server
@@ -7112,7 +7117,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
});
let format = editor
.update(cx, |editor, cx| {
editor.perform_format(project, FormatTrigger::Manual, cx)
editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx)
})
.unwrap();
cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -10309,7 +10314,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
editor
.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.unwrap()
.await;
@@ -10323,7 +10333,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
});
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
});
format.await.unwrap();
assert_eq!(

View File

@@ -376,6 +376,13 @@ impl EditorElement {
cx.propagate();
}
});
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.format_selections(action, cx) {
task.detach_and_log_err(cx);
} else {
cx.propagate();
}
});
register_action(view, cx, Editor::restart_language_server);
register_action(view, cx, Editor::cancel_language_server_work);
register_action(view, cx, Editor::show_character_palette);

View File

@@ -525,7 +525,7 @@ async fn parse_blocks(
font_family: Some(buffer_font_family),
..Default::default()
},
rule_color: Color::Muted.color(cx),
rule_color: cx.theme().colors().border,
block_quote_border_color: Color::Muted.color(cx),
block_quote: TextStyleRefinement {
color: Some(Color::Muted.color(cx)),

View File

@@ -27,6 +27,7 @@ use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
use project::lsp_store::FormatTarget;
use std::{
any::TypeId,
borrow::Cow,
@@ -722,7 +723,12 @@ impl Item for Editor {
cx.spawn(|this, mut cx| async move {
if format {
this.update(&mut cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Save, cx)
editor.perform_format(
project.clone(),
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})?
.await?;
}

View File

@@ -1,5 +1,4 @@
use std::ops::Range;
use crate::actions::FormatSelections;
use crate::{
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
@@ -8,6 +7,8 @@ use crate::{
};
use gpui::prelude::FluentBuilder;
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use std::ops::Range;
use text::PointUtf16;
use workspace::OpenInTerminal;
#[derive(Debug)]
@@ -164,6 +165,12 @@ pub fn deploy_context_menu(
} else {
"Reveal in File Manager"
};
let has_selections = editor
.selections
.all::<PointUtf16>(cx)
.into_iter()
.any(|s| !s.is_empty());
ui::ContextMenu::build(cx, |menu, _cx| {
let builder = menu
.on_blur_subscription(Subscription::new(|| {}))
@@ -175,6 +182,9 @@ pub fn deploy_context_menu(
.separator()
.action("Rename Symbol", Box::new(Rename))
.action("Format Buffer", Box::new(Format))
.when(has_selections, |cx| {
cx.action("Format Selections", Box::new(FormatSelections))
})
.action(
"Code Actions",
Box::new(ToggleCodeActions {

View File

@@ -133,14 +133,7 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
let mut element = (self.animator)(element, delta).into_any_element();
if !done {
let parent_id = cx.parent_view_id();
cx.on_next_frame(move |cx| {
if let Some(parent_id) = parent_id {
cx.notify(parent_id)
} else {
cx.refresh()
}
})
cx.request_animation_frame();
}
((element.request_layout(cx), element), state)

View File

@@ -2575,4 +2575,9 @@ impl ScrollHandle {
pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) {
self.0.borrow_mut().requested_scroll_top = Some((ix, px));
}
/// Get the count of children for scrollable item.
pub fn children_count(&self) -> usize {
self.0.borrow().child_bounds.len()
}
}

View File

@@ -52,12 +52,13 @@ pub struct WindowsWindowState {
pub display: WindowsDisplay,
fullscreen: Option<StyleAndBounds>,
initial_placement: Option<WINDOWPLACEMENT>,
initial_placement: Option<WindowOpenStatus>,
hwnd: HWND,
}
pub(crate) struct WindowsWindowStatePtr {
hwnd: HWND,
this: Weak<Self>,
pub(crate) state: RefCell<WindowsWindowState>,
pub(crate) handle: AnyWindowHandle,
pub(crate) hide_title_bar: bool,
@@ -222,9 +223,10 @@ impl WindowsWindowStatePtr {
context.display,
)?);
Ok(Rc::new(Self {
state,
Ok(Rc::new_cyclic(|this| Self {
hwnd,
this: this.clone(),
state,
handle: context.handle,
hide_title_bar: context.hide_title_bar,
is_movable: context.is_movable,
@@ -235,11 +237,86 @@ impl WindowsWindowStatePtr {
}))
}
fn toggle_fullscreen(&self) {
let Some(state_ptr) = self.this.upgrade() else {
log::error!("Unable to toggle fullscreen: window has been dropped");
return;
};
self.executor
.spawn(async move {
let mut lock = state_ptr.state.borrow_mut();
let StyleAndBounds {
style,
x,
y,
cx,
cy,
} = if let Some(state) = lock.fullscreen.take() {
state
} else {
let (window_bounds, _) = lock.calculate_window_bounds();
lock.fullscreen_restore_bounds = window_bounds;
let style =
WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _);
let mut rc = RECT::default();
unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err();
let _ = lock.fullscreen.insert(StyleAndBounds {
style,
x: rc.left,
y: rc.top,
cx: rc.right - rc.left,
cy: rc.bottom - rc.top,
});
let style = style
& !(WS_THICKFRAME
| WS_SYSMENU
| WS_MAXIMIZEBOX
| WS_MINIMIZEBOX
| WS_CAPTION);
let physical_bounds = lock.display.physical_bounds();
StyleAndBounds {
style,
x: physical_bounds.left().0,
y: physical_bounds.top().0,
cx: physical_bounds.size.width.0,
cy: physical_bounds.size.height.0,
}
};
drop(lock);
unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) };
unsafe {
SetWindowPos(
state_ptr.hwnd,
HWND::default(),
x,
y,
cx,
cy,
SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER,
)
}
.log_err();
})
.detach();
}
fn set_window_placement(&self) -> Result<()> {
let Some(placement) = self.state.borrow_mut().initial_placement.take() else {
let Some(open_status) = self.state.borrow_mut().initial_placement.take() else {
return Ok(());
};
unsafe { SetWindowPlacement(self.hwnd, &placement)? };
match open_status.state {
WindowOpenState::Maximized => unsafe {
SetWindowPlacement(self.hwnd, &open_status.placement)?;
ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?;
},
WindowOpenState::Fullscreen => {
unsafe { SetWindowPlacement(self.hwnd, &open_status.placement)? };
self.toggle_fullscreen();
}
WindowOpenState::Windowed => unsafe {
SetWindowPlacement(self.hwnd, &open_status.placement)?;
},
}
Ok(())
}
}
@@ -361,7 +438,10 @@ impl WindowsWindow {
if params.show {
unsafe { SetWindowPlacement(hwnd, &placement)? };
} else {
state_ptr.state.borrow_mut().initial_placement = Some(placement);
state_ptr.state.borrow_mut().initial_placement = Some(WindowOpenStatus {
placement,
state: WindowOpenState::Windowed,
});
}
Ok(Self(state_ptr))
@@ -579,68 +659,21 @@ impl PlatformWindow for WindowsWindow {
}
fn zoom(&self) {
unsafe { ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE).ok().log_err() };
unsafe {
if IsWindowVisible(self.0.hwnd).as_bool() {
ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE).ok().log_err();
} else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() {
status.state = WindowOpenState::Maximized;
}
}
}
fn toggle_fullscreen(&self) {
let state_ptr = self.0.clone();
self.0
.executor
.spawn(async move {
let mut lock = state_ptr.state.borrow_mut();
let StyleAndBounds {
style,
x,
y,
cx,
cy,
} = if let Some(state) = lock.fullscreen.take() {
state
} else {
let (window_bounds, _) = lock.calculate_window_bounds();
lock.fullscreen_restore_bounds = window_bounds;
let style =
WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _);
let mut rc = RECT::default();
unsafe { GetWindowRect(state_ptr.hwnd, &mut rc) }.log_err();
let _ = lock.fullscreen.insert(StyleAndBounds {
style,
x: rc.left,
y: rc.top,
cx: rc.right - rc.left,
cy: rc.bottom - rc.top,
});
let style = style
& !(WS_THICKFRAME
| WS_SYSMENU
| WS_MAXIMIZEBOX
| WS_MINIMIZEBOX
| WS_CAPTION);
let physical_bounds = lock.display.physical_bounds();
StyleAndBounds {
style,
x: physical_bounds.left().0,
y: physical_bounds.top().0,
cx: physical_bounds.size.width.0,
cy: physical_bounds.size.height.0,
}
};
drop(lock);
unsafe { set_window_long(state_ptr.hwnd, GWL_STYLE, style.0 as isize) };
unsafe {
SetWindowPos(
state_ptr.hwnd,
HWND::default(),
x,
y,
cx,
cy,
SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER,
)
}
.log_err();
})
.detach();
if unsafe { IsWindowVisible(self.0.hwnd).as_bool() } {
self.0.toggle_fullscreen();
} else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() {
status.state = WindowOpenState::Fullscreen;
}
}
fn is_fullscreen(&self) -> bool {
@@ -925,6 +958,17 @@ impl WindowBorderOffset {
}
}
struct WindowOpenStatus {
placement: WINDOWPLACEMENT,
state: WindowOpenState,
}
enum WindowOpenState {
Maximized,
Fullscreen,
Windowed,
}
fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
const CLASS_NAME: PCWSTR = w!("Zed::Window");

View File

@@ -112,8 +112,18 @@ impl ProjectEnvironment {
let worktree = worktree_id.zip(worktree_abs_path);
let cli_environment = self.get_cli_environment();
if cli_environment.is_some() {
Task::ready(cli_environment)
if let Some(environment) = cli_environment {
cx.spawn(|_, _| async move {
let path = environment
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables from CLI. PATH={:?}",
path
);
Some(environment)
})
} else if let Some((worktree_id, worktree_abs_path)) = worktree {
self.get_worktree_env(worktree_id, worktree_abs_path, cx)
} else {
@@ -143,6 +153,15 @@ impl ProjectEnvironment {
.await;
if let Some(shell_env) = shell_env.as_mut() {
let path = shell_env
.get("PATH")
.map(|path| path.as_str())
.unwrap_or_default();
log::info!(
"using project environment variables shell launched in {:?}. PATH={:?}",
worktree_abs_path,
path
);
this.update(&mut cx, |this, _| {
this.cached_shell_environments
.insert(worktree_id, shell_env.clone());

View File

@@ -2439,15 +2439,13 @@ impl InlayHints {
ResolveState::Resolved => (0, None),
ResolveState::CanResolve(server_id, resolve_data) => (
1,
resolve_data
.map(|json_data| {
Some(proto::resolve_state::LspResolveState {
server_id: server_id.0 as u64,
value: resolve_data.map(|json_data| {
serde_json::to_string(&json_data)
.expect("failed to serialize resolve json data")
})
.map(|value| proto::resolve_state::LspResolveState {
server_id: server_id.0 as u64,
value,
}),
}),
),
ResolveState::Resolving => (2, None),
};
@@ -2515,9 +2513,11 @@ impl InlayHints {
let resolve_state_data = resolve_state
.lsp_resolve_state.as_ref()
.map(|lsp_resolve_state| {
serde_json::from_str::<Option<lsp::LSPAny>>(&lsp_resolve_state.value)
.with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
.map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state))
let value = lsp_resolve_state.value.as_deref().map(|value| {
serde_json::from_str::<Option<lsp::LSPAny>>(value)
.with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
}).transpose()?.flatten();
anyhow::Ok((LanguageServerId(lsp_resolve_state.server_id as usize), value))
})
.transpose()?;
let resolve_state = match resolve_state.state {

View File

@@ -72,7 +72,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use text::{Anchor, BufferId, LineEnding};
use text::{Anchor, BufferId, LineEnding, Point, Selection};
use util::{
debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _,
};
@@ -96,6 +96,20 @@ pub enum FormatTrigger {
Manual,
}
pub enum FormatTarget {
Buffer,
Ranges(Vec<Selection<Point>>),
}
impl FormatTarget {
pub fn as_selections(&self) -> Option<&[Selection<Point>]> {
match self {
FormatTarget::Buffer => None,
FormatTarget::Ranges(selections) => Some(selections.as_slice()),
}
}
}
// Currently, formatting operations are represented differently depending on
// whether they come from a language server or an external command.
#[derive(Debug)]
@@ -161,6 +175,7 @@ impl LocalLspStore {
mut buffers: Vec<FormattableBuffer>,
push_to_history: bool,
trigger: FormatTrigger,
target: FormatTarget,
mut cx: AsyncAppContext,
) -> anyhow::Result<ProjectTransaction> {
// Do not allow multiple concurrent formatting requests for the
@@ -286,6 +301,7 @@ impl LocalLspStore {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -299,6 +315,7 @@ impl LocalLspStore {
} else {
Self::perform_format(
&Formatter::LanguageServer { name: None },
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -310,9 +327,8 @@ impl LocalLspStore {
)
.await
}
}
.log_err()
.flatten();
}?;
if let Some(op) = diff {
format_operations.push(op);
}
@@ -321,6 +337,7 @@ impl LocalLspStore {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -330,9 +347,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@@ -346,6 +361,7 @@ impl LocalLspStore {
for formatter in formatters.as_ref() {
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -355,9 +371,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@@ -373,6 +387,7 @@ impl LocalLspStore {
if prettier_settings.allowed {
Self::perform_format(
&Formatter::Prettier,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -384,8 +399,14 @@ impl LocalLspStore {
)
.await
} else {
let formatter = Formatter::LanguageServer {
name: primary_language_server
.as_ref()
.map(|server| server.name().to_string()),
};
Self::perform_format(
&Formatter::LanguageServer { name: None },
&formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -397,9 +418,7 @@ impl LocalLspStore {
)
.await
}
}
.log_err()
.flatten();
}?;
if let Some(op) = diff {
format_operations.push(op)
@@ -410,6 +429,7 @@ impl LocalLspStore {
// format with formatter
let diff = Self::perform_format(
formatter,
&target,
server_and_buffer,
lsp_store.clone(),
buffer,
@@ -419,9 +439,7 @@ impl LocalLspStore {
&mut project_transaction,
&mut cx,
)
.await
.log_err()
.flatten();
.await?;
if let Some(op) = diff {
format_operations.push(op);
}
@@ -483,6 +501,7 @@ impl LocalLspStore {
#[allow(clippy::too_many_arguments)]
async fn perform_format(
formatter: &Formatter,
format_target: &FormatTarget,
primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
lsp_store: WeakModel<LspStore>,
buffer: &FormattableBuffer,
@@ -506,18 +525,33 @@ impl LocalLspStore {
language_server
};
Some(FormatOperation::Lsp(
LspStore::format_via_lsp(
&lsp_store,
&buffer.handle,
buffer_abs_path,
language_server,
settings,
cx,
)
.await
.context("failed to format via language server")?,
))
match format_target {
FormatTarget::Buffer => Some(FormatOperation::Lsp(
LspStore::format_via_lsp(
&lsp_store,
&buffer.handle,
buffer_abs_path,
language_server,
settings,
cx,
)
.await
.context("failed to format via language server")?,
)),
FormatTarget::Ranges(selections) => Some(FormatOperation::Lsp(
LspStore::format_range_via_lsp(
&lsp_store,
&buffer.handle,
selections.as_slice(),
buffer_abs_path,
language_server,
settings,
cx,
)
.await
.context("failed to format ranges via language server")?,
)),
}
} else {
None
}
@@ -1203,16 +1237,19 @@ impl LspStore {
None
};
if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) {
let prettier_store = self.as_local().map(|s| s.prettier_store.clone());
if let Some(prettier_store) = prettier_store {
prettier_store.update(cx, |prettier_store, cx| {
prettier_store.install_default_prettier(
worktree_id,
prettier_plugins.iter().map(|s| Arc::from(s.as_str())),
cx,
)
})
if settings.prettier.allowed {
if let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings)
{
let prettier_store = self.as_local().map(|s| s.prettier_store.clone());
if let Some(prettier_store) = prettier_store {
prettier_store.update(cx, |prettier_store, cx| {
prettier_store.install_default_prettier(
worktree_id,
prettier_plugins.iter().map(|s| Arc::from(s.as_str())),
cx,
)
})
}
}
}
@@ -1859,10 +1896,9 @@ impl LspStore {
} else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
let buffer_start = lsp::Position::new(0, 0);
let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
language_server
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
text_document,
text_document: text_document.clone(),
range: lsp::Range::new(buffer_start, buffer_end),
options: lsp_command::lsp_formatting_options(settings),
work_done_progress_params: Default::default(),
@@ -1878,7 +1914,62 @@ impl LspStore {
})?
.await
} else {
Ok(Vec::new())
Ok(Vec::with_capacity(0))
}
}
pub async fn format_range_via_lsp(
this: &WeakModel<Self>,
buffer: &Model<Buffer>,
selections: &[Selection<Point>],
abs_path: &Path,
language_server: &Arc<LanguageServer>,
settings: &LanguageSettings,
cx: &mut AsyncAppContext,
) -> Result<Vec<(Range<Anchor>, String)>> {
let capabilities = &language_server.capabilities();
let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) {
return Err(anyhow!(
"{} language server does not support range formatting",
language_server.name()
));
}
let uri = lsp::Url::from_file_path(abs_path)
.map_err(|_| anyhow!("failed to convert abs path to uri"))?;
let text_document = lsp::TextDocumentIdentifier::new(uri);
let lsp_edits = {
let ranges = selections.into_iter().map(|s| {
let start = lsp::Position::new(s.start.row, s.start.column);
let end = lsp::Position::new(s.end.row, s.end.column);
lsp::Range::new(start, end)
});
let mut edits = None;
for range in ranges {
if let Some(mut edit) = language_server
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
text_document: text_document.clone(),
range,
options: lsp_command::lsp_formatting_options(settings),
work_done_progress_params: Default::default(),
})
.await?
{
edits.get_or_insert_with(Vec::new).append(&mut edit);
}
}
edits
};
if let Some(lsp_edits) = lsp_edits {
this.update(cx, |this, cx| {
this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx)
})?
.await
} else {
Ok(Vec::with_capacity(0))
}
}
@@ -2648,44 +2739,44 @@ impl LspStore {
};
requests.push(
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
server
.request::<lsp::request::WorkspaceSymbolRequest>(
lsp::WorkspaceSymbolParams {
query: query.to_string(),
..Default::default()
},
)
.log_err()
.map(move |response| {
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
flat_responses.into_iter().map(|lsp_symbol| {
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
}).collect::<Vec<_>>()
}
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
nested_responses.into_iter().filter_map(|lsp_symbol| {
let location = match lsp_symbol.location {
OneOf::Left(location) => location,
OneOf::Right(_) => {
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
return None
}
};
Some((lsp_symbol.name, lsp_symbol.kind, location))
}).collect::<Vec<_>>()
}
}).unwrap_or_default();
WorkspaceSymbolsResult {
lsp_adapter,
language,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
);
WorkspaceSymbolsResult {
lsp_adapter,
language,
worktree: worktree_handle.downgrade(),
worktree_abs_path,
lsp_symbols,
}
}),
);
}
cx.spawn(move |this, mut cx| async move {
@@ -3412,7 +3503,7 @@ impl LspStore {
language_server_name: LanguageServerName,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
cx.spawn(move |this, mut cx| async move {
cx.spawn(move |lsp_store, mut cx| async move {
// Escape percent-encoded string.
let current_scheme = abs_path.scheme().to_owned();
let _ = abs_path.set_scheme("file");
@@ -3421,9 +3512,9 @@ impl LspStore {
.to_file_path()
.map_err(|_| anyhow!("can't convert URI to path"))?;
let p = abs_path.clone();
let yarn_worktree = this
.update(&mut cx, move |this, cx| {
this.as_local().unwrap().yarn.update(cx, |_, cx| {
let yarn_worktree = lsp_store
.update(&mut cx, move |lsp_store, cx| match lsp_store.as_local() {
Some(local_lsp_store) => local_lsp_store.yarn.update(cx, |_, cx| {
cx.spawn(|this, mut cx| async move {
let t = this
.update(&mut cx, |this, cx| {
@@ -3432,7 +3523,8 @@ impl LspStore {
.ok()?;
t.await
})
})
}),
None => Task::ready(None),
})?
.await;
let (worktree_root_target, known_relative_path) =
@@ -3442,8 +3534,8 @@ impl LspStore {
(Arc::<Path>::from(abs_path.as_path()), None)
};
let (worktree, relative_path) = if let Some(result) =
this.update(&mut cx, |this, cx| {
this.worktree_store.update(cx, |worktree_store, cx| {
lsp_store.update(&mut cx, |lsp_store, cx| {
lsp_store.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.find_worktree(&worktree_root_target, cx)
})
})? {
@@ -3451,22 +3543,25 @@ impl LspStore {
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
(result.0, relative_path)
} else {
let worktree = this
.update(&mut cx, |this, cx| {
this.worktree_store.update(cx, |worktree_store, cx| {
let worktree = lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.worktree_store.update(cx, |worktree_store, cx| {
worktree_store.create_worktree(&worktree_root_target, false, cx)
})
})?
.await?;
this.update(&mut cx, |this, cx| {
this.register_language_server(
worktree.read(cx).id(),
language_server_name,
language_server_id,
)
})
.ok();
let worktree_root = worktree.update(&mut cx, |this, _| this.abs_path())?;
if worktree.update(&mut cx, |worktree, _| worktree.is_local())? {
lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.register_language_server(
worktree.read(cx).id(),
language_server_name,
language_server_id,
)
})
.ok();
}
let worktree_root = worktree.update(&mut cx, |worktree, _| worktree.abs_path())?;
let relative_path = if let Some(known_path) = known_relative_path {
known_path
} else {
@@ -3478,12 +3573,13 @@ impl LspStore {
worktree_id: worktree.update(&mut cx, |worktree, _| worktree.id())?,
path: relative_path,
};
this.update(&mut cx, |this, cx| {
this.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.open_buffer(project_path, cx)
})
})?
.await
lsp_store
.update(&mut cx, |lsp_store, cx| {
lsp_store.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.open_buffer(project_path, cx)
})
})?
.await
})
}
@@ -4579,16 +4675,16 @@ impl LspStore {
if registrations.remove(registration_id).is_some() {
log::info!(
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
registration_id
);
"language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
language_server_id,
registration_id
);
} else {
log::warn!(
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
language_server_id,
registration_id
);
"language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
language_server_id,
registration_id
);
}
self.rebuild_watched_paths(language_server_id, cx);
@@ -5078,6 +5174,7 @@ impl LspStore {
buffers: HashSet<Model<Buffer>>,
push_to_history: bool,
trigger: FormatTrigger,
target: FormatTarget,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<ProjectTransaction>> {
if let Some(_) = self.as_local() {
@@ -5114,6 +5211,7 @@ impl LspStore {
formattable_buffers,
push_to_history,
trigger,
target,
cx.clone(),
)
.await;
@@ -5172,7 +5270,7 @@ impl LspStore {
buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
}
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, FormatTarget::Buffer, cx))
})??;
let project_transaction = format.await?;
@@ -6485,11 +6583,11 @@ impl LspStore {
})?;
let found_snapshot = snapshots
.binary_search_by_key(&version, |e| e.version)
.map(|ix| snapshots[ix].snapshot.clone())
.map_err(|_| {
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
})?;
.binary_search_by_key(&version, |e| e.version)
.map(|ix| snapshots[ix].snapshot.clone())
.map_err(|_| {
anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
})?;
snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
Ok(found_snapshot)
@@ -7203,74 +7301,74 @@ impl LanguageServerWatchedPathsBuilder {
let project = cx.weak_model();
cx.new_model(|cx| {
let this_id = cx.entity_id();
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
let abs_paths = self
.abs_paths
.into_iter()
.map(|(abs_path, globset)| {
let task = cx.spawn({
let abs_path = abs_path.clone();
let fs = fs.clone();
let this_id = cx.entity_id();
const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
let abs_paths = self
.abs_paths
.into_iter()
.map(|(abs_path, globset)| {
let task = cx.spawn({
let abs_path = abs_path.clone();
let fs = fs.clone();
let lsp_store = project.clone();
|_, mut cx| async move {
maybe!(async move {
let mut push_updates =
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
while let Some(update) = push_updates.0.next().await {
let action = lsp_store
.update(&mut cx, |this, cx| {
let Some(local) = this.as_local() else {
return ControlFlow::Break(());
};
let Some(watcher) = local
.language_server_watched_paths
.get(&language_server_id)
else {
return ControlFlow::Break(());
};
if watcher.entity_id() != this_id {
// This watcher is no longer registered on the project, which means that we should
// cease operations.
return ControlFlow::Break(());
}
let (globs, _) = watcher
.read(cx)
.abs_paths
.get(&abs_path)
.expect(
"Watched abs path is not registered with a watcher",
);
let matching_entries = update
.into_iter()
.filter(|event| globs.is_match(&event.path))
.collect::<Vec<_>>();
this.lsp_notify_abs_paths_changed(
language_server_id,
matching_entries,
);
ControlFlow::Continue(())
})
.ok()?;
let lsp_store = project.clone();
|_, mut cx| async move {
maybe!(async move {
let mut push_updates =
fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
while let Some(update) = push_updates.0.next().await {
let action = lsp_store
.update(&mut cx, |this, cx| {
let Some(local) = this.as_local() else {
return ControlFlow::Break(());
};
let Some(watcher) = local
.language_server_watched_paths
.get(&language_server_id)
else {
return ControlFlow::Break(());
};
if watcher.entity_id() != this_id {
// This watcher is no longer registered on the project, which means that we should
// cease operations.
return ControlFlow::Break(());
}
let (globs, _) = watcher
.read(cx)
.abs_paths
.get(&abs_path)
.expect(
"Watched abs path is not registered with a watcher",
);
let matching_entries = update
.into_iter()
.filter(|event| globs.is_match(&event.path))
.collect::<Vec<_>>();
this.lsp_notify_abs_paths_changed(
language_server_id,
matching_entries,
);
ControlFlow::Continue(())
})
.ok()?;
if action.is_break() {
break;
if action.is_break() {
break;
}
}
}
Some(())
})
.await;
}
});
(abs_path, (globset, task))
})
.collect();
LanguageServerWatchedPaths {
worktree_paths: self.worktree_paths,
abs_paths,
}
})
Some(())
})
.await;
}
});
(abs_path, (globset, task))
})
.collect();
LanguageServerWatchedPaths {
worktree_paths: self.worktree_paths,
abs_paths,
}
})
}
}

View File

@@ -610,11 +610,13 @@ impl PrettierStore {
) {
let mut prettier_plugins_by_worktree = HashMap::default();
for (worktree, language_settings) in language_formatters_to_check {
if let Some(plugins) = prettier_plugins_for_language(&language_settings) {
prettier_plugins_by_worktree
.entry(worktree)
.or_insert_with(HashSet::default)
.extend(plugins.iter().cloned());
if language_settings.prettier.allowed {
if let Some(plugins) = prettier_plugins_for_language(&language_settings) {
prettier_plugins_by_worktree
.entry(worktree)
.or_insert_with(HashSet::default)
.extend(plugins.iter().cloned());
}
}
}
for (worktree, prettier_plugins) in prettier_plugins_by_worktree {

View File

@@ -2505,10 +2505,11 @@ impl Project {
buffers: HashSet<Model<Buffer>>,
push_to_history: bool,
trigger: lsp_store::FormatTrigger,
target: lsp_store::FormatTarget,
cx: &mut ModelContext<Project>,
) -> Task<anyhow::Result<ProjectTransaction>> {
self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.format(buffers, push_to_history, trigger, cx)
lsp_store.format(buffers, push_to_history, trigger, target, cx)
})
}
@@ -3978,17 +3979,6 @@ impl Project {
.read(cx)
.language_servers_for_buffer(buffer, cx)
}
pub fn language_server_for_buffer<'a>(
&'a self,
buffer: &'a Buffer,
server_id: LanguageServerId,
cx: &'a AppContext,
) -> Option<(&'a Arc<CachedLspAdapter>, &'a Arc<LanguageServer>)> {
self.lsp_store
.read(cx)
.language_server_for_buffer(buffer, server_id, cx)
}
}
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {

View File

@@ -6,6 +6,7 @@ use itertools::Itertools;
use settings::{Settings, SettingsLocation};
use smol::channel::bounded;
use std::{
borrow::Cow,
env::{self},
iter,
path::{Path, PathBuf},
@@ -341,10 +342,9 @@ pub fn wrap_for_ssh(
venv_directory: Option<PathBuf>,
) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command {
iter::once(command)
.chain(args)
.filter_map(|arg| shlex::try_quote(arg).ok())
.join(" ")
let command = Cow::Borrowed(command.as_str());
let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
iter::once(command).chain(args).join(" ")
} else {
"exec ${SHELL:-sh} -l".to_string()
};
@@ -390,9 +390,7 @@ pub fn wrap_for_ssh(
SshCommand::Direct(ssh_args) => ("ssh".to_string(), ssh_args.clone()),
};
if command.is_none() {
args.push("-t".to_string())
}
args.push("-t".to_string());
args.push(shell_invocation);
(program, args)
}

View File

@@ -247,14 +247,9 @@ impl WorktreeStore {
if abs_path.starts_with("/~") {
abs_path = abs_path[1..].to_string();
}
if abs_path.is_empty() {
if abs_path.is_empty() || abs_path == "/" {
abs_path = "~/".to_string();
}
let root_name = PathBuf::from(abs_path.clone())
.file_name()
.unwrap()
.to_string_lossy()
.to_string();
cx.spawn(|this, mut cx| async move {
let this = this.upgrade().context("Dropped worktree store")?;
@@ -272,6 +267,11 @@ impl WorktreeStore {
return Ok(existing_worktree);
}
let root_name = PathBuf::from(&response.canonicalized_path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or(response.canonicalized_path.to_string());
let worktree = cx.update(|cx| {
Worktree::remote(
SSH_PROJECT_ID,
@@ -280,7 +280,7 @@ impl WorktreeStore {
id: response.worktree_id,
root_name,
visible,
abs_path,
abs_path: response.canonicalized_path,
},
client,
cx,

View File

@@ -1,8 +1,8 @@
mod project_panel_settings;
mod scrollbar;
use client::{ErrorCode, ErrorExt};
use scrollbar::ProjectPanelScrollbar;
use settings::{Settings, SettingsStore};
use ui::{Scrollbar, ScrollbarState};
use db::kvp::KEY_VALUE_STORE;
use editor::{
@@ -14,16 +14,14 @@ use file_icons::FileIcons;
use anyhow::{anyhow, Context as _, Result};
use collections::{hash_map, BTreeSet, HashMap};
use core::f32;
use git::repository::GitFileStatus;
use gpui::{
actions, anchored, deferred, div, impl_actions, px, uniform_list, Action, AnyElement,
AppContext, AssetSource, AsyncWindowContext, ClipboardItem, DismissEvent, Div, DragMoveEvent,
Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement,
KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton,
MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView,
WindowContext,
EventEmitter, ExternalPaths, FocusHandle, FocusableView, InteractiveElement, KeyContext,
ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton, MouseDownEvent,
ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled, Subscription, Task,
UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
};
use indexmap::IndexMap;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev};
@@ -34,12 +32,11 @@ use project::{
use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
use serde::{Deserialize, Serialize};
use std::{
cell::{Cell, OnceCell},
cell::OnceCell,
collections::HashSet,
ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
time::Duration,
};
@@ -59,8 +56,8 @@ const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
pub struct ProjectPanel {
project: Model<Project>,
fs: Arc<dyn Fs>,
scroll_handle: UniformListScrollHandle,
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
visible_entries: Vec<(WorktreeId, Vec<Entry>, OnceCell<HashSet<Arc<Path>>>)>,
/// Maps from leaf project entry ID to the currently selected ancestor.
/// Relevant only for auto-fold dirs, where a single project panel entry may actually consist of several
@@ -82,8 +79,8 @@ pub struct ProjectPanel {
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
show_scrollbar: bool,
vertical_scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
horizontal_scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
vertical_scrollbar_state: ScrollbarState,
horizontal_scrollbar_state: ScrollbarState,
hide_scrollbar_task: Option<Task<()>>,
max_width_item_index: Option<usize>,
}
@@ -297,10 +294,10 @@ impl ProjectPanel {
})
.detach();
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
project: project.clone(),
fs: workspace.app_state().fs.clone(),
scroll_handle: UniformListScrollHandle::new(),
focus_handle,
visible_entries: Default::default(),
ancestors: Default::default(),
@@ -320,9 +317,12 @@ impl ProjectPanel {
pending_serialization: Task::ready(None),
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
vertical_scrollbar_drag_thumb_offset: Default::default(),
horizontal_scrollbar_drag_thumb_offset: Default::default(),
vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_view(cx.view()),
horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone())
.parent_view(cx.view()),
max_width_item_index: None,
scroll_handle,
};
this.update_visible_entries(None, cx);
@@ -2606,37 +2606,11 @@ impl ProjectPanel {
}
fn render_vertical_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx) {
return None;
}
let scroll_handle = self.scroll_handle.0.borrow();
let total_list_length = scroll_handle
.last_item_size
.filter(|_| {
self.show_scrollbar || self.vertical_scrollbar_drag_thumb_offset.get().is_some()
})?
.contents
.height
.0 as f64;
let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
let mut percentage = current_offset / total_list_length;
let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.height.0 as f64)
/ total_list_length;
// Uniform scroll handle might briefly report an offset greater than the length of a list;
// in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
let overshoot = (end_offset - 1.).clamp(0., 1.);
if overshoot > 0. {
percentage -= overshoot;
}
const MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT: f64 = 0.005;
if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT > 1.0 || end_offset > total_list_length
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
{
return None;
}
if total_list_length < scroll_handle.base_handle.bounds().size.height.0 as f64 {
return None;
}
let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_HEIGHT, 1.);
Some(
div()
.occlude()
@@ -2654,7 +2628,7 @@ impl ProjectPanel {
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, cx| {
if this.vertical_scrollbar_drag_thumb_offset.get().is_none()
if !this.vertical_scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(cx)
{
this.hide_scrollbar(cx);
@@ -2674,48 +2648,20 @@ impl ProjectPanel {
.bottom_1()
.w(px(12.))
.cursor_default()
.child(ProjectPanelScrollbar::vertical(
percentage as f32..end_offset as f32,
self.scroll_handle.clone(),
self.vertical_scrollbar_drag_thumb_offset.clone(),
cx.view().entity_id(),
.children(Scrollbar::vertical(
// percentage as f32..end_offset as f32,
self.vertical_scrollbar_state.clone(),
)),
)
}
fn render_horizontal_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<Stateful<Div>> {
if !Self::should_show_scrollbar(cx) {
return None;
}
let scroll_handle = self.scroll_handle.0.borrow();
let longest_item_width = scroll_handle
.last_item_size
.filter(|_| {
self.show_scrollbar || self.horizontal_scrollbar_drag_thumb_offset.get().is_some()
})
.filter(|size| size.contents.width > size.item.width)?
.contents
.width
.0 as f64;
let current_offset = scroll_handle.base_handle.offset().x.0.min(0.).abs() as f64;
let mut percentage = current_offset / longest_item_width;
let end_offset = (current_offset + scroll_handle.base_handle.bounds().size.width.0 as f64)
/ longest_item_width;
// Uniform scroll handle might briefly report an offset greater than the length of a list;
// in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
let overshoot = (end_offset - 1.).clamp(0., 1.);
if overshoot > 0. {
percentage -= overshoot;
}
const MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH: f64 = 0.005;
if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH > 1.0 || end_offset > longest_item_width
if !Self::should_show_scrollbar(cx)
|| !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
{
return None;
}
if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
return None;
}
let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_WIDTH, 1.);
Some(
div()
.occlude()
@@ -2733,7 +2679,7 @@ impl ProjectPanel {
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, cx| {
if this.horizontal_scrollbar_drag_thumb_offset.get().is_none()
if !this.horizontal_scrollbar_state.is_dragging()
&& !this.focus_handle.contains_focused(cx)
{
this.hide_scrollbar(cx);
@@ -2754,11 +2700,9 @@ impl ProjectPanel {
.h(px(12.))
.cursor_default()
.when(self.width.is_some(), |this| {
this.child(ProjectPanelScrollbar::horizontal(
percentage as f32..end_offset as f32,
self.scroll_handle.clone(),
self.horizontal_scrollbar_drag_thumb_offset.clone(),
cx.view().entity_id(),
this.children(Scrollbar::horizontal(
//percentage as f32..end_offset as f32,
self.horizontal_scrollbar_state.clone(),
))
}),
)

View File

@@ -1,277 +0,0 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use gpui::{
point, quad, Bounds, ContentMask, Corners, Edges, EntityId, Hitbox, Hsla, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ScrollWheelEvent, Style, UniformListScrollHandle,
};
use ui::{prelude::*, px, relative, IntoElement};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScrollbarKind {
Horizontal,
Vertical,
}
pub(crate) struct ProjectPanelScrollbar {
thumb: Range<f32>,
scroll: UniformListScrollHandle,
// If Some(), there's an active drag, offset by percentage from the top of thumb.
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
kind: ScrollbarKind,
parent_id: EntityId,
}
impl ProjectPanelScrollbar {
pub(crate) fn vertical(
thumb: Range<f32>,
scroll: UniformListScrollHandle,
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
parent_id: EntityId,
) -> Self {
Self {
thumb,
scroll,
scrollbar_drag_state,
kind: ScrollbarKind::Vertical,
parent_id,
}
}
pub(crate) fn horizontal(
thumb: Range<f32>,
scroll: UniformListScrollHandle,
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
parent_id: EntityId,
) -> Self {
Self {
thumb,
scroll,
scrollbar_drag_state,
kind: ScrollbarKind::Horizontal,
parent_id,
}
}
}
impl gpui::Element for ProjectPanelScrollbar {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ui::ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&gpui::GlobalElementId>,
cx: &mut ui::WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.flex_grow = 1.;
style.flex_shrink = 1.;
if self.kind == ScrollbarKind::Vertical {
style.size.width = px(12.).into();
style.size.height = relative(1.).into();
} else {
style.size.width = relative(1.).into();
style.size.height = px(12.).into();
}
(cx.request_layout(style, None), ())
}
fn prepaint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
bounds: Bounds<ui::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut ui::WindowContext,
) -> Self::PrepaintState {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
cx.insert_hitbox(bounds, false)
})
}
fn paint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
bounds: Bounds<ui::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
cx: &mut ui::WindowContext,
) {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
let colors = cx.theme().colors();
let thumb_background = colors.scrollbar_thumb_background;
let is_vertical = self.kind == ScrollbarKind::Vertical;
let extra_padding = px(5.0);
let padded_bounds = if is_vertical {
Bounds::from_corners(
bounds.origin + point(Pixels::ZERO, extra_padding),
bounds.lower_right() - point(Pixels::ZERO, extra_padding * 3),
)
} else {
Bounds::from_corners(
bounds.origin + point(extra_padding, Pixels::ZERO),
bounds.lower_right() - point(extra_padding * 3, Pixels::ZERO),
)
};
let mut thumb_bounds = if is_vertical {
let thumb_offset = self.thumb.start * padded_bounds.size.height;
let thumb_end = self.thumb.end * padded_bounds.size.height;
let thumb_upper_left = point(
padded_bounds.origin.x,
padded_bounds.origin.y + thumb_offset,
);
let thumb_lower_right = point(
padded_bounds.origin.x + padded_bounds.size.width,
padded_bounds.origin.y + thumb_end,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
} else {
let thumb_offset = self.thumb.start * padded_bounds.size.width;
let thumb_end = self.thumb.end * padded_bounds.size.width;
let thumb_upper_left = point(
padded_bounds.origin.x + thumb_offset,
padded_bounds.origin.y,
);
let thumb_lower_right = point(
padded_bounds.origin.x + thumb_end,
padded_bounds.origin.y + padded_bounds.size.height,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
};
let corners = if is_vertical {
thumb_bounds.size.width /= 1.5;
Corners::all(thumb_bounds.size.width / 2.0)
} else {
thumb_bounds.size.height /= 1.5;
Corners::all(thumb_bounds.size.height / 2.0)
};
cx.paint_quad(quad(
thumb_bounds,
corners,
thumb_background,
Edges::default(),
Hsla::transparent_black(),
));
let scroll = self.scroll.clone();
let kind = self.kind;
let thumb_percentage_size = self.thumb.end - self.thumb.start;
cx.on_mouse_event({
let scroll = self.scroll.clone();
let is_dragging = self.scrollbar_drag_state.clone();
move |event: &MouseDownEvent, phase, _cx| {
if phase.bubble() && bounds.contains(&event.position) {
if !thumb_bounds.contains(&event.position) {
let scroll = scroll.0.borrow();
if let Some(item_size) = scroll.last_item_size {
match kind {
ScrollbarKind::Horizontal => {
let percentage = (event.position.x - bounds.origin.x)
/ bounds.size.width;
let max_offset = item_size.contents.width;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll.base_handle.set_offset(point(
-max_offset * percentage,
scroll.base_handle.offset().y,
));
}
ScrollbarKind::Vertical => {
let percentage = (event.position.y - bounds.origin.y)
/ bounds.size.height;
let max_offset = item_size.contents.height;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll.base_handle.set_offset(point(
scroll.base_handle.offset().x,
-max_offset * percentage,
));
}
}
}
} else {
let thumb_offset = if is_vertical {
(event.position.y - thumb_bounds.origin.y) / bounds.size.height
} else {
(event.position.x - thumb_bounds.origin.x) / bounds.size.width
};
is_dragging.set(Some(thumb_offset));
}
}
}
});
cx.on_mouse_event({
let scroll = self.scroll.clone();
move |event: &ScrollWheelEvent, phase, cx| {
if phase.bubble() && bounds.contains(&event.position) {
let scroll = scroll.0.borrow_mut();
let current_offset = scroll.base_handle.offset();
scroll
.base_handle
.set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
}
}
});
let drag_state = self.scrollbar_drag_state.clone();
let view_id = self.parent_id;
let kind = self.kind;
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
let scroll = scroll.0.borrow();
if let Some(item_size) = scroll.last_item_size {
match kind {
ScrollbarKind::Horizontal => {
let max_offset = item_size.contents.width;
let percentage = (event.position.x - bounds.origin.x)
/ bounds.size.width
- drag_state;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll.base_handle.set_offset(point(
-max_offset * percentage,
scroll.base_handle.offset().y,
));
}
ScrollbarKind::Vertical => {
let max_offset = item_size.contents.height;
let percentage = (event.position.y - bounds.origin.y)
/ bounds.size.height
- drag_state;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll.base_handle.set_offset(point(
scroll.base_handle.offset().x,
-max_offset * percentage,
));
}
};
cx.notify(view_id);
}
} else {
drag_state.set(None);
}
});
let is_dragging = self.scrollbar_drag_state.clone();
cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
is_dragging.set(None);
cx.notify(view_id);
}
});
})
}
}
impl IntoElement for ProjectPanelScrollbar {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}

View File

@@ -1207,7 +1207,7 @@ message ResolveState {
}
message LspResolveState {
string value = 1;
optional string value = 1;
uint64 server_id = 2;
}
}
@@ -2464,6 +2464,7 @@ message AddWorktree {
message AddWorktreeResponse {
uint64 worktree_id = 1;
string canonicalized_path = 2;
}
message UpdateUserSettings {

View File

@@ -13,20 +13,19 @@ use futures::channel::oneshot;
use futures::future::Shared;
use futures::FutureExt;
use gpui::canvas;
use gpui::pulsating_between;
use gpui::AsyncWindowContext;
use gpui::ClipboardItem;
use gpui::Subscription;
use gpui::Task;
use gpui::WeakView;
use gpui::{
Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, FontWeight, Model, ScrollHandle, View, ViewContext,
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, FontWeight,
Model, PromptLevel, ScrollHandle, View, ViewContext,
};
use picker::Picker;
use project::terminals::wrap_for_ssh;
use project::terminals::SshCommand;
use project::Project;
use remote::SshConnectionOptions;
use rpc::proto::DevServerStatus;
use settings::update_settings_file;
use settings::Settings;
@@ -34,6 +33,8 @@ use task::HideStrategy;
use task::RevealStrategy;
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use ui::Scrollbar;
use ui::ScrollbarState;
use ui::Section;
use ui::{prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip};
use util::ResultExt;
@@ -58,16 +59,15 @@ pub struct DevServerProjects {
mode: Mode,
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
dev_server_store: Model<dev_server_projects::Store>,
workspace: WeakView<Workspace>,
_dev_server_subscription: Subscription,
selectable_items: SelectableItemList,
}
struct CreateDevServer {
address_editor: View<Editor>,
creating: Option<Task<Option<()>>>,
address_error: Option<SharedString>,
ssh_prompt: Option<View<SshPrompt>>,
_creating: Option<Task<Option<()>>>,
}
impl CreateDevServer {
@@ -78,8 +78,9 @@ impl CreateDevServer {
});
Self {
address_editor,
creating: None,
address_error: None,
ssh_prompt: None,
_creating: None,
}
}
}
@@ -301,13 +302,19 @@ impl gpui::Render for ProjectPicker {
}
}
enum Mode {
Default,
Default(ScrollbarState),
ViewServerOptions(usize, SshConnection),
EditNickname(EditNicknameState),
ProjectPicker(View<ProjectPicker>),
CreateDevServer(CreateDevServer),
}
impl Mode {
fn default_mode() -> Self {
let handle = ScrollHandle::new();
Self::Default(ScrollbarState::new(handle))
}
}
impl DevServerProjects {
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &OpenRemote, cx| {
@@ -325,11 +332,6 @@ impl DevServerProjects {
pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {
let focus_handle = cx.focus_handle();
let dev_server_store = dev_server_projects::Store::global(cx);
let subscription = cx.observe(&dev_server_store, |_, _, cx| {
cx.notify();
});
let mut base_style = cx.text_style();
base_style.refine(&gpui::TextStyleRefinement {
@@ -338,24 +340,22 @@ impl DevServerProjects {
});
Self {
mode: Mode::Default,
mode: Mode::default_mode(),
focus_handle,
scroll_handle: ScrollHandle::new(),
dev_server_store,
workspace,
_dev_server_subscription: subscription,
selectable_items: Default::default(),
}
}
fn next_item(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
if !matches!(self.mode, Mode::Default | Mode::ViewServerOptions(_, _)) {
if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
return;
}
self.selectable_items.next(cx);
}
fn prev_item(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
if !matches!(self.mode, Mode::Default | Mode::ViewServerOptions(_, _)) {
if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
return;
}
self.selectable_items.prev(cx);
@@ -380,34 +380,22 @@ impl DevServerProjects {
}
fn create_ssh_server(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let host = get_text(&editor, cx);
if host.is_empty() {
let input = get_text(&editor, cx);
if input.is_empty() {
return;
}
let mut host = host.trim_start_matches("ssh ");
let mut username: Option<String> = None;
let mut port: Option<u16> = None;
if let Some((u, rest)) = host.split_once('@') {
host = rest;
username = Some(u.to_string());
}
if let Some((rest, p)) = host.split_once(':') {
host = rest;
port = p.parse().ok()
}
if let Some((rest, p)) = host.split_once(" -p") {
host = rest;
port = p.trim().parse().ok()
}
let connection_options = remote::SshConnectionOptions {
host: host.to_string(),
username: username.clone(),
port,
password: None,
let connection_options = match SshConnectionOptions::parse_command_line(&input) {
Ok(c) => c,
Err(e) => {
self.mode = Mode::CreateDevServer(CreateDevServer {
address_editor: editor,
address_error: Some(format!("could not parse: {:?}", e).into()),
ssh_prompt: None,
_creating: None,
});
return;
}
};
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
@@ -419,6 +407,7 @@ impl DevServerProjects {
)
.prompt_err("Failed to connect", cx, |_, _| None);
let address_editor = editor.clone();
let creating = cx.spawn(move |this, mut cx| async move {
match connection.await {
Some(_) => this
@@ -431,25 +420,38 @@ impl DevServerProjects {
});
this.add_ssh_server(connection_options, cx);
this.mode = Mode::Default;
this.mode = Mode::default_mode();
this.selectable_items.reset_selection();
cx.notify()
})
.log_err(),
None => this
.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer::new(cx));
address_editor.update(cx, |this, _| {
this.set_read_only(false);
});
this.mode = Mode::CreateDevServer(CreateDevServer {
address_editor,
address_error: None,
ssh_prompt: None,
_creating: None,
});
cx.notify()
})
.log_err(),
};
None
});
let mut state = CreateDevServer::new(cx);
state.address_editor = editor;
state.ssh_prompt = Some(ssh_prompt.clone());
state.creating = Some(creating);
self.mode = Mode::CreateDevServer(state);
editor.update(cx, |this, _| {
this.set_read_only(true);
});
self.mode = Mode::CreateDevServer(CreateDevServer {
address_editor: editor,
address_error: None,
ssh_prompt: Some(ssh_prompt.clone()),
_creating: Some(creating),
});
}
fn view_server_options(
@@ -535,7 +537,7 @@ impl DevServerProjects {
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
match &self.mode {
Mode::Default | Mode::ViewServerOptions(_, _) => {
Mode::Default(_) | Mode::ViewServerOptions(_, _) => {
let items = std::mem::take(&mut self.selectable_items);
items.confirm(self, cx);
self.selectable_items = items;
@@ -549,9 +551,6 @@ impl DevServerProjects {
return;
}
state.address_editor.update(cx, |this, _| {
this.set_read_only(true);
});
self.create_ssh_server(state.address_editor.clone(), cx);
}
Mode::EditNickname(state) => {
@@ -566,7 +565,7 @@ impl DevServerProjects {
}
}
});
self.mode = Mode::Default;
self.mode = Mode::default_mode();
self.selectable_items.reset_selection();
self.focus_handle.focus(cx);
}
@@ -575,14 +574,14 @@ impl DevServerProjects {
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
match &self.mode {
Mode::Default => cx.emit(DismissEvent),
Mode::Default(_) => cx.emit(DismissEvent),
Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
self.mode = Mode::CreateDevServer(CreateDevServer::new(cx));
self.selectable_items.reset_selection();
cx.notify();
}
_ => {
self.mode = Mode::Default;
self.mode = Mode::default_mode();
self.selectable_items.reset_selection();
self.focus_handle(cx).focus(cx);
cx.notify();
@@ -814,6 +813,7 @@ impl DevServerProjects {
port: connection_options.port,
projects: vec![],
nickname: None,
args: connection_options.args.unwrap_or_default(),
})
});
}
@@ -827,10 +827,7 @@ impl DevServerProjects {
state.address_editor.update(cx, |editor, cx| {
if editor.text(cx).is_empty() {
editor.set_placeholder_text(
"Enter the command you use to SSH into this server: e.g., ssh me@my.server",
cx,
);
editor.set_placeholder_text("ssh user@example -p 2222", cx);
}
});
@@ -856,27 +853,38 @@ impl DevServerProjects {
.map(|this| {
if let Some(ssh_prompt) = ssh_prompt {
this.child(h_flex().w_full().child(ssh_prompt))
} else if let Some(address_error) = &state.address_error {
this.child(
h_flex().p_2().w_full().gap_2().child(
Label::new(address_error.clone())
.size(LabelSize::Small)
.color(Color::Error),
),
)
} else {
let color = Color::Muted.color(cx);
this.child(
h_flex()
.p_2()
.w_full()
.items_center()
.justify_center()
.gap_2()
.gap_1()
.child(
div().size_1p5().rounded_full().bg(color).with_animation(
"pulse-ssh-waiting-for-connection",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.2, 0.5)),
move |this, progress| this.bg(color.opacity(progress)),
),
Label::new(
"Enter the command you use to SSH into this server.",
)
.color(Color::Muted)
.size(LabelSize::Small),
)
.child(
Label::new("Waiting for connection")
.size(LabelSize::Small),
Button::new("learn-more", "Learn more")
.label_size(LabelSize::Small)
.size(ButtonSize::None)
.color(Color::Accent)
.style(ButtonStyle::Transparent)
.on_click(|_, cx| {
cx.open_url(
"https://zed.dev/docs/remote-development",
);
}),
),
)
}
@@ -988,46 +996,38 @@ impl DevServerProjects {
.child({
fn remove_ssh_server(
dev_servers: View<DevServerProjects>,
workspace: WeakView<Workspace>,
index: usize,
connection_string: SharedString,
cx: &mut WindowContext<'_>,
) {
workspace
.update(cx, |this, cx| {
struct SshServerRemoval;
let notification = format!(
"Do you really want to remove server `{}`?",
connection_string
);
this.show_toast(
Toast::new(
NotificationId::composite::<SshServerRemoval>(
connection_string.clone(),
),
notification,
)
.on_click(
"Yes, delete it",
move |cx| {
dev_servers.update(cx, |this, cx| {
this.delete_ssh_server(index, cx);
this.mode = Mode::Default;
cx.notify();
})
},
),
cx,
);
})
.ok();
let prompt_message = format!("Remove server `{}`?", connection_string);
let confirmation = cx.prompt(
PromptLevel::Warning,
&prompt_message,
None,
&["Yes, remove it", "No, keep it"],
);
cx.spawn(|mut cx| async move {
if confirmation.await.ok() == Some(0) {
dev_servers
.update(&mut cx, |this, cx| {
this.delete_ssh_server(index, cx);
this.mode = Mode::default_mode();
cx.notify();
})
.ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
self.selectable_items.add_item(Box::new({
let connection_string = connection_string.clone();
move |this, cx| {
move |_, cx| {
remove_ssh_server(
cx.view().clone(),
this.workspace.clone(),
index,
connection_string.clone(),
cx,
@@ -1035,16 +1035,15 @@ impl DevServerProjects {
}
}));
let is_selected = self.selectable_items.is_selected();
ListItem::new("delete-server")
ListItem::new("remove-server")
.selected(is_selected)
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Trash).color(Color::Error))
.child(Label::new("Delete Server").color(Color::Error))
.on_click(cx.listener(move |this, _, cx| {
.child(Label::new("Remove Server").color(Color::Error))
.on_click(cx.listener(move |_, _, cx| {
remove_ssh_server(
cx.view().clone(),
this.workspace.clone(),
index,
connection_string.clone(),
cx,
@@ -1055,7 +1054,7 @@ impl DevServerProjects {
.child({
self.selectable_items.add_item(Box::new({
move |this, cx| {
this.mode = Mode::Default;
this.mode = Mode::default_mode();
cx.notify();
}
}));
@@ -1067,7 +1066,7 @@ impl DevServerProjects {
.start_slot(Icon::new(IconName::ArrowLeft).color(Color::Muted))
.child(Label::new("Go Back"))
.on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
this.mode = Mode::default_mode();
cx.notify()
}))
}),
@@ -1099,8 +1098,12 @@ impl DevServerProjects {
.child(h_flex().p_2().child(state.editor.clone()))
}
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let dev_servers = self.dev_server_store.read(cx).dev_servers();
fn render_default(
&mut self,
scroll_state: ScrollbarState,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let scroll_state = scroll_state.parent_view(cx.view());
let ssh_connections = SshSettings::get_global(cx)
.ssh_connections()
.collect::<Vec<_>>();
@@ -1124,64 +1127,78 @@ impl DevServerProjects {
cx.notify();
}));
let ui::ScrollableHandle::NonUniform(scroll_handle) = scroll_state.scroll_handle() else {
unreachable!()
};
let mut modal_section = v_flex()
.id("ssh-server-list")
.overflow_y_scroll()
.track_scroll(&scroll_handle)
.size_full()
.child(connect_button)
.child(
List::new()
.empty_message(
v_flex()
.child(ListSeparator)
.child(div().px_3().child(
Label::new("No dev servers registered yet.").color(Color::Muted),
))
.into_any_element(),
)
.children(ssh_connections.iter().cloned().enumerate().map(
|(ix, connection)| {
self.render_ssh_connection(ix, connection, cx)
.into_any_element()
},
)),
h_flex().child(
List::new()
.empty_message(
v_flex()
.child(ListSeparator)
.child(
div().px_3().child(
Label::new("No dev servers registered yet.")
.color(Color::Muted),
),
)
.into_any_element(),
)
.children(ssh_connections.iter().cloned().enumerate().map(
|(ix, connection)| {
self.render_ssh_connection(ix, connection, cx)
.into_any_element()
},
)),
),
)
.into_any_element();
let server_count = format!("Servers: {}", ssh_connections.len() + dev_servers.len());
Modal::new("remote-projects", Some(self.scroll_handle.clone()))
.header(
ModalHeader::new().child(
h_flex()
.items_center()
.justify_between()
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall))
.child(Label::new(server_count).size(LabelSize::Small)),
),
ModalHeader::new()
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall)),
)
.section(
Section::new().padded(false).child(
v_flex()
h_flex()
.min_h(rems(20.))
.flex_1()
.size_full()
.child(ListSeparator)
.child(
canvas(
|bounds, cx| {
modal_section.prepaint_as_root(
bounds.origin,
bounds.size.into(),
cx,
);
modal_section
},
|_, mut modal_section, cx| {
modal_section.paint(cx);
},
)
.size_full(),
v_flex().size_full().child(ListSeparator).child(
canvas(
|bounds, cx| {
modal_section.prepaint_as_root(
bounds.origin,
bounds.size.into(),
cx,
);
modal_section
},
|_, mut modal_section, cx| {
modal_section.paint(cx);
},
)
.size_full(),
),
)
.child(
div()
.occlude()
.h_full()
.absolute()
.right_1()
.top_1()
.bottom_1()
.w(px(12.))
.children(Scrollbar::vertical(scroll_state)),
),
),
)
@@ -1217,13 +1234,13 @@ impl Render for DevServerProjects {
this.focus_handle(cx).focus(cx);
}))
.on_mouse_down_out(cx.listener(|this, _, cx| {
if matches!(this.mode, Mode::Default) {
if matches!(this.mode, Mode::Default(_)) {
cx.emit(DismissEvent)
}
}))
.w(rems(34.))
.child(match &self.mode {
Mode::Default => self.render_default(cx).into_any_element(),
Mode::Default(state) => self.render_default(state.clone(), cx).into_any_element(),
Mode::ViewServerOptions(index, connection) => self
.render_view_options(*index, connection.clone(), cx)
.into_any_element(),

View File

@@ -21,7 +21,7 @@ use picker::{
use rpc::proto::DevServerStatus;
use serde::Deserialize;
use settings::Settings;
use ssh_connections::SshSettings;
pub use ssh_connections::SshSettings;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -384,11 +384,13 @@ impl PickerDelegate for RecentProjectsDelegate {
..Default::default()
};
let args = SshSettings::get_global(cx).args_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
password: None,
args,
};
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();

View File

@@ -32,6 +32,23 @@ impl SshSettings {
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> {
self.ssh_connections.clone().into_iter().flatten()
}
pub fn args_for(
&self,
host: &str,
port: Option<u16>,
user: &Option<String>,
) -> Option<Vec<String>> {
self.ssh_connections()
.filter_map(|conn| {
if conn.host == host && &conn.username == user && conn.port == port {
Some(conn.args)
} else {
None
}
})
.next()
}
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
@@ -45,6 +62,9 @@ pub struct SshConnection {
/// Name to use for this server in UI.
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<SharedString>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub args: Vec<String>,
}
impl From<SshConnection> for SshConnectionOptions {
fn from(val: SshConnection) -> Self {
@@ -53,6 +73,7 @@ impl From<SshConnection> for SshConnectionOptions {
username: val.username,
port: val.port,
password: None,
args: Some(val.args),
}
}
}
@@ -151,11 +172,9 @@ impl Render for SshPrompt {
v_flex()
.key_context("PasswordPrompt")
.size_full()
.justify_center()
.child(
h_flex()
.p_2()
.justify_center()
.flex_wrap()
.child(if self.error_message.is_some() {
Icon::new(IconName::XCircle)
@@ -174,24 +193,19 @@ impl Render for SshPrompt {
)
.into_any_element()
})
.child(
div()
.ml_1()
.child(Label::new("SSH Connection").size(LabelSize::Small)),
)
.child(
div()
.text_ellipsis()
.overflow_x_hidden()
.when_some(self.error_message.as_ref(), |el, error| {
el.child(Label::new(format!("{}", error)).size(LabelSize::Small))
el.child(Label::new(format!("{}", error)).size(LabelSize::Small))
})
.when(
self.error_message.is_none() && self.status_message.is_some(),
|el| {
el.child(
Label::new(format!(
"{}",
"{}",
self.status_message.clone().unwrap()
))
.size(LabelSize::Small),

View File

@@ -29,6 +29,7 @@ prost.workspace = true
rpc = { workspace = true, features = ["gpui"] }
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
smol.workspace = true
tempfile.workspace = true
thiserror.workspace = true

View File

@@ -61,9 +61,89 @@ pub struct SshConnectionOptions {
pub username: Option<String>,
pub port: Option<u16>,
pub password: Option<String>,
pub args: Option<Vec<String>>,
}
impl SshConnectionOptions {
pub fn parse_command_line(input: &str) -> Result<Self> {
let input = input.trim_start_matches("ssh ");
let mut hostname: Option<String> = None;
let mut username: Option<String> = None;
let mut port: Option<u16> = None;
let mut args = Vec::new();
// disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W
const ALLOWED_OPTS: &[&str] = &[
"-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y",
];
const ALLOWED_ARGS: &[&str] = &[
"-B", "-b", "-c", "-D", "-I", "-i", "-J", "-L", "-l", "-m", "-o", "-P", "-p", "-R",
"-w",
];
let mut tokens = shlex::split(input)
.ok_or_else(|| anyhow!("invalid input"))?
.into_iter();
'outer: while let Some(arg) = tokens.next() {
if ALLOWED_OPTS.contains(&(&arg as &str)) {
args.push(arg.to_string());
continue;
}
if arg == "-p" {
port = tokens.next().and_then(|arg| arg.parse().ok());
continue;
} else if let Some(p) = arg.strip_prefix("-p") {
port = p.parse().ok();
continue;
}
if arg == "-l" {
username = tokens.next();
continue;
} else if let Some(l) = arg.strip_prefix("-l") {
username = Some(l.to_string());
continue;
}
for a in ALLOWED_ARGS {
if arg == *a {
args.push(arg);
if let Some(next) = tokens.next() {
args.push(next);
}
continue 'outer;
} else if arg.starts_with(a) {
args.push(arg);
continue 'outer;
}
}
if arg.starts_with("-") || hostname.is_some() {
anyhow::bail!("unsupported argument: {:?}", arg);
}
let mut input = &arg as &str;
if let Some((u, rest)) = input.split_once('@') {
input = rest;
username = Some(u.to_string());
}
if let Some((rest, p)) = input.split_once(':') {
input = rest;
port = p.parse().ok()
}
hostname = Some(input.to_string())
}
let Some(hostname) = hostname else {
anyhow::bail!("missing hostname");
};
Ok(Self {
host: hostname.to_string(),
username: username.clone(),
port,
password: None,
args: Some(args),
})
}
pub fn ssh_url(&self) -> String {
let mut result = String::from("ssh://");
if let Some(username) = &self.username {
@@ -78,6 +158,10 @@ impl SshConnectionOptions {
result
}
pub fn additional_args(&self) -> Option<&Vec<String>> {
self.args.as_ref()
}
fn scp_url(&self) -> String {
if let Some(username) = &self.username {
format!("{}@{}", username, self.host)
@@ -743,7 +827,11 @@ impl SshRemoteClient {
loop {
select_biased! {
_ = connection_activity_rx.next().fuse() => {
result = connection_activity_rx.next().fuse() => {
if result.is_none() {
log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping.");
return Ok(());
}
keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse());
}
_ = keepalive_timer => {
@@ -975,16 +1063,9 @@ impl SshRemoteClient {
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
let platform = ssh_connection.query_platform().await?;
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
ssh_connection
.ensure_server_binary(
&delegate,
&local_binary_path,
&remote_binary_path,
version,
cx,
)
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
let socket = ssh_connection.socket.clone();
@@ -1182,7 +1263,15 @@ impl SshRemoteConnection {
.stderr(Stdio::piped())
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", &askpass_script_path)
.args(["-N", "-o", "ControlMaster=yes", "-o"])
.args(connection_options.additional_args().unwrap_or(&Vec::new()))
.args([
"-N",
"-o",
"ControlPersist=no",
"-o",
"ControlMaster=yes",
"-o",
])
.arg(format!("ControlPath={}", socket_path.display()))
.arg(&url)
.spawn()?;
@@ -1241,11 +1330,19 @@ impl SshRemoteConnection {
async fn ensure_server_binary(
&self,
delegate: &Arc<dyn SshClientDelegate>,
src_path: &Path,
dst_path: &Path,
version: SemanticVersion,
platform: SshPlatform,
cx: &mut AsyncAppContext,
) -> Result<()> {
if std::env::var("ZED_USE_CACHED_REMOTE_SERVER").is_ok() {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
log::info!("using cached server binary version {}", installed_version);
return Ok(());
}
}
let mut dst_path_gz = dst_path.to_path_buf();
dst_path_gz.set_extension("gz");
@@ -1253,8 +1350,10 @@ impl SshRemoteConnection {
run_cmd(self.socket.ssh_command("mkdir").arg("-p").arg(parent)).await?;
}
let (src_path, version) = delegate.get_server_binary(platform, cx).await??;
let mut server_binary_exists = false;
if cfg!(not(debug_assertions)) {
if !server_binary_exists && cfg!(not(debug_assertions)) {
if let Ok(installed_version) =
run_cmd(self.socket.ssh_command(dst_path).arg("version")).await
{
@@ -1269,14 +1368,14 @@ impl SshRemoteConnection {
return Ok(());
}
let src_stat = fs::metadata(src_path).await?;
let src_stat = fs::metadata(&src_path).await?;
let size = src_stat.len();
let server_mode = 0o755;
let t0 = Instant::now();
delegate.set_status(Some("uploading remote development server"), cx);
log::info!("uploading remote development server ({}kb)", size / 1024);
self.upload_file(src_path, &dst_path_gz)
self.upload_file(&src_path, &dst_path_gz)
.await
.context("failed to upload server binary")?;
log::info!("uploaded remote development server in {:?}", t0.elapsed());

View File

@@ -275,7 +275,7 @@ impl HeadlessProject {
let worktree = this
.update(&mut cx.clone(), |this, _| {
Worktree::local(
Arc::from(canonicalized),
Arc::from(canonicalized.as_path()),
message.payload.visible,
this.fs.clone(),
this.next_entry_id.clone(),
@@ -287,6 +287,7 @@ impl HeadlessProject {
let response = this.update(&mut cx, |_, cx| {
worktree.update(cx, |worktree, _| proto::AddWorktreeResponse {
worktree_id: worktree.id().to_proto(),
canonicalized_path: canonicalized.to_string_lossy().to_string(),
})
})?;

View File

@@ -1,6 +1,5 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
@@ -40,13 +39,13 @@ fn main() {
}
#[cfg(not(windows))]
fn main() -> Result<()> {
fn main() {
use remote::proxy::ProxyLaunchError;
use remote_server::unix::{execute_proxy, execute_run};
let cli = Cli::parse();
match cli.command {
let result = match cli.command {
Some(Commands::Run {
log_file,
pid_file,
@@ -74,11 +73,15 @@ fn main() -> Result<()> {
},
Some(Commands::Version) => {
eprintln!("{}", env!("ZED_PKG_VERSION"));
Ok(())
std::process::exit(0);
}
None => {
eprintln!("usage: remote <run|proxy|version>");
std::process::exit(1);
}
};
if let Err(error) = result {
log::error!("exiting due to error: {}", error);
std::process::exit(1);
}
}

View File

@@ -186,7 +186,6 @@ fn start_server(
log::info!("accepting new connections");
let result = select! {
streams = streams.fuse() => {
log::warn!("stdin {:?}, stdout: {:?}, stderr: {:?}", streams.0, streams.1, streams.2);
let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream)), Some(Ok(stderr_stream))) = streams else {
break;
};
@@ -211,8 +210,6 @@ fn start_server(
break;
};
log::info!("yep! we got connections");
let mut input_buffer = Vec::new();
let mut output_buffer = Vec::new();
loop {
@@ -253,7 +250,6 @@ fn start_server(
}
}
// // TODO: How do we handle backpressure?
log_message = log_rx.next().fuse() => {
if let Some(log_message) = log_message {
if let Err(error) = stderr_stream.write_all(&log_message).await {
@@ -316,7 +312,7 @@ pub fn execute_run(
let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?;
log::debug!("starting gpui app");
log::info!("starting headless gpui app");
gpui::App::headless().run(move |cx| {
settings::init(cx);
HeadlessProject::init(cx);
@@ -404,7 +400,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
init_logging_proxy();
init_panic_hook();
log::debug!("starting up. PID: {}", std::process::id());
log::info!("starting proxy process. PID: {}", std::process::id());
let server_paths = ServerPaths::new(&identifier)?;
@@ -417,7 +413,7 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
}
} else {
if let Some(pid) = server_pid {
log::debug!("found server already running with PID {}. Killing process and cleaning up files...", pid);
log::info!("proxy found server already running with PID {}. Killing process and cleaning up files...", pid);
kill_running_server(pid, &server_paths)?;
}
@@ -443,7 +439,9 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
loop {
match stream.read(&mut stderr_buffer).await {
Ok(0) => {
return anyhow::Ok(());
let error =
std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "stderr closed");
Err(anyhow!(error))?;
}
Ok(n) => {
stderr.write_all(&mut stderr_buffer[..n]).await?;
@@ -463,6 +461,12 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
result = stderr_task.fuse() => result,
}
}) {
if let Some(error) = forwarding_result.downcast_ref::<std::io::Error>() {
if error.kind() == std::io::ErrorKind::UnexpectedEof {
log::error!("connection to server closed due to unexpected EOF");
return Err(anyhow!("connection to server closed"));
}
}
log::error!(
"failed to forward messages: {:?}, terminating...",
forwarding_result
@@ -518,7 +522,10 @@ fn spawn_server(paths: &ServerPaths) -> Result<()> {
.arg(&paths.stderr_socket)
.spawn()?;
log::debug!("server started. PID: {:?}", server_process.id());
log::info!(
"proxy spawned server process. PID: {:?}",
server_process.id()
);
let mut total_time_waited = std::time::Duration::from_secs(0);
let wait_duration = std::time::Duration::from_millis(20);

View File

@@ -384,13 +384,6 @@ impl PickerDelegate for TasksModalDelegate {
.start_slot::<Icon>(icon)
.end_slot::<AnyElement>(history_run_icon)
.spacing(ListItemSpacing::Sparse)
// .map(|this| {
// if Some(ix) <= self.divider_index {
// this.start_slot(Icon::new(IconName::HistoryRerun).size(IconSize::Small))
// } else {
// this.start_slot(v_flex().flex_none().size(IconSize::Small.rems()))
// }
// })
.when_some(tooltip_label, |list_item, item_label| {
list_item.tooltip(move |_| item_label.clone())
})

View File

@@ -395,7 +395,21 @@ impl TerminalPanel {
let mut spawn_task = spawn_in_terminal.clone();
// Set up shell args unconditionally, as tasks are always spawned inside of a shell.
let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
Shell::System => retrieve_system_shell().map(|shell| (shell, Vec::new())),
Shell::System => {
match self
.workspace
.update(cx, |workspace, cx| workspace.project().read(cx).is_local())
{
Ok(local) => {
if local {
retrieve_system_shell().map(|shell| (shell, Vec::new()))
} else {
Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
}
}
Err(_no_window_e) => return,
}
}
Shell::Program(shell) => Some((shell, Vec::new())),
Shell::WithArguments { program, args } => Some((program, args)),
}) else {

View File

@@ -24,8 +24,8 @@ use smallvec::SmallVec;
use std::sync::Arc;
use theme::ActiveTheme;
use ui::{
h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
IconButtonShape, IconName, IconSize, Indicator, PopoverMenu, Tooltip,
h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
};
use util::ResultExt;
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
@@ -281,8 +281,6 @@ impl TitleBar {
}
};
let indicator_border_color = cx.theme().colors().title_bar_background;
let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
remote::ConnectionState::Connecting => Color::Info,
remote::ConnectionState::Connected => Color::Default,
@@ -293,42 +291,22 @@ impl TitleBar {
let meta = SharedString::from(meta);
let indicator = h_flex()
// We're using the circle inside a circle approach because, otherwise, by using borders
// we'd get a very thin, leaking indicator color, which is not what we want.
.absolute()
.size_2p5()
.right_0()
.bottom_0()
.bg(indicator_border_color)
.size_2p5()
.rounded_full()
.items_center()
.justify_center()
.overflow_hidden()
.child(Indicator::dot().color(indicator_color));
Some(
div()
.relative()
ButtonLike::new("ssh-server-icon")
.child(
IconButton::new("ssh-server-icon", IconName::Server)
.icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.icon_color(icon_color)
.tooltip(move |cx| {
Tooltip::with_meta(
"Remote Project",
Some(&OpenRemote),
meta.clone(),
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(OpenRemote.boxed_clone());
}),
IconWithIndicator::new(
Icon::new(IconName::Server).color(icon_color),
Some(Indicator::dot().color(indicator_color)),
)
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
.into_any_element(),
)
.child(indicator)
.tooltip(move |cx| {
Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)
})
.on_click(|_, cx| {
cx.dispatch_action(OpenRemote.boxed_clone());
})
.into_any_element(),
)
}

View File

@@ -18,6 +18,7 @@ mod popover;
mod popover_menu;
mod radio;
mod right_click_menu;
mod scrollbar;
mod settings_container;
mod settings_group;
mod stack;
@@ -49,6 +50,7 @@ pub use popover::*;
pub use popover_menu::*;
pub use radio::*;
pub use right_click_menu::*;
pub use scrollbar::*;
pub use settings_container::*;
pub use settings_group::*;
pub use stack::*;

View File

@@ -473,13 +473,12 @@ impl RenderOnce for IconWithIndicator {
this.child(
div()
.absolute()
.w_2()
.h_2()
.border_1()
.size_2p5()
.border_2()
.border_color(indicator_border_color)
.rounded_full()
.bottom_neg_0p5()
.right_neg_1()
.right_neg_0p5()
.child(indicator),
)
})

View File

@@ -0,0 +1,396 @@
#![allow(missing_docs)]
use std::{cell::Cell, ops::Range, rc::Rc};
use crate::{prelude::*, px, relative, IntoElement};
use gpui::{
point, quad, Along, Axis as ScrollbarAxis, Bounds, ContentMask, Corners, Edges, Element,
ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent, Size, Style,
UniformListScrollHandle, View, WindowContext,
};
pub struct Scrollbar {
thumb: Range<f32>,
state: ScrollbarState,
kind: ScrollbarAxis,
}
/// Wrapper around scroll handles.
#[derive(Clone)]
pub enum ScrollableHandle {
Uniform(UniformListScrollHandle),
NonUniform(ScrollHandle),
}
#[derive(Debug)]
struct ContentSize {
size: Size<Pixels>,
scroll_adjustment: Option<Point<Pixels>>,
}
impl ScrollableHandle {
fn content_size(&self) -> Option<ContentSize> {
match self {
ScrollableHandle::Uniform(handle) => Some(ContentSize {
size: handle.0.borrow().last_item_size.map(|size| size.contents)?,
scroll_adjustment: None,
}),
ScrollableHandle::NonUniform(handle) => {
let last_children_index = handle.children_count().checked_sub(1)?;
// todo: PO: this is slightly wrong for horizontal scrollbar, as the last item is not necessarily the longest one.
let mut last_item = handle.bounds_for_item(last_children_index)?;
last_item.size.height += last_item.origin.y;
last_item.size.width += last_item.origin.x;
let mut scroll_adjustment = None;
if last_children_index != 0 {
let first_item = handle.bounds_for_item(0)?;
scroll_adjustment = Some(first_item.origin);
last_item.size.height -= first_item.origin.y;
last_item.size.width -= first_item.origin.x;
}
Some(ContentSize {
size: last_item.size,
scroll_adjustment,
})
}
}
}
fn set_offset(&self, point: Point<Pixels>) {
let base_handle = match self {
ScrollableHandle::Uniform(handle) => &handle.0.borrow().base_handle,
ScrollableHandle::NonUniform(handle) => &handle,
};
base_handle.set_offset(point);
}
fn offset(&self) -> Point<Pixels> {
let base_handle = match self {
ScrollableHandle::Uniform(handle) => &handle.0.borrow().base_handle,
ScrollableHandle::NonUniform(handle) => &handle,
};
base_handle.offset()
}
fn viewport(&self) -> Bounds<Pixels> {
let base_handle = match self {
ScrollableHandle::Uniform(handle) => &handle.0.borrow().base_handle,
ScrollableHandle::NonUniform(handle) => &handle,
};
base_handle.bounds()
}
}
impl From<UniformListScrollHandle> for ScrollableHandle {
fn from(value: UniformListScrollHandle) -> Self {
Self::Uniform(value)
}
}
impl From<ScrollHandle> for ScrollableHandle {
fn from(value: ScrollHandle) -> Self {
Self::NonUniform(value)
}
}
/// A scrollbar state that should be persisted across frames.
#[derive(Clone)]
pub struct ScrollbarState {
// If Some(), there's an active drag, offset by percentage from the origin of a thumb.
drag: Rc<Cell<Option<f32>>>,
parent_id: Option<EntityId>,
scroll_handle: ScrollableHandle,
}
impl ScrollbarState {
pub fn new(scroll: impl Into<ScrollableHandle>) -> Self {
Self {
drag: Default::default(),
parent_id: None,
scroll_handle: scroll.into(),
}
}
/// Set a parent view which should be notified whenever this Scrollbar gets a scroll event.
pub fn parent_view<V: 'static>(mut self, v: &View<V>) -> Self {
self.parent_id = Some(v.entity_id());
self
}
pub fn scroll_handle(&self) -> ScrollableHandle {
self.scroll_handle.clone()
}
pub fn is_dragging(&self) -> bool {
self.drag.get().is_some()
}
fn thumb_range(&self, axis: ScrollbarAxis) -> Option<Range<f32>> {
const MINIMUM_SCROLLBAR_PERCENTAGE_SIZE: f32 = 0.005;
let ContentSize {
size: main_dimension_size,
scroll_adjustment,
} = self.scroll_handle.content_size()?;
let main_dimension_size = main_dimension_size.along(axis).0;
let mut current_offset = self.scroll_handle.offset().along(axis).min(px(0.)).abs().0;
if let Some(adjustment) = scroll_adjustment.and_then(|adjustment| {
let adjust = adjustment.along(axis).0;
if adjust < 0.0 {
Some(adjust)
} else {
None
}
}) {
current_offset -= adjustment;
}
let mut percentage = current_offset / main_dimension_size;
let viewport_size = self.scroll_handle.viewport().size;
let end_offset = (current_offset + viewport_size.along(axis).0) / main_dimension_size;
// Scroll handle might briefly report an offset greater than the length of a list;
// in such case we'll adjust the starting offset as well to keep the scrollbar thumb length stable.
let overshoot = (end_offset - 1.).clamp(0., 1.);
if overshoot > 0. {
percentage -= overshoot;
}
if percentage + MINIMUM_SCROLLBAR_PERCENTAGE_SIZE > 1.0 || end_offset > main_dimension_size
{
return None;
}
if main_dimension_size < viewport_size.along(axis).0 {
return None;
}
let end_offset = end_offset.clamp(percentage + MINIMUM_SCROLLBAR_PERCENTAGE_SIZE, 1.);
Some(percentage..end_offset)
}
}
impl Scrollbar {
pub fn vertical(state: ScrollbarState) -> Option<Self> {
Self::new(state, ScrollbarAxis::Vertical)
}
pub fn horizontal(state: ScrollbarState) -> Option<Self> {
Self::new(state, ScrollbarAxis::Horizontal)
}
fn new(state: ScrollbarState, kind: ScrollbarAxis) -> Option<Self> {
let thumb = state.thumb_range(kind)?;
Some(Self { thumb, state, kind })
}
}
impl Element for Scrollbar {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.flex_grow = 1.;
style.flex_shrink = 1.;
if self.kind == ScrollbarAxis::Vertical {
style.size.width = px(12.).into();
style.size.height = relative(1.).into();
} else {
style.size.width = relative(1.).into();
style.size.height = px(12.).into();
}
(cx.request_layout(style, None), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
cx.insert_hitbox(bounds, false)
})
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
let colors = cx.theme().colors();
let thumb_background = colors.scrollbar_thumb_background;
let is_vertical = self.kind == ScrollbarAxis::Vertical;
let extra_padding = px(5.0);
let padded_bounds = if is_vertical {
Bounds::from_corners(
bounds.origin + point(Pixels::ZERO, extra_padding),
bounds.lower_right() - point(Pixels::ZERO, extra_padding * 3),
)
} else {
Bounds::from_corners(
bounds.origin + point(extra_padding, Pixels::ZERO),
bounds.lower_right() - point(extra_padding * 3, Pixels::ZERO),
)
};
let mut thumb_bounds = if is_vertical {
let thumb_offset = self.thumb.start * padded_bounds.size.height;
let thumb_end = self.thumb.end * padded_bounds.size.height;
let thumb_upper_left = point(
padded_bounds.origin.x,
padded_bounds.origin.y + thumb_offset,
);
let thumb_lower_right = point(
padded_bounds.origin.x + padded_bounds.size.width,
padded_bounds.origin.y + thumb_end,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
} else {
let thumb_offset = self.thumb.start * padded_bounds.size.width;
let thumb_end = self.thumb.end * padded_bounds.size.width;
let thumb_upper_left = point(
padded_bounds.origin.x + thumb_offset,
padded_bounds.origin.y,
);
let thumb_lower_right = point(
padded_bounds.origin.x + thumb_end,
padded_bounds.origin.y + padded_bounds.size.height,
);
Bounds::from_corners(thumb_upper_left, thumb_lower_right)
};
let corners = if is_vertical {
thumb_bounds.size.width /= 1.5;
Corners::all(thumb_bounds.size.width / 2.0)
} else {
thumb_bounds.size.height /= 1.5;
Corners::all(thumb_bounds.size.height / 2.0)
};
cx.paint_quad(quad(
thumb_bounds,
corners,
thumb_background,
Edges::default(),
Hsla::transparent_black(),
));
let scroll = self.state.scroll_handle.clone();
let kind = self.kind;
let thumb_percentage_size = self.thumb.end - self.thumb.start;
cx.on_mouse_event({
let scroll = scroll.clone();
let state = self.state.clone();
let axis = self.kind;
move |event: &MouseDownEvent, phase, _cx| {
if !(phase.bubble() && bounds.contains(&event.position)) {
return;
}
if thumb_bounds.contains(&event.position) {
let thumb_offset = (event.position.along(axis)
- thumb_bounds.origin.along(axis))
/ bounds.size.along(axis);
state.drag.set(Some(thumb_offset));
} else if let Some(ContentSize {
size: item_size, ..
}) = scroll.content_size()
{
match kind {
ScrollbarAxis::Horizontal => {
let percentage =
(event.position.x - bounds.origin.x) / bounds.size.width;
let max_offset = item_size.width;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll
.set_offset(point(-max_offset * percentage, scroll.offset().y));
}
ScrollbarAxis::Vertical => {
let percentage =
(event.position.y - bounds.origin.y) / bounds.size.height;
let max_offset = item_size.height;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll
.set_offset(point(scroll.offset().x, -max_offset * percentage));
}
}
}
}
});
cx.on_mouse_event({
let scroll = scroll.clone();
move |event: &ScrollWheelEvent, phase, cx| {
if phase.bubble() && bounds.contains(&event.position) {
let current_offset = scroll.offset();
scroll
.set_offset(current_offset + event.delta.pixel_delta(cx.line_height()));
}
}
});
let state = self.state.clone();
let kind = self.kind;
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) {
if let Some(ContentSize {
size: item_size, ..
}) = scroll.content_size()
{
match kind {
ScrollbarAxis::Horizontal => {
let max_offset = item_size.width;
let percentage = (event.position.x - bounds.origin.x)
/ bounds.size.width
- drag_state;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll
.set_offset(point(-max_offset * percentage, scroll.offset().y));
}
ScrollbarAxis::Vertical => {
let max_offset = item_size.height;
let percentage = (event.position.y - bounds.origin.y)
/ bounds.size.height
- drag_state;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll
.set_offset(point(scroll.offset().x, -max_offset * percentage));
}
};
if let Some(id) = state.parent_id {
cx.notify(id);
}
}
} else {
state.drag.set(None);
}
});
let state = self.state.clone();
cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
state.drag.take();
if let Some(id) = state.parent_id {
cx.notify(id);
}
}
});
})
}
}
impl IntoElement for Scrollbar {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}

View File

@@ -11,7 +11,7 @@ use db::sqlez::{
};
use gpui::{AsyncWindowContext, Model, View, WeakView};
use project::Project;
use remote::{ssh_session::SshProjectId, SshConnectionOptions};
use remote::ssh_session::SshProjectId;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
@@ -50,15 +50,6 @@ impl SerializedSshProject {
})
.collect()
}
pub fn connection_options(&self) -> SshConnectionOptions {
SshConnectionOptions {
host: self.host.clone(),
username: self.user.clone(),
port: self.port,
password: None,
}
}
}
impl StaticColumnCount for SerializedSshProject {

View File

@@ -1,4 +1,5 @@
use project::TaskSourceKind;
use remote::ConnectionState;
use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::ViewContext;
@@ -12,6 +13,19 @@ pub fn schedule_task(
omit_history: bool,
cx: &mut ViewContext<'_, Workspace>,
) {
match workspace.project.read(cx).ssh_connection_state(cx) {
None | Some(ConnectionState::Connected) => {}
Some(
ConnectionState::Connecting
| ConnectionState::Disconnected
| ConnectionState::HeartbeatMissed
| ConnectionState::Reconnecting,
) => {
log::warn!("Cannot schedule tasks when disconnected from a remote host");
return;
}
}
if let Some(spawn_in_terminal) =
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
{

View File

@@ -46,7 +46,9 @@ use itertools::Itertools;
use language::{LanguageRegistry, Rope};
pub use modal_layer::*;
use node_runtime::NodeRuntime;
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
use notifications::{
simple_message_notification::MessageNotification, DetachAndPromptErr, NotificationHandle,
};
pub use pane::*;
pub use pane_group::*;
pub use persistence::{
@@ -4359,17 +4361,17 @@ impl Workspace {
.on_action(cx.listener(|workspace, action: &Save, cx| {
workspace
.save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to save", cx, |_, _| None);
}))
.on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
workspace
.save_active_item(SaveIntent::SaveWithoutFormat, cx)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to save", cx, |_, _| None);
}))
.on_action(cx.listener(|workspace, _: &SaveAs, cx| {
workspace
.save_active_item(SaveIntent::SaveAs, cx)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to save", cx, |_, _| None);
}))
.on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
@@ -5566,6 +5568,12 @@ pub fn open_ssh_project(
};
}
if project_paths_to_open.is_empty() {
return Err(project_path_errors
.pop()
.unwrap_or_else(|| anyhow!("no paths given")));
}
cx.update_window(window.into(), |_, cx| {
cx.replace_root_view(|cx| {
let mut workspace =

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.158.0"
version = "0.159.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -33,7 +33,7 @@ use assets::Assets;
use node_runtime::{NodeBinaryOptions, NodeRuntime};
use parking_lot::Mutex;
use project::project_settings::ProjectSettings;
use recent_projects::open_ssh_project;
use recent_projects::{open_ssh_project, SshSettings};
use release_channel::{AppCommitSha, AppVersion};
use session::{AppSession, Session};
use settings::{
@@ -214,6 +214,7 @@ fn init_common(app_state: Arc<AppState>, cx: &mut AppContext) -> Arc<PromptBuild
ThemeRegistry::global(cx),
cx,
);
recent_projects::init(cx);
prompt_builder
}
@@ -248,7 +249,6 @@ fn init_ui(
audio::init(Assets, cx);
workspace::init(app_state.clone(), cx);
recent_projects::init(cx);
go_to_line::init(cx);
file_finder::init(cx);
tab_switcher::init(cx);
@@ -881,18 +881,25 @@ async fn restore_or_create_workspace(
})?;
task.await?;
}
SerializedWorkspaceLocation::Ssh(ssh_project) => {
SerializedWorkspaceLocation::Ssh(ssh) => {
let args = cx
.update(|cx| {
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
})
.ok()
.flatten();
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
args,
host: ssh.host.clone(),
username: ssh.user.clone(),
port: ssh.port,
password: None,
};
let app_state = app_state.clone();
cx.spawn(move |mut cx| async move {
recent_projects::open_ssh_project(
connection_options,
ssh_project.paths.into_iter().map(PathBuf::from).collect(),
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
workspace::OpenOptions::default(),
&mut cx,

View File

@@ -16,8 +16,9 @@ use futures::future::join_all;
use futures::{FutureExt, SinkExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
use language::{Bias, Point};
use recent_projects::open_ssh_project;
use recent_projects::{open_ssh_project, SshSettings};
use remote::SshConnectionOptions;
use settings::Settings;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@@ -48,7 +49,7 @@ impl OpenRequest {
} else if let Some(file) = url.strip_prefix("zed://file") {
this.parse_file_path(file)
} else if url.starts_with("ssh://") {
this.parse_ssh_file_path(&url)?
this.parse_ssh_file_path(&url, cx)?
} else if let Some(request_path) = parse_zed_link(&url, cx) {
this.parse_request_path(request_path).log_err();
} else {
@@ -65,7 +66,7 @@ impl OpenRequest {
}
}
fn parse_ssh_file_path(&mut self, file: &str) -> Result<()> {
fn parse_ssh_file_path(&mut self, file: &str, cx: &AppContext) -> Result<()> {
let url = url::Url::parse(file)?;
let host = url
.host()
@@ -77,11 +78,13 @@ impl OpenRequest {
if !self.open_paths.is_empty() {
return Err(anyhow!("cannot open both local and ssh paths"));
}
let args = SshSettings::get_global(cx).args_for(&host, port, &username);
let connection = SshConnectionOptions {
username,
password,
host,
port,
args,
};
if let Some(ssh_connection) = &self.ssh_connection {
if *ssh_connection != connection {
@@ -419,12 +422,25 @@ async fn open_workspaces(
errored = true
}
}
SerializedWorkspaceLocation::Ssh(ssh_project) => {
SerializedWorkspaceLocation::Ssh(ssh) => {
let app_state = app_state.clone();
let args = cx
.update(|cx| {
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
})
.ok()
.flatten();
let connection_options = SshConnectionOptions {
args,
host: ssh.host.clone(),
username: ssh.user.clone(),
port: ssh.port,
password: None,
};
cx.spawn(|mut cx| async move {
open_ssh_project(
ssh_project.connection_options(),
ssh_project.paths.into_iter().map(PathBuf::from).collect(),
connection_options,
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
&mut cx,

View File

@@ -38,6 +38,23 @@ If you want to disable Zed looking for a `clangd` binary, you can set `ignore_sy
}
```
## Arguments
You can pass any number of arguments to clangd. To see a full set of available options, run `clangd --help` from the command line. For example with `--function-arg-placeholders=0` completions contain only parentheses for function calls, while the default (`--function-arg-placeholders=1`) completions also contain placeholders for method parameters.
```json
{
"lsp": {
"clangd": {
"binary": {
"path": "/path/to/clangd",
"arguments": ["--function-arg-placeholders=0"]
}
}
}
}
```
## More server configuration
In the root of your project, it is generally common to create a `.clangd` file to set extra configuration.

View File

@@ -1,6 +1,203 @@
# Java
Java language support in Zed is provided the [zed Java extension](https://github.com/zed-extensions/java).
Report issues to: [https://github.com/zed-extensions/java/issues](https://github.com/zed-extensions/java/issues)
There are two extensions that provide Java language support for Zed:
- Zed Java: [zed-extensions/java](https://github.com/zed-extensions/java) and
- Java with Eclipse JDTLS: [zed-java-eclipse-jdtls](https://github.com/ABckh/zed-java-eclipse-jdtls).
Both use:
- Tree Sitter: [tree-sitter/tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java)
- Language Server: [eclipse-jdtls/eclipse.jdt.ls](https://github.com/eclipse-jdtls/eclipse.jdt.ls)
## Pre-requisites
You will need to install both a Java runtime (OpenJDK) and Eclipse JDT Language Server (`eclipse.jdt.ls`).
### Install OpenJDK
- MacOS: `brew install openjdk`
- Ubuntu: `sudo add-apt-repository ppa:openjdk-23 && sudo apt-get install openjdk-23`
- Windows: `choco install openjdk`
- Arch Linux: `sudo pacman -S jre-openjdk-headless`
Or manually download and install [OpenJDK 23](https://jdk.java.net/23/).
### Install JDTLS
- MacOS: `brew install jdtls`
- Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls)
Or manually download install:
- [JDTLS Milestone Builds](http://download.eclipse.org/jdtls/milestones/) (updated every two weeks)
- [JDTLS Snapshot Builds](https://download.eclipse.org/jdtls/snapshots/) (frequent updates)
## Extension Install
You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`.
We recommend you install one or the other and not both.
## Settings / Initialization Options
See [JDTLS Language Server Settings & Capabilities](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Language-Server-Settings-&-Capabilities) for a complete list of options.
Add the following to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}).
### Zed Java Settings
```json
{
"lsp": {
"jdtls": {
"settings": {},
"initialization_options": {}
}
}
}
}
```
### Java with Eclipse JDTLS settings
```json
{
"lsp": {
"java": {
"settings": {},
"initialization_options": {}
}
}
}
```
## See also
- [Zed Java Readme](https://github.com/zed-extensions/java)
- [Java with Eclipse JDTLS Readme](https://github.com/ABckh/zed-java-eclipse-jdtls)
### Support
If you have issues with either of these plugins, please open issues on their respective repositories:
- [Zed Java Issues](https://github.com/zed-extensions/java/issues)
- [Java with Eclipse JDTLS Issues](https://github.com/ABckh/zed-java-eclipse-jdtls/issues)
## Example Configs
### Zed Java Classpath
You can optionally configure the class path that JDTLS uses with:
```json
{
"lsp": {
"jdtls": {
"settings": {
"classpath": "/path/to/classes.jar:/path/to/more/classes/"
}
}
}
}
```
#### Zed Java Initialization Options
There are also many more options you can pass directly to the language server, for example:
```json
{
"lsp": {
"jdtls": {
"initialization_options": {
"bundles": [],
"workspaceFolders": ["file:///home/snjeza/Project"],
"settings": {
"java": {
"home": "/usr/local/jdk-9.0.1",
"errors": {
"incompleteClasspath": {
"severity": "warning"
}
},
"configuration": {
"updateBuildConfiguration": "interactive",
"maven": {
"userSettings": null
}
},
"trace": {
"server": "verbose"
},
"import": {
"gradle": {
"enabled": true
},
"maven": {
"enabled": true
},
"exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**",
"/**/test/**"
]
},
"referencesCodeLens": {
"enabled": false
},
"signatureHelp": {
"enabled": false
},
"implementationsCodeLens": {
"enabled": false
},
"format": {
"enabled": true
},
"saveActions": {
"organizeImports": false
},
"contentProvider": {
"preferred": null
},
"autobuild": {
"enabled": false
},
"completion": {
"favoriteStaticMembers": [
"org.junit.Assert.*",
"org.junit.Assume.*",
"org.junit.jupiter.api.Assertions.*",
"org.junit.jupiter.api.Assumptions.*",
"org.junit.jupiter.api.DynamicContainer.*",
"org.junit.jupiter.api.DynamicTest.*"
],
"importOrder": ["java", "javax", "com", "org"]
}
}
}
}
}
}
}
```
## Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration}
Configuration options match those provided in the [redhat-developer/vscode-java extension](https://github.com/redhat-developer/vscode-java#supported-vs-code-settings).
For example, to enable [Lombok Support](https://github.com/redhat-developer/vscode-java/wiki/Lombok-support):
```json
{
"lsp": {
"java": {
"settings": {
"java.jdt.ls.lombokSupport.enabled:": true
}
}
}
}
```

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_astro"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "astro"
name = "Astro"
description = "Astro support."
version = "0.1.0"
version = "0.1.1"
schema_version = 1
authors = ["Alvaro Gaona <alvgaona@gmail.com>", "0xk1f0 <dev@k1f0.dev>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_elixir"
version = "0.0.9"
version = "0.1.0"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "elixir"
name = "Elixir"
description = "Elixir support."
version = "0.0.9"
version = "0.1.0"
schema_version = 1
authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_html"
version = "0.1.2"
version = "0.1.3"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "html"
name = "HTML"
description = "HTML support."
version = "0.1.2"
version = "0.1.3"
schema_version = 1
authors = ["Isaac Clayton <slightknack@gmail.com>"]
repository = "https://github.com/zed-industries/zed"

View File

@@ -1,6 +1,6 @@
[package]
name = "zed_zig"
version = "0.3.0"
version = "0.3.1"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "zig"
name = "Zig"
description = "Zig support."
version = "0.3.0"
version = "0.3.1"
schema_version = 1
authors = ["Allan Calix <contact@acx.dev>"]
repository = "https://github.com/zed-industries/zed"