Compare commits
5 Commits
fix-depend
...
keymap-shr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
203c4b2406 | ||
|
|
33dcfbbc4a | ||
|
|
c758744687 | ||
|
|
8d3c7b6eb8 | ||
|
|
2c53899c5d |
14
.github/workflows/check_deps.yml
vendored
14
.github/workflows/check_deps.yml
vendored
@@ -1,14 +0,0 @@
|
||||
name: "Dependency Review"
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -251,8 +251,8 @@ jobs:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: |
|
||||
target/zed-remote-server-macos-x86_64.gz
|
||||
target/zed-remote-server-macos-aarch64.gz
|
||||
target/zed-remote-server-mac-x86_64.gz
|
||||
target/zed-remote-server-mac-aarch64.gz
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
|
||||
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -4013,7 +4013,6 @@ dependencies = [
|
||||
"env_logger",
|
||||
"extension",
|
||||
"fs",
|
||||
"http 0.1.0",
|
||||
"language",
|
||||
"log",
|
||||
"rpc",
|
||||
@@ -5316,7 +5315,6 @@ name = "http"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more",
|
||||
"futures 0.3.28",
|
||||
"futures-lite 1.13.0",
|
||||
"isahc",
|
||||
@@ -13910,7 +13908,7 @@ dependencies = [
|
||||
name = "zed_erlang"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
"zed_extension_api 0.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14013,13 +14011,6 @@ dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_ruff"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_snippets"
|
||||
version = "0.0.5"
|
||||
|
||||
@@ -141,7 +141,6 @@ members = [
|
||||
"extensions/php",
|
||||
"extensions/prisma",
|
||||
"extensions/purescript",
|
||||
"extensions/ruff",
|
||||
"extensions/ruby",
|
||||
"extensions/snippets",
|
||||
"extensions/svelte",
|
||||
|
||||
@@ -606,7 +606,6 @@
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"shift-ctrl-c": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
"ctrl-a": "editor::SelectAll",
|
||||
"shift-ctrl-v": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
|
||||
@@ -628,7 +628,6 @@
|
||||
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
|
||||
"cmd-c": "terminal::Copy",
|
||||
"cmd-v": "terminal::Paste",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-k": "terminal::Clear",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
// Some nice conveniences
|
||||
|
||||
@@ -705,7 +705,7 @@
|
||||
//
|
||||
"file_types": {
|
||||
"JSON": ["flake.lock"],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "tsconfig.json"]
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
@@ -743,9 +743,6 @@
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Erlang": {
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
"source.organizeImports": true
|
||||
@@ -815,9 +812,6 @@
|
||||
"plugins": ["prettier-plugin-sql"]
|
||||
}
|
||||
},
|
||||
"Starlark": {
|
||||
"language_servers": ["starpls", "!buck2-lsp", "..."]
|
||||
},
|
||||
"Svelte": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
|
||||
@@ -20,12 +20,6 @@ pub enum Model {
|
||||
Claude3Sonnet,
|
||||
#[serde(alias = "claude-3-haiku", rename = "claude-3-haiku-20240307")]
|
||||
Claude3Haiku,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
max_tokens: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -39,41 +33,30 @@ impl Model {
|
||||
} else if id.starts_with("claude-3-haiku") {
|
||||
Ok(Self::Claude3Haiku)
|
||||
} else {
|
||||
Ok(Self::Custom {
|
||||
name: id.to_string(),
|
||||
max_tokens: None,
|
||||
})
|
||||
Err(anyhow!("Invalid model id: {}", id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet-20240620",
|
||||
Model::Claude3Opus => "claude-3-opus-20240229",
|
||||
Model::Claude3Sonnet => "claude-3-sonnet-20240229",
|
||||
Model::Claude3Haiku => "claude-3-opus-20240307",
|
||||
Model::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &str {
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Claude3_5Sonnet
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200_000,
|
||||
Self::Custom { max_tokens, .. } => max_tokens.unwrap_or(200_000),
|
||||
}
|
||||
200_000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +90,6 @@ impl From<Role> for String {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(serialize_with = "serialize_request_model")]
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
@@ -115,13 +97,6 @@ pub struct Request {
|
||||
pub max_tokens: u32,
|
||||
}
|
||||
|
||||
fn serialize_request_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&model.id())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
|
||||
@@ -668,11 +668,7 @@ mod tests {
|
||||
"version": "1",
|
||||
"provider": {
|
||||
"name": "zed.dev",
|
||||
"default_model": {
|
||||
"custom": {
|
||||
"name": "custom-provider"
|
||||
}
|
||||
}
|
||||
"default_model": "custom"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
@@ -683,10 +679,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).provider,
|
||||
AssistantProvider::ZedDotDev {
|
||||
model: CloudModel::Custom {
|
||||
name: "custom-provider".into(),
|
||||
max_tokens: None
|
||||
}
|
||||
model: CloudModel::Custom("custom".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ use futures::{
|
||||
SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
point, AppContext, EventEmitter, FocusHandle, FocusableView, Global, HighlightStyle, Model,
|
||||
ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView,
|
||||
WindowContext,
|
||||
point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, Global, HighlightStyle,
|
||||
Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
@@ -45,7 +45,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
use util::RangeExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -1461,7 +1461,7 @@ impl Render for PromptEditor {
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
@@ -1473,7 +1473,7 @@ impl Render for PromptEditor {
|
||||
.display_name()
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
"Click to Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -1864,8 +1864,12 @@ impl PromptEditor {
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
|
||||
@@ -13,8 +13,8 @@ use editor::{
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
|
||||
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
|
||||
AppContext, Context, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
|
||||
Model, ModelContext, Subscription, Task, TextStyle, UpdateGlobal, View, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
|
||||
@@ -27,7 +27,7 @@ use std::{
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ContextMenu, IconButtonShape, PopoverMenu, Tooltip};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
|
||||
@@ -588,7 +588,7 @@ impl Render for PromptEditor {
|
||||
})
|
||||
.trigger(
|
||||
IconButton::new("context", IconName::Settings)
|
||||
.shape(IconButtonShape::Square)
|
||||
.size(ButtonSize::None)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |cx| {
|
||||
@@ -600,7 +600,7 @@ impl Render for PromptEditor {
|
||||
.display_name()
|
||||
),
|
||||
None,
|
||||
"Change Model",
|
||||
"Click to Change Model",
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
@@ -945,9 +945,13 @@ impl PromptEditor {
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_weight: FontWeight::NORMAL,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
|
||||
@@ -28,7 +28,7 @@ use std::{
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -401,15 +401,7 @@ impl AutoUpdater {
|
||||
release_channel = ReleaseChannel::Nightly;
|
||||
}
|
||||
|
||||
let release = Self::get_latest_release(
|
||||
&this,
|
||||
"zed-remote-server",
|
||||
os,
|
||||
arch,
|
||||
Some(release_channel),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
let release = Self::get_latest_release(&this, "zed-remote-server", os, arch, cx).await?;
|
||||
|
||||
let servers_dir = paths::remote_servers_dir();
|
||||
let channel_dir = servers_dir.join(release_channel.dev_name());
|
||||
@@ -431,7 +423,6 @@ impl AutoUpdater {
|
||||
asset: &str,
|
||||
os: &str,
|
||||
arch: &str,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<JsonRelease> {
|
||||
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
|
||||
@@ -439,10 +430,14 @@ impl AutoUpdater {
|
||||
"/api/releases/latest?asset={}&os={}&arch={}",
|
||||
asset, os, arch
|
||||
));
|
||||
if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
}
|
||||
cx.update(|cx| {
|
||||
if let Some(param) = ReleaseChannel::try_global(cx)
|
||||
.and_then(|release_channel| release_channel.release_query_param())
|
||||
{
|
||||
url_string += "&";
|
||||
url_string += param;
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut response = client.get(&url_string, Default::default(), true).await?;
|
||||
|
||||
@@ -453,34 +448,17 @@ impl AutoUpdater {
|
||||
.await
|
||||
.context("error reading release")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"failed to fetch release: {:?}",
|
||||
String::from_utf8_lossy(&body),
|
||||
))?;
|
||||
}
|
||||
|
||||
serde_json::from_slice(body.as_slice()).with_context(|| {
|
||||
format!(
|
||||
"error deserializing release {:?}",
|
||||
String::from_utf8_lossy(&body),
|
||||
)
|
||||
})
|
||||
serde_json::from_slice(body.as_slice()).context("error deserializing release")
|
||||
}
|
||||
|
||||
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
|
||||
let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
|
||||
let (client, current_version) = this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
(this.http_client.clone(), this.current_version)
|
||||
})?;
|
||||
|
||||
let release =
|
||||
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
|
||||
let release = Self::get_latest_release(&this, "zed", OS, ARCH, &mut cx).await?;
|
||||
|
||||
let should_download = match *RELEASE_CHANNEL {
|
||||
ReleaseChannel::Nightly => cx
|
||||
@@ -507,14 +485,7 @@ impl AutoUpdater {
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-auto-update")
|
||||
.tempdir()?;
|
||||
|
||||
let filename = match OS {
|
||||
"macos" => Ok("Zed.dmg"),
|
||||
"linux" => Ok("zed.tar.gz"),
|
||||
_ => Err(anyhow!("not supported: {:?}", OS)),
|
||||
}?;
|
||||
let downloaded_asset = temp_dir.path().join(filename);
|
||||
download_release(&downloaded_asset, release, client, &cx).await?;
|
||||
let downloaded_asset = download_release(&temp_dir, release, "zed", client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
@@ -595,11 +566,13 @@ async fn download_remote_server_binary(
|
||||
}
|
||||
|
||||
async fn download_release(
|
||||
target_path: &Path,
|
||||
temp_dir: &tempfile::TempDir,
|
||||
release: JsonRelease,
|
||||
target_filename: &str,
|
||||
client: Arc<HttpClientWithUrl>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
) -> Result<PathBuf> {
|
||||
let target_path = temp_dir.path().join(target_filename);
|
||||
let mut target_file = File::create(&target_path).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
|
||||
@@ -621,7 +594,7 @@ async fn download_release(
|
||||
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", target_path);
|
||||
|
||||
Ok(())
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
async fn install_release_linux(
|
||||
|
||||
@@ -4514,7 +4514,7 @@ impl RateLimit for CompleteWithLanguageModelRateLimit {
|
||||
}
|
||||
|
||||
async fn complete_with_language_model(
|
||||
mut request: proto::CompleteWithLanguageModel,
|
||||
request: proto::CompleteWithLanguageModel,
|
||||
response: StreamingResponse<proto::CompleteWithLanguageModel>,
|
||||
session: Session,
|
||||
open_ai_api_key: Option<Arc<str>>,
|
||||
@@ -4530,43 +4530,18 @@ async fn complete_with_language_model(
|
||||
.check::<CompleteWithLanguageModelRateLimit>(session.user_id())
|
||||
.await?;
|
||||
|
||||
let mut provider_and_model = request.model.split('/');
|
||||
let (provider, model) = match (
|
||||
provider_and_model.next().unwrap(),
|
||||
provider_and_model.next(),
|
||||
) {
|
||||
(provider, Some(model)) => (provider, model),
|
||||
(model, None) => {
|
||||
if model.starts_with("gpt") {
|
||||
("openai", model)
|
||||
} else if model.starts_with("gemini") {
|
||||
("google", model)
|
||||
} else if model.starts_with("claude") {
|
||||
("anthropic", model)
|
||||
} else {
|
||||
("unknown", model)
|
||||
}
|
||||
}
|
||||
};
|
||||
let provider = provider.to_string();
|
||||
request.model = model.to_string();
|
||||
|
||||
match provider.as_str() {
|
||||
"openai" => {
|
||||
let api_key = open_ai_api_key.context("no OpenAI API key configured on the server")?;
|
||||
complete_with_open_ai(request, response, session, api_key).await?;
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_key =
|
||||
anthropic_api_key.context("no Anthropic AI API key configured on the server")?;
|
||||
complete_with_anthropic(request, response, session, api_key).await?;
|
||||
}
|
||||
"google" => {
|
||||
let api_key =
|
||||
google_ai_api_key.context("no Google AI API key configured on the server")?;
|
||||
complete_with_google_ai(request, response, session, api_key).await?;
|
||||
}
|
||||
provider => return Err(anyhow!("unknown provider {:?}", provider))?,
|
||||
if request.model.starts_with("gpt") {
|
||||
let api_key =
|
||||
open_ai_api_key.ok_or_else(|| anyhow!("no OpenAI API key configured on the server"))?;
|
||||
complete_with_open_ai(request, response, session, api_key).await?;
|
||||
} else if request.model.starts_with("gemini") {
|
||||
let api_key = google_ai_api_key
|
||||
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
|
||||
complete_with_google_ai(request, response, session, api_key).await?;
|
||||
} else if request.model.starts_with("claude") {
|
||||
let api_key = anthropic_api_key
|
||||
.ok_or_else(|| anyhow!("no Anthropic AI API key configured on the server"))?;
|
||||
complete_with_anthropic(request, response, session, api_key).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,7 +6,7 @@ use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorSt
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||
Render, Task, TextStyle, View, ViewContext, WeakView,
|
||||
Render, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
||||
};
|
||||
use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
@@ -537,7 +537,10 @@ impl Render for MessageEditor {
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
div()
|
||||
|
||||
@@ -16,7 +16,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, FocusableView, FontStyle, InteractiveElement, IntoElement,
|
||||
ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
|
||||
Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
WeakView, WhiteSpace,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||
use project::{Fs, Project};
|
||||
@@ -2194,7 +2194,10 @@ impl CollabPanel {
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
|
||||
@@ -4,7 +4,7 @@ use anthropic::{stream_completion, Model as AnthropicModel, Request, RequestMess
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task, TextStyle, View};
|
||||
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
|
||||
use http::HttpClient;
|
||||
use language_model::Role;
|
||||
use settings::Settings;
|
||||
@@ -257,8 +257,12 @@ impl AuthenticationPrompt {
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key,
|
||||
|
||||
@@ -54,15 +54,15 @@ impl CloudCompletionProvider {
|
||||
|
||||
impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
fn available_models(&self) -> Vec<LanguageModel> {
|
||||
let mut custom_model = if matches!(self.model, CloudModel::Custom { .. }) {
|
||||
Some(self.model.clone())
|
||||
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
|
||||
Some(custom_model)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
CloudModel::iter()
|
||||
.filter_map(move |model| {
|
||||
if let CloudModel::Custom { .. } = model {
|
||||
custom_model.take()
|
||||
if let CloudModel::Custom(_) = model {
|
||||
Some(CloudModel::Custom(custom_model.take()?))
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
@@ -117,9 +117,9 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
|
||||
count_open_ai_tokens(request, cx.background_executor())
|
||||
}
|
||||
LanguageModel::Cloud(CloudModel::Custom { name, .. }) => {
|
||||
LanguageModel::Cloud(CloudModel::Custom(model)) => {
|
||||
let request = self.client.request(proto::CountTokensWithLanguageModel {
|
||||
model: name,
|
||||
model,
|
||||
messages: request
|
||||
.messages
|
||||
.iter()
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::LanguageModelCompletionProvider;
|
||||
use anyhow::{anyhow, Result};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
||||
use gpui::{AnyView, AppContext, Task, TextStyle, View};
|
||||
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
|
||||
use http::HttpClient;
|
||||
use language_model::{CloudModel, LanguageModel, LanguageModelRequest, Role};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
@@ -241,7 +241,6 @@ pub fn count_open_ai_tokens(
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku)
|
||||
| LanguageModel::Cloud(CloudModel::Custom { .. })
|
||||
| LanguageModel::OpenAi(OpenAiModel::Custom { .. }) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
@@ -299,8 +298,12 @@ impl AuthenticationPrompt {
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.api_key,
|
||||
|
||||
@@ -954,7 +954,6 @@ fn random_diagnostic(
|
||||
is_primary,
|
||||
is_disk_based: false,
|
||||
is_unnecessary: false,
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@ use futures::Future;
|
||||
use git::blame::BlameEntry;
|
||||
use git::Oid;
|
||||
use gpui::{
|
||||
Asset, ClipboardItem, Element, ParentElement, Render, ScrollHandle, StatefulInteractiveElement,
|
||||
WeakView, WindowContext,
|
||||
Asset, Element, ParentElement, Render, ScrollHandle, StatefulInteractiveElement, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::ThemeSettings;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use time::UtcOffset;
|
||||
use ui::{prelude::*, tooltip_container, Avatar};
|
||||
use ui::{
|
||||
div, h_flex, tooltip_container, v_flex, Avatar, Button, ButtonStyle, Clickable as _, Color,
|
||||
FluentBuilder, Icon, IconName, IconPosition, InteractiveElement as _, IntoElement,
|
||||
SharedString, Styled as _, ViewContext,
|
||||
};
|
||||
use ui::{ButtonCommon, Disableable as _};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::git::blame::{CommitDetails, GitRemote};
|
||||
@@ -125,7 +130,6 @@ impl Render for BlameEntryTooltip {
|
||||
let author_email = self.blame_entry.author_mail.clone();
|
||||
|
||||
let short_commit_id = self.blame_entry.sha.display_short();
|
||||
let full_sha = self.blame_entry.sha.to_string().clone();
|
||||
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry);
|
||||
|
||||
let message = self
|
||||
@@ -236,16 +240,6 @@ impl Render for BlameEntryTooltip {
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("copy-sha-button", IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new(
|
||||
full_sha.clone(),
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -136,12 +136,12 @@ impl From<BlockId> for EntityId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockId> for ElementId {
|
||||
fn from(value: BlockId) -> Self {
|
||||
match value {
|
||||
BlockId::Custom(CustomBlockId(id)) => ("Block", id).into(),
|
||||
BlockId::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
|
||||
BlockId::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
|
||||
impl Into<ElementId> for BlockId {
|
||||
fn into(self) -> ElementId {
|
||||
match self {
|
||||
Self::Custom(CustomBlockId(id)) => ("Block", id).into(),
|
||||
Self::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
|
||||
Self::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
|
||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
|
||||
UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
|
||||
WeakView, WindowContext,
|
||||
FocusableView, FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText,
|
||||
KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||
SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakFocusHandle, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -12438,8 +12438,12 @@ impl Render for Editor {
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
},
|
||||
EditorMode::Full => TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
@@ -12447,8 +12451,12 @@ impl Render for Editor {
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -64,10 +64,10 @@ struct CargoTomlPackage {
|
||||
}
|
||||
|
||||
impl ExtensionBuilder {
|
||||
pub fn new(http_client: Arc<dyn HttpClient>, cache_dir: PathBuf) -> Self {
|
||||
pub fn new(cache_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
cache_dir,
|
||||
http: http_client,
|
||||
http: http::client(None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,10 +243,7 @@ impl ExtensionStore {
|
||||
extension_index: Default::default(),
|
||||
installed_dir,
|
||||
index_path,
|
||||
builder: Arc::new(ExtensionBuilder::new(
|
||||
http::client(http_client.proxy().cloned()),
|
||||
build_dir,
|
||||
)),
|
||||
builder: Arc::new(ExtensionBuilder::new(build_dir)),
|
||||
outstanding_operations: Default::default(),
|
||||
modified_extensions: Default::default(),
|
||||
reload_complete_senders: Vec::new(),
|
||||
|
||||
@@ -16,9 +16,8 @@ path = "src/main.rs"
|
||||
anyhow.workspace = true
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
env_logger.workspace = true
|
||||
extension = { workspace = true, features = ["no-webrtc"] }
|
||||
fs.workspace = true
|
||||
http.workspace = true
|
||||
extension = { workspace = true, features = ["no-webrtc"] }
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
rpc.workspace = true
|
||||
|
||||
@@ -13,7 +13,6 @@ use extension::{
|
||||
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
|
||||
ExtensionManifest,
|
||||
};
|
||||
use http::HttpClientWithProxy;
|
||||
use language::LanguageConfig;
|
||||
use theme::ThemeRegistry;
|
||||
use tree_sitter::{Language, Query, WasmStore};
|
||||
@@ -59,9 +58,7 @@ async fn main() -> Result<()> {
|
||||
let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?;
|
||||
|
||||
log::info!("compiling extension");
|
||||
|
||||
let http_client = Arc::new(HttpClientWithProxy::new(None));
|
||||
let builder = ExtensionBuilder::new(http_client, scratch_dir);
|
||||
let builder = ExtensionBuilder::new(scratch_dir);
|
||||
builder
|
||||
.compile_extension(
|
||||
&extension_path,
|
||||
|
||||
@@ -14,9 +14,9 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, InteractiveElement,
|
||||
KeyContext, ParentElement, Render, Styled, Task, TextStyle, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext, WeakView, WindowContext,
|
||||
actions, uniform_list, AppContext, EventEmitter, Flatten, FocusableView, FontStyle,
|
||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||
};
|
||||
use num_format::{Locale, ToFormattedString};
|
||||
use project::DirectoryLister;
|
||||
@@ -804,8 +804,12 @@ impl ExtensionsPage {
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
|
||||
@@ -375,7 +375,6 @@ impl Asset for Image {
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(ImageCacheError::BadStatus {
|
||||
uri,
|
||||
status: response.status(),
|
||||
body: String::from_utf8_lossy(&body).into_owned(),
|
||||
});
|
||||
@@ -418,10 +417,8 @@ pub enum ImageCacheError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(Arc<std::io::Error>),
|
||||
/// An error that occurred while processing an image.
|
||||
#[error("unexpected http status for {uri}: {status}, body: {body}")]
|
||||
#[error("unexpected http status: {status}, body: {body}")]
|
||||
BadStatus {
|
||||
/// The URI of the image.
|
||||
uri: SharedUri,
|
||||
/// The HTTP status code.
|
||||
status: http::StatusCode,
|
||||
/// The HTTP response body.
|
||||
|
||||
@@ -90,7 +90,9 @@ impl Keymap {
|
||||
/// bindings are evaluated with the same precedence rules so you can disable a rule in
|
||||
/// a given context only.
|
||||
///
|
||||
/// In the case of multi-key bindings, the
|
||||
/// In the case where a binding conflicts with a longer binding, precedence is resolved
|
||||
/// only using the order in the keymap file. So binding `cmd-k` in the workspace will disable
|
||||
/// built-in bindings for `cmd-k X` throughout the app.
|
||||
pub fn bindings_for_input(
|
||||
&self,
|
||||
input: &[Keystroke],
|
||||
|
||||
@@ -17,7 +17,6 @@ doctest = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
derive_more.workspace = true
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod github;
|
||||
|
||||
pub use anyhow::{anyhow, Result};
|
||||
use derive_more::Deref;
|
||||
use futures::future::BoxFuture;
|
||||
use futures_lite::FutureExt;
|
||||
use isahc::config::{Configurable, RedirectPolicy};
|
||||
@@ -17,119 +16,61 @@ use std::{
|
||||
};
|
||||
pub use url::Url;
|
||||
|
||||
pub trait HttpClient: Send + Sync {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
|
||||
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.redirect_policy(if follow_redirects {
|
||||
RedirectPolicy::Follow
|
||||
} else {
|
||||
RedirectPolicy::None
|
||||
})
|
||||
.method(Method::GET)
|
||||
.uri(uri)
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
fn get_proxy(proxy: Option<String>) -> Option<isahc::http::Uri> {
|
||||
macro_rules! try_env {
|
||||
($($env:literal),+) => {
|
||||
$(
|
||||
if let Ok(env) = std::env::var($env) {
|
||||
return env.parse::<isahc::http::Uri>().ok();
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
fn post_json<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri>;
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that may have a proxy.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithProxy {
|
||||
#[deref]
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<Uri>,
|
||||
}
|
||||
|
||||
impl HttpClientWithProxy {
|
||||
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
|
||||
pub fn new(proxy_url: Option<String>) -> Self {
|
||||
let proxy_url = proxy_url
|
||||
.and_then(|input| {
|
||||
input
|
||||
.parse::<Uri>()
|
||||
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.or_else(read_proxy_from_env);
|
||||
|
||||
Self {
|
||||
client: client(proxy_url.clone()),
|
||||
proxy: proxy_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithProxy {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for Arc<HttpClientWithProxy> {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
proxy
|
||||
.and_then(|input| {
|
||||
input
|
||||
.parse::<isahc::http::Uri>()
|
||||
.inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
|
||||
.ok()
|
||||
})
|
||||
.or_else(|| {
|
||||
try_env!(
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy"
|
||||
);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
client: HttpClientWithProxy,
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<String>,
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>, proxy_url: Option<String>) -> Self {
|
||||
let client = HttpClientWithProxy::new(proxy_url);
|
||||
|
||||
pub fn new(base_url: impl Into<String>, unparsed_proxy: Option<String>) -> Self {
|
||||
let parsed_proxy = get_proxy(unparsed_proxy);
|
||||
let proxy_string = parsed_proxy.as_ref().map(|p| {
|
||||
// Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
|
||||
// NodeRuntime without environment information can not parse `localhost`
|
||||
// correctly.
|
||||
// TODO: map to `[::1]` if we are using ipv6
|
||||
p.to_string()
|
||||
.to_ascii_lowercase()
|
||||
.replace("localhost", "127.0.0.1")
|
||||
});
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
client: client(parsed_proxy),
|
||||
proxy: proxy_string,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,8 +122,8 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
fn proxy(&self) -> Option<&str> {
|
||||
self.proxy.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,42 +135,66 @@ impl HttpClient for HttpClientWithUrl {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
fn proxy(&self) -> Option<&str> {
|
||||
self.proxy.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client(proxy: Option<Uri>) -> Arc<dyn HttpClient> {
|
||||
Arc::new(HttpClientWithProxy {
|
||||
client: Arc::new(
|
||||
isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.proxy(proxy.clone())
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
proxy,
|
||||
})
|
||||
}
|
||||
pub trait HttpClient: Send + Sync {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<Response<AsyncBody>, Error>>;
|
||||
|
||||
fn read_proxy_from_env() -> Option<Uri> {
|
||||
const ENV_VARS: &[&str] = &[
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
|
||||
for var in ENV_VARS {
|
||||
if let Ok(env) = std::env::var(var) {
|
||||
return env.parse::<Uri>().ok();
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.redirect_policy(if follow_redirects {
|
||||
RedirectPolicy::Follow
|
||||
} else {
|
||||
RedirectPolicy::None
|
||||
})
|
||||
.method(Method::GET)
|
||||
.uri(uri)
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
fn post_json<'a>(
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(error) => async move { Err(error.into()) }.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
pub fn client(proxy: Option<isahc::http::Uri>) -> Arc<dyn HttpClient> {
|
||||
Arc::new(
|
||||
isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
impl HttpClient for isahc::HttpClient {
|
||||
@@ -241,7 +206,7 @@ impl HttpClient for isahc::HttpClient {
|
||||
Box::pin(async move { client.send_async(req).await })
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
fn proxy(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -268,12 +233,10 @@ impl FakeHttpClient {
|
||||
{
|
||||
Arc::new(HttpClientWithUrl {
|
||||
base_url: Mutex::new("http://test.example".into()),
|
||||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
client: Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
}),
|
||||
proxy: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -313,7 +276,7 @@ impl HttpClient for FakeHttpClient {
|
||||
Box::pin(async move { future.await.map(Into::into) })
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
fn proxy(&self) -> Option<&str> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,9 +298,7 @@ impl InlineCompletionButton {
|
||||
|
||||
fn build_supermaven_context_menu(&self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
self.build_language_settings_menu(menu, cx)
|
||||
.separator()
|
||||
.action("Sign Out", supermaven::SignOut.boxed_clone())
|
||||
self.build_language_settings_menu(menu, cx).separator()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ use gpui::{
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use smallvec::SmallVec;
|
||||
use smol::future::yield_now;
|
||||
@@ -214,8 +213,6 @@ pub struct Diagnostic {
|
||||
pub is_disk_based: bool,
|
||||
/// Whether this diagnostic marks unnecessary code.
|
||||
pub is_unnecessary: bool,
|
||||
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
|
||||
/// TODO - move this into the `project` crate and make it private.
|
||||
@@ -3847,7 +3844,6 @@ impl Default for Diagnostic {
|
||||
is_primary: false,
|
||||
is_disk_based: false,
|
||||
is_unnecessary: false,
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ impl DiagnosticEntry<PointUtf16> {
|
||||
severity: Some(self.diagnostic.severity),
|
||||
source: self.diagnostic.source.clone(),
|
||||
message: self.diagnostic.message.clone(),
|
||||
data: self.diagnostic.data.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{relative, AppContext, BackgroundExecutor, HighlightStyle, StyledText, TextStyle};
|
||||
use gpui::{
|
||||
relative, AppContext, BackgroundExecutor, FontStyle, HighlightStyle, StyledText, TextStyle,
|
||||
WhiteSpace,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use theme::{color_alpha, ActiveTheme, ThemeSettings};
|
||||
@@ -164,8 +167,12 @@ pub fn render_item<T>(
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
let highlights = gpui::combine_highlights(
|
||||
custom_highlights,
|
||||
|
||||
@@ -5,8 +5,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use clock::ReplicaId;
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
use rpc::proto;
|
||||
use serde_json::Value;
|
||||
use std::{ops::Range, str::FromStr, sync::Arc};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use text::*;
|
||||
|
||||
pub use proto::{BufferState, Operation};
|
||||
@@ -214,7 +213,6 @@ pub fn serialize_diagnostics<'a>(
|
||||
code: entry.diagnostic.code.clone(),
|
||||
is_disk_based: entry.diagnostic.is_disk_based,
|
||||
is_unnecessary: entry.diagnostic.is_unnecessary,
|
||||
data: entry.diagnostic.data.as_ref().map(|data| data.to_string()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -398,11 +396,6 @@ pub fn deserialize_diagnostics(
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.filter_map(|diagnostic| {
|
||||
let data = if let Some(data) = diagnostic.data {
|
||||
Some(Value::from_str(&data).ok()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(DiagnosticEntry {
|
||||
range: deserialize_anchor(diagnostic.start?)?..deserialize_anchor(diagnostic.end?)?,
|
||||
diagnostic: Diagnostic {
|
||||
@@ -420,7 +413,6 @@ pub fn deserialize_diagnostics(
|
||||
is_primary: diagnostic.is_primary,
|
||||
is_disk_based: diagnostic.is_disk_based,
|
||||
is_unnecessary: diagnostic.is_unnecessary,
|
||||
data,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,40 +2,100 @@ use crate::LanguageModelRequest;
|
||||
pub use anthropic::Model as AnthropicModel;
|
||||
pub use ollama::Model as OllamaModel;
|
||||
pub use open_ai::Model as OpenAiModel;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::EnumIter;
|
||||
use schemars::{
|
||||
schema::{InstanceType, Metadata, Schema, SchemaObject},
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::{
|
||||
de::{self, Visitor},
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use std::fmt;
|
||||
use strum::{EnumIter, IntoEnumIterator};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, EnumIter)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
|
||||
pub enum CloudModel {
|
||||
#[serde(rename = "gpt-3.5-turbo")]
|
||||
Gpt3Point5Turbo,
|
||||
#[serde(rename = "gpt-4")]
|
||||
Gpt4,
|
||||
#[serde(rename = "gpt-4-turbo-preview")]
|
||||
Gpt4Turbo,
|
||||
#[serde(rename = "gpt-4o")]
|
||||
#[default]
|
||||
Gpt4Omni,
|
||||
#[serde(rename = "gpt-4o-mini")]
|
||||
Gpt4OmniMini,
|
||||
#[serde(rename = "claude-3-5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(rename = "claude-3-opus")]
|
||||
Claude3Opus,
|
||||
#[serde(rename = "claude-3-sonnet")]
|
||||
Claude3Sonnet,
|
||||
#[serde(rename = "claude-3-haiku")]
|
||||
Claude3Haiku,
|
||||
#[serde(rename = "gemini-1.5-pro")]
|
||||
Gemini15Pro,
|
||||
#[serde(rename = "gemini-1.5-flash")]
|
||||
Gemini15Flash,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
max_tokens: Option<usize>,
|
||||
},
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl Serialize for CloudModel {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CloudModel {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ZedDotDevModelVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
|
||||
type Value = CloudModel;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
let model = CloudModel::iter()
|
||||
.find(|model| model.id() == value)
|
||||
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(ZedDotDevModelVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for CloudModel {
|
||||
fn schema_name() -> String {
|
||||
"ZedDotDevModel".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let variants = CloudModel::iter()
|
||||
.filter_map(|model| {
|
||||
let id = model.id();
|
||||
if id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(id.to_string())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some("ZedDotDevModel".to_owned()),
|
||||
default: Some(CloudModel::default().id().into()),
|
||||
examples: variants.into_iter().map(Into::into).collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CloudModel {
|
||||
@@ -52,7 +112,7 @@ impl CloudModel {
|
||||
Self::Claude3Haiku => "claude-3-haiku",
|
||||
Self::Gemini15Pro => "gemini-1.5-pro",
|
||||
Self::Gemini15Flash => "gemini-1.5-flash",
|
||||
Self::Custom { name, .. } => name,
|
||||
Self::Custom(id) => id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +129,7 @@ impl CloudModel {
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Gemini15Pro => "Gemini 1.5 Pro",
|
||||
Self::Gemini15Flash => "Gemini 1.5 Flash",
|
||||
Self::Custom { name, .. } => name,
|
||||
Self::Custom(id) => id.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,20 +145,14 @@ impl CloudModel {
|
||||
| Self::Claude3Haiku => 200000,
|
||||
Self::Gemini15Pro => 128000,
|
||||
Self::Gemini15Flash => 32000,
|
||||
Self::Custom { max_tokens, .. } => max_tokens.unwrap_or(200_000),
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
|
||||
match self {
|
||||
Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku
|
||||
| Self::Claude3_5Sonnet => {
|
||||
request.preprocess_anthropic();
|
||||
}
|
||||
Self::Custom { name, .. } if name.starts_with("anthropic/") => {
|
||||
request.preprocess_anthropic();
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
|
||||
request.preprocess_anthropic()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use util::ResultExt;
|
||||
#[cfg(windows)]
|
||||
use smol::process::windows::CommandExt;
|
||||
|
||||
const VERSION: &str = "v22.5.1";
|
||||
const VERSION: &str = "v18.15.0";
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const NODE_PATH: &str = "bin/node";
|
||||
@@ -269,16 +269,7 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
}
|
||||
|
||||
if let Some(proxy) = self.http.proxy() {
|
||||
// Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809`
|
||||
// NodeRuntime without environment information can not parse `localhost`
|
||||
// correctly.
|
||||
// TODO: map to `[::1]` if we are using ipv6
|
||||
let proxy = proxy
|
||||
.to_string()
|
||||
.to_ascii_lowercase()
|
||||
.replace("localhost", "127.0.0.1");
|
||||
|
||||
command.args(["--proxy", &proxy]);
|
||||
command.args(["--proxy", proxy]);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -457,9 +457,7 @@ impl OutlinePanel {
|
||||
}
|
||||
|
||||
fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
|
||||
if self.filter_editor.focus_handle(cx).is_focused(cx) {
|
||||
cx.propagate()
|
||||
} else if let Some(selected_entry) = self.selected_entry.clone() {
|
||||
if let Some(selected_entry) = self.selected_entry.clone() {
|
||||
self.open_entry(&selected_entry, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4595,7 +4595,6 @@ impl Project {
|
||||
is_primary: true,
|
||||
is_disk_based,
|
||||
is_unnecessary,
|
||||
data: diagnostic.data.clone(),
|
||||
},
|
||||
});
|
||||
if let Some(infos) = &diagnostic.related_information {
|
||||
@@ -4613,7 +4612,6 @@ impl Project {
|
||||
is_primary: false,
|
||||
is_disk_based,
|
||||
is_unnecessary: false,
|
||||
data: diagnostic.data.clone(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1890,7 +1890,6 @@ message Diagnostic {
|
||||
Information = 3;
|
||||
Hint = 4;
|
||||
}
|
||||
optional string data = 12;
|
||||
}
|
||||
|
||||
message Operation {
|
||||
|
||||
@@ -85,7 +85,6 @@ pub trait SshClientDelegate {
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>>;
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
|
||||
}
|
||||
|
||||
type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, oneshot::Sender<()>)>>>;
|
||||
@@ -100,18 +99,16 @@ impl SshSession {
|
||||
) -> Result<Arc<Self>> {
|
||||
let client_state = SshClientState::new(user, host, port, delegate.clone(), cx).await?;
|
||||
|
||||
let platform = client_state.query_platform().await?;
|
||||
let platform = query_platform(&client_state).await?;
|
||||
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||
let remote_binary_path = delegate.remote_server_binary_path(cx)?;
|
||||
client_state
|
||||
.ensure_server_binary(
|
||||
&delegate,
|
||||
&local_binary_path,
|
||||
&remote_binary_path,
|
||||
version,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
ensure_server_binary(
|
||||
&client_state,
|
||||
&local_binary_path,
|
||||
&remote_binary_path,
|
||||
version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (spawn_process_tx, mut spawn_process_rx) = mpsc::unbounded::<SpawnRequest>();
|
||||
let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
@@ -416,7 +413,7 @@ impl SshClientState {
|
||||
host: String,
|
||||
port: u16,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
Err(anyhow!("ssh is not supported on this platform"))
|
||||
}
|
||||
@@ -427,29 +424,25 @@ impl SshClientState {
|
||||
host: String,
|
||||
port: u16,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
use smol::fs::unix::PermissionsExt as _;
|
||||
use util::ResultExt as _;
|
||||
|
||||
delegate.set_status(Some("connecting"), cx);
|
||||
|
||||
let url = format!("{user}@{host}");
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("zed-ssh-session")
|
||||
.tempdir()?;
|
||||
|
||||
// Create a domain socket listener to handle requests from the askpass program.
|
||||
let askpass_socket = temp_dir.path().join("askpass.sock");
|
||||
let listener =
|
||||
UnixListener::bind(&askpass_socket).context("failed to create askpass socket")?;
|
||||
|
||||
// Create a TCP listener to handle requests from the askpass program.
|
||||
let listener = smol::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("failed to find open port");
|
||||
let askpass_port = listener.local_addr().unwrap().port();
|
||||
let askpass_task = cx.spawn(|mut cx| async move {
|
||||
while let Ok((mut stream, _)) = listener.accept().await {
|
||||
let mut buffer = Vec::new();
|
||||
let mut reader = BufReader::new(&mut stream);
|
||||
if reader.read_until(b'\0', &mut buffer).await.is_err() {
|
||||
if stream.read_to_end(&mut buffer).await.is_err() {
|
||||
buffer.clear();
|
||||
}
|
||||
let password_prompt = String::from_utf8_lossy(&buffer);
|
||||
@@ -465,12 +458,10 @@ impl SshClientState {
|
||||
}
|
||||
});
|
||||
|
||||
// Create an askpass script that communicates back to this process.
|
||||
// Create an askpass script that communicates back to this process using TCP.
|
||||
let askpass_script = format!(
|
||||
"{shebang}\n{print_args} | nc -U {askpass_socket} 2> /dev/null \n",
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
"{shebang}\n echo \"$@\" | nc 127.0.0.1 {askpass_port} 2> /dev/null",
|
||||
shebang = "#!/bin/sh"
|
||||
);
|
||||
let askpass_script_path = temp_dir.path().join("askpass.sh");
|
||||
fs::write(&askpass_script_path, askpass_script).await?;
|
||||
@@ -510,87 +501,14 @@ impl SshClientState {
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
port,
|
||||
socket_path,
|
||||
_master_process: master_process,
|
||||
port,
|
||||
_temp_dir: temp_dir,
|
||||
socket_path,
|
||||
url,
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_server_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn SshClientDelegate>,
|
||||
src_path: &Path,
|
||||
dst_path: &Path,
|
||||
version: SemanticVersion,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let mut dst_path_gz = dst_path.to_path_buf();
|
||||
dst_path_gz.set_extension("gz");
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
run_cmd(self.ssh_command("mkdir").arg("-p").arg(parent)).await?;
|
||||
}
|
||||
|
||||
let mut server_binary_exists = false;
|
||||
if let Ok(installed_version) = run_cmd(self.ssh_command(&dst_path).arg("version")).await {
|
||||
if installed_version.trim() == version.to_string() {
|
||||
server_binary_exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if server_binary_exists {
|
||||
log::info!("remote development server already present",);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src_stat = fs::metadata(src_path).await?;
|
||||
let size = src_stat.len();
|
||||
let server_mode = 0o755;
|
||||
|
||||
let t0 = Instant::now();
|
||||
delegate.set_status(Some("uploading remote development server"), cx);
|
||||
log::info!("uploading remote development server ({}kb)", size / 1024);
|
||||
self.upload_file(src_path, &dst_path_gz)
|
||||
.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);
|
||||
run_cmd(self.ssh_command("gunzip").arg("--force").arg(&dst_path_gz)).await?;
|
||||
|
||||
delegate.set_status(Some("unzipping remote development server"), cx);
|
||||
run_cmd(
|
||||
self.ssh_command("chmod")
|
||||
.arg(format!("{:o}", server_mode))
|
||||
.arg(&dst_path),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn query_platform(&self) -> Result<SshPlatform> {
|
||||
let os = run_cmd(self.ssh_command("uname").arg("-s")).await?;
|
||||
let arch = run_cmd(self.ssh_command("uname").arg("-m")).await?;
|
||||
|
||||
let os = match os.trim() {
|
||||
"Darwin" => "macos",
|
||||
"Linux" => "linux",
|
||||
_ => Err(anyhow!("unknown uname os {os:?}"))?,
|
||||
};
|
||||
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
|
||||
"aarch64"
|
||||
} else if arch.starts_with("x86") || arch.starts_with("i686") {
|
||||
"x86_64"
|
||||
} else {
|
||||
Err(anyhow!("unknown uname architecture {arch:?}"))?
|
||||
};
|
||||
|
||||
Ok(SshPlatform { os, arch })
|
||||
}
|
||||
|
||||
async fn upload_file(&self, src_path: &Path, dest_path: &Path) -> Result<()> {
|
||||
let mut command = process::Command::new("scp");
|
||||
let output = self
|
||||
@@ -645,3 +563,81 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_platform(session: &SshClientState) -> Result<SshPlatform> {
|
||||
let os = run_cmd(session.ssh_command("uname").arg("-s")).await?;
|
||||
let arch = run_cmd(session.ssh_command("uname").arg("-m")).await?;
|
||||
|
||||
let os = match os.trim() {
|
||||
"Darwin" => "macos",
|
||||
"Linux" => "linux",
|
||||
_ => Err(anyhow!("unknown uname os {os:?}"))?,
|
||||
};
|
||||
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
|
||||
"aarch64"
|
||||
} else if arch.starts_with("x86") || arch.starts_with("i686") {
|
||||
"x86_64"
|
||||
} else {
|
||||
Err(anyhow!("unknown uname architecture {arch:?}"))?
|
||||
};
|
||||
|
||||
Ok(SshPlatform { os, arch })
|
||||
}
|
||||
|
||||
async fn ensure_server_binary(
|
||||
session: &SshClientState,
|
||||
src_path: &Path,
|
||||
dst_path: &Path,
|
||||
version: SemanticVersion,
|
||||
) -> Result<()> {
|
||||
let mut dst_path_gz = dst_path.to_path_buf();
|
||||
dst_path_gz.set_extension("gz");
|
||||
|
||||
if let Some(parent) = dst_path.parent() {
|
||||
run_cmd(session.ssh_command("mkdir").arg("-p").arg(parent)).await?;
|
||||
}
|
||||
|
||||
let mut server_binary_exists = false;
|
||||
if let Ok(installed_version) = run_cmd(session.ssh_command(&dst_path).arg("version")).await {
|
||||
if installed_version.trim() == version.to_string() {
|
||||
server_binary_exists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if server_binary_exists {
|
||||
log::info!("remote development server already present",);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src_stat = fs::metadata(src_path).await?;
|
||||
let size = src_stat.len();
|
||||
let server_mode = 0o755;
|
||||
|
||||
let t0 = Instant::now();
|
||||
log::info!("uploading remote development server ({}kb)", size / 1024);
|
||||
session
|
||||
.upload_file(src_path, &dst_path_gz)
|
||||
.await
|
||||
.context("failed to upload server binary")?;
|
||||
log::info!("uploaded remote development server in {:?}", t0.elapsed());
|
||||
|
||||
log::info!("extracting remote development server");
|
||||
run_cmd(
|
||||
session
|
||||
.ssh_command("gunzip")
|
||||
.arg("--force")
|
||||
.arg(&dst_path_gz),
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::info!("unzipping remote development server");
|
||||
run_cmd(
|
||||
session
|
||||
.ssh_command("chmod")
|
||||
.arg(format!("{:o}", server_mode))
|
||||
.arg(&dst_path),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ pub struct KernelSpecification {
|
||||
|
||||
impl KernelSpecification {
|
||||
#[must_use]
|
||||
fn command(&self, connection_path: &PathBuf) -> Result<Command> {
|
||||
fn command(&self, connection_path: &PathBuf) -> anyhow::Result<Command> {
|
||||
let argv = &self.kernelspec.argv;
|
||||
|
||||
anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name);
|
||||
@@ -60,7 +60,7 @@ impl KernelSpecification {
|
||||
|
||||
// Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope.
|
||||
// There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol.
|
||||
async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> {
|
||||
async fn peek_ports(ip: IpAddr) -> anyhow::Result<[u16; 5]> {
|
||||
let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0);
|
||||
addr_zeroport.set_port(0);
|
||||
let mut ports: [u16; 5] = [0; 5];
|
||||
@@ -166,10 +166,10 @@ impl Kernel {
|
||||
|
||||
pub struct RunningKernel {
|
||||
pub process: smol::process::Child,
|
||||
_shell_task: Task<Result<()>>,
|
||||
_iopub_task: Task<Result<()>>,
|
||||
_control_task: Task<Result<()>>,
|
||||
_routing_task: Task<Result<()>>,
|
||||
_shell_task: Task<anyhow::Result<()>>,
|
||||
_iopub_task: Task<anyhow::Result<()>>,
|
||||
_control_task: Task<anyhow::Result<()>>,
|
||||
_routing_task: Task<anyhow::Result<()>>,
|
||||
connection_path: PathBuf,
|
||||
pub working_directory: PathBuf,
|
||||
pub request_tx: mpsc::Sender<JupyterMessage>,
|
||||
@@ -194,7 +194,7 @@ impl RunningKernel {
|
||||
working_directory: PathBuf,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<(Self, JupyterMessageChannel)>> {
|
||||
) -> Task<anyhow::Result<(Self, JupyterMessageChannel)>> {
|
||||
cx.spawn(|cx| async move {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
let ports = peek_ports(ip).await?;
|
||||
@@ -332,7 +332,7 @@ async fn read_kernelspec_at(
|
||||
// /usr/local/share/jupyter/kernels/python3
|
||||
kernel_dir: PathBuf,
|
||||
fs: &dyn Fs,
|
||||
) -> Result<KernelSpecification> {
|
||||
) -> anyhow::Result<KernelSpecification> {
|
||||
let path = kernel_dir;
|
||||
let kernel_name = if let Some(kernel_name) = path.file_name() {
|
||||
kernel_name.to_string_lossy().to_string()
|
||||
@@ -356,7 +356,7 @@ async fn read_kernelspec_at(
|
||||
}
|
||||
|
||||
/// Read a directory of kernelspec directories
|
||||
async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<KernelSpecification>> {
|
||||
async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> anyhow::Result<Vec<KernelSpecification>> {
|
||||
let mut kernelspec_dirs = fs.read_dir(&path).await?;
|
||||
|
||||
let mut valid_kernelspecs = Vec::new();
|
||||
@@ -376,7 +376,7 @@ async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result<Vec<KernelSpecif
|
||||
Ok(valid_kernelspecs)
|
||||
}
|
||||
|
||||
pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> Result<Vec<KernelSpecification>> {
|
||||
pub async fn kernel_specifications(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<KernelSpecification>> {
|
||||
let data_dirs = dirs::data_dirs();
|
||||
let kernel_dirs = data_dirs
|
||||
.iter()
|
||||
|
||||
@@ -281,7 +281,7 @@ impl ErrorView {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(colors.background)
|
||||
.py(cx.line_height() / 2.)
|
||||
.p_4()
|
||||
.border_l_1()
|
||||
.border_color(theme.status().error_border)
|
||||
.child(
|
||||
@@ -297,7 +297,7 @@ impl ErrorView {
|
||||
|
||||
impl LineHeight for ErrorView {
|
||||
fn num_lines(&self, cx: &mut WindowContext) -> u8 {
|
||||
let mut height: u8 = 1; // Start at 1 to account for the y padding
|
||||
let mut height: u8 = 0;
|
||||
height = height.saturating_add(self.ename.lines().count() as u8);
|
||||
height = height.saturating_add(self.evalue.lines().count() as u8);
|
||||
height = height.saturating_add(self.traceback.num_lines(cx));
|
||||
@@ -503,38 +503,29 @@ impl ExecutionView {
|
||||
impl Render for ExecutionView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if self.outputs.len() == 0 {
|
||||
return v_flex()
|
||||
.min_h(cx.line_height())
|
||||
.justify_center()
|
||||
.child(match &self.status {
|
||||
ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...")
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::Executing => Label::new("Executing...")
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::Finished => Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::Unknown => Label::new("Unknown status")
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...")
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::Shutdown => Label::new("Kernel shutdown")
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
ExecutionStatus::Queued => {
|
||||
Label::new("Queued").color(Color::Muted).into_any_element()
|
||||
}
|
||||
ExecutionStatus::KernelErrored(error) => {
|
||||
Label::new(format!("Kernel error: {}", error))
|
||||
.color(Color::Error)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
.into_any_element();
|
||||
return match &self.status {
|
||||
ExecutionStatus::ConnectingToKernel => {
|
||||
div().child(Label::new("Connecting to kernel...").color(Color::Muted))
|
||||
}
|
||||
ExecutionStatus::Executing => {
|
||||
div().child(Label::new("Executing...").color(Color::Muted))
|
||||
}
|
||||
ExecutionStatus::Finished => div().child(Icon::new(IconName::Check)),
|
||||
ExecutionStatus::Unknown => {
|
||||
div().child(div().child(Label::new("Unknown status").color(Color::Muted)))
|
||||
}
|
||||
ExecutionStatus::ShuttingDown => {
|
||||
div().child(Label::new("Kernel shutting down...").color(Color::Muted))
|
||||
}
|
||||
ExecutionStatus::Shutdown => {
|
||||
div().child(Label::new("Kernel shutdown").color(Color::Muted))
|
||||
}
|
||||
ExecutionStatus::Queued => div().child(Label::new("Queued").color(Color::Muted)),
|
||||
ExecutionStatus::KernelErrored(error) => {
|
||||
div().child(Label::new(format!("Kernel error: {}", error)).color(Color::Error))
|
||||
}
|
||||
}
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
@@ -553,10 +544,9 @@ impl LineHeight for ExecutionView {
|
||||
self.outputs
|
||||
.iter()
|
||||
.map(|output| output.num_lines(cx))
|
||||
.fold(0_u8, |acc, additional_height| {
|
||||
.fold(0, |acc, additional_height| {
|
||||
acc.saturating_add(additional_height)
|
||||
})
|
||||
.max(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use async_dispatcher::{set_dispatcher, Dispatcher, Runnable};
|
||||
use gpui::{AppContext, PlatformDispatcher};
|
||||
use project::Fs;
|
||||
use settings::Settings as _;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
mod jupyter_settings;
|
||||
mod kernels;
|
||||
mod outputs;
|
||||
mod repl_store;
|
||||
mod runtime_panel;
|
||||
mod session;
|
||||
mod stdio;
|
||||
@@ -19,8 +17,6 @@ pub use runtime_panel::{RuntimePanel, SessionSupport};
|
||||
pub use runtimelib::ExecutionState;
|
||||
pub use session::Session;
|
||||
|
||||
use crate::repl_store::ReplStore;
|
||||
|
||||
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
struct ZedDispatcher {
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
@@ -45,10 +41,8 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
set_dispatcher(zed_dispatcher(cx));
|
||||
JupyterSettings::register(cx);
|
||||
editor::init_settings(cx);
|
||||
runtime_panel::init(cx);
|
||||
ReplStore::init(fs, cx);
|
||||
runtime_panel::init(cx)
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
prelude::*, AppContext, EntityId, Global, Model, ModelContext, Subscription, Task, View,
|
||||
};
|
||||
use language::Language;
|
||||
use project::Fs;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
use crate::kernels::kernel_specifications;
|
||||
use crate::{JupyterSettings, KernelSpecification, Session};
|
||||
|
||||
struct GlobalReplStore(Model<ReplStore>);
|
||||
|
||||
impl Global for GlobalReplStore {}
|
||||
|
||||
pub struct ReplStore {
|
||||
fs: Arc<dyn Fs>,
|
||||
enabled: bool,
|
||||
sessions: HashMap<EntityId, View<Session>>,
|
||||
kernel_specifications: Vec<KernelSpecification>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ReplStore {
|
||||
pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let store = cx.new_model(move |cx| Self::new(fs, cx));
|
||||
|
||||
cx.set_global(GlobalReplStore(store))
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Model<Self> {
|
||||
cx.global::<GlobalReplStore>().0.clone()
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let subscriptions = vec![cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.set_enabled(JupyterSettings::enabled(cx), cx);
|
||||
})];
|
||||
|
||||
Self {
|
||||
fs,
|
||||
enabled: JupyterSettings::enabled(cx),
|
||||
sessions: HashMap::default(),
|
||||
kernel_specifications: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
pub fn kernel_specifications(&self) -> impl Iterator<Item = &KernelSpecification> {
|
||||
self.kernel_specifications.iter()
|
||||
}
|
||||
|
||||
pub fn sessions(&self) -> impl Iterator<Item = &View<Session>> {
|
||||
self.sessions.values()
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut ModelContext<Self>) {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_kernelspecs(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let kernel_specifications = kernel_specifications(self.fs.clone());
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let kernel_specifications = kernel_specifications.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.kernel_specifications = kernel_specifications;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kernelspec(
|
||||
&self,
|
||||
language: &Language,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<KernelSpecification> {
|
||||
let settings = JupyterSettings::get_global(cx);
|
||||
let language_name = language.code_fence_block_name();
|
||||
let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
|
||||
|
||||
self.kernel_specifications
|
||||
.iter()
|
||||
.find(|runtime_specification| {
|
||||
if let Some(selected) = selected_kernel {
|
||||
// Top priority is the selected kernel
|
||||
runtime_specification.name.to_lowercase() == selected.to_lowercase()
|
||||
} else {
|
||||
// Otherwise, we'll try to find a kernel that matches the language
|
||||
runtime_specification.kernelspec.language.to_lowercase()
|
||||
== language_name.to_lowercase()
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn get_session(&self, entity_id: EntityId) -> Option<&View<Session>> {
|
||||
self.sessions.get(&entity_id)
|
||||
}
|
||||
|
||||
pub fn insert_session(&mut self, entity_id: EntityId, session: View<Session>) {
|
||||
self.sessions.insert(entity_id, session);
|
||||
}
|
||||
|
||||
pub fn remove_session(&mut self, entity_id: EntityId) {
|
||||
self.sessions.remove(&entity_id);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::repl_store::ReplStore;
|
||||
use crate::{
|
||||
jupyter_settings::{JupyterDockPosition, JupyterSettings},
|
||||
kernels::KernelSpecification,
|
||||
kernels::{kernel_specifications, KernelSpecification},
|
||||
session::{Session, SessionEvent},
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Editor, RangeToAnchorExt};
|
||||
use gpui::{
|
||||
actions, prelude::*, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusOutEvent,
|
||||
FocusableView, Subscription, Task, View, WeakView,
|
||||
actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
|
||||
FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
|
||||
};
|
||||
use language::{Language, Point};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::Fs;
|
||||
use settings::Settings as _;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
|
||||
use util::ResultExt as _;
|
||||
@@ -28,13 +28,6 @@ actions!(
|
||||
);
|
||||
actions!(repl_panel, [ToggleFocus]);
|
||||
|
||||
pub enum SessionSupport {
|
||||
ActiveSession(View<Session>),
|
||||
Inactive(Box<KernelSpecification>),
|
||||
RequiresSetup(Arc<str>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||
@@ -42,11 +35,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.toggle_panel_focus::<RuntimePanel>(cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
|
||||
let store = ReplStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
store.refresh_kernelspecs(cx).detach();
|
||||
});
|
||||
workspace.register_action(|workspace, _: &RefreshKernelspecs, cx| {
|
||||
if let Some(panel) = workspace.panel::<RuntimePanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.refresh_kernelspecs(cx).detach();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
@@ -151,8 +145,11 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
pub struct RuntimePanel {
|
||||
fs: Arc<dyn Fs>,
|
||||
enabled: bool,
|
||||
focus_handle: FocusHandle,
|
||||
width: Option<Pixels>,
|
||||
sessions: HashMap<EntityId, View<Session>>,
|
||||
kernel_specifications: Vec<KernelSpecification>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -171,29 +168,39 @@ impl RuntimePanel {
|
||||
let subscriptions = vec![
|
||||
cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||
cx.on_focus_out(&focus_handle, Self::focus_out),
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.set_enabled(JupyterSettings::enabled(cx), cx);
|
||||
}),
|
||||
];
|
||||
|
||||
let runtime_panel = Self {
|
||||
fs,
|
||||
fs: fs.clone(),
|
||||
width: None,
|
||||
focus_handle,
|
||||
kernel_specifications: Vec::new(),
|
||||
sessions: Default::default(),
|
||||
_subscriptions: subscriptions,
|
||||
enabled: JupyterSettings::enabled(cx),
|
||||
};
|
||||
|
||||
runtime_panel
|
||||
})
|
||||
})?;
|
||||
|
||||
view.update(&mut cx, |_panel, cx| {
|
||||
let store = ReplStore::global(cx);
|
||||
store.update(cx, |store, cx| store.refresh_kernelspecs(cx))
|
||||
})?
|
||||
.await?;
|
||||
view.update(&mut cx, |this, cx| this.refresh_kernelspecs(cx))?
|
||||
.await?;
|
||||
|
||||
Ok(view)
|
||||
})
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
|
||||
if self.enabled != enabled {
|
||||
self.enabled = enabled;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
@@ -202,7 +209,8 @@ impl RuntimePanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn snippet(
|
||||
pub fn snippet(
|
||||
&self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
|
||||
@@ -247,59 +255,93 @@ impl RuntimePanel {
|
||||
Some((selected_text, start_language.clone(), anchor_range))
|
||||
}
|
||||
|
||||
fn language(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Option<Arc<Language>> {
|
||||
pub fn language(
|
||||
&self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Arc<Language>> {
|
||||
let editor = editor.upgrade()?;
|
||||
let selection = editor.read(cx).selections.newest::<usize>(cx);
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
buffer.language_at(selection.head()).cloned()
|
||||
}
|
||||
|
||||
pub fn run(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Result<()> {
|
||||
let store = ReplStore::global(cx);
|
||||
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
let kernel_specifications = kernel_specifications(self.fs.clone());
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let kernel_specifications = kernel_specifications.await?;
|
||||
|
||||
if !store.read(cx).is_enabled() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.kernel_specifications = kernel_specifications;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kernelspec(
|
||||
&self,
|
||||
language: &Language,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<KernelSpecification> {
|
||||
let settings = JupyterSettings::get_global(cx);
|
||||
let language_name = language.code_fence_block_name();
|
||||
let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
|
||||
|
||||
self.kernel_specifications
|
||||
.iter()
|
||||
.find(|runtime_specification| {
|
||||
if let Some(selected) = selected_kernel {
|
||||
// Top priority is the selected kernel
|
||||
runtime_specification.name.to_lowercase() == selected.to_lowercase()
|
||||
} else {
|
||||
// Otherwise, we'll try to find a kernel that matches the language
|
||||
runtime_specification.kernelspec.language.to_lowercase()
|
||||
== language_name.to_lowercase()
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
&mut self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> anyhow::Result<()> {
|
||||
if !self.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (selected_text, language, anchor_range) = match Self::snippet(editor.clone(), cx) {
|
||||
let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
|
||||
Some(snippet) => snippet,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let entity_id = editor.entity_id();
|
||||
|
||||
let kernel_specification = store.update(cx, |store, cx| {
|
||||
store
|
||||
.kernelspec(&language, cx)
|
||||
.with_context(|| format!("No kernel found for language: {}", language.name()))
|
||||
})?;
|
||||
let kernel_specification = self
|
||||
.kernelspec(&language, cx)
|
||||
.with_context(|| format!("No kernel found for language: {}", language.name()))?;
|
||||
|
||||
let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||
session
|
||||
} else {
|
||||
let session =
|
||||
let session = self.sessions.entry(entity_id).or_insert_with(|| {
|
||||
let view =
|
||||
cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
|
||||
cx.notify();
|
||||
|
||||
let subscription = cx.subscribe(&session, {
|
||||
let store = store.clone();
|
||||
move |_this, _session, event, cx| match event {
|
||||
SessionEvent::Shutdown(shutdown_event) => {
|
||||
store.update(cx, |store, _cx| {
|
||||
store.remove_session(shutdown_event.entity_id());
|
||||
});
|
||||
let subscription = cx.subscribe(
|
||||
&view,
|
||||
|panel: &mut RuntimePanel, _session: View<Session>, event: &SessionEvent, _cx| {
|
||||
match event {
|
||||
SessionEvent::Shutdown(shutdown_event) => {
|
||||
panel.sessions.remove(&shutdown_event.entity_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
subscription.detach();
|
||||
|
||||
store.update(cx, |store, _cx| {
|
||||
store.insert_session(entity_id, session.clone());
|
||||
});
|
||||
|
||||
session
|
||||
};
|
||||
view
|
||||
});
|
||||
|
||||
session.update(cx, |session, cx| {
|
||||
session.execute(&selected_text, anchor_range, cx);
|
||||
@@ -308,38 +350,9 @@ impl RuntimePanel {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn session(
|
||||
&mut self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> SessionSupport {
|
||||
let store = ReplStore::global(cx);
|
||||
let entity_id = editor.entity_id();
|
||||
|
||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||
return SessionSupport::ActiveSession(session);
|
||||
};
|
||||
|
||||
let language = Self::language(editor, cx);
|
||||
let language = match language {
|
||||
Some(language) => language,
|
||||
None => return SessionSupport::Unsupported,
|
||||
};
|
||||
let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
|
||||
|
||||
match kernelspec {
|
||||
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
|
||||
None => match language.name().as_ref() {
|
||||
"TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
|
||||
_ => SessionSupport::Unsupported,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let store = ReplStore::global(cx);
|
||||
let entity_id = editor.entity_id();
|
||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||
if let Some(session) = self.sessions.get_mut(&entity_id) {
|
||||
session.update(cx, |session, cx| {
|
||||
session.clear_outputs(cx);
|
||||
});
|
||||
@@ -348,9 +361,8 @@ impl RuntimePanel {
|
||||
}
|
||||
|
||||
pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let store = ReplStore::global(cx);
|
||||
let entity_id = editor.entity_id();
|
||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||
if let Some(session) = self.sessions.get_mut(&entity_id) {
|
||||
session.update(cx, |session, cx| {
|
||||
session.interrupt(cx);
|
||||
});
|
||||
@@ -358,10 +370,9 @@ impl RuntimePanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let store = ReplStore::global(cx);
|
||||
pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<RuntimePanel>) {
|
||||
let entity_id = editor.entity_id();
|
||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||
if let Some(session) = self.sessions.get(&entity_id) {
|
||||
session.update(cx, |session, cx| {
|
||||
session.shutdown(cx);
|
||||
});
|
||||
@@ -370,6 +381,51 @@ impl RuntimePanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SessionSupport {
|
||||
ActiveSession(View<Session>),
|
||||
Inactive(Box<KernelSpecification>),
|
||||
RequiresSetup(Arc<str>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl RuntimePanel {
|
||||
pub fn session(
|
||||
&mut self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> SessionSupport {
|
||||
let entity_id = editor.entity_id();
|
||||
let session = self.sessions.get(&entity_id).cloned();
|
||||
|
||||
match session {
|
||||
Some(session) => SessionSupport::ActiveSession(session),
|
||||
None => {
|
||||
let language = self.language(editor, cx);
|
||||
let language = match language {
|
||||
Some(language) => language,
|
||||
None => return SessionSupport::Unsupported,
|
||||
};
|
||||
// Check for kernelspec
|
||||
let kernelspec = self.kernelspec(&language, cx);
|
||||
|
||||
match kernelspec {
|
||||
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
|
||||
None => {
|
||||
// If no kernelspec but language is one of typescript or python
|
||||
// then we return RequiresSetup
|
||||
match language.name().as_ref() {
|
||||
"TypeScript" | "Python" => {
|
||||
SessionSupport::RequiresSetup(language.name())
|
||||
}
|
||||
_ => SessionSupport::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for RuntimePanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"RuntimePanel"
|
||||
@@ -412,10 +468,8 @@ impl Panel for RuntimePanel {
|
||||
self.width = size;
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &ui::WindowContext) -> Option<ui::IconName> {
|
||||
let store = ReplStore::global(cx);
|
||||
|
||||
if !store.read(cx).is_enabled() {
|
||||
fn icon(&self, _cx: &ui::WindowContext) -> Option<ui::IconName> {
|
||||
if !self.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -441,47 +495,38 @@ impl FocusableView for RuntimePanel {
|
||||
|
||||
impl Render for RuntimePanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let store = ReplStore::global(cx);
|
||||
|
||||
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
|
||||
(
|
||||
store.kernel_specifications().cloned().collect::<Vec<_>>(),
|
||||
store.sessions().cloned().collect::<Vec<_>>(),
|
||||
)
|
||||
});
|
||||
|
||||
// When there are no kernel specifications, show a link to the Zed docs explaining how to
|
||||
// install kernels. It can be assumed they don't have a running kernel if we have no
|
||||
// specifications.
|
||||
if kernel_specifications.is_empty() {
|
||||
if self.kernel_specifications.is_empty() {
|
||||
return v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
|
||||
.size(LabelSize::Default),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().p_4().justify_center().gap_2().child(
|
||||
ButtonLike::new("install-kernels")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Install Kernels"))
|
||||
.on_click(move |_, cx| {
|
||||
cx.open_url(
|
||||
"https://docs.jupyter.org/en/latest/install/kernels.html",
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
|
||||
.size(LabelSize::Default),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().p_4().justify_center().gap_2().child(
|
||||
ButtonLike::new("install-kernels")
|
||||
.style(ButtonStyle::Filled)
|
||||
.size(ButtonSize::Large)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.child(Label::new("Install Kernels"))
|
||||
.on_click(move |_, cx| {
|
||||
cx.open_url(
|
||||
"https://docs.jupyter.org/en/latest/install/kernels.html",
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
// When there are no sessions, show the command to run code in an editor
|
||||
if sessions.is_empty() {
|
||||
if self.sessions.is_empty() {
|
||||
return v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
@@ -501,7 +546,7 @@ impl Render for RuntimePanel {
|
||||
)
|
||||
.child(Label::new("Kernels available").size(LabelSize::Large))
|
||||
.children(
|
||||
kernel_specifications.into_iter().map(|spec| {
|
||||
self.kernel_specifications.iter().map(|spec| {
|
||||
h_flex().gap_2().child(Label::new(spec.name.clone()))
|
||||
.child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
|
||||
})
|
||||
@@ -514,8 +559,8 @@ impl Render for RuntimePanel {
|
||||
.p_4()
|
||||
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
|
||||
.children(
|
||||
sessions
|
||||
.into_iter()
|
||||
self.sessions
|
||||
.values()
|
||||
.map(|session| session.clone().into_any_element()),
|
||||
)
|
||||
.into_any_element()
|
||||
|
||||
@@ -5,16 +5,14 @@ use crate::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId,
|
||||
RenderBlock,
|
||||
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
},
|
||||
scroll::Autoscroll,
|
||||
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
|
||||
};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{
|
||||
div, prelude::*, EntityId, EventEmitter, Model, Render, Subscription, Task, View, ViewContext,
|
||||
WeakView,
|
||||
div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Point;
|
||||
use project::Fs;
|
||||
@@ -24,7 +22,7 @@ use runtimelib::{
|
||||
use settings::Settings as _;
|
||||
use std::{env::temp_dir, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, IconButtonShape, Label, Tooltip};
|
||||
use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
|
||||
|
||||
pub struct Session {
|
||||
pub editor: WeakView<Editor>,
|
||||
@@ -41,18 +39,13 @@ struct EditorBlock {
|
||||
invalidation_anchor: Anchor,
|
||||
block_id: CustomBlockId,
|
||||
execution_view: View<ExecutionView>,
|
||||
on_close: CloseBlockFn,
|
||||
}
|
||||
|
||||
type CloseBlockFn =
|
||||
Arc<dyn for<'a> Fn(CustomBlockId, &'a mut WindowContext) + Send + Sync + 'static>;
|
||||
|
||||
impl EditorBlock {
|
||||
fn new(
|
||||
editor: WeakView<Editor>,
|
||||
code_range: Range<Anchor>,
|
||||
status: ExecutionStatus,
|
||||
on_close: CloseBlockFn,
|
||||
cx: &mut ViewContext<Session>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
|
||||
@@ -80,7 +73,7 @@ impl EditorBlock {
|
||||
position: code_range.end,
|
||||
height: execution_view.num_lines(cx).saturating_add(1),
|
||||
style: BlockStyle::Sticky,
|
||||
render: Self::create_output_area_render(execution_view.clone(), on_close.clone()),
|
||||
render: Self::create_output_area_render(execution_view.clone()),
|
||||
disposition: BlockDisposition::Below,
|
||||
};
|
||||
|
||||
@@ -94,7 +87,6 @@ impl EditorBlock {
|
||||
invalidation_anchor,
|
||||
block_id,
|
||||
execution_view,
|
||||
on_close,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,15 +98,11 @@ impl EditorBlock {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
let mut replacements = HashMap::default();
|
||||
|
||||
replacements.insert(
|
||||
self.block_id,
|
||||
(
|
||||
Some(self.execution_view.num_lines(cx).saturating_add(1)),
|
||||
Self::create_output_area_render(
|
||||
self.execution_view.clone(),
|
||||
self.on_close.clone(),
|
||||
),
|
||||
Self::create_output_area_render(self.execution_view.clone()),
|
||||
),
|
||||
);
|
||||
editor.replace_blocks(replacements, None, cx);
|
||||
@@ -122,74 +110,31 @@ impl EditorBlock {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn create_output_area_render(
|
||||
execution_view: View<ExecutionView>,
|
||||
on_close: CloseBlockFn,
|
||||
) -> RenderBlock {
|
||||
fn create_output_area_render(execution_view: View<ExecutionView>) -> RenderBlock {
|
||||
let render = move |cx: &mut BlockContext| {
|
||||
let execution_view = execution_view.clone();
|
||||
let text_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
|
||||
let text_font_size = ThemeSettings::get_global(cx).buffer_font_size;
|
||||
// Note: we'll want to use `cx.anchor_x` when someone runs something with no output -- just show a checkmark and not make the full block below the line
|
||||
|
||||
let gutter = cx.gutter_dimensions;
|
||||
let close_button_size = IconSize::XSmall;
|
||||
let gutter_width = cx.gutter_dimensions.width;
|
||||
|
||||
let block_id = cx.block_id;
|
||||
let on_close = on_close.clone();
|
||||
|
||||
let rem_size = cx.rem_size();
|
||||
let line_height = cx.text_style().line_height_in_pixels(rem_size);
|
||||
|
||||
let (close_button_width, close_button_padding) =
|
||||
close_button_size.square_components(cx);
|
||||
|
||||
div()
|
||||
.min_h(line_height)
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_start()
|
||||
h_flex()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
v_flex().min_h(cx.line_height()).justify_center().child(
|
||||
h_flex()
|
||||
.w(gutter.full_width())
|
||||
.justify_end()
|
||||
.pt(line_height / 2.)
|
||||
.child(
|
||||
h_flex()
|
||||
.pr(gutter.width / 2. - close_button_width
|
||||
+ close_button_padding / 2.)
|
||||
.child(
|
||||
IconButton::new(
|
||||
("close_output_area", EntityId::from(cx.block_id)),
|
||||
IconName::Close,
|
||||
)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(close_button_size)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(|cx| Tooltip::text("Close output area", cx))
|
||||
.on_click(
|
||||
move |_, cx| {
|
||||
if let BlockId::Custom(block_id) = block_id {
|
||||
(on_close)(block_id, cx)
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.pl(gutter_width)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.my_2()
|
||||
.mr(gutter.width)
|
||||
.text_size(text_font_size)
|
||||
.font_family(text_font)
|
||||
// .ml(gutter_width)
|
||||
.mx_1()
|
||||
.my_2()
|
||||
.h_full()
|
||||
.w_full()
|
||||
.mr(gutter_width)
|
||||
.child(execution_view),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -428,30 +373,8 @@ impl Session {
|
||||
Kernel::Shutdown => ExecutionStatus::Shutdown,
|
||||
};
|
||||
|
||||
let parent_message_id = message.header.msg_id.clone();
|
||||
let session_view = cx.view().downgrade();
|
||||
let weak_editor = self.editor.clone();
|
||||
|
||||
let on_close: CloseBlockFn =
|
||||
Arc::new(move |block_id: CustomBlockId, cx: &mut WindowContext| {
|
||||
if let Some(session) = session_view.upgrade() {
|
||||
session.update(cx, |session, cx| {
|
||||
session.blocks.remove(&parent_message_id);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(editor) = weak_editor.upgrade() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let mut block_ids = HashSet::default();
|
||||
block_ids.insert(block_id);
|
||||
editor.remove_blocks(block_ids, None, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let editor_block = if let Ok(editor_block) =
|
||||
EditorBlock::new(self.editor.clone(), anchor_range, status, on_close, cx)
|
||||
EditorBlock::new(self.editor.clone(), anchor_range, status, cx)
|
||||
{
|
||||
editor_block
|
||||
} else {
|
||||
|
||||
@@ -78,14 +78,7 @@ impl TerminalOutput {
|
||||
})
|
||||
.collect::<Vec<TextRun>>();
|
||||
|
||||
// Trim the last trailing newline for visual appeal
|
||||
let trimmed = self
|
||||
.handler
|
||||
.buffer
|
||||
.strip_suffix('\n')
|
||||
.unwrap_or(&self.handler.buffer);
|
||||
|
||||
let text = StyledText::new(trimmed.to_string()).with_runs(runs);
|
||||
let text = StyledText::new(self.handler.buffer.trim_end().to_string()).with_runs(runs);
|
||||
div()
|
||||
.font_family(buffer_font)
|
||||
.child(text)
|
||||
@@ -95,7 +88,7 @@ impl TerminalOutput {
|
||||
|
||||
impl LineHeight for TerminalOutput {
|
||||
fn num_lines(&self, _cx: &mut WindowContext) -> u8 {
|
||||
self.handler.buffer.lines().count().max(1) as u8
|
||||
self.handler.buffer.lines().count() as u8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +212,7 @@ impl Perform for TerminalHandler {
|
||||
}
|
||||
_ => {
|
||||
// Format as hex
|
||||
// println!("[execute] byte={:02x}", byte);
|
||||
println!("[execute] byte={:02x}", byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ use editor::{
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, Hsla,
|
||||
InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
|
||||
Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _, WindowContext,
|
||||
actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView,
|
||||
FontStyle, Hsla, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render,
|
||||
ScrollHandle, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext as _,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use project::{
|
||||
search::SearchQuery,
|
||||
@@ -116,8 +117,12 @@ impl BufferSearchBar {
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
|
||||
@@ -13,9 +13,10 @@ use editor::{
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, EntityId, EventEmitter,
|
||||
FocusHandle, FocusableView, Global, Hsla, InteractiveElement, IntoElement, KeyContext, Model,
|
||||
ModelContext, ParentElement, Point, Render, SharedString, Styled, Subscription, Task,
|
||||
TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement, IntoElement,
|
||||
KeyContext, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled,
|
||||
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel,
|
||||
WhiteSpace, WindowContext,
|
||||
};
|
||||
use menu::Confirm;
|
||||
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
|
||||
@@ -1340,8 +1341,12 @@ impl ProjectSearchBar {
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
white_space: WhiteSpace::Normal,
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
|
||||
@@ -47,20 +47,10 @@ fn parse_snippet<'a>(
|
||||
source = parse_tabstop(&source[1..], text, tabstops)?;
|
||||
}
|
||||
Some('\\') => {
|
||||
// As specified in the LSP spec (`Grammar` section),
|
||||
// backslashes can escape some characters:
|
||||
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
|
||||
source = &source[1..];
|
||||
if let Some(c) = source.chars().next() {
|
||||
if c == '$' || c == '\\' || c == '}' {
|
||||
text.push(c);
|
||||
// All escapable characters are 1 byte long:
|
||||
source = &source[1..];
|
||||
} else {
|
||||
text.push('\\');
|
||||
}
|
||||
} else {
|
||||
text.push('\\');
|
||||
text.push(c);
|
||||
source = &source[c.len_utf8()..];
|
||||
}
|
||||
}
|
||||
Some('}') => {
|
||||
@@ -207,17 +197,6 @@ mod tests {
|
||||
let snippet = Snippet::parse("{a\\}").unwrap();
|
||||
assert_eq!(snippet.text, "{a}");
|
||||
assert_eq!(tabstops(&snippet), &[vec![3..3]]);
|
||||
|
||||
// backslash not functioning as an escape
|
||||
let snippet = Snippet::parse("a\\b").unwrap();
|
||||
assert_eq!(snippet.text, "a\\b");
|
||||
assert_eq!(tabstops(&snippet), &[vec![3..3]]);
|
||||
|
||||
// first backslash cancelling escaping that would
|
||||
// have happened with second backslash
|
||||
let snippet = Snippet::parse("one\\\\$1two").unwrap();
|
||||
assert_eq!(snippet.text, "one\\two");
|
||||
assert_eq!(tabstops(&snippet), &[vec![4..4], vec![7..7]]);
|
||||
}
|
||||
|
||||
fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> {
|
||||
|
||||
@@ -14,7 +14,6 @@ pub enum OutboundMessage {
|
||||
StateUpdate(StateUpdateMessage),
|
||||
#[allow(dead_code)]
|
||||
UseFreeVersion,
|
||||
Logout,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -9,9 +9,7 @@ use client::{proto, Client};
|
||||
use collections::BTreeMap;
|
||||
|
||||
use futures::{channel::mpsc, io::BufReader, AsyncBufReadExt, StreamExt};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, EntityId, Global, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use gpui::{AppContext, AsyncAppContext, EntityId, Global, Model, ModelContext, Task, WeakModel};
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, ToOffset,
|
||||
};
|
||||
@@ -27,8 +25,6 @@ use std::{path::PathBuf, process::Stdio, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
|
||||
actions!(supermaven, [SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let supermaven = cx.new_model(|_| Supermaven::Starting);
|
||||
Supermaven::set_global(supermaven.clone(), cx);
|
||||
@@ -50,12 +46,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(supermaven) = Supermaven::global(cx) {
|
||||
supermaven.update(cx, |supermaven, _cx| supermaven.sign_out());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Supermaven {
|
||||
@@ -185,19 +175,6 @@ impl Supermaven {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_out(&mut self) {
|
||||
if let Self::Spawned(agent) = self {
|
||||
agent
|
||||
.outgoing_tx
|
||||
.unbounded_send(OutboundMessage::Logout)
|
||||
.ok();
|
||||
// The account status will get set to RequiresActivation or Ready when the next
|
||||
// message from the agent comes in. Until that happens, set the status to Unknown
|
||||
// to disable the button.
|
||||
agent.account_status = AccountStatus::Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_relevant_completion<'a>(
|
||||
|
||||
@@ -3,7 +3,7 @@ pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::{actions::SelectAll, scroll::Autoscroll, Editor};
|
||||
use editor::{scroll::Autoscroll, Editor};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use gpui::{
|
||||
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
|
||||
@@ -33,8 +33,8 @@ use workspace::{
|
||||
notifications::NotifyResultExt,
|
||||
register_serializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, Pane, ToolbarItemLocation,
|
||||
Workspace, WorkspaceId,
|
||||
CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
@@ -214,13 +214,7 @@ impl TerminalView {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let context_menu = ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("New Terminal", Box::new(NewTerminal))
|
||||
.separator()
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.action("Select All", Box::new(SelectAll))
|
||||
.action("Clear", Box::new(Clear))
|
||||
.separator()
|
||||
menu.action("Clear", Box::new(Clear))
|
||||
.action("Close", Box::new(CloseActiveItem { save_intent: None }))
|
||||
});
|
||||
|
||||
@@ -264,7 +258,7 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
||||
fn select_all(&mut self, _: &editor::actions::SelectAll, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.update(cx, |term, _| term.select_all());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ fn main() -> Result<()> {
|
||||
TermLogger::init(
|
||||
LevelFilter::Trace,
|
||||
log_config,
|
||||
TerminalMode::Stderr,
|
||||
TerminalMode::Mixed,
|
||||
ColorChoice::Auto,
|
||||
)
|
||||
.expect("could not initialize logger");
|
||||
|
||||
@@ -76,12 +76,8 @@ impl IconSize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the individual components of the square that contains this [`IconSize`].
|
||||
///
|
||||
/// The returned tuple contains:
|
||||
/// 1. The length of one side of the square
|
||||
/// 2. The padding of one side of the square
|
||||
pub fn square_components(&self, cx: &mut WindowContext) -> (Pixels, Pixels) {
|
||||
/// Returns the length of a side of the square that contains this [`IconSize`], with padding.
|
||||
pub(crate) fn square(&self, cx: &mut WindowContext) -> Pixels {
|
||||
let icon_size = self.rems() * cx.rem_size();
|
||||
let padding = match self {
|
||||
IconSize::Indicator => Spacing::None.px(cx),
|
||||
@@ -90,13 +86,6 @@ impl IconSize {
|
||||
IconSize::Medium => Spacing::XSmall.px(cx),
|
||||
};
|
||||
|
||||
(icon_size, padding)
|
||||
}
|
||||
|
||||
/// Returns the length of a side of the square that contains this [`IconSize`], with padding.
|
||||
pub fn square(&self, cx: &mut WindowContext) -> Pixels {
|
||||
let (icon_size, padding) = self.square_components(cx);
|
||||
|
||||
icon_size + padding * 2.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ impl SettingStory {
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-weight",
|
||||
"font-weifht",
|
||||
"Font Weight".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("400".to_string().into()),
|
||||
|
||||
@@ -117,21 +117,6 @@ impl RenderOnce for Tab {
|
||||
),
|
||||
};
|
||||
|
||||
let (start_slot, end_slot) = {
|
||||
let start_slot = h_flex().size_3().justify_center().children(self.start_slot);
|
||||
|
||||
let end_slot = h_flex()
|
||||
.size_3()
|
||||
.justify_center()
|
||||
.visible_on_hover("")
|
||||
.children(self.end_slot);
|
||||
|
||||
match self.close_side {
|
||||
TabCloseSide::End => (start_slot, end_slot),
|
||||
TabCloseSide::Start => (end_slot, start_slot),
|
||||
}
|
||||
};
|
||||
|
||||
self.div
|
||||
.h(rems(Self::CONTAINER_HEIGHT_IN_REMS))
|
||||
.bg(tab_bg)
|
||||
@@ -161,12 +146,35 @@ impl RenderOnce for Tab {
|
||||
.group("")
|
||||
.relative()
|
||||
.h(rems(Self::CONTENT_HEIGHT_IN_REMS))
|
||||
.px(crate::custom_spacing(cx, 4.))
|
||||
.px(crate::custom_spacing(cx, 20.))
|
||||
.gap(Spacing::Small.rems(cx))
|
||||
.text_color(text_color)
|
||||
.child(start_slot)
|
||||
.children(self.children)
|
||||
.child(end_slot),
|
||||
// .hover(|style| style.bg(tab_hover_bg))
|
||||
// .active(|style| style.bg(tab_active_bg))
|
||||
.child(
|
||||
h_flex()
|
||||
.size_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match self.close_side {
|
||||
TabCloseSide::Start => this.right(Spacing::Small.rems(cx)),
|
||||
TabCloseSide::End => this.left(Spacing::Small.rems(cx)),
|
||||
})
|
||||
.children(self.start_slot),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_3()
|
||||
.justify_center()
|
||||
.absolute()
|
||||
.map(|this| match self.close_side {
|
||||
TabCloseSide::Start => this.left(Spacing::Small.rems(cx)),
|
||||
TabCloseSide::End => this.right(Spacing::Small.rems(cx)),
|
||||
})
|
||||
.visible_on_hover("")
|
||||
.children(self.end_slot),
|
||||
)
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,9 +125,7 @@ where
|
||||
{
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.store_visual_marks(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1) as u32;
|
||||
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
|
||||
@@ -44,7 +44,6 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||
}
|
||||
|
||||
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
|
||||
vim.store_visual_marks(cx);
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let mut edits = Vec::new();
|
||||
let mut new_anchors = Vec::new();
|
||||
|
||||
@@ -30,9 +30,7 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.store_visual_marks(cx);
|
||||
let count = vim.take_count(cx).unwrap_or(1);
|
||||
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let text_layout_details = editor.text_layout_details(cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
|
||||
@@ -155,7 +155,6 @@ fn search_deploy(_: &mut Workspace, _: &buffer_search::Deploy, cx: &mut ViewCont
|
||||
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
|
||||
let mut motion = None;
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.store_visual_marks(cx);
|
||||
let pane = workspace.active_pane().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||
|
||||
@@ -29,7 +29,6 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
|
||||
}
|
||||
|
||||
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
|
||||
vim.store_visual_marks(cx);
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
|
||||
@@ -391,15 +391,6 @@ impl Vim {
|
||||
self.stop_recording();
|
||||
}
|
||||
|
||||
// When handling an action, you must create visual marks if you will switch to normal
|
||||
// mode without the default selection behaviour.
|
||||
fn store_visual_marks(&mut self, cx: &mut WindowContext) {
|
||||
let mode = self.state().mode;
|
||||
if mode.is_visual() {
|
||||
create_visual_marks(self, mode, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
let last_mode = state.mode;
|
||||
@@ -422,14 +413,14 @@ impl Vim {
|
||||
// Sync editor settings like clip mode
|
||||
self.sync_vim_settings(cx);
|
||||
|
||||
if leave_selections {
|
||||
return;
|
||||
}
|
||||
|
||||
if !mode.is_visual() && last_mode.is_visual() {
|
||||
create_visual_marks(self, last_mode, cx);
|
||||
}
|
||||
|
||||
if leave_selections {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust selections
|
||||
self.update_active_editor(cx, |_, editor, cx| {
|
||||
if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock
|
||||
|
||||
@@ -410,7 +410,6 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
|
||||
}
|
||||
|
||||
pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
|
||||
vim.store_visual_marks(cx);
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = line_mode || editor.selections.line_mode;
|
||||
@@ -464,7 +463,6 @@ pub fn delete(vim: &mut Vim, line_mode: bool, cx: &mut WindowContext) {
|
||||
}
|
||||
|
||||
pub fn yank(vim: &mut Vim, cx: &mut WindowContext) {
|
||||
vim.store_visual_marks(cx);
|
||||
vim.update_active_editor(cx, |vim, editor, cx| {
|
||||
let line_mode = editor.selections.line_mode;
|
||||
yank_selections_content(vim, editor, line_mode, cx);
|
||||
@@ -1359,15 +1357,5 @@ mod test {
|
||||
"},
|
||||
Mode::Visual,
|
||||
);
|
||||
cx.simulate_keystrokes("y g v");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
«fishˇ» one
|
||||
«fishˇ» two
|
||||
«fishˇ» red
|
||||
«fishˇ» blue
|
||||
"},
|
||||
Mode::Visual,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ fn init_common(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
supermaven::init(app_state.client.clone(), cx);
|
||||
inline_completion_registry::init(app_state.client.telemetry().clone(), cx);
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
repl::init(cx);
|
||||
extension::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
|
||||
@@ -5,7 +5,7 @@ pub(crate) mod linux_prompts;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) mod only_instance;
|
||||
mod open_listener;
|
||||
mod ssh_connection_modal;
|
||||
mod password_prompt;
|
||||
|
||||
pub use app_menus::*;
|
||||
use breadcrumbs::Breadcrumbs;
|
||||
@@ -3417,7 +3417,7 @@ mod tests {
|
||||
outline_panel::init((), cx);
|
||||
terminal_view::init(cx);
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
repl::init(cx);
|
||||
tasks_ui::init(cx);
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
app_state
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::{
|
||||
handle_open_request, init_headless, init_ui, zed::ssh_connection_modal::SshConnectionModal,
|
||||
};
|
||||
use crate::{handle_open_request, init_headless, init_ui, zed::password_prompt::PasswordPrompt};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use auto_update::AutoUpdater;
|
||||
use cli::{ipc, IpcHandshake};
|
||||
@@ -14,7 +12,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Global, SemanticVersion, View, VisualContext as _, WindowHandle,
|
||||
AppContext, AsyncAppContext, Global, SemanticVersion, VisualContext as _, WindowHandle,
|
||||
};
|
||||
use language::{Bias, Point};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
@@ -157,10 +155,8 @@ impl OpenListener {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SshClientDelegate {
|
||||
window: WindowHandle<Workspace>,
|
||||
modal: View<SshConnectionModal>,
|
||||
known_password: Option<String>,
|
||||
}
|
||||
|
||||
@@ -172,34 +168,27 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
tx.send(Ok(password)).ok();
|
||||
} else {
|
||||
self.window
|
||||
.update(cx, |_, cx| {
|
||||
self.modal.update(cx, |modal, cx| {
|
||||
modal.set_prompt(prompt, tx, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
self.window
|
||||
.update(cx, |workspace, cx| {
|
||||
cx.activate_window();
|
||||
if let Some(password) = known_password.take() {
|
||||
tx.send(Ok(password)).ok();
|
||||
} else {
|
||||
workspace.toggle_modal(cx, |cx| PasswordPrompt::new(prompt, tx, cx));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
rx
|
||||
}
|
||||
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn get_server_binary(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
tx.send(this.get_server_binary_impl(platform, &mut cx).await)
|
||||
.ok();
|
||||
tx.send(get_server_binary(platform, &mut cx).await).ok();
|
||||
})
|
||||
.detach();
|
||||
rx
|
||||
@@ -211,63 +200,48 @@ impl remote::SshClientDelegate for SshClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
impl SshClientDelegate {
|
||||
fn update_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
||||
self.window
|
||||
.update(cx, |_, cx| {
|
||||
self.modal.update(cx, |modal, cx| {
|
||||
modal.set_status(status.map(|s| s.to_string()), cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
async fn get_server_binary(
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(PathBuf, SemanticVersion)> {
|
||||
let (version, release_channel) =
|
||||
cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?;
|
||||
|
||||
async fn get_server_binary_impl(
|
||||
&self,
|
||||
platform: SshPlatform,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(PathBuf, SemanticVersion)> {
|
||||
let (version, release_channel) =
|
||||
cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?;
|
||||
// In dev mode, build the remote server binary from source
|
||||
#[cfg(debug_assertions)]
|
||||
if crate::stdout_is_a_pty()
|
||||
&& release_channel == ReleaseChannel::Dev
|
||||
&& platform.arch == std::env::consts::ARCH
|
||||
&& platform.os == std::env::consts::OS
|
||||
{
|
||||
use smol::process::{Command, Stdio};
|
||||
|
||||
// In dev mode, build the remote server binary from source
|
||||
#[cfg(debug_assertions)]
|
||||
if crate::stdout_is_a_pty()
|
||||
&& release_channel == ReleaseChannel::Dev
|
||||
&& platform.arch == std::env::consts::ARCH
|
||||
&& platform.os == std::env::consts::OS
|
||||
{
|
||||
use smol::process::{Command, Stdio};
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
|
||||
run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
|
||||
run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
|
||||
|
||||
self.update_status(Some("building remote server binary from source"), cx);
|
||||
log::info!("building remote server binary from source");
|
||||
run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
|
||||
run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
|
||||
run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
|
||||
let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
|
||||
return Ok((path, version));
|
||||
|
||||
let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
|
||||
return Ok((path, version));
|
||||
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command.stderr(Stdio::inherit()).output().await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||
let output = command.stderr(Stdio::inherit()).output().await?;
|
||||
if !output.status.success() {
|
||||
Err(anyhow!("failed to run command: {:?}", command))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
self.update_status(Some("checking for latest version of remote server"), cx);
|
||||
let binary_path = AutoUpdater::get_latest_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((binary_path, version))
|
||||
}
|
||||
|
||||
let binary_path = AutoUpdater::get_latest_remote_server_release(
|
||||
platform.os,
|
||||
platform.arch,
|
||||
release_channel,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((binary_path, version))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -341,21 +315,12 @@ pub async fn open_ssh_paths(
|
||||
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||
})?;
|
||||
|
||||
let modal = window.update(cx, |workspace, cx| {
|
||||
cx.activate_window();
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
SshConnectionModal::new(connection_info.host.clone(), cx)
|
||||
});
|
||||
workspace.active_modal::<SshConnectionModal>(cx).unwrap()
|
||||
})?;
|
||||
|
||||
let session = remote::SshSession::client(
|
||||
connection_info.username,
|
||||
connection_info.host,
|
||||
connection_info.port,
|
||||
Arc::new(SshClientDelegate {
|
||||
window,
|
||||
modal,
|
||||
known_password: connection_info.password,
|
||||
}),
|
||||
cx,
|
||||
|
||||
69
crates/zed/src/zed/password_prompt.rs
Normal file
69
crates/zed/src/zed/password_prompt.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use anyhow::Result;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View,
|
||||
};
|
||||
use ui::{v_flex, InteractiveElement, Label, Styled, StyledExt as _, ViewContext, VisualContext};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct PasswordPrompt {
|
||||
prompt: SharedString,
|
||||
tx: Option<oneshot::Sender<Result<String>>>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl PasswordPrompt {
|
||||
pub fn new(
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<Result<String>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
prompt: SharedString::from(prompt),
|
||||
tx: Some(tx),
|
||||
editor: cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_redact_all(true, cx);
|
||||
editor
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
let text = self.editor.read(cx).text(cx);
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(Ok(text)).ok();
|
||||
};
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PasswordPrompt {
|
||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w(px(400.))
|
||||
.child(Label::new(self.prompt.clone()))
|
||||
.child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for PasswordPrompt {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PasswordPrompt {}
|
||||
|
||||
impl ModalView for PasswordPrompt {}
|
||||
@@ -1,97 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View,
|
||||
};
|
||||
use ui::{
|
||||
v_flex, FluentBuilder as _, InteractiveElement, Label, LabelCommon, Styled, StyledExt as _,
|
||||
ViewContext, VisualContext,
|
||||
};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct SshConnectionModal {
|
||||
host: SharedString,
|
||||
status_message: Option<SharedString>,
|
||||
prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
|
||||
editor: View<Editor>,
|
||||
}
|
||||
|
||||
impl SshConnectionModal {
|
||||
pub fn new(host: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
host: host.into(),
|
||||
prompt: None,
|
||||
status_message: None,
|
||||
editor: cx.new_view(|cx| Editor::single_line(cx)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_prompt(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<Result<String>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if prompt.contains("yes/no") {
|
||||
editor.set_redact_all(false, cx);
|
||||
} else {
|
||||
editor.set_redact_all(true, cx);
|
||||
}
|
||||
});
|
||||
self.prompt = Some((prompt.into(), tx));
|
||||
self.status_message.take();
|
||||
cx.focus_view(&self.editor);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: Option<String>, cx: &mut ViewContext<Self>) {
|
||||
self.status_message = status.map(|s| s.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some((_, tx)) = self.prompt.take() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
tx.send(Ok(editor.text(cx))).ok();
|
||||
editor.clear(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.remove_window();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SshConnectionModal {
|
||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w(px(400.))
|
||||
.child(Label::new(format!("SSH: {}", self.host)).size(ui::LabelSize::Large))
|
||||
.when_some(self.status_message.as_ref(), |el, status| {
|
||||
el.child(Label::new(status.clone()))
|
||||
})
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(Label::new(prompt.0.clone()))
|
||||
.child(self.editor.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for SshConnectionModal {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SshConnectionModal {}
|
||||
|
||||
impl ModalView for SshConnectionModal {}
|
||||
@@ -16,7 +16,6 @@ For more information, see the Pyright [configuration documentation](https://micr
|
||||
The [pyright](https://github.com/microsoft/pyright) language server also accepts specific LSP related settings, not necessarily connected to a project. These can be changed in the `lsp` section of your `settings.json`.
|
||||
|
||||
For example, in order to:
|
||||
|
||||
- use strict type-checking level
|
||||
- diagnose all files in the workspace instead of the only open files default
|
||||
- provide the path to a specific python interpreter
|
||||
@@ -94,30 +93,23 @@ You can also configure this option directly in your `settings.json` file ([pyrig
|
||||
}
|
||||
```
|
||||
|
||||
### Code formatting & Linting
|
||||
### Code formatting
|
||||
|
||||
The Pyright language server does not provide code formatting or linting. If you want to detect lint errors and reformat your Python code upon saving, you'll need to set up.
|
||||
The Pyright language server does not provide code formatting. If you want to automatically reformat your Python code when saving, you'll need to specify an \_external_code formatter in your settings. See the [configuration](../configuring-zed.md) documentation for more information.
|
||||
|
||||
A common tool for formatting Python code is [Ruff](https://black.readthedocs.io/en/stable/). It is another tool written in Rust, an extremely fast Python linter and code formatter.
|
||||
|
||||
It is available through the [Ruff extension](https://docs.astral.sh/ruff/). However, the code formatting through the extension is not yet available. You can set up the formatter to run on save by adding the following configuration to your `settings.json`, assuming that [`Ruff`](https://docs.astral.sh/ruff/) is installed in your Python environment.
|
||||
A common tool for formatting python code is [Black](https://black.readthedocs.io/en/stable/). If you have Black installed globally, you can use it to format Python files by adding the following to your `settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"languages": {
|
||||
..., // other languages
|
||||
"Python": {
|
||||
"format_on_save": {
|
||||
"external": {
|
||||
"command": "python",
|
||||
"arguments": [
|
||||
"-m",
|
||||
"ruff",
|
||||
"format",
|
||||
"-"
|
||||
]
|
||||
"formatter": {
|
||||
"external": {
|
||||
"command": "black",
|
||||
"arguments": ["-"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"format_on_save": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ path = "src/erlang.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.0.6"
|
||||
zed_extension_api = "0.0.4"
|
||||
|
||||
@@ -3,17 +3,13 @@ name = "Erlang"
|
||||
description = "Erlang support."
|
||||
version = "0.0.1"
|
||||
schema_version = 1
|
||||
authors = ["Dairon M <dairon.medina@gmail.com>", "Fabian Bergström <fabian@fmbb.se>"]
|
||||
authors = ["Dairon M <dairon.medina@gmail.com>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.erlang-ls]
|
||||
name = "Erlang Language Server"
|
||||
language = "Erlang"
|
||||
|
||||
[language_servers.elp]
|
||||
name = "Erlang Language Platform"
|
||||
language = "Erlang"
|
||||
|
||||
[grammars.erlang]
|
||||
repository = "https://github.com/WhatsApp/tree-sitter-erlang"
|
||||
commit = "57e69513efd831f9cc8207d65d96bad917ca4aa4"
|
||||
|
||||
@@ -1,45 +1,26 @@
|
||||
mod language_servers;
|
||||
|
||||
use zed_extension_api::{self as zed, Result};
|
||||
|
||||
use crate::language_servers::{ErlangLanguagePlatform, ErlangLs};
|
||||
|
||||
struct ErlangExtension {
|
||||
erlang_ls: Option<ErlangLs>,
|
||||
erlang_language_platform: Option<ErlangLanguagePlatform>,
|
||||
}
|
||||
struct ErlangExtension;
|
||||
|
||||
impl zed::Extension for ErlangExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
erlang_ls: None,
|
||||
erlang_language_platform: None,
|
||||
}
|
||||
Self
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &zed::LanguageServerId,
|
||||
_config: zed::LanguageServerConfig,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
match language_server_id.as_ref() {
|
||||
ErlangLs::LANGUAGE_SERVER_ID => {
|
||||
let erlang_ls = self.erlang_ls.get_or_insert_with(|| ErlangLs::new());
|
||||
let path = worktree
|
||||
.which("erlang_ls")
|
||||
.ok_or_else(|| "erlang_ls must be installed and available on your $PATH".to_string())?;
|
||||
|
||||
Ok(zed::Command {
|
||||
command: erlang_ls.language_server_binary_path(language_server_id, worktree)?,
|
||||
args: vec![],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
ErlangLanguagePlatform::LANGUAGE_SERVER_ID => {
|
||||
let erlang_language_platform = self
|
||||
.erlang_language_platform
|
||||
.get_or_insert_with(|| ErlangLanguagePlatform::new());
|
||||
erlang_language_platform.language_server_command(language_server_id, worktree)
|
||||
}
|
||||
language_server_id => Err(format!("unknown language server: {language_server_id}")),
|
||||
}
|
||||
Ok(zed::Command {
|
||||
command: path,
|
||||
args: Vec::new(),
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
mod erlang_language_platform;
|
||||
mod erlang_ls;
|
||||
|
||||
pub use erlang_language_platform::*;
|
||||
pub use erlang_ls::*;
|
||||
@@ -1,112 +0,0 @@
|
||||
use std::fs;
|
||||
|
||||
use zed_extension_api::{self as zed, LanguageServerId, Result};
|
||||
|
||||
pub struct ErlangLanguagePlatform {
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ErlangLanguagePlatform {
|
||||
pub const LANGUAGE_SERVER_ID: &'static str = "elp";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
Ok(zed::Command {
|
||||
command: self.language_server_binary_path(language_server_id, worktree)?,
|
||||
args: vec!["server".to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_binary_path(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<String> {
|
||||
if let Some(path) = worktree.which("elp") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
if let Some(path) = &self.cached_binary_path {
|
||||
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
let release = zed::latest_github_release(
|
||||
"WhatsApp/erlang-language-platform",
|
||||
zed::GithubReleaseOptions {
|
||||
require_assets: true,
|
||||
pre_release: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
let (platform, arch) = zed::current_platform();
|
||||
let asset_name = {
|
||||
let otp_version = "26.2";
|
||||
let (os, os_target) = match platform {
|
||||
zed::Os::Mac => ("macos", "apple-darwin"),
|
||||
zed::Os::Linux => ("linux", "unknown-linux-gnu"),
|
||||
zed::Os::Windows => return Err(format!("unsupported platform: {platform:?}")),
|
||||
};
|
||||
|
||||
format!(
|
||||
"elp-{os}-{arch}-{os_target}-otp-{otp_version}.tar.gz",
|
||||
arch = match arch {
|
||||
zed::Architecture::Aarch64 => "aarch64",
|
||||
zed::Architecture::X8664 => "x86_64",
|
||||
zed::Architecture::X86 =>
|
||||
return Err(format!("unsupported architecture: {arch:?}")),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
|
||||
|
||||
let version_dir = format!("elp-{}", release.version);
|
||||
let binary_path = format!("{version_dir}/elp");
|
||||
|
||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
|
||||
zed::download_file(
|
||||
&asset.download_url,
|
||||
&version_dir,
|
||||
zed::DownloadedFileType::GzipTar,
|
||||
)
|
||||
.map_err(|e| format!("failed to download file: {e}"))?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
|
||||
if entry.file_name().to_str() != Some(&version_dir) {
|
||||
fs::remove_dir_all(&entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_binary_path = Some(binary_path.clone());
|
||||
Ok(binary_path)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use zed_extension_api::{self as zed, LanguageServerId, Result};
|
||||
|
||||
pub struct ErlangLs;
|
||||
|
||||
impl ErlangLs {
|
||||
pub const LANGUAGE_SERVER_ID: &'static str = "erlang-ls";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn language_server_binary_path(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<String> {
|
||||
worktree
|
||||
.which("erlang_ls")
|
||||
.ok_or_else(|| "erlang_ls must be installed and available on your $PATH".to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
[package]
|
||||
name = "zed_ruff"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/ruff.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
zed_extension_api = "0.0.6"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-APACHE
|
||||
@@ -1,11 +0,0 @@
|
||||
id = "ruff"
|
||||
name = "Ruff"
|
||||
description = "Support for Ruff, the Python linter and formatter"
|
||||
version = "0.0.1"
|
||||
schema_version = 1
|
||||
authors = []
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
[language_servers.ruff]
|
||||
name = "Ruff"
|
||||
languages = ["Python"]
|
||||
@@ -1,134 +0,0 @@
|
||||
use std::fs;
|
||||
use zed::LanguageServerId;
|
||||
use zed_extension_api::{self as zed, settings::LspSettings, Result};
|
||||
|
||||
struct RuffExtension {
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
impl RuffExtension {
|
||||
fn language_server_binary_path(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<String> {
|
||||
if let Some(path) = worktree.which("ruff") {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
|
||||
);
|
||||
let release = zed::latest_github_release(
|
||||
"astral-sh/ruff",
|
||||
zed::GithubReleaseOptions {
|
||||
require_assets: true,
|
||||
pre_release: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
let (platform, arch) = zed::current_platform();
|
||||
|
||||
let asset_stem = format!(
|
||||
"ruff-{arch}-{os}",
|
||||
arch = match arch {
|
||||
zed::Architecture::Aarch64 => "aarch64",
|
||||
zed::Architecture::X86 => "x86",
|
||||
zed::Architecture::X8664 => "x86_64",
|
||||
},
|
||||
os = match platform {
|
||||
zed::Os::Mac => "apple-darwin",
|
||||
zed::Os::Linux => "unknown-linux-gnu",
|
||||
zed::Os::Windows => "pc-windows-msvc",
|
||||
}
|
||||
);
|
||||
let asset_name = format!(
|
||||
"{asset_stem}.{suffix}",
|
||||
suffix = match platform {
|
||||
zed::Os::Windows => "zip",
|
||||
_ => "tar.gz",
|
||||
}
|
||||
);
|
||||
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| format!("no asset found matching {:?}", asset_name))?;
|
||||
|
||||
let version_dir = format!("ruff-{}", release.version);
|
||||
let binary_path = format!("{version_dir}/{asset_stem}/ruff");
|
||||
|
||||
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
|
||||
zed::set_language_server_installation_status(
|
||||
&language_server_id,
|
||||
&zed::LanguageServerInstallationStatus::Downloading,
|
||||
);
|
||||
let file_kind = match platform {
|
||||
zed::Os::Windows => zed::DownloadedFileType::Zip,
|
||||
_ => zed::DownloadedFileType::GzipTar,
|
||||
};
|
||||
zed::download_file(&asset.download_url, &version_dir, file_kind)
|
||||
.map_err(|e| format!("failed to download file: {e}"))?;
|
||||
|
||||
let entries =
|
||||
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
|
||||
if entry.file_name().to_str() != Some(&version_dir) {
|
||||
fs::remove_dir_all(&entry.path()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_binary_path = Some(binary_path.clone());
|
||||
Ok(binary_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl zed::Extension for RuffExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
Ok(zed::Command {
|
||||
command: self.language_server_binary_path(language_server_id, worktree)?,
|
||||
args: vec!["server".into(), "--preview".into()],
|
||||
env: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
server_id: &LanguageServerId,
|
||||
worktree: &zed_extension_api::Worktree,
|
||||
) -> Result<Option<zed_extension_api::serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree(server_id.as_ref(), worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.initialization_options.clone())
|
||||
.unwrap_or_default();
|
||||
Ok(Some(settings))
|
||||
}
|
||||
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
server_id: &LanguageServerId,
|
||||
worktree: &zed_extension_api::Worktree,
|
||||
) -> Result<Option<zed_extension_api::serde_json::Value>> {
|
||||
let settings = LspSettings::for_worktree(server_id.as_ref(), worktree)
|
||||
.ok()
|
||||
.and_then(|lsp_settings| lsp_settings.settings.clone())
|
||||
.unwrap_or_default();
|
||||
Ok(Some(settings))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(RuffExtension);
|
||||
@@ -24,7 +24,7 @@ Build the application bundle for macOS.
|
||||
|
||||
Options:
|
||||
-d Compile in debug mode
|
||||
-l Compile for local architecture only.
|
||||
-l Compile for local architecture and copy bundle to /Applications.
|
||||
-o Open dir with the resulting DMG or launch the app itself in local mode.
|
||||
-i Install the resulting DMG into /Applications in local mode. Noop without -l.
|
||||
-h Display this help and exit.
|
||||
@@ -390,6 +390,6 @@ else
|
||||
|
||||
sign_binary "target/x86_64-apple-darwin/release/remote_server"
|
||||
sign_binary "target/aarch64-apple-darwin/release/remote_server"
|
||||
gzip --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-macos-x86_64.gz
|
||||
gzip --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-macos-aarch64.gz
|
||||
gzip --stdout --best target/x86_64-apple-darwin/release/remote_server > target/zed-remote-server-mac-x86_64.gz
|
||||
gzip --stdout --best target/aarch64-apple-darwin/release/remote_server > target/zed-remote-server-mac-aarch64.gz
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user