Compare commits

...

37 Commits

Author SHA1 Message Date
Conrad Irwin
1ba60d1a8d Undo refactoring that broke things 2024-10-18 16:34:01 -06:00
Conrad Irwin
06f18bc365 Revert "ssh remoting: Undo the spawning of message handlers (#19409)"
This reverts commit ea460014ab.
2024-10-18 16:11:15 -06:00
Conrad Irwin
f5833010aa Merge branch 'main' into ssh-reconnect-reliability 2024-10-18 16:10:25 -06:00
Conrad Irwin
f86f06c81a Revert "Revert "SSH reconnect reliability (#19398)""
This reverts commit 9b0ee7d30b.
2024-10-18 16:05:04 -06:00
Conrad Irwin
9b0ee7d30b Revert "SSH reconnect reliability (#19398)"
This reverts commit 98ecb43b2d.
2024-10-18 15:57:01 -06:00
Conrad Irwin
47a764553d Revert "remote: Fix formatting (#19438)"
This reverts commit 47380001cc.
2024-10-18 15:57:00 -06:00
Marshall Bowers
47380001cc remote: Fix formatting (#19438)
This PR fixes some formatting issues from #19398 that slipped past CI,
somehow.

Release Notes:

- N/A
2024-10-18 17:31:41 -04:00
Conrad Irwin
98ecb43b2d SSH reconnect reliability (#19398)
Release Notes:

- SSH Remoting: Fix message reliability across restarts

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-10-18 15:28:08 -06:00
Valentine Briese
be474a6d6f docs: Update info on JDTLS install for Java (#19436) 2024-10-18 17:12:34 -04:00
Marshall Bowers
b44bed0115 collab: Unconditionally execute billing checks (#19432)
This PR removes the conditional checks around the billing-related
enforcement for LLM completions.

These were just in place to prevent executing any billing code before we
had rolled it out. Now that it is rolled out, we don't need this
conditional execution anymore.

Release Notes:

- N/A
2024-10-18 15:55:28 -04:00
Joseph T. Lyons
11a82e3347 Do not run CI on changes to community action config files (#19430)
Release Notes:

- N/A
2024-10-18 15:36:20 -04:00
Joseph T. Lyons
be81e29b0f Prefer users to open new issues if closed stale issue is valid (#19428)
I no longer want to have to keep my ears and eyes open for GitHub
notifications that relate to someone requesting a closed stale issue be
reopened. As a community maintainer, I can get hundreds or even
thousands of notifications a week, and a lot of those are about activity
on closed issues. If everyone following an issue did not react fast
enough (7 days) to keep an issue flagged as `stale` open, let's instruct
them to open new issues, so we are forced to see it during next triage.

Release Notes:

- N/A
2024-10-18 15:04:26 -04:00
Valentine Briese
6a463be1ae docs: Direct Java extension users to JDTLS initialization options (#19401)
Continuation of #19390
2024-10-18 15:02:42 -04:00
Vladimir Varankin
64a6e9cafb docs: Outline Jsonnet language (#19410)
This PR adds a basic documentation about the Jsonnet language support.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-18 14:20:15 -04:00
Marshall Bowers
fa738ee5e1 vue: Extract to zed-extensions/vue repository (#19426)
This PR extracts the Vue extension to the
[zed-extensions/vue](https://github.com/zed-extensions/vue) repository.

Release Notes:

- N/A
2024-10-18 14:08:32 -04:00
Marshall Bowers
15449cdf30 svelte: Extract to zed-extensions/svelte repository (#19425)
This PR extracts the Svelte extension to the
[zed-extensions/svelte](https://github.com/zed-extensions/svelte)
repository.

Release Notes:

- N/A
2024-10-18 13:36:07 -04:00
Shish
2db9090a2f remote: Polish for connection progress & error dialogs (#19379)
Before/after:

![err1-before](https://github.com/user-attachments/assets/43d959b3-c9d9-45dd-938e-42d34ec1cfc5)

![err1-after](https://github.com/user-attachments/assets/311d53e0-752c-4eb8-9816-64b1970c228d)

Before/after (I feel like text-wrapping would be more useful than
text-ellipsis here, but I don't see any wrap function):

![err2-before](https://github.com/user-attachments/assets/1626cda9-bf06-43fe-9b7d-3ec64f4db08a)

![err2-after](https://github.com/user-attachments/assets/749a6950-1409-4e75-808e-a1a96dbfc87e)

Before/after:

![prog-before](https://github.com/user-attachments/assets/f5f5a171-db42-4797-bab0-ad71c750bb20)

![prog-after](https://github.com/user-attachments/assets/b52a7694-36f6-4f7a-8a90-ceb223f12ec1)

Release Notes:

- N/A
2024-10-18 11:09:52 -06:00
Thomas
34b8655bf6 Improve increment/decrement with leading zeros in vim mode (#18362)
- Closes: 18360

Release Notes:

- Added support for incrementing and decrementing numbers with leading
zeros
2024-10-18 11:03:34 -06:00
张小白
5b745a82e1 reqwest_client: Fix socks proxy settings (#19123)
Closes #19362

This pull request includes several updates to the `reqwest_client` crate
and its dependencies. The most important changes involve adding support
for SOCKS proxies, improving error handling for proxy URIs, and adding
tests for proxy functionality.

### Dependency Updates:
*
[`Cargo.toml`](diffhunk://#diff-2e9d962a08321605940b5a657135052fbcef87b5e360662bb527c96d9a615542L394-R401):
Added support for SOCKS proxies in the `reqwest` dependency by including
the `socks` feature.

### Code Improvements:
*
[`crates/reqwest_client/src/reqwest_client.rs`](diffhunk://#diff-8e036b034e987390be2f57373864b75d6983f0cf84e85c43793eb431d13538f3L47-R52):
Improved error handling when parsing proxy URIs by logging errors
instead of directly panicking.

### Testing Enhancements:
*
[`crates/reqwest_client/src/reqwest_client.rs`](diffhunk://#diff-8e036b034e987390be2f57373864b75d6983f0cf84e85c43793eb431d13538f3R274-R317):
Added tests to verify the handling of various proxy URIs, including
valid and invalid cases.

Release Notes:

- N/A
2024-10-18 09:57:00 -07:00
renovate[bot]
c59a75db1d Update actions/upload-artifact digest to b4b15b8 (#19310)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | digest | `604373d` -> `b4b15b8` |

---

### 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-18 12:41:36 -04:00
David Baldwin
b3c93130ec elixir: Support describe, test, setup, setup_all in outlines (#19135)
Closes #9894 

Release Notes:

- N/A

### Before


![2024-10-12T204848@2x](https://github.com/user-attachments/assets/84b7f123-8845-4e6d-b1b1-444e54ea6599)

### After


![2024-10-12T204749@2x](https://github.com/user-attachments/assets/67fdcead-bad3-4967-9ac4-0b85f1da7bca)
2024-10-18 12:39:24 -04:00
Marshall Bowers
73a6c542f3 vue: Bump to v0.1.1 (#19421)
This PR bumps the Vue extension to v0.1.1.

Changes:

- https://github.com/zed-industries/zed/pull/19419

Release Notes:

- N/A
2024-10-18 11:56:16 -04:00
Marshall Bowers
2cd6c19873 svelte: Bump to v0.2.1 (#19420)
This PR bumps the Svelte extension to v0.2.1.

Changes:

- https://github.com/zed-industries/zed/pull/19418

Release Notes:

- N/A
2024-10-18 11:37:51 -04:00
Marshall Bowers
6f24c1da79 vue: Support lang attribute for style tag injections (#19419)
This PR adds support for injecting languages into `<style>` tags using
`<style lang="...">` in Vue.

Extracted from https://github.com/zed-industries/zed/pull/18052.

Release Notes:

- N/A

Co-authored-by: Albert Marashi <albert@lumina.earth>
2024-10-18 11:36:30 -04:00
Marshall Bowers
5508832ba6 svelte: Adjust block keyword highlighting (#19418)
This PR adjusts the highlights for `{#each ...}` and `{#if ...}`
expression blocks to use keyword highlighting.

Extracted from https://github.com/zed-industries/zed/pull/18052.

Release Notes:

- N/A

Co-authored-by: Albert Marashi <albert@lumina.earth>
2024-10-18 11:25:03 -04:00
Marshall Bowers
35f2f2aac4 Treat .postcss files as CSS (#19416)
This PR makes it so `.postcss` files are recognized as CSS.

The `tree-sitter-css` grammar has basic support for PostCSS:
https://github.com/tree-sitter/tree-sitter-css/issues/17#issuecomment-1830349808.

Closes #18051.

Release Notes:

- `.postcss` files are now recognized as CSS.
2024-10-18 11:11:29 -04:00
Peter Tripp
9e27b6694a keymap: Add cmd-o 'Go to symbol' for JetBrains MacOS key bindings (#19415) 2024-10-18 10:12:39 -04:00
CharlesChen0823
f5124c21d1 scrollbar: Fix horizontal scrollbar display overflow last item (#19403)
currently, the laste item display is overflow by scrollbar, cause it
cannot clickable.
So remain one item height for display horizontal scrollbar,.
![Screenshot 2024-10-18
192352](https://github.com/user-attachments/assets/d686f0e8-93f8-426e-8816-6f00ed17a599)



Release Notes:

- N/A
2024-10-18 14:07:50 +02:00
Thorsten Ball
ea460014ab ssh remoting: Undo the spawning of message handlers (#19409)
I suspect that this might have something to do with saving files not
being possible after a while.

I want to merge this and bump nightly.

Release Notes:

- N/A
2024-10-18 10:48:11 +02:00
Peter Tripp
5168fc27a1 docs: More Java extension documentation (#19390)
Follow up of: https://github.com/zed-industries/zed/pull/19113
2024-10-17 20:21:45 -04:00
Conrad Irwin
d7ff85e2a1 Clippity Cloppity 2024-10-17 16:56:11 -06:00
Conrad Irwin
b2b6b1e8a1 Don't rebuild/re-upload binary on reconnect. 2024-10-17 16:19:06 -06:00
Conrad Irwin
023186b7a0 Fix message buffering during reconnect
Co-Authored-By: Nathan <nathan@zed.dev>
2024-10-17 16:07:57 -06:00
Conrad Irwin
e5ec08e8f8 Failing test for edits in parallel with disconnects 2024-10-17 14:29:09 -06:00
Marshall Bowers
2bcf9fc490 Add client::zed_urls module for constructing zed.dev URLs (#19391)
This PR adds a new `zed_urls` module to the `client` crate.

This module contains functions for constructing URLs to Zed properties,
such as zed.dev.

The URLs produced by this module will respect the server URL set via
settings or the `ZED_SERVER_URL` environment variable. This allows them
to correctly reflect the current environment (such as when testing Zed
against a local collab/zed.dev).

Release Notes:

- N/A
2024-10-17 16:18:35 -04:00
Conrad Irwin
2f6c7a2939 Implement the fake connection stuff 2024-10-17 14:10:03 -06:00
Conrad Irwin
2f26a74c83 Refactor Ssh connections to make faking more possible 2024-10-17 12:56:21 -06:00
61 changed files with 779 additions and 1056 deletions

View File

@@ -14,6 +14,7 @@ on:
- "**"
paths-ignore:
- "docs/**"
- ".github/workflows/community_*"
concurrency:
# Allow only one workflow per any non-`main` branch.
@@ -266,20 +267,20 @@ jobs:
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
- name: Upload app bundle (x86_64) to workflow run if main branch or specific label
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
@@ -330,7 +331,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
@@ -377,7 +378,7 @@ jobs:
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz

View File

@@ -17,7 +17,7 @@ jobs:
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. Are you able to reproduce this issue in the latest version of Zed? If so, please let us know by commenting on this issue and we will keep it open; otherwise, we'll close it in 7 days. Feel free to open a new issue if you're seeing this message after the issue has been closed.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, feel free to ping a Zed team member to reopen this issue or open a new one."
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
# We will increase `days-before-stale` to 365 on or after Jan 24th,
# 2024. This date marks one year since migrating issues from
# 'community' to 'zed' repository. The migration added activity to all

18
Cargo.lock generated
View File

@@ -9119,6 +9119,7 @@ name = "remote"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"fs",
"futures 0.3.30",
@@ -9303,6 +9304,7 @@ dependencies = [
"system-configuration 0.6.1",
"tokio",
"tokio-rustls 0.26.0",
"tokio-socks",
"tokio-util",
"tower-service",
"url",
@@ -12001,6 +12003,7 @@ dependencies = [
"futures-io",
"futures-util",
"thiserror",
"tokio",
]
[[package]]
@@ -14864,13 +14867,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_svelte"
version = "0.2.0"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_terraform"
version = "0.1.1"
@@ -14899,14 +14895,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_vue"
version = "0.1.0"
dependencies = [
"serde",
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_zig"
version = "0.3.1"

View File

@@ -158,12 +158,10 @@ members = [
"extensions/ruff",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/svelte",
"extensions/terraform",
"extensions/test-extension",
"extensions/toml",
"extensions/uiua",
"extensions/vue",
"extensions/zig",
#
@@ -391,7 +389,14 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
rand = "0.8.5"
regex = "1.5"
repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = ["charset", "http2", "macos-system-configuration", "rustls-tls-native-roots", "stream"]}
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
"rustls-tls-native-roots",
"socks",
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.15", default-features = false, features = [
"async-dispatcher-runtime",

View File

@@ -128,6 +128,7 @@
"php": "php",
"plist": "template",
"png": "image",
"postcss": "css",
"ppt": "document",
"pptx": "document",
"prettierignore": "prettier",

View File

@@ -34,7 +34,7 @@
"cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
"cmd-alt-f7": "editor::FindAllReferences",
"cmd-b": "editor::GoToDefinition",
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"cmd-alt-b": "editor::GoToDefinitionSplit",
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
@@ -64,7 +64,8 @@
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
"cmd-o": "project_symbols::Toggle", // JetBrains: Go to Class
"cmd-1": "workspace::ToggleLeftDock",
"cmd-6": "diagnostics::Deploy"
}

View File

@@ -21,7 +21,7 @@ use crate::{
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use assistant_tool::ToolRegistry;
use client::{proto, Client, Status};
use client::{proto, zed_urls, Client, Status};
use collections::{BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
@@ -3675,7 +3675,6 @@ 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 ACCOUNT_URL: &str = "https://zed.dev/account";
v_flex()
.gap_0p5()
@@ -3700,7 +3699,7 @@ impl ContextEditor {
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.open_url(ACCOUNT_URL);
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)))
@@ -3716,7 +3715,6 @@ impl ContextEditor {
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
const ACCOUNT_URL: &str = "https://zed.dev/account";
v_flex()
.gap_0p5()
@@ -3742,7 +3740,7 @@ impl ContextEditor {
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.last_error = None;
cx.open_url(ACCOUNT_URL);
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),

View File

@@ -4,6 +4,7 @@ pub mod test;
mod socks;
pub mod telemetry;
pub mod user;
pub mod zed_urls;
use anyhow::{anyhow, bail, Context as _, Result};
use async_recursion::async_recursion;

View File

@@ -0,0 +1,19 @@
//! Contains helper functions for constructing URLs to various Zed-related pages.
//!
//! These URLs will adapt to the configured server URL in order to construct
//! links appropriate for the environment (e.g., by linking to a local copy of
//! zed.dev in development).
use gpui::AppContext;
use settings::Settings;
use crate::ClientSettings;
fn server_url(cx: &AppContext) -> &str {
&ClientSettings::get_global(cx).server_url
}
/// Returns the URL to the account page on zed.dev.
pub fn account_url(cx: &AppContext) -> String {
format!("{server_url}/account", server_url = server_url(cx))
}

View File

@@ -198,10 +198,6 @@ impl Config {
}
}
pub fn is_llm_billing_enabled(&self) -> bool {
self.stripe_api_key.is_some()
}
#[cfg(test)]
pub fn test() -> Self {
Self {

View File

@@ -460,29 +460,27 @@ async fn check_usage_limit(
)
.await?;
if state.config.is_llm_billing_enabled() {
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
if !claims.has_llm_subscription {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"Maximum spending limit reached for this month.".to_string(),
));
}
if usage.spending_this_month >= FREE_TIER_MONTHLY_SPENDING_LIMIT {
if !claims.has_llm_subscription {
return Err(Error::http(
StatusCode::PAYMENT_REQUIRED,
"Maximum spending limit reached for this month.".to_string(),
));
}
if (usage.spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT)
>= Cents(claims.max_monthly_spend_in_cents)
{
return Err(Error::Http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached for this month.".to_string(),
[(
HeaderName::from_static(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME),
HeaderValue::from_static("true"),
)]
.into_iter()
.collect(),
));
}
if (usage.spending_this_month - FREE_TIER_MONTHLY_SPENDING_LIMIT)
>= Cents(claims.max_monthly_spend_in_cents)
{
return Err(Error::Http(
StatusCode::FORBIDDEN,
"Maximum spending limit reached for this month.".to_string(),
[(
HeaderName::from_static(MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME),
HeaderValue::from_static("true"),
)]
.into_iter()
.collect(),
));
}
}
@@ -627,7 +625,6 @@ where
impl<S> Drop for TokenCountingStream<S> {
fn drop(&mut self) {
let state = self.state.clone();
let is_llm_billing_enabled = state.config.is_llm_billing_enabled();
let claims = self.claims.clone();
let provider = self.provider;
let model = std::mem::take(&mut self.model);
@@ -641,14 +638,7 @@ impl<S> Drop for TokenCountingStream<S> {
provider,
&model,
tokens,
// We're passing `false` here if LLM billing is not enabled
// so that we don't write any records to the
// `billing_events` table until we're ready to bill users.
if is_llm_billing_enabled {
claims.has_llm_subscription
} else {
false
},
claims.has_llm_subscription,
Cents(claims.max_monthly_spend_in_cents),
Utc::now(),
)

View File

@@ -26,7 +26,7 @@ async fn test_sharing_an_ssh_remote_project(
.await;
// Set up project on remote FS
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
let (forwarder, server_ssh) = SshRemoteClient::fake_server(server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -67,6 +67,7 @@ async fn test_sharing_an_ssh_remote_project(
)
});
let client_ssh = SshRemoteClient::fake_client(forwarder, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/code/project1", client_ssh, cx_a)
.await;

View File

@@ -8,7 +8,7 @@ use crate::{
use anthropic::AnthropicError;
use anyhow::{anyhow, Result};
use client::{
Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME,
zed_urls, Client, PerformCompletionParams, UserStore, EXPIRED_LLM_TOKEN_HEADER_NAME,
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
};
use collections::BTreeMap;
@@ -905,7 +905,6 @@ impl ConfigurationView {
impl Render for ConfigurationView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const ZED_AI_URL: &str = "https://zed.dev/ai";
const ACCOUNT_SETTINGS_URL: &str = "https://zed.dev/account";
let is_connected = !self.state.read(cx).is_signed_out();
let plan = self.state.read(cx).user_store.read(cx).current_plan();
@@ -922,7 +921,7 @@ impl Render for ConfigurationView {
h_flex().child(
Button::new("manage_settings", "Manage Subscription")
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(cx.listener(|_, _, cx| cx.open_url(ACCOUNT_SETTINGS_URL))),
.on_click(cx.listener(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))),
),
)
} else if cx.has_flag::<ZedPro>() {
@@ -938,7 +937,9 @@ impl Render for ConfigurationView {
Button::new("upgrade", "Upgrade")
.style(ButtonStyle::Subtle)
.color(Color::Accent)
.on_click(cx.listener(|_, _, cx| cx.open_url(ACCOUNT_SETTINGS_URL))),
.on_click(
cx.listener(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
),
),
)
} else {

View File

@@ -1,6 +1,6 @@
name = "CSS"
grammar = "css"
path_suffixes = ["css"]
path_suffixes = ["css", "postcss"]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },

View File

@@ -1243,6 +1243,10 @@ impl Project {
self.client.clone()
}
pub fn ssh_client(&self) -> Option<Model<SshRemoteClient>> {
self.ssh_client.clone()
}
pub fn user_store(&self) -> Model<UserStore> {
self.user_store.clone()
}

View File

@@ -2701,7 +2701,6 @@ impl ProjectPanel {
.cursor_default()
.when(self.width.is_some(), |this| {
this.children(Scrollbar::horizontal(
//percentage as f32..end_offset as f32,
self.horizontal_scrollbar_state.clone(),
))
}),
@@ -2918,7 +2917,9 @@ impl Render for ProjectPanel {
.track_scroll(self.scroll_handle.clone()),
)
.children(self.render_vertical_scrollbar(cx))
.children(self.render_horizontal_scrollbar(cx))
.when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| {
this.pb_4().child(scrollbar)
})
.children(self.context_menu.as_ref().map(|(menu, position, _)| {
deferred(
anchored()

View File

@@ -12,6 +12,7 @@ message Envelope {
uint32 id = 1;
optional uint32 responding_to = 2;
optional PeerId original_sender_id = 3;
optional uint32 ack_id = 266;
oneof payload {
Hello hello = 4;
@@ -295,7 +296,9 @@ message Envelope {
OpenServerSettings open_server_settings = 263;
GetPermalinkToLine get_permalink_to_line = 264;
GetPermalinkToLineResponse get_permalink_to_line_response = 265; // current max
GetPermalinkToLineResponse get_permalink_to_line_response = 265;
FlushBufferedMessages flush_buffered_messages = 267;
}
reserved 87 to 88;
@@ -2521,3 +2524,6 @@ message GetPermalinkToLine {
message GetPermalinkToLineResponse {
string permalink = 1;
}
message FlushBufferedMessages {}
message FlushBufferedMessagesResponse {}

View File

@@ -32,6 +32,7 @@ macro_rules! messages {
responding_to,
original_sender_id,
payload: Some(envelope::Payload::$name(self)),
ack_id: None,
}
}

View File

@@ -372,6 +372,7 @@ messages!(
(OpenServerSettings, Foreground),
(GetPermalinkToLine, Foreground),
(GetPermalinkToLineResponse, Foreground),
(FlushBufferedMessages, Foreground),
);
request_messages!(
@@ -498,6 +499,7 @@ request_messages!(
(RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse),
(FlushBufferedMessages, Ack),
);
entity_messages!(

View File

@@ -175,7 +175,7 @@ impl Render for SshPrompt {
.child(
h_flex()
.p_2()
.flex_wrap()
.flex()
.child(if self.error_message.is_some() {
Icon::new(IconName::XCircle)
.size(IconSize::Medium)
@@ -195,6 +195,7 @@ impl Render for SshPrompt {
})
.child(
div()
.ml_1()
.text_ellipsis()
.overflow_x_hidden()
.when_some(self.error_message.as_ref(), |el, error| {
@@ -205,7 +206,7 @@ impl Render for SshPrompt {
|el| {
el.child(
Label::new(format!(
"{}",
"{}",
self.status_message.clone().unwrap()
))
.size(LabelSize::Small),

View File

@@ -19,6 +19,7 @@ test-support = ["fs/test-support"]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true

View File

@@ -6,6 +6,7 @@ use crate::{
proxy::ProxyLaunchError,
};
use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::{
channel::{
@@ -31,6 +32,7 @@ use smol::{
};
use std::{
any::TypeId,
collections::VecDeque,
ffi::OsStr,
fmt,
ops::ControlFlow,
@@ -276,7 +278,7 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
}
}
struct ChannelForwarder {
pub struct ChannelForwarder {
quit_tx: UnboundedSender<()>,
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
}
@@ -347,7 +349,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
enum State {
Connecting,
Connected {
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -357,7 +359,7 @@ enum State {
HeartbeatMissed {
missed_heartbeats: usize,
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -366,7 +368,7 @@ enum State {
},
Reconnecting,
ReconnectFailed {
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -392,11 +394,11 @@ impl fmt::Display for State {
}
impl State {
fn ssh_connection(&self) -> Option<&SshRemoteConnection> {
fn ssh_connection(&self) -> Option<&dyn SshRemoteProcess> {
match self {
Self::Connected { ssh_connection, .. } => Some(ssh_connection),
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection),
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection),
Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
_ => None,
}
}
@@ -530,7 +532,8 @@ impl SshRemoteClient {
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
let client =
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
let this = cx.new_model(|_| Self {
client: client.clone(),
unique_identifier: unique_identifier.clone(),
@@ -541,23 +544,19 @@ impl SshRemoteClient {
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
let (ssh_connection, io_task) = Self::establish_connection(
unique_identifier,
false,
connection_options,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
delegate.clone(),
&mut cx,
)
.await?;
let multiplex_task = Self::multiplex(
this.downgrade(),
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
log::error!("failed to establish connection: {}", error);
@@ -703,30 +702,24 @@ impl SshRemoteClient {
};
}
if let Err(error) = ssh_connection.master_process.kill() {
if let Err(error) = ssh_connection.kill().await.context("Failed to kill ssh process") {
failed!(error, attempts, ssh_connection, delegate, forwarder);
};
if let Err(error) = ssh_connection
.master_process
.status()
.await
.context("Failed to kill ssh process")
{
failed!(error, attempts, ssh_connection, delegate, forwarder);
}
let connection_options = ssh_connection.socket.connection_options.clone();
let connection_options = ssh_connection.connection_options();
let (incoming_tx, outgoing_rx) = forwarder.into_channels().await;
let (forwarder, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let (ssh_connection, ssh_process) = match Self::establish_connection(
let (ssh_connection, io_task) = match Self::establish_connection(
identifier,
true,
connection_options,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
delegate.clone(),
&mut cx,
)
@@ -738,16 +731,9 @@ impl SshRemoteClient {
}
};
let multiplex_task = Self::multiplex(
this.clone(),
ssh_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
let multiplex_task = Self::monitor(this.clone(), io_task, &cx);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
failed!(error, attempts, ssh_connection, delegate, forwarder);
};
@@ -798,7 +784,7 @@ impl SshRemoteClient {
cx.emit(SshRemoteEvent::Disconnected);
Ok(())
} else {
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
Ok(())
}
})
@@ -911,18 +897,17 @@ impl SshRemoteClient {
}
fn multiplex(
this: WeakModel<Self>,
mut ssh_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
) -> Task<Result<Option<i32>>> {
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
let io_task = cx.background_executor().spawn(async move {
cx.background_executor().spawn(async move {
let mut stdin_buffer = Vec::new();
let mut stdout_buffer = Vec::new();
let mut stderr_buffer = Vec::new();
@@ -1001,8 +986,14 @@ impl SshRemoteClient {
}
}
}
});
})
}
fn monitor(
this: WeakModel<Self>,
io_task: Task<Result<Option<i32>>>,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let result = io_task.await;
@@ -1061,21 +1052,40 @@ impl SshRemoteClient {
cx.notify();
}
#[allow(clippy::too_many_arguments)]
async fn establish_connection(
unique_identifier: String,
reconnect: bool,
connection_options: SshConnectionOptions,
proxy_incoming_tx: UnboundedSender<Envelope>,
proxy_outgoing_rx: UnboundedReceiver<Envelope>,
connection_activity_tx: Sender<()>,
delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Result<(SshRemoteConnection, Child)> {
) -> Result<(Box<dyn SshRemoteProcess>, Task<Result<Option<i32>>>)> {
#[cfg(any(test, feature = "test-support"))]
if let Some(fake) = fake::SshRemoteConnection::new(&connection_options) {
let io_task = fake::SshRemoteConnection::multiplex(
fake.connection_options(),
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
cx,
)
.await;
return Ok((fake, io_task));
}
let ssh_connection =
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
let platform = ssh_connection.query_platform().await?;
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
ssh_connection
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
if !reconnect {
ssh_connection
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
}
let socket = ssh_connection.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
@@ -1100,7 +1110,15 @@ impl SshRemoteClient {
.spawn()
.context("failed to spawn remote server")?;
Ok((ssh_connection, ssh_proxy_process))
let io_task = Self::multiplex(
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&cx,
);
Ok((Box::new(ssh_connection), io_task))
}
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
@@ -1112,7 +1130,7 @@ impl SshRemoteClient {
.lock()
.as_ref()
.and_then(|state| state.ssh_connection())
.map(|ssh_connection| ssh_connection.socket.ssh_args())
.map(|ssh_connection| ssh_connection.ssh_args())
}
pub fn proto_client(&self) -> AnyProtoClient {
@@ -1127,7 +1145,6 @@ impl SshRemoteClient {
self.connection_options.clone()
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn connection_state(&self) -> ConnectionState {
self.state
.lock()
@@ -1136,37 +1153,69 @@ impl SshRemoteClient {
.unwrap_or(ConnectionState::Disconnected)
}
#[cfg(any(test, feature = "test-support"))]
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::Connected
}
pub fn is_disconnected(&self) -> bool {
self.connection_state() == ConnectionState::Disconnected
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(
client_cx: &mut gpui::TestAppContext,
pub fn simulate_disconnect(&self, cx: &mut AppContext) -> Task<()> {
use gpui::BorrowAppContext;
let port = self.connection_options().port.unwrap();
let disconnect =
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.take(port).into_channels());
cx.spawn(|mut cx| async move {
let (input_rx, output_tx) = disconnect.await;
let (forwarder, _, _) = ChannelForwarder::new(input_rx, output_tx, &mut cx);
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.replace(port, forwarder))
.unwrap()
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_server(
server_cx: &mut gpui::TestAppContext,
) -> (Model<Self>, Arc<ChannelClient>) {
use gpui::Context;
) -> (ChannelForwarder, Arc<ChannelClient>) {
server_cx.update(|cx| {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
// We use the forwarder on the server side (in production we only use one on the client side)
// the idea is that we can simulate a disconnect/reconnect by just messing with the forwarder.
let (forwarder, _, _) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx.to_async());
(
client_cx.update(|cx| {
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
cx.new_model(|_| Self {
client,
unique_identifier: "fake".to_string(),
connection_options: SshConnectionOptions::default(),
state: Arc::new(Mutex::new(None)),
})
}),
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
)
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server");
(forwarder, client)
})
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_client(
forwarder: ChannelForwarder,
client_cx: &mut gpui::TestAppContext,
) -> Model<Self> {
use gpui::BorrowAppContext;
client_cx
.update(|cx| {
let port = cx.update_default_global(|c: &mut fake::GlobalConnections, _cx| {
c.push(forwarder)
});
Self::new(
"fake".to_string(),
SshConnectionOptions {
host: "<fake>".to_string(),
port: Some(port),
..Default::default()
},
Arc::new(fake::Delegate),
cx,
)
})
.await
.unwrap()
}
}
@@ -1176,6 +1225,13 @@ impl From<SshRemoteClient> for AnyProtoClient {
}
}
#[async_trait]
trait SshRemoteProcess: Send + Sync {
async fn kill(&mut self) -> Result<()>;
fn ssh_args(&self) -> Vec<String>;
fn connection_options(&self) -> SshConnectionOptions;
}
struct SshRemoteConnection {
socket: SshSocket,
master_process: process::Child,
@@ -1190,6 +1246,25 @@ impl Drop for SshRemoteConnection {
}
}
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
self.master_process.kill()?;
self.master_process.status().await?;
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
self.socket.ssh_args()
}
fn connection_options(&self) -> SshConnectionOptions {
self.socket.connection_options.clone()
}
}
impl SshRemoteConnection {
#[cfg(not(unix))]
async fn new(
@@ -1321,7 +1396,10 @@ impl SshRemoteConnection {
let mut stderr = master_process.stderr.take().unwrap();
stderr.read_to_end(&mut output).await?;
let error_message = format!("failed to connect: {}", String::from_utf8_lossy(&output));
let error_message = format!(
"failed to connect: {}",
String::from_utf8_lossy(&output).trim()
);
delegate.set_error(error_message.clone(), cx);
Err(anyhow!(error_message))?;
}
@@ -1382,14 +1460,14 @@ impl SshRemoteConnection {
let server_mode = 0o755;
let t0 = Instant::now();
delegate.set_status(Some("uploading remote development server"), cx);
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)
.await
.context("failed to upload server binary")?;
log::info!("uploaded remote development server in {:?}", t0.elapsed());
delegate.set_status(Some("extracting remote development server"), cx);
delegate.set_status(Some("Extracting remote development server"), cx);
run_cmd(
self.socket
.ssh_command("gunzip")
@@ -1398,7 +1476,7 @@ impl SshRemoteConnection {
)
.await?;
delegate.set_status(Some("unzipping remote development server"), cx);
delegate.set_status(Some("Marking remote development server executable"), cx);
run_cmd(
self.socket
.ssh_command("chmod")
@@ -1469,8 +1547,10 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
pub struct ChannelClient {
next_message_id: AtomicU32,
outgoing_tx: mpsc::UnboundedSender<Envelope>,
response_channels: ResponseChannels, // Lock
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
buffer: Mutex<VecDeque<Envelope>>,
response_channels: ResponseChannels,
message_handlers: Mutex<ProtoMessageHandlerSet>,
max_received: AtomicU32,
}
impl ChannelClient {
@@ -1478,15 +1558,18 @@ impl ChannelClient {
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
outgoing_tx: mpsc::UnboundedSender<Envelope>,
cx: &AppContext,
name: &'static str,
) -> Arc<Self> {
let this = Arc::new(Self {
outgoing_tx,
next_message_id: AtomicU32::new(0),
max_received: AtomicU32::new(0),
response_channels: ResponseChannels::default(),
message_handlers: Default::default(),
buffer: Mutex::new(VecDeque::new()),
});
Self::start_handling_messages(this.clone(), incoming_rx, cx);
Self::start_handling_messages(this.clone(), incoming_rx, cx, name);
this
}
@@ -1495,6 +1578,7 @@ impl ChannelClient {
this: Arc<Self>,
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
cx: &AppContext,
name: &'static str,
) {
cx.spawn(|cx| {
let this = Arc::downgrade(&this);
@@ -1504,6 +1588,26 @@ impl ChannelClient {
let Some(this) = this.upgrade() else {
return anyhow::Ok(());
};
if let Some(ack_id) = incoming.ack_id {
let mut buffer = this.buffer.lock();
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
buffer.pop_front();
}
}
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload {
{
let buffer = this.buffer.lock();
for envelope in buffer.iter() {
this.outgoing_tx.unbounded_send(envelope.clone()).ok();
}
}
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
this.outgoing_tx.unbounded_send(envelope)?;
continue;
}
this.max_received.store(incoming.id, SeqCst);
if let Some(request_id) = incoming.responding_to {
let request_id = MessageId(request_id);
@@ -1525,22 +1629,22 @@ impl ChannelClient {
this.clone().into(),
cx.clone(),
) {
log::debug!("ssh message received. name:{type_name}");
log::debug!("{name}:ssh message received. name:{type_name}");
cx.foreground_executor().spawn(async move {
match future.await {
Ok(_) => {
log::debug!("ssh message handled. name:{type_name}");
log::debug!("{name}:ssh message handled. name:{type_name}");
}
Err(error) => {
log::error!(
"error handling message. type:{type_name}, error:{error}",
"{name}:error handling message. type:{type_name}, error:{error}",
);
}
}
}).detach();
} else {
log::error!("unhandled ssh message name:{type_name}");
log::error!("{name}:unhandled ssh message name:{type_name}");
}
}
}
@@ -1583,6 +1687,23 @@ impl ChannelClient {
}
}
pub async fn resync(&self, timeout: Duration) -> Result<()> {
smol::future::or(
async {
self.request(proto::FlushBufferedMessages {}).await?;
for envelope in self.buffer.lock().iter() {
self.outgoing_tx.unbounded_send(envelope.clone()).ok();
}
Ok(())
},
async {
smol::Timer::after(timeout).await;
Err(anyhow!("Timeout detected"))
},
)
.await
}
pub async fn ping(&self, timeout: Duration) -> Result<()> {
smol::future::or(
async {
@@ -1612,7 +1733,8 @@ impl ChannelClient {
let mut response_channels_lock = self.response_channels.lock();
response_channels_lock.insert(MessageId(envelope.id), tx);
drop(response_channels_lock);
let result = self.outgoing_tx.unbounded_send(envelope);
let result = self.send_buffered(envelope);
async move {
if let Err(error) = &result {
log::error!("failed to send message: {}", error);
@@ -1629,6 +1751,12 @@ impl ChannelClient {
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
self.send_buffered(envelope)
}
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.ack_id = Some(self.max_received.load(SeqCst));
self.buffer.lock().push_back(envelope.clone());
self.outgoing_tx.unbounded_send(envelope)?;
Ok(())
}
@@ -1659,3 +1787,165 @@ impl ProtoClient for ChannelClient {
false
}
}
#[cfg(any(test, feature = "test-support"))]
mod fake {
use std::path::PathBuf;
use anyhow::Result;
use async_trait::async_trait;
use futures::{
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, BorrowAppContext, Global, SemanticVersion, Task};
use rpc::proto::Envelope;
use super::{
ChannelForwarder, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteProcess,
};
pub(super) struct SshRemoteConnection {
connection_options: SshConnectionOptions,
}
impl SshRemoteConnection {
pub(super) fn new(
connection_options: &SshConnectionOptions,
) -> Option<Box<dyn SshRemoteProcess>> {
if connection_options.host == "<fake>" {
return Some(Box::new(Self {
connection_options: connection_options.clone(),
}));
}
return None;
}
pub(super) async fn multiplex(
connection_options: SshConnectionOptions,
mut client_tx: mpsc::UnboundedSender<Envelope>,
mut client_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &mut AsyncAppContext,
) -> Task<Result<Option<i32>>> {
let (server_tx, server_rx) = cx
.update(|cx| {
cx.update_global(|conns: &mut GlobalConnections, _| {
conns.take(connection_options.port.unwrap())
})
})
.unwrap()
.into_channels()
.await;
let (forwarder, mut proxy_tx, mut proxy_rx) =
ChannelForwarder::new(server_tx, server_rx, cx);
cx.update(|cx| {
cx.update_global(|conns: &mut GlobalConnections, _| {
conns.replace(connection_options.port.unwrap(), forwarder)
})
})
.unwrap();
cx.background_executor().spawn(async move {
loop {
select_biased! {
server_to_client = proxy_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(Some(1))
};
connection_activity_tx.try_send(()).ok();
client_tx.send(server_to_client).await.ok();
}
client_to_server = client_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(None)
};
proxy_tx.send(client_to_server).await.ok();
}
}
}
})
}
}
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
Vec::new()
}
fn connection_options(&self) -> SshConnectionOptions {
self.connection_options.clone()
}
}
#[derive(Default)]
pub(super) struct GlobalConnections(Vec<Option<ChannelForwarder>>);
impl Global for GlobalConnections {}
impl GlobalConnections {
pub(super) fn push(&mut self, forwarder: ChannelForwarder) -> u16 {
self.0.push(Some(forwarder));
self.0.len() as u16 - 1
}
pub(super) fn take(&mut self, port: u16) -> ChannelForwarder {
self.0
.get_mut(port as usize)
.expect("no fake server for port")
.take()
.expect("fake server is already borrowed")
}
pub(super) fn replace(&mut self, port: u16, forwarder: ChannelForwarder) {
let ret = self
.0
.get_mut(port as usize)
.expect("no fake server for port")
.replace(forwarder);
if ret.is_some() {
panic!("fake server is already replaced");
}
}
}
pub(super) struct Delegate;
impl SshClientDelegate for Delegate {
fn ask_password(
&self,
_: String,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<String>> {
unreachable!()
}
fn remote_server_binary_path(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> Result<PathBuf> {
unreachable!()
}
fn get_server_binary(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
unreachable!()
}
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
unreachable!()
}
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
unreachable!()
}
}
}

View File

@@ -641,6 +641,47 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test
})
}
#[gpui::test(iterations = 20)]
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (project, _headless, fs) = init_test(cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
let ix = buffer.text().find('1').unwrap();
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
client
.update(cx, |client, cx| client.simulate_disconnect(cx))
.detach();
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
assert_eq!(
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
"fn one() -> usize { 100 }"
);
}
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok();
@@ -651,9 +692,9 @@ async fn init_test(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
init_logger();
let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(server_cx);
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
@@ -694,8 +735,9 @@ async fn init_test(
cx,
)
});
let project = build_project(ssh_remote_client, cx);
let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
let project = build_project(ssh, cx);
project
.update(cx, {
let headless = headless.clone();

View File

@@ -270,7 +270,7 @@ fn start_server(
})
.detach();
ChannelClient::new(incoming_rx, outgoing_tx, cx)
ChannelClient::new(incoming_rx, outgoing_tx, cx, "server")
}
fn init_paths() -> anyhow::Result<()> {

View File

@@ -44,8 +44,12 @@ impl ReqwestClient {
let mut client = reqwest::Client::builder()
.use_rustls_tls()
.default_headers(map);
if let Some(proxy) = proxy.clone() {
client = client.proxy(reqwest::Proxy::all(proxy.to_string())?);
if let Some(proxy) = proxy.clone().and_then(|proxy_uri| {
reqwest::Proxy::all(proxy_uri.to_string())
.inspect_err(|e| log::error!("Failed to parse proxy URI {}: {}", proxy_uri, e))
.ok()
}) {
client = client.proxy(proxy);
}
let client = client.build()?;
let mut client: ReqwestClient = client.into();
@@ -232,3 +236,47 @@ impl http_client::HttpClient for ReqwestClient {
.boxed()
}
}
#[cfg(test)]
mod tests {
use http_client::{http, HttpClient};
use crate::ReqwestClient;
#[test]
fn test_proxy_uri() {
let client = ReqwestClient::new();
assert_eq!(client.proxy(), None);
let proxy = http::Uri::from_static("http://localhost:10809");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = http::Uri::from_static("https://localhost:10809");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = http::Uri::from_static("socks4://localhost:10808");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = http::Uri::from_static("socks4a://localhost:10808");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = http::Uri::from_static("socks5://localhost:10808");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = http::Uri::from_static("socks5h://localhost:10808");
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
}
#[test]
#[should_panic]
fn test_invalid_proxy_uri() {
let proxy = http::Uri::from_static("file:///etc/hosts");
ReqwestClient::proxy_and_user_agent(Some(proxy), "test").unwrap();
}
}

View File

@@ -98,32 +98,40 @@ impl Vim {
}
}
fn increment_decimal_string(mut num: &str, mut delta: i64) -> String {
let mut negative = false;
if num.chars().next() == Some('-') {
negative = true;
delta = 0 - delta;
num = &num[1..];
}
let result = if let Ok(value) = u64::from_str_radix(num, 10) {
let wrapped = value.wrapping_add_signed(delta);
if delta < 0 && wrapped > value {
negative = !negative;
(u64::MAX - wrapped).wrapping_add(1)
} else if delta > 0 && wrapped < value {
negative = !negative;
u64::MAX - wrapped
} else {
wrapped
fn increment_decimal_string(num: &str, delta: i64) -> String {
let (negative, delta, num_str) = match num.strip_prefix('-') {
Some(n) => (true, -delta, n),
None => (false, delta, num),
};
let num_length = num_str.len();
let leading_zero = num_str.starts_with('0');
let (result, new_negative) = match u64::from_str_radix(num_str, 10) {
Ok(value) => {
let wrapped = value.wrapping_add_signed(delta);
if delta < 0 && wrapped > value {
((u64::MAX - wrapped).wrapping_add(1), !negative)
} else if delta > 0 && wrapped < value {
(u64::MAX - wrapped, !negative)
} else {
(wrapped, negative)
}
}
} else {
u64::MAX
Err(_) => (u64::MAX, negative),
};
if result == 0 || !negative {
format!("{}", result)
let formatted = format!("{}", result);
let new_significant_digits = formatted.len();
let padding = if leading_zero {
num_length.saturating_sub(new_significant_digits)
} else {
format!("-{}", result)
0
};
if new_negative && result != 0 {
format!("-{}{}", "0".repeat(padding), formatted)
} else {
format!("{}{}", "0".repeat(padding), formatted)
}
}
@@ -286,6 +294,63 @@ mod test {
"});
}
#[gpui::test]
async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
000ˇ9
"})
.await;
cx.simulate_shared_keystrokes("ctrl-a").await;
cx.shared_state().await.assert_eq(indoc! {"
001ˇ0
"});
cx.simulate_shared_keystrokes("2 ctrl-x").await;
cx.shared_state().await.assert_eq(indoc! {"
000ˇ8
"});
}
#[gpui::test]
async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
01ˇ1
"})
.await;
cx.simulate_shared_keystrokes("ctrl-a").await;
cx.shared_state().await.assert_eq(indoc! {"
01ˇ2
"});
cx.simulate_shared_keystrokes("1 2 ctrl-x").await;
cx.shared_state().await.assert_eq(indoc! {"
00ˇ0
"});
}
#[gpui::test]
async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
099ˇ9
"})
.await;
cx.simulate_shared_keystrokes("ctrl-a").await;
cx.shared_state().await.assert_eq(indoc! {"
100ˇ0
"});
cx.simulate_shared_keystrokes("2 ctrl-x").await;
cx.shared_state().await.assert_eq(indoc! {"
99ˇ8
"});
}
#[gpui::test]
async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -322,6 +387,27 @@ mod test {
"});
}
#[gpui::test]
async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
00ˇ1
"})
.await;
cx.simulate_shared_keystrokes("ctrl-x").await;
cx.shared_state().await.assert_eq(indoc! {"
00ˇ0
"});
cx.simulate_shared_keystrokes("ctrl-x").await;
cx.shared_state().await.assert_eq(indoc! {"
-00ˇ1
"});
cx.simulate_shared_keystrokes("2 ctrl-a").await;
cx.shared_state().await.assert_eq(indoc! {"
00ˇ1
"});
}
#[gpui::test]
async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View File

@@ -0,0 +1,8 @@
{"Put":{"state":"00ˇ1\n"}}
{"Key":"ctrl-x"}
{"Get":{"state":"00ˇ0\n","mode":"Normal"}}
{"Key":"ctrl-x"}
{"Get":{"state":"-00ˇ1\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"ctrl-a"}
{"Get":{"state":"00ˇ1\n","mode":"Normal"}}

View File

@@ -0,0 +1,6 @@
{"Put":{"state":"099ˇ9\n"}}
{"Key":"ctrl-a"}
{"Get":{"state":"100ˇ0\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"ctrl-x"}
{"Get":{"state":"99ˇ8\n","mode":"Normal"}}

View File

@@ -0,0 +1,6 @@
{"Put":{"state":"000ˇ9\n"}}
{"Key":"ctrl-a"}
{"Get":{"state":"001ˇ0\n","mode":"Normal"}}
{"Key":"2"}
{"Key":"ctrl-x"}
{"Get":{"state":"000ˇ8\n","mode":"Normal"}}

View File

@@ -0,0 +1,7 @@
{"Put":{"state":"01ˇ1\n"}}
{"Key":"ctrl-a"}
{"Get":{"state":"01ˇ2\n","mode":"Normal"}}
{"Key":"1"}
{"Key":"2"}
{"Key":"ctrl-x"}
{"Get":{"state":"00ˇ0\n","mode":"Normal"}}

View File

@@ -0,0 +1,4 @@
{"Put":{"state":"001ˇ0\n"}}
{"Key":"10"}
{"Key":"ctrl-x"}
{"Get":{"state":"000ˇ9\n","mode":"Normal"}}

View File

@@ -11,7 +11,7 @@ pub(crate) mod windows_only_instance;
pub use app_menus::*;
use assistant::PromptBuilder;
use breadcrumbs::Breadcrumbs;
use client::ZED_URL_SCHEME;
use client::{zed_urls, ZED_URL_SCHEME};
use collections::VecDeque;
use command_palette_hooks::CommandPaletteFilter;
use editor::ProposedChangesEditorToolbar;
@@ -419,8 +419,7 @@ pub fn initialize_workspace(
)
.register_action(
|_: &mut Workspace, _: &OpenAccountSettings, cx: &mut ViewContext<Workspace>| {
let server_url = &client::ClientSettings::get_global(cx).server_url;
cx.open_url(&format!("{server_url}/account"));
cx.open_url(&zed_urls::account_url(cx));
},
)
.register_action(

View File

@@ -32,6 +32,7 @@ Zed supports hundreds of programming languages and text formats. Some work out-o
- [JavaScript](./languages/javascript.md)
- [Julia](./languages/julia.md)
- [JSON](./languages/json.md)
- [Jsonnet](./languages/jsonnet.md)
- [Kotlin](./languages/kotlin.md)
- [Lua](./languages/lua.md)
- [Luau](./languages/luau.md)
@@ -104,7 +105,6 @@ Zed supports hundreds of programming languages and text formats. Some work out-o
- [Groq](https://github.com/juice49/zed-groq)
- [INI](https://github.com/bajrangCoder/zed-ini)
- [Java](https://github.com/zed-extensions/java)
- [Jsonnet](https://github.com/narqo/zed-jsonnet)
- [Justfiles](https://github.com/jackTabsCode/zed-just)
- [LaTeX](https://github.com/rzukic/zed-latex)
- [Ledger](https://github.com/mrkstwrt/zed-ledger)

View File

@@ -10,11 +10,9 @@ 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
## Install OpenJDK
You will need to install both a Java runtime (OpenJDK) and Eclipse JDT Language Server (`eclipse.jdt.ls`).
### Install OpenJDK
You will need to install a Java runtime (OpenJDK).
- MacOS: `brew install openjdk`
- Ubuntu: `sudo add-apt-repository ppa:openjdk-23 && sudo apt-get install openjdk-23`
@@ -23,26 +21,19 @@ You will need to install both a Java runtime (OpenJDK) and Eclipse JDT Language
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.
Both extensions will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself.
Add the following to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}).
For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request).
You can add these customizations to your Zed Settings by launching {#action zed::OpenSettings}({#kb zed::OpenSettings}) or by using a `.zed/setting.json` inside your project.
### Zed Java Settings
@@ -50,9 +41,11 @@ Add the following to your Zed Settings by launching {#action zed::OpenSettings}(
{
"lsp": {
"jdtls": {
"settings": {},
"settings": {
"version": "1.40.0", // jdtls version to download and use
"classpath": "/path/to/classes.jar:/path/to/more/classes/"
},
"initialization_options": {}
}
}
}
}
@@ -71,37 +64,9 @@ Add the following to your Zed Settings by launching {#action zed::OpenSettings}(
}
```
## 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
### Zed Java Initialization Options
There are also many more options you can pass directly to the language server, for example:
@@ -184,7 +149,7 @@ There are also many more options you can pass directly to the language server, f
}
```
## Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration}
### 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).
@@ -201,3 +166,27 @@ For example, to enable [Lombok Support](https://github.com/redhat-developer/vsco
}
}
```
## Manual JDTLS Install
If you prefer, you can install JDTLS yourself and both extensions can be configured to use that instead.
- 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)
## 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)

View File

@@ -0,0 +1,24 @@
# Jsonnet
Jsonnet language support in Zed is provided by the community-maintained [Jsonnet extension](https://github.com/narqo/zed-jsonnet).
- Tree Sitter: [sourcegraph/tree-sitter-jsonnet](https://github.com/sourcegraph/tree-sitter-jsonnet)
- Language Server: [grafana/jsonnet-language-server](https://github.com/grafana/jsonnet-language-server)
## Configuration
Workspace configuration options can be passed to the language server via the `lsp` settings of the `settings.json`.
The following example enables support for resolving [tanka](https://tanka.dev) import paths in `jsonnet-language-server`:
```json
{
"lsp": {
"jsonnet-language-server": {
"settings": {
"resolve_paths_with_tanka": true
}
}
}
}
```

View File

@@ -3,6 +3,16 @@
(arguments (alias) @name)
(#match? @context "^(defmodule|defprotocol)$")) @item
(call
target: (identifier) @context
(arguments (_) @name)?
(#match? @context "^(setup|setup_all)$")) @item
(call
target: (identifier) @context
(arguments (string) @name)
(#match? @context "^(describe|test)$")) @item
(unary_operator
operator: "@" @name
operand: (call

View File

@@ -1,3 +0,0 @@
target
*.wasm
grammars

View File

@@ -1,16 +0,0 @@
[package]
name = "zed_svelte"
version = "0.2.0"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/svelte.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View File

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

View File

@@ -1,15 +0,0 @@
id = "svelte"
name = "Svelte"
description = "Svelte support"
version = "0.2.0"
schema_version = 1
authors = []
repository = "https://github.com/zed-extensions/svelte"
[language_servers.svelte-language-server]
name = "Svelte Language Server"
language = "Svelte"
[grammars.svelte]
repository = "https://github.com/tree-sitter-grammars/tree-sitter-svelte"
commit = "3f06f705410683adb17d146b5eca28c62fe81ba6"

View File

@@ -1,7 +0,0 @@
("<" @open ">" @close)
("{" @open "}" @close)
("'" @open "'" @close)
("\"" @open "\"" @close)
("(" @open ")" @close)
; ("[" @open "]" @close)
; ("`" @open "`" @close)

View File

@@ -1,22 +0,0 @@
name = "Svelte"
grammar = "svelte"
path_suffixes = ["svelte"]
block_comment = ["<!-- ", " -->"]
autoclose_before = ":\"'}]>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "<", end = ">", close = true, newline = true, not_in = ["string"] },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "!--", end = " --", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = true, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = true, not_in = ["string"] },
{ start = "`", end = "`", close = true, newline = true, not_in = ["string"] },
]
scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "svelte"
prettier_plugins = ["prettier-plugin-svelte"]
[overrides.string]
word_characters = ["-"]
opt_into_language_servers = ["tailwindcss-language-server"]

View File

@@ -1,107 +0,0 @@
; comments
(comment) @comment
; property attribute
(attribute_directive) @attribute.function
(attribute_identifier) @attribute
(attribute_modifier) @attribute.special
; Style component attributes as @property
(start_tag
(
(tag_name) @_tag_name
(#match? @_tag_name "^[A-Z]")
)
(attribute
(attribute_name
(attribute_identifier) @tag.property
)
)
)
(self_closing_tag
(
(tag_name) @_tag_name
(#match? @_tag_name "^[A-Z]")
)
(attribute
(attribute_name
(attribute_identifier) @tag.property
)
)
)
; style elements starting with lowercase letters as tags
(
(tag_name) @tag
(#match? @tag "^[a-z]")
)
; style elements starting with uppercase letters as components (types)
; Also valid might be to treat them as constructors
(
(tag_name) @tag @tag.component.type.constructor
(#match? @tag "^[A-Z]")
)
[
"<"
">"
"</"
"/>"
] @tag.punctuation.bracket
[
"{"
"}"
] @punctuation.bracket
[
"|"
] @punctuation.delimiter
[
"@"
"#"
":"
"/"
] @tag.punctuation.special
"=" @operator
; Treating (if, each, ...) as a keyword inside of blocks
; like {#if ...} or {#each ...}
(block_start_tag
tag: _ @tag.keyword
)
(block_tag
tag: _ @tag.keyword
)
(block_end_tag
tag: _ @tag.keyword
)
(expression_tag
tag: _ @tag.keyword
)
; Style quoted string attribute values
(quoted_attribute_value) @string
; Highlight the `as` keyword in each blocks
(each_start
("as") @tag.keyword
)
; Highlight the snippet name as a function
; (e.g. {#snippet foo(bar)}
(snippet_name) @function

View File

@@ -1,9 +0,0 @@
[
(element)
(if_statement)
(each_statement)
(await_statement)
(snippet_statement)
(script_element)
(style_element)
] @indent

View File

@@ -1,86 +0,0 @@
; ; injections.scm
; ; --------------
; Match script tags with a lang attribute
(script_element
(start_tag
(attribute
(attribute_name) @_attr_name
(#eq? @_attr_name "lang")
(quoted_attribute_value
(attribute_value) @language
)
)
)
(raw_text) @content
)
; Match script tags without a lang attribute
(script_element
(start_tag
(attribute
(attribute_name) @_attr_name
)*
)
(raw_text) @content
(#not-any-of? @_attr_name "lang")
(#set! language "javascript")
)
; Match the contents of the script's generics="T extends string" as typescript code
;
; Disabled for the time-being because tree-sitter is treating the generics
; attribute as a top-level typescript statement, where `T extends string` is
; not a valid top-level typescript statement.
;
; (script_element
; (start_tag
; (attribute
; (attribute_name) @_attr_name
; (#eq? @_attr_name "generics")
; (quoted_attribute_value
; (attribute_value) @content
; )
; )
; )
; (#set! language "typescript")
; )
; Mark everything as typescript because it's
; a more generic superset of javascript
; Not sure if it's possible to somehow refer to the
; script's language attribute here.
((svelte_raw_text) @content
(#set! "language" "ts")
)
; Match style tags with a lang attribute
(style_element
(start_tag
(attribute
(attribute_name) @_attr_name
(#eq? @_attr_name "lang")
(quoted_attribute_value
(attribute_value) @language
)
)
)
(raw_text) @content
)
; Match style tags without a lang attribute
(style_element
(start_tag
(attribute
(attribute_name) @_attr_name
)*
)
(raw_text) @content
(#not-any-of? @_attr_name "lang")
(#set! language "css")
)
; Downstream TODO: Style highlighting for `style:background="red"` and `style="background: red"` strings
; Downstream TODO: Style component comments as markdown

View File

@@ -1,69 +0,0 @@
(script_element
(start_tag) @name
(raw_text) @context @item
)
(script_element
(end_tag) @name @item
)
(style_element
(start_tag) @name
(raw_text) @context
) @item
(document) @item
(comment) @annotation
(if_statement
(if_start) @name
) @item
(else_block
(else_start) @name
) @item
(else_if_block
(else_if_start) @name
) @item
(element
(start_tag) @name
) @item
(element
(self_closing_tag) @name
) @item
; (if_end) @name @item
(each_statement
(each_start) @name
) @item
(snippet_statement
(snippet_start) @name
) @item
(snippet_end) @name @item
(html_tag) @name @item
(const_tag) @name @item
(await_statement
(await_start) @name
) @item
(then_block
(then_start) @name
) @item
(catch_block
(catch_start) @name
) @item

View File

@@ -1,7 +0,0 @@
(comment) @comment
[
(raw_text)
(attribute_value)
(quoted_attribute_value)
] @string

View File

@@ -1,124 +0,0 @@
use std::{env, fs};
use zed_extension_api::{self as zed, serde_json, Result};
struct SvelteExtension {
did_find_server: bool,
}
const SERVER_PATH: &str = "node_modules/svelte-language-server/bin/server.js";
const PACKAGE_NAME: &str = "svelte-language-server";
impl SvelteExtension {
fn server_exists(&self) -> bool {
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
}
fn server_script_path(&mut self, id: &zed::LanguageServerId) -> Result<String> {
let server_exists = self.server_exists();
if self.did_find_server && server_exists {
return Ok(SERVER_PATH.to_string());
}
zed::set_language_server_installation_status(
id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
if !server_exists
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
{
zed::set_language_server_installation_status(
id,
&zed::LanguageServerInstallationStatus::Downloading,
);
let result = zed::npm_install_package(PACKAGE_NAME, &version);
match result {
Ok(()) => {
if !self.server_exists() {
Err(format!(
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
))?;
}
}
Err(error) => {
if !self.server_exists() {
Err(error)?;
}
}
}
}
self.did_find_server = true;
Ok(SERVER_PATH.to_string())
}
}
impl zed::Extension for SvelteExtension {
fn new() -> Self {
Self {
did_find_server: false,
}
}
fn language_server_command(
&mut self,
id: &zed::LanguageServerId,
_: &zed::Worktree,
) -> Result<zed::Command> {
let server_path = self.server_script_path(id)?;
Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![
env::current_dir()
.unwrap()
.join(&server_path)
.to_string_lossy()
.to_string(),
"--stdio".to_string(),
],
env: Default::default(),
})
}
fn language_server_initialization_options(
&mut self,
_: &zed::LanguageServerId,
_: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
let config = serde_json::json!({
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": false
},
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true,
"suppressWhenTypeMatchesName": false
},
"propertyDeclarationTypes": {
"enabled": true
},
"functionLikeReturnTypes": {
"enabled": true
},
"enumMemberValues": {
"enabled": true
}
}
});
Ok(Some(serde_json::json!({
"provideFormatter": true,
"configuration": {
"typescript": config,
"javascript": config
}
})))
}
}
zed::register_extension!(SvelteExtension);

View File

@@ -1,17 +0,0 @@
[package]
name = "zed_vue"
version = "0.1.0"
edition = "2021"
publish = false
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/vue.rs"
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
zed_extension_api = "0.1.0"

View File

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

View File

@@ -1,19 +0,0 @@
id = "vue"
name = "Vue"
description = "Vue support."
version = "0.1.0"
schema_version = 1
authors = ["Piotr Osiewicz <piotr@zed.dev>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.vue-language-server]
name = "Vue Language Server"
language = "Vue.js"
language_ids = { "Vue.js" = "vue" }
# REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
# sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
code_action_kinds = ["", "quickfix", "refactor.rewrite"]
[grammars.vue]
repository = "https://github.com/tree-sitter-grammars/tree-sitter-vue"
commit = "7e48557b903a9db9c38cea3b7839ef7e1f36c693"

View File

@@ -1,2 +0,0 @@
("<" @open ">" @close)
("\"" @open "\"" @close)

View File

@@ -1,22 +0,0 @@
name = "Vue.js"
code_fence_block_name = "vue"
grammar = "vue"
path_suffixes = ["vue"]
block_comment = ["<!-- ", " -->"]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
]
word_characters = ["-"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "vue"
[overrides.string]
word_characters = ["-"]
opt_into_language_servers = ["tailwindcss-language-server"]

View File

@@ -1,15 +0,0 @@
(attribute) @property
(directive_attribute) @property
(quoted_attribute_value) @string
(interpolation) @punctuation.special
(raw_text) @embedded
((tag_name) @type
(#match? @type "^[A-Z]"))
(directive_name) @keyword
(directive_argument) @constant
(start_tag) @tag
(end_tag) @tag
(self_closing_tag) @tag

View File

@@ -1,60 +0,0 @@
; <script>
((script_element
(start_tag) @_no_lang
(raw_text) @content)
(#not-match? @_no_lang "lang=")
(#set! "language" "javascript"))
; <script lang="js">
((script_element
(start_tag
(attribute
(attribute_name) @_lang
(quoted_attribute_value
(attribute_value) @_js)))
(raw_text) @content)
(#eq? @_lang "lang")
(#eq? @_js "js")
(#set! "language" "javascript"))
; <script lang="ts">
((script_element
(start_tag
(attribute
(attribute_name) @_lang
(quoted_attribute_value
(attribute_value) @_ts)))
(raw_text) @content)
(#eq? @_lang "lang")
(#eq? @_ts "ts")
(#set! "language" "typescript"))
; <script lang="tsx">
; <script lang="jsx">
; Zed built-in tsx, we mark it as tsx ^:)
(script_element
(start_tag
(attribute
(attribute_name) @_attr
(quoted_attribute_value
(attribute_value) @language)))
(#eq? @_attr "lang")
(#any-of? @language "tsx" "jsx")
(raw_text) @content)
; {{ }}
((interpolation
(raw_text) @content)
(#set! "language" "typescript"))
; v-
(directive_attribute
(quoted_attribute_value
(attribute_value) @content
(#set! "language" "typescript")))
; TODO: support less/sass/scss
(style_element
(raw_text) @content
(#set! "language" "css"))

View File

@@ -1,7 +0,0 @@
(comment) @comment
[
(raw_text)
(attribute_value)
(quoted_attribute_value)
] @string

View File

@@ -1,205 +0,0 @@
use std::collections::HashMap;
use std::{env, fs};
use serde::Deserialize;
use zed::lsp::{Completion, CompletionKind};
use zed::CodeLabelSpan;
use zed_extension_api::{self as zed, serde_json, Result};
const SERVER_PATH: &str = "node_modules/@vue/language-server/bin/vue-language-server.js";
const PACKAGE_NAME: &str = "@vue/language-server";
const TYPESCRIPT_PACKAGE_NAME: &str = "typescript";
/// The relative path to TypeScript's SDK.
const TYPESCRIPT_TSDK_PATH: &str = "node_modules/typescript/lib";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PackageJson {
#[serde(default)]
dependencies: HashMap<String, String>,
#[serde(default)]
dev_dependencies: HashMap<String, String>,
}
struct VueExtension {
did_find_server: bool,
typescript_tsdk_path: String,
}
impl VueExtension {
fn server_exists(&self) -> bool {
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
}
fn server_script_path(
&mut self,
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<String> {
let server_exists = self.server_exists();
if self.did_find_server && server_exists {
self.install_typescript_if_needed(worktree)?;
return Ok(SERVER_PATH.to_string());
}
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
// We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
let version = "1.8".to_string();
if !server_exists
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
{
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
let result = zed::npm_install_package(PACKAGE_NAME, &version);
match result {
Ok(()) => {
if !self.server_exists() {
Err(format!(
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
))?;
}
}
Err(error) => {
if !self.server_exists() {
Err(error)?;
}
}
}
}
self.install_typescript_if_needed(worktree)?;
self.did_find_server = true;
Ok(SERVER_PATH.to_string())
}
/// Returns whether a local copy of TypeScript exists in the worktree.
fn typescript_exists_for_worktree(&self, worktree: &zed::Worktree) -> Result<bool> {
let package_json = worktree.read_text_file("package.json")?;
let package_json: PackageJson = serde_json::from_str(&package_json)
.map_err(|err| format!("failed to parse package.json: {err}"))?;
let dev_dependencies = &package_json.dev_dependencies;
let dependencies = &package_json.dependencies;
// Since the extension is not allowed to read the filesystem within the project
// except through the worktree (which does not contains `node_modules`), we check
// the `package.json` to see if `typescript` is listed in the dependencies.
Ok(dev_dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME)
|| dependencies.contains_key(TYPESCRIPT_PACKAGE_NAME))
}
fn install_typescript_if_needed(&mut self, worktree: &zed::Worktree) -> Result<()> {
if self
.typescript_exists_for_worktree(worktree)
.unwrap_or_default()
{
println!("found local TypeScript installation at '{TYPESCRIPT_TSDK_PATH}'");
return Ok(());
}
let installed_typescript_version =
zed::npm_package_installed_version(TYPESCRIPT_PACKAGE_NAME)?;
let latest_typescript_version = zed::npm_package_latest_version(TYPESCRIPT_PACKAGE_NAME)?;
if installed_typescript_version.as_ref() != Some(&latest_typescript_version) {
println!("installing {TYPESCRIPT_PACKAGE_NAME}@{latest_typescript_version}");
zed::npm_install_package(TYPESCRIPT_PACKAGE_NAME, &latest_typescript_version)?;
} else {
println!("typescript already installed");
}
self.typescript_tsdk_path = env::current_dir()
.unwrap()
.join(TYPESCRIPT_TSDK_PATH)
.to_string_lossy()
.to_string();
Ok(())
}
}
impl zed::Extension for VueExtension {
fn new() -> Self {
Self {
did_find_server: false,
typescript_tsdk_path: TYPESCRIPT_TSDK_PATH.to_owned(),
}
}
fn language_server_command(
&mut self,
language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let server_path = self.server_script_path(language_server_id, worktree)?;
Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![
env::current_dir()
.unwrap()
.join(&server_path)
.to_string_lossy()
.to_string(),
"--stdio".to_string(),
],
env: Default::default(),
})
}
fn language_server_initialization_options(
&mut self,
_language_server_id: &zed::LanguageServerId,
_worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
Ok(Some(serde_json::json!({
"typescript": {
"tsdk": self.typescript_tsdk_path
}
})))
}
fn label_for_completion(
&self,
_language_server_id: &zed::LanguageServerId,
completion: Completion,
) -> Option<zed::CodeLabel> {
let highlight_name = match completion.kind? {
CompletionKind::Class | CompletionKind::Interface => "type",
CompletionKind::Constructor => "type",
CompletionKind::Constant => "constant",
CompletionKind::Function | CompletionKind::Method => "function",
CompletionKind::Property | CompletionKind::Field => "tag",
CompletionKind::Variable => "type",
CompletionKind::Keyword => "keyword",
CompletionKind::Value => "tag",
_ => return None,
};
let len = completion.label.len();
let name_span = CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string()));
Some(zed::CodeLabel {
code: Default::default(),
spans: if let Some(detail) = completion.detail {
vec![
name_span,
CodeLabelSpan::literal(" ", None),
CodeLabelSpan::literal(detail, None),
]
} else {
vec![name_span]
},
filter_range: (0..len).into(),
})
}
}
zed::register_extension!(VueExtension);