Compare commits
37 Commits
chat-or-ed
...
ssh-reconn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ba60d1a8d | ||
|
|
06f18bc365 | ||
|
|
f5833010aa | ||
|
|
f86f06c81a | ||
|
|
9b0ee7d30b | ||
|
|
47a764553d | ||
|
|
47380001cc | ||
|
|
98ecb43b2d | ||
|
|
be474a6d6f | ||
|
|
b44bed0115 | ||
|
|
11a82e3347 | ||
|
|
be81e29b0f | ||
|
|
6a463be1ae | ||
|
|
64a6e9cafb | ||
|
|
fa738ee5e1 | ||
|
|
15449cdf30 | ||
|
|
2db9090a2f | ||
|
|
34b8655bf6 | ||
|
|
5b745a82e1 | ||
|
|
c59a75db1d | ||
|
|
b3c93130ec | ||
|
|
73a6c542f3 | ||
|
|
2cd6c19873 | ||
|
|
6f24c1da79 | ||
|
|
5508832ba6 | ||
|
|
35f2f2aac4 | ||
|
|
9e27b6694a | ||
|
|
f5124c21d1 | ||
|
|
ea460014ab | ||
|
|
5168fc27a1 | ||
|
|
d7ff85e2a1 | ||
|
|
b2b6b1e8a1 | ||
|
|
023186b7a0 | ||
|
|
e5ec08e8f8 | ||
|
|
2bcf9fc490 | ||
|
|
2f6c7a2939 | ||
|
|
2f26a74c83 |
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
18
Cargo.lock
generated
@@ -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"
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -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",
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"php": "php",
|
||||
"plist": "template",
|
||||
"png": "image",
|
||||
"postcss": "css",
|
||||
"ppt": "document",
|
||||
"pptx": "document",
|
||||
"prettierignore": "prettier",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
crates/client/src/zed_urls.rs
Normal file
19
crates/client/src/zed_urls.rs
Normal 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))
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "CSS"
|
||||
grammar = "css"
|
||||
path_suffixes = ["css"]
|
||||
path_suffixes = ["css", "postcss"]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -32,6 +32,7 @@ macro_rules! messages {
|
||||
responding_to,
|
||||
original_sender_id,
|
||||
payload: Some(envelope::Payload::$name(self)),
|
||||
ack_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"}}
|
||||
@@ -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"}}
|
||||
@@ -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"}}
|
||||
@@ -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"}}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"Put":{"state":"001ˇ0\n"}}
|
||||
{"Key":"10"}
|
||||
{"Key":"ctrl-x"}
|
||||
{"Get":{"state":"000ˇ9\n","mode":"Normal"}}
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
24
docs/src/languages/jsonnet.md
Normal file
24
docs/src/languages/jsonnet.md
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
|
||||
3
extensions/svelte/.gitignore
vendored
3
extensions/svelte/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
target
|
||||
*.wasm
|
||||
grammars
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -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"
|
||||
@@ -1,7 +0,0 @@
|
||||
("<" @open ">" @close)
|
||||
("{" @open "}" @close)
|
||||
("'" @open "'" @close)
|
||||
("\"" @open "\"" @close)
|
||||
("(" @open ")" @close)
|
||||
; ("[" @open "]" @close)
|
||||
; ("`" @open "`" @close)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
(element)
|
||||
(if_statement)
|
||||
(each_statement)
|
||||
(await_statement)
|
||||
(snippet_statement)
|
||||
(script_element)
|
||||
(style_element)
|
||||
] @indent
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,7 +0,0 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(raw_text)
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
||||
@@ -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);
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -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"
|
||||
@@ -1,2 +0,0 @@
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
@@ -1,7 +0,0 @@
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(raw_text)
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
] @string
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user