Compare commits
7 Commits
why-does-t
...
embeddings
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
139c5c59b7 | ||
|
|
781fff220c | ||
|
|
d209eab058 | ||
|
|
3e0c5c10b7 | ||
|
|
8a912726d7 | ||
|
|
30e081b3f7 | ||
|
|
a5492b3ea6 |
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -9119,7 +9119,6 @@ name = "remote"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.30",
|
||||
@@ -14743,7 +14742,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_elixir"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
@@ -146,12 +146,28 @@ impl ResolvedEdit {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(description) = &mut self.description {
|
||||
if let Some(other_description) = &other.description {
|
||||
let other_offset_range = other_range.to_offset(buffer);
|
||||
let offset_range = range.to_offset(buffer);
|
||||
|
||||
// If the other range is empty at the start of this edit's range, combine the new text
|
||||
if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {
|
||||
self.new_text = format!("{}\n{}", other.new_text, self.new_text);
|
||||
self.range.start = other_range.start;
|
||||
|
||||
if let Some((description, other_description)) =
|
||||
self.description.as_mut().zip(other.description.as_ref())
|
||||
{
|
||||
*description = format!("{}\n{}", other_description, description)
|
||||
}
|
||||
} else {
|
||||
if let Some((description, other_description)) =
|
||||
self.description.as_mut().zip(other.description.as_ref())
|
||||
{
|
||||
description.push('\n');
|
||||
description.push_str(other_description);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -699,6 +715,73 @@ mod tests {
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// Ensure InsertBefore merges correctly with Update of the same text
|
||||
|
||||
assert_edits(
|
||||
"
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
vec![
|
||||
AssistantEditKind::InsertBefore {
|
||||
old_text: "
|
||||
fn foo() {"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn bar() {
|
||||
qux();
|
||||
}"
|
||||
.unindent(),
|
||||
description: "implement bar".into(),
|
||||
},
|
||||
AssistantEditKind::Update {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn foo() {
|
||||
bar();
|
||||
}"
|
||||
.unindent(),
|
||||
description: "call bar in foo".into(),
|
||||
},
|
||||
AssistantEditKind::InsertAfter {
|
||||
old_text: "
|
||||
fn foo() {
|
||||
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
new_text: "
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
description: "implement qux".into(),
|
||||
},
|
||||
],
|
||||
"
|
||||
fn bar() {
|
||||
qux();
|
||||
}
|
||||
|
||||
fn foo() {
|
||||
bar();
|
||||
}
|
||||
|
||||
fn qux() {
|
||||
// todo
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -21,6 +21,24 @@ use axum::{
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use collections::HashMap;
|
||||
use db::TokenUsage;
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ComputeEmbeddingsRequest {
|
||||
pub model: String,
|
||||
pub texts: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ComputeEmbeddingsResponse {
|
||||
pub embeddings: Vec<Embedding>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Embedding {
|
||||
pub digest: Vec<u8>,
|
||||
pub dimensions: Vec<f32>,
|
||||
}
|
||||
|
||||
use db::{usage_measure::UsageMeasure, ActiveUserCount, LlmDatabase};
|
||||
use futures::{Stream, StreamExt as _};
|
||||
use reqwest_client::ReqwestClient;
|
||||
@@ -28,6 +46,7 @@ use rpc::{
|
||||
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
};
|
||||
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
|
||||
use sha2::Sha256;
|
||||
use std::{
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@@ -113,10 +132,79 @@ impl LlmState {
|
||||
}
|
||||
}
|
||||
|
||||
async fn compute_embeddings_http(
|
||||
Extension(state): Extension<Arc<LlmState>>,
|
||||
Extension(claims): Extension<LlmTokenClaims>,
|
||||
Json(request): Json<proto::ComputeEmbeddings>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let api_key = state
|
||||
.config
|
||||
.openai_api_key
|
||||
.as_ref()
|
||||
.context("no OpenAI API key configured on the server")?;
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match claims.plan {
|
||||
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
};
|
||||
|
||||
state
|
||||
.app_state
|
||||
.rate_limiter
|
||||
.check(&*rate_limit, UserId::from_proto(claims.user_id))
|
||||
.await?;
|
||||
|
||||
let embeddings = match request.model.as_str() {
|
||||
"openai/text-embedding-3-small" => {
|
||||
open_ai::embed(
|
||||
&state.http_client,
|
||||
OPEN_AI_API_URL,
|
||||
api_key,
|
||||
OpenAiEmbeddingModel::TextEmbedding3Small,
|
||||
request.texts.iter().map(|text| text.as_str()),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
provider => return Err(anyhow!("unsupported embedding provider {:?}", provider))?,
|
||||
};
|
||||
|
||||
let embeddings = request
|
||||
.texts
|
||||
.iter()
|
||||
.map(|text| {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(text.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
result.to_vec()
|
||||
})
|
||||
.zip(
|
||||
embeddings
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|embedding| embedding.embedding),
|
||||
)
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
state
|
||||
.db
|
||||
.save_embeddings(&request.model, &embeddings)
|
||||
.await
|
||||
.context("failed to save embeddings")
|
||||
.trace_err();
|
||||
|
||||
Ok(Json(proto::ComputeEmbeddingsResponse {
|
||||
embeddings: embeddings
|
||||
.into_iter()
|
||||
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn routes() -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/models", get(list_models))
|
||||
.route("/completion", post(perform_completion))
|
||||
.route("/compute_embeddings", post(compute_embeddings_http))
|
||||
.layer(middleware::from_fn(validate_api_token))
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
.await;
|
||||
|
||||
// Set up project on remote FS
|
||||
let (forwarder, server_ssh) = SshRemoteClient::fake_server(server_cx);
|
||||
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
@@ -67,7 +67,6 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = SshRemoteClient::fake_client(forwarder, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project("/code/project1", client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
@@ -962,7 +962,6 @@ fn random_diagnostic(
|
||||
|
||||
const FILE_HEADER: &str = "file header";
|
||||
const EXCERPT_HEADER: &str = "excerpt header";
|
||||
const EXCERPT_FOOTER: &str = "excerpt footer";
|
||||
|
||||
fn editor_blocks(
|
||||
editor: &View<Editor>,
|
||||
@@ -998,7 +997,7 @@ fn editor_blocks(
|
||||
.ok()?
|
||||
}
|
||||
|
||||
Block::ExcerptHeader {
|
||||
Block::ExcerptBoundary {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
if *starts_new_buffer {
|
||||
@@ -1007,7 +1006,6 @@ fn editor_blocks(
|
||||
EXCERPT_HEADER.into()
|
||||
}
|
||||
}
|
||||
Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
|
||||
};
|
||||
|
||||
Some((row, name))
|
||||
|
||||
@@ -5,8 +5,8 @@ use super::{
|
||||
use crate::{EditorStyle, GutterDimensions};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
|
||||
use language::{Chunk, Patch, Point};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, ToPoint as _};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
@@ -128,26 +128,17 @@ pub struct BlockContext<'a, 'b> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum BlockId {
|
||||
Custom(CustomBlockId),
|
||||
ExcerptHeader(ExcerptId),
|
||||
ExcerptFooter(ExcerptId),
|
||||
}
|
||||
|
||||
impl From<BlockId> for EntityId {
|
||||
fn from(value: BlockId) -> Self {
|
||||
match value {
|
||||
BlockId::Custom(CustomBlockId(id)) => EntityId::from(id as u64),
|
||||
BlockId::ExcerptHeader(id) => id.into(),
|
||||
BlockId::ExcerptFooter(id) => id.into(),
|
||||
}
|
||||
}
|
||||
ExcerptBoundary(Option<ExcerptId>),
|
||||
}
|
||||
|
||||
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(),
|
||||
BlockId::ExcerptBoundary(next_excerpt) => match next_excerpt {
|
||||
Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(),
|
||||
None => "LastExcerptBoundary".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,8 +147,7 @@ impl std::fmt::Display for BlockId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Custom(id) => write!(f, "Block({id:?})"),
|
||||
Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
|
||||
Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
|
||||
Self::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,8 +167,7 @@ struct Transform {
|
||||
|
||||
pub(crate) enum BlockType {
|
||||
Custom(CustomBlockId),
|
||||
Header,
|
||||
Footer,
|
||||
ExcerptBoundary,
|
||||
}
|
||||
|
||||
pub(crate) trait BlockLike {
|
||||
@@ -191,27 +180,20 @@ pub(crate) trait BlockLike {
|
||||
#[derive(Clone)]
|
||||
pub enum Block {
|
||||
Custom(Arc<CustomBlock>),
|
||||
ExcerptHeader {
|
||||
id: ExcerptId,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
ExcerptBoundary {
|
||||
prev_excerpt: Option<ExcerptInfo>,
|
||||
next_excerpt: Option<ExcerptInfo>,
|
||||
height: u32,
|
||||
starts_new_buffer: bool,
|
||||
show_excerpt_controls: bool,
|
||||
},
|
||||
ExcerptFooter {
|
||||
id: ExcerptId,
|
||||
disposition: BlockDisposition,
|
||||
height: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl BlockLike for Block {
|
||||
fn block_type(&self) -> BlockType {
|
||||
match self {
|
||||
Block::Custom(block) => BlockType::Custom(block.id),
|
||||
Block::ExcerptHeader { .. } => BlockType::Header,
|
||||
Block::ExcerptFooter { .. } => BlockType::Footer,
|
||||
Block::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +204,7 @@ impl BlockLike for Block {
|
||||
fn priority(&self) -> usize {
|
||||
match self {
|
||||
Block::Custom(block) => block.priority,
|
||||
Block::ExcerptHeader { .. } => usize::MAX,
|
||||
Block::ExcerptFooter { .. } => 0,
|
||||
Block::ExcerptBoundary { .. } => usize::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,32 +213,36 @@ impl Block {
|
||||
pub fn id(&self) -> BlockId {
|
||||
match self {
|
||||
Block::Custom(block) => BlockId::Custom(block.id),
|
||||
Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id),
|
||||
Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id),
|
||||
Block::ExcerptBoundary { next_excerpt, .. } => {
|
||||
BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
Block::Custom(block) => block.disposition,
|
||||
Block::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
Block::ExcerptFooter { disposition, .. } => *disposition,
|
||||
Block::ExcerptBoundary { next_excerpt, .. } => {
|
||||
if next_excerpt.is_some() {
|
||||
BlockDisposition::Above
|
||||
} else {
|
||||
BlockDisposition::Below
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
match self {
|
||||
Block::Custom(block) => block.height,
|
||||
Block::ExcerptHeader { height, .. } => *height,
|
||||
Block::ExcerptFooter { height, .. } => *height,
|
||||
Block::ExcerptBoundary { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self) -> BlockStyle {
|
||||
match self {
|
||||
Block::Custom(block) => block.style,
|
||||
Block::ExcerptHeader { .. } => BlockStyle::Sticky,
|
||||
Block::ExcerptFooter { .. } => BlockStyle::Sticky,
|
||||
Block::ExcerptBoundary { .. } => BlockStyle::Sticky,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,24 +251,17 @@ impl Debug for Block {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
|
||||
Self::ExcerptHeader {
|
||||
buffer,
|
||||
Self::ExcerptBoundary {
|
||||
starts_new_buffer,
|
||||
id,
|
||||
next_excerpt,
|
||||
prev_excerpt,
|
||||
..
|
||||
} => f
|
||||
.debug_struct("ExcerptHeader")
|
||||
.field("id", &id)
|
||||
.field("path", &buffer.file().map(|f| f.path()))
|
||||
.debug_struct("ExcerptBoundary")
|
||||
.field("prev_excerpt", &prev_excerpt)
|
||||
.field("next_excerpt", &next_excerpt)
|
||||
.field("starts_new_buffer", &starts_new_buffer)
|
||||
.finish(),
|
||||
Block::ExcerptFooter {
|
||||
id, disposition, ..
|
||||
} => f
|
||||
.debug_struct("ExcerptFooter")
|
||||
.field("id", &id)
|
||||
.field("disposition", &disposition)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,66 +573,62 @@ impl BlockMap {
|
||||
{
|
||||
buffer
|
||||
.excerpt_boundaries_in_range(range)
|
||||
.flat_map(move |excerpt_boundary| {
|
||||
let mut wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||
.row();
|
||||
|
||||
[
|
||||
show_excerpt_controls
|
||||
.then(|| {
|
||||
let disposition;
|
||||
if excerpt_boundary.next.is_some() {
|
||||
disposition = BlockDisposition::Above;
|
||||
} else {
|
||||
wrap_row = wrap_snapshot
|
||||
.make_wrap_point(
|
||||
Point::new(
|
||||
excerpt_boundary.row.0,
|
||||
buffer.line_len(excerpt_boundary.row),
|
||||
),
|
||||
Bias::Left,
|
||||
)
|
||||
.row();
|
||||
disposition = BlockDisposition::Below;
|
||||
}
|
||||
|
||||
excerpt_boundary.prev.as_ref().map(|prev| {
|
||||
(
|
||||
wrap_row,
|
||||
Block::ExcerptFooter {
|
||||
id: prev.id,
|
||||
height: excerpt_footer_height,
|
||||
disposition,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.flatten(),
|
||||
excerpt_boundary.next.map(|next| {
|
||||
let starts_new_buffer = excerpt_boundary
|
||||
.prev
|
||||
.map_or(true, |prev| prev.buffer_id != next.buffer_id);
|
||||
|
||||
(
|
||||
wrap_row,
|
||||
Block::ExcerptHeader {
|
||||
id: next.id,
|
||||
buffer: next.buffer,
|
||||
range: next.range,
|
||||
height: if starts_new_buffer {
|
||||
buffer_header_height
|
||||
} else {
|
||||
excerpt_header_height
|
||||
},
|
||||
starts_new_buffer,
|
||||
show_excerpt_controls,
|
||||
},
|
||||
.filter_map(move |excerpt_boundary| {
|
||||
let wrap_row;
|
||||
if excerpt_boundary.next.is_some() {
|
||||
wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||
.row();
|
||||
} else {
|
||||
wrap_row = wrap_snapshot
|
||||
.make_wrap_point(
|
||||
Point::new(
|
||||
excerpt_boundary.row.0,
|
||||
buffer.line_len(excerpt_boundary.row),
|
||||
),
|
||||
Bias::Left,
|
||||
)
|
||||
}),
|
||||
]
|
||||
.row();
|
||||
}
|
||||
|
||||
let starts_new_buffer = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
|
||||
(_, None) => false,
|
||||
(None, Some(_)) => true,
|
||||
(Some(prev), Some(next)) => prev.buffer_id != next.buffer_id,
|
||||
};
|
||||
|
||||
let mut height = 0;
|
||||
if excerpt_boundary.prev.is_some() {
|
||||
if show_excerpt_controls {
|
||||
height += excerpt_footer_height;
|
||||
}
|
||||
}
|
||||
if excerpt_boundary.next.is_some() {
|
||||
if starts_new_buffer {
|
||||
height += buffer_header_height;
|
||||
if show_excerpt_controls {
|
||||
height += excerpt_header_height;
|
||||
}
|
||||
} else {
|
||||
height += excerpt_header_height;
|
||||
}
|
||||
}
|
||||
|
||||
if height == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
wrap_row,
|
||||
Block::ExcerptBoundary {
|
||||
prev_excerpt: excerpt_boundary.prev,
|
||||
next_excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
starts_new_buffer,
|
||||
show_excerpt_controls,
|
||||
},
|
||||
))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn sort_blocks<B: BlockLike>(blocks: &mut [(u32, B)]) {
|
||||
@@ -665,12 +639,9 @@ impl BlockMap {
|
||||
.disposition()
|
||||
.cmp(&block_b.disposition())
|
||||
.then_with(|| match ((block_a.block_type()), (block_b.block_type())) {
|
||||
(BlockType::Footer, BlockType::Footer) => Ordering::Equal,
|
||||
(BlockType::Footer, _) => Ordering::Less,
|
||||
(_, BlockType::Footer) => Ordering::Greater,
|
||||
(BlockType::Header, BlockType::Header) => Ordering::Equal,
|
||||
(BlockType::Header, _) => Ordering::Less,
|
||||
(_, BlockType::Header) => Ordering::Greater,
|
||||
(BlockType::ExcerptBoundary, BlockType::ExcerptBoundary) => Ordering::Equal,
|
||||
(BlockType::ExcerptBoundary, _) => Ordering::Less,
|
||||
(_, BlockType::ExcerptBoundary) => Ordering::Greater,
|
||||
(BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b
|
||||
.priority()
|
||||
.cmp(&block_a.priority())
|
||||
@@ -1045,33 +1016,19 @@ impl BlockSnapshot {
|
||||
let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
|
||||
Some(Block::Custom(custom_block.clone()))
|
||||
}
|
||||
BlockId::ExcerptHeader(excerpt_id) => {
|
||||
let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(excerpt_range.start, Bias::Left);
|
||||
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
|
||||
cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
|
||||
while let Some(transform) = cursor.item() {
|
||||
if let Some(block) = transform.block.as_ref() {
|
||||
if block.id() == block_id {
|
||||
return Some(block.clone());
|
||||
}
|
||||
} else if cursor.start().0 > WrapRow(wrap_point.row()) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.next(&());
|
||||
BlockId::ExcerptBoundary(next_excerpt_id) => {
|
||||
let wrap_point;
|
||||
if let Some(next_excerpt_id) = next_excerpt_id {
|
||||
let excerpt_range = buffer.range_for_excerpt::<Point>(next_excerpt_id)?;
|
||||
wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(excerpt_range.start, Bias::Left);
|
||||
} else {
|
||||
wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(buffer.max_point(), Bias::Left);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
BlockId::ExcerptFooter(excerpt_id) => {
|
||||
let excerpt_range = buffer.range_for_excerpt::<Point>(excerpt_id)?;
|
||||
let wrap_point = self
|
||||
.wrap_snapshot
|
||||
.make_wrap_point(excerpt_range.end, Bias::Left);
|
||||
|
||||
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
|
||||
cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &());
|
||||
while let Some(transform) = cursor.item() {
|
||||
@@ -1468,7 +1425,7 @@ mod tests {
|
||||
};
|
||||
use gpui::{div, font, px, AppContext, Context as _, Element};
|
||||
use language::{Buffer, Capability};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use multi_buffer::{ExcerptRange, MultiBuffer};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::env;
|
||||
@@ -1724,22 +1681,20 @@ mod tests {
|
||||
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
|
||||
assert_eq!(
|
||||
snapshot.text(),
|
||||
"\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n"
|
||||
"\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
|
||||
);
|
||||
|
||||
let blocks: Vec<_> = snapshot
|
||||
.blocks_in_range(0..u32::MAX)
|
||||
.map(|(row, block)| (row, block.id()))
|
||||
.map(|(row, block)| (row..row + block.height(), block.id()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
blocks,
|
||||
vec![
|
||||
(0, BlockId::ExcerptHeader(excerpt_ids[0])),
|
||||
(3, BlockId::ExcerptFooter(excerpt_ids[0])),
|
||||
(4, BlockId::ExcerptHeader(excerpt_ids[1])),
|
||||
(7, BlockId::ExcerptFooter(excerpt_ids[1])),
|
||||
(8, BlockId::ExcerptHeader(excerpt_ids[2])),
|
||||
(11, BlockId::ExcerptFooter(excerpt_ids[2]))
|
||||
(0..2, BlockId::ExcerptBoundary(Some(excerpt_ids[0]))), // path, header
|
||||
(4..7, BlockId::ExcerptBoundary(Some(excerpt_ids[1]))), // footer, path, header
|
||||
(9..12, BlockId::ExcerptBoundary(Some(excerpt_ids[2]))), // footer, path, header
|
||||
(14..15, BlockId::ExcerptBoundary(None)), // footer
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -2283,13 +2238,10 @@ mod tests {
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum ExpectedBlock {
|
||||
ExcerptHeader {
|
||||
ExcerptBoundary {
|
||||
height: u32,
|
||||
starts_new_buffer: bool,
|
||||
},
|
||||
ExcerptFooter {
|
||||
height: u32,
|
||||
disposition: BlockDisposition,
|
||||
is_last: bool,
|
||||
},
|
||||
Custom {
|
||||
disposition: BlockDisposition,
|
||||
@@ -2303,8 +2255,7 @@ mod tests {
|
||||
fn block_type(&self) -> BlockType {
|
||||
match self {
|
||||
ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id),
|
||||
ExpectedBlock::ExcerptHeader { .. } => BlockType::Header,
|
||||
ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer,
|
||||
ExpectedBlock::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2315,8 +2266,7 @@ mod tests {
|
||||
fn priority(&self) -> usize {
|
||||
match self {
|
||||
ExpectedBlock::Custom { priority, .. } => *priority,
|
||||
ExpectedBlock::ExcerptHeader { .. } => usize::MAX,
|
||||
ExpectedBlock::ExcerptFooter { .. } => 0,
|
||||
ExpectedBlock::ExcerptBoundary { .. } => usize::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2324,17 +2274,21 @@ mod tests {
|
||||
impl ExpectedBlock {
|
||||
fn height(&self) -> u32 {
|
||||
match self {
|
||||
ExpectedBlock::ExcerptHeader { height, .. } => *height,
|
||||
ExpectedBlock::ExcerptBoundary { height, .. } => *height,
|
||||
ExpectedBlock::Custom { height, .. } => *height,
|
||||
ExpectedBlock::ExcerptFooter { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
|
||||
fn disposition(&self) -> BlockDisposition {
|
||||
match self {
|
||||
ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
|
||||
ExpectedBlock::ExcerptBoundary { is_last, .. } => {
|
||||
if *is_last {
|
||||
BlockDisposition::Below
|
||||
} else {
|
||||
BlockDisposition::Above
|
||||
}
|
||||
}
|
||||
ExpectedBlock::Custom { disposition, .. } => *disposition,
|
||||
ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2348,21 +2302,15 @@ mod tests {
|
||||
height: block.height,
|
||||
priority: block.priority,
|
||||
},
|
||||
Block::ExcerptHeader {
|
||||
Block::ExcerptBoundary {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
next_excerpt,
|
||||
..
|
||||
} => ExpectedBlock::ExcerptHeader {
|
||||
} => ExpectedBlock::ExcerptBoundary {
|
||||
height,
|
||||
starts_new_buffer,
|
||||
},
|
||||
Block::ExcerptFooter {
|
||||
height,
|
||||
disposition,
|
||||
..
|
||||
} => ExpectedBlock::ExcerptFooter {
|
||||
height,
|
||||
disposition,
|
||||
is_last: next_excerpt.is_none(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2380,8 +2328,7 @@ mod tests {
|
||||
fn as_custom(&self) -> Option<&CustomBlock> {
|
||||
match self {
|
||||
Block::Custom(block) => Some(block),
|
||||
Block::ExcerptHeader { .. } => None,
|
||||
Block::ExcerptFooter { .. } => None,
|
||||
Block::ExcerptBoundary { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ use git::blame::GitBlame;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
|
||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
||||
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,
|
||||
UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler,
|
||||
VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
||||
ClipboardItem, Context, DispatchPhase, ElementId, 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, UTF16Selection,
|
||||
UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
|
||||
WeakFocusHandle, WeakView, WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
@@ -171,7 +171,7 @@ use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSetti
|
||||
use crate::hover_links::find_url;
|
||||
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
|
||||
|
||||
pub const FILE_HEADER_HEIGHT: u32 = 1;
|
||||
pub const FILE_HEADER_HEIGHT: u32 = 2;
|
||||
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
|
||||
pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1;
|
||||
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
|
||||
@@ -640,7 +640,6 @@ pub struct Editor {
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
file_header_size: u32,
|
||||
breadcrumb_header: Option<String>,
|
||||
focused_block: Option<FocusedBlock>,
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
@@ -1846,7 +1845,6 @@ impl Editor {
|
||||
}),
|
||||
merge_adjacent: true,
|
||||
};
|
||||
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
|
||||
let display_map = cx.new_model(|cx| {
|
||||
DisplayMap::new(
|
||||
buffer.clone(),
|
||||
@@ -1854,7 +1852,7 @@ impl Editor {
|
||||
font_size,
|
||||
None,
|
||||
show_excerpt_controls,
|
||||
file_header_size,
|
||||
FILE_HEADER_HEIGHT,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
|
||||
fold_placeholder,
|
||||
@@ -2038,7 +2036,6 @@ impl Editor {
|
||||
.restore_unsaved_buffers,
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
file_header_size,
|
||||
tasks: Default::default(),
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
@@ -12808,7 +12805,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn file_header_size(&self) -> u32 {
|
||||
self.file_header_size
|
||||
FILE_HEADER_HEIGHT
|
||||
}
|
||||
|
||||
pub fn revert(
|
||||
@@ -14120,7 +14117,7 @@ pub fn diagnostic_block_renderer(
|
||||
|
||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
||||
|
||||
let buttons = |diagnostic: &Diagnostic, block_id: BlockId| {
|
||||
let buttons = |diagnostic: &Diagnostic| {
|
||||
if multi_line_diagnostic {
|
||||
v_flex()
|
||||
} else {
|
||||
@@ -14128,7 +14125,7 @@ pub fn diagnostic_block_renderer(
|
||||
}
|
||||
.when(allow_closing, |div| {
|
||||
div.children(diagnostic.is_primary.then(|| {
|
||||
IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
|
||||
IconButton::new("close-block", IconName::XCircle)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
@@ -14138,7 +14135,7 @@ pub fn diagnostic_block_renderer(
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
|
||||
IconButton::new("copy-block", IconName::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
@@ -14153,7 +14150,7 @@ pub fn diagnostic_block_renderer(
|
||||
)
|
||||
};
|
||||
|
||||
let icon_size = buttons(&diagnostic, cx.block_id)
|
||||
let icon_size = buttons(&diagnostic)
|
||||
.into_any_element()
|
||||
.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
|
||||
@@ -14170,7 +14167,7 @@ pub fn diagnostic_block_renderer(
|
||||
.w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
|
||||
.flex_shrink(),
|
||||
)
|
||||
.child(buttons(&diagnostic, cx.block_id))
|
||||
.child(buttons(&diagnostic))
|
||||
.child(div().flex().flex_shrink_0().child(
|
||||
StyledText::new(text_without_backticks.clone()).with_highlights(
|
||||
&text_style,
|
||||
|
||||
@@ -21,7 +21,8 @@ use crate::{
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown,
|
||||
PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -31,7 +32,7 @@ use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
EntityId, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
|
||||
@@ -46,7 +47,7 @@ use language::{
|
||||
ChunkRendererContext,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
ProjectPath,
|
||||
@@ -1632,7 +1633,7 @@ impl EditorElement {
|
||||
let mut block_offset = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
|
||||
if matches!(block, Block::ExcerptHeader { .. }) {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
found_excerpt_header = true;
|
||||
break;
|
||||
}
|
||||
@@ -1649,7 +1650,7 @@ impl EditorElement {
|
||||
let mut block_height = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
|
||||
if matches!(block, Block::ExcerptHeader { .. }) {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
found_excerpt_header = true;
|
||||
}
|
||||
block_height += block.height();
|
||||
@@ -2100,23 +2101,14 @@ impl EditorElement {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Block::ExcerptHeader {
|
||||
buffer,
|
||||
range,
|
||||
Block::ExcerptBoundary {
|
||||
prev_excerpt,
|
||||
next_excerpt,
|
||||
show_excerpt_controls,
|
||||
starts_new_buffer,
|
||||
height,
|
||||
id,
|
||||
show_excerpt_controls,
|
||||
..
|
||||
} => {
|
||||
let include_root = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
#[derive(Clone)]
|
||||
struct JumpData {
|
||||
position: Point,
|
||||
@@ -2125,233 +2117,227 @@ impl EditorElement {
|
||||
line_offset_from_top: u32,
|
||||
}
|
||||
|
||||
let jump_data = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
|
||||
let excerpt_start = range.context.start;
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
|
||||
0
|
||||
} else {
|
||||
let excerpt_start_row =
|
||||
language::ToPoint::to_point(&jump_anchor, buffer).row;
|
||||
jump_position.row - excerpt_start_row
|
||||
};
|
||||
|
||||
let line_offset_from_top =
|
||||
block_row_start.0 + *height + offset_from_excerpt_start
|
||||
- snapshot
|
||||
.scroll_anchor
|
||||
.scroll_position(&snapshot.display_snapshot)
|
||||
.y as u32;
|
||||
|
||||
JumpData {
|
||||
position: jump_position,
|
||||
anchor: jump_anchor,
|
||||
path: jump_path,
|
||||
line_offset_from_top,
|
||||
}
|
||||
});
|
||||
|
||||
let icon_offset = gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
|
||||
|
||||
let element = if *starts_new_buffer {
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path = path
|
||||
.parent()
|
||||
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
|
||||
}
|
||||
let header_padding = px(6.0);
|
||||
|
||||
let header_padding = px(6.0);
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
v_flex()
|
||||
.id(("path excerpt header", EntityId::from(block_id)))
|
||||
.w_full()
|
||||
.px(header_padding)
|
||||
.pt(header_padding)
|
||||
.child(
|
||||
if let Some(prev_excerpt) = prev_excerpt {
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(
|
||||
h_flex()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||
.id("path header block")
|
||||
.h(2. * cx.line_height())
|
||||
.px(gpui::px(12.))
|
||||
.rounded_md()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.justify_between()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex().gap_3().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(
|
||||
div()
|
||||
.child(path)
|
||||
.text_color(cx.theme().colors().text_muted),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(jump_data.clone(), |el, jump_data| {
|
||||
el.child(Icon::new(IconName::ArrowUpRight))
|
||||
.cursor_pointer()
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action("Jump to File", &OpenExcerpts, cx)
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| {
|
||||
cx.stop_propagation()
|
||||
})
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
move |editor, _, cx| {
|
||||
editor.jump(
|
||||
jump_data.path.clone(),
|
||||
jump_data.position,
|
||||
jump_data.anchor,
|
||||
jump_data.line_offset_from_top,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.children(show_excerpt_controls.then(|| {
|
||||
h_flex()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.333)))
|
||||
.h(1. * cx.line_height())
|
||||
.pt_1()
|
||||
.justify_end()
|
||||
.w(icon_offset)
|
||||
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
|
||||
.flex_none()
|
||||
.w(icon_offset - header_padding)
|
||||
.child(
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::ArrowUpFromLine.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group("")
|
||||
.hover(|style| {
|
||||
style.text_color(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_active_line_number,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
let id = *id;
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(
|
||||
id,
|
||||
multi_buffer::ExpandExcerptDirection::Up,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
v_flex()
|
||||
.id(("excerpt header", EntityId::from(block_id)))
|
||||
.w_full()
|
||||
.h(snapshot.excerpt_header_height() as f32 * cx.line_height())
|
||||
.child(
|
||||
.justify_end()
|
||||
.child(self.render_expand_excerpt_button(
|
||||
prev_excerpt.id,
|
||||
ExpandExcerptDirection::Down,
|
||||
IconName::ArrowDownFromLine,
|
||||
cx,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_excerpt) = next_excerpt {
|
||||
let buffer = &next_excerpt.buffer;
|
||||
let range = &next_excerpt.range;
|
||||
let jump_data = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
|
||||
let excerpt_start = range.context.start;
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
|
||||
0
|
||||
} else {
|
||||
let excerpt_start_row =
|
||||
language::ToPoint::to_point(&jump_anchor, buffer).row;
|
||||
jump_position.row - excerpt_start_row
|
||||
};
|
||||
|
||||
let line_offset_from_top =
|
||||
block_row_start.0 + *height + offset_from_excerpt_start
|
||||
- snapshot
|
||||
.scroll_anchor
|
||||
.scroll_position(&snapshot.display_snapshot)
|
||||
.y as u32;
|
||||
|
||||
JumpData {
|
||||
position: jump_position,
|
||||
anchor: jump_anchor,
|
||||
path: jump_path,
|
||||
line_offset_from_top,
|
||||
}
|
||||
});
|
||||
|
||||
if *starts_new_buffer {
|
||||
let include_root = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let filename = path
|
||||
.as_ref()
|
||||
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
|
||||
let parent_path = path.as_ref().and_then(|path| {
|
||||
Some(path.parent()?.to_string_lossy().to_string() + "/")
|
||||
});
|
||||
|
||||
result = result.child(
|
||||
div()
|
||||
.flex()
|
||||
.v_flex()
|
||||
.px(header_padding)
|
||||
.pt(header_padding)
|
||||
.w_full()
|
||||
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.id("path header block")
|
||||
.size_full()
|
||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(
|
||||
0.667,
|
||||
)))
|
||||
.px(gpui::px(12.))
|
||||
.rounded_md()
|
||||
.shadow_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_subheader_background)
|
||||
.justify_between()
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex().gap_3().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
filename
|
||||
.map(SharedString::from)
|
||||
.unwrap_or_else(|| "untitled".into()),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
cx.theme().colors().text_muted,
|
||||
))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.when_some(jump_data, |el, jump_data| {
|
||||
el.child(Icon::new(IconName::ArrowUpRight))
|
||||
.cursor_pointer()
|
||||
.tooltip(|cx| {
|
||||
Tooltip::for_action(
|
||||
"Jump to File",
|
||||
&OpenExcerpts,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| {
|
||||
cx.stop_propagation()
|
||||
})
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
move |editor, _, cx| {
|
||||
editor.jump(
|
||||
jump_data.path.clone(),
|
||||
jump_data.position,
|
||||
jump_data.anchor,
|
||||
jump_data.line_offset_from_top,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
}),
|
||||
),
|
||||
);
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(
|
||||
h_flex()
|
||||
.w(icon_offset)
|
||||
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(self.render_expand_excerpt_button(
|
||||
next_excerpt.id,
|
||||
ExpandExcerptDirection::Up,
|
||||
IconName::ArrowUpFromLine,
|
||||
cx,
|
||||
)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
result = result.child(
|
||||
h_flex()
|
||||
.id("excerpt header block")
|
||||
.group("excerpt-jump-action")
|
||||
.justify_start()
|
||||
.id("jump to collapsed context")
|
||||
.w(relative(1.0))
|
||||
.h_full()
|
||||
.w_full()
|
||||
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.top(px(0.))
|
||||
.absolute()
|
||||
.w_full()
|
||||
.h_px()
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
.group_hover("excerpt-jump-action", |style| {
|
||||
style.bg(cx.theme().colors().border)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(icon_offset)
|
||||
.h_full()
|
||||
)
|
||||
.cursor_pointer()
|
||||
.when_some(jump_data.clone(), |this, jump_data| {
|
||||
this.on_click(cx.listener_for(&self.editor, {
|
||||
let path = jump_data.path.clone();
|
||||
move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
|
||||
editor.jump(
|
||||
path.clone(),
|
||||
jump_data.position,
|
||||
jump_data.anchor,
|
||||
jump_data.line_offset_from_top,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
format!(
|
||||
"Jump to {}:L{}",
|
||||
jump_data.path.path.display(),
|
||||
jump_data.position.row + 1
|
||||
),
|
||||
&OpenExcerpts,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
show_excerpt_controls
|
||||
.then(|| {
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::ArrowUpFromLine.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(
|
||||
cx.theme().colors().editor_line_number,
|
||||
)
|
||||
.group("")
|
||||
.hover(|style| {
|
||||
style.text_color(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_active_line_number,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
let id = *id;
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(
|
||||
id,
|
||||
multi_buffer::ExpandExcerptDirection::Up,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
h_flex()
|
||||
.w(icon_offset)
|
||||
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32
|
||||
* cx.line_height())
|
||||
.flex_none()
|
||||
.justify_end()
|
||||
.child(if *show_excerpt_controls {
|
||||
self.render_expand_excerpt_button(
|
||||
next_excerpt.id,
|
||||
ExpandExcerptDirection::Up,
|
||||
IconName::ArrowUpFromLine,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
ButtonLike::new("jump-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
@@ -2361,7 +2347,6 @@ impl EditorElement {
|
||||
.text_color(
|
||||
cx.theme().colors().border_variant,
|
||||
)
|
||||
.group("excerpt-jump-action")
|
||||
.group_hover(
|
||||
"excerpt-jump-action",
|
||||
|style| {
|
||||
@@ -2371,118 +2356,13 @@ impl EditorElement {
|
||||
},
|
||||
),
|
||||
)
|
||||
.when_some(jump_data.clone(), |this, jump_data| {
|
||||
this.on_click(cx.listener_for(&self.editor, {
|
||||
let path = jump_data.path.clone();
|
||||
move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
|
||||
editor.jump(
|
||||
path.clone(),
|
||||
jump_data.position,
|
||||
jump_data.anchor,
|
||||
jump_data.line_offset_from_top,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
format!(
|
||||
"Jump to {}:L{}",
|
||||
jump_data.path.path.display(),
|
||||
jump_data.position.row + 1
|
||||
),
|
||||
&OpenExcerpts,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.group("excerpt-jump-action")
|
||||
.cursor_pointer()
|
||||
.when_some(jump_data.clone(), |this, jump_data| {
|
||||
this.on_click(cx.listener_for(&self.editor, {
|
||||
let path = jump_data.path.clone();
|
||||
move |editor, _, cx| {
|
||||
cx.stop_propagation();
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editor.jump(
|
||||
path.clone(),
|
||||
jump_data.position,
|
||||
jump_data.anchor,
|
||||
jump_data.line_offset_from_top,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::for_action(
|
||||
format!(
|
||||
"Jump to {}:L{}",
|
||||
jump_data.path.path.display(),
|
||||
jump_data.position.row + 1
|
||||
),
|
||||
&OpenExcerpts,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
element.into_any()
|
||||
}
|
||||
|
||||
Block::ExcerptFooter { id, .. } => {
|
||||
let element = v_flex()
|
||||
.id(("excerpt footer", EntityId::from(block_id)))
|
||||
.w_full()
|
||||
.h(snapshot.excerpt_footer_height() as f32 * cx.line_height())
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.w(gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding + gutter_dimensions.margin))
|
||||
.h_full()
|
||||
.child(
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(IconName::ArrowDownFromLine.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group("")
|
||||
.hover(|style| {
|
||||
style.text_color(
|
||||
cx.theme().colors().editor_active_line_number,
|
||||
)
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
let id = *id;
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(
|
||||
id,
|
||||
multi_buffer::ExpandExcerptDirection::Down,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| {
|
||||
Tooltip::for_action(
|
||||
"Expand Excerpt",
|
||||
&ExpandExcerpts { lines: 0 },
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
element.into_any()
|
||||
result.into_any()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2509,6 +2389,33 @@ impl EditorElement {
|
||||
(element, final_size)
|
||||
}
|
||||
|
||||
fn render_expand_excerpt_button(
|
||||
&self,
|
||||
excerpt_id: ExcerptId,
|
||||
direction: ExpandExcerptDirection,
|
||||
icon: IconName,
|
||||
cx: &mut WindowContext,
|
||||
) -> ButtonLike {
|
||||
ButtonLike::new("expand-icon")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.child(
|
||||
svg()
|
||||
.path(icon.path())
|
||||
.size(IconSize::XSmall.rems())
|
||||
.text_color(cx.theme().colors().editor_line_number)
|
||||
.group("")
|
||||
.hover(|style| style.text_color(cx.theme().colors().editor_active_line_number)),
|
||||
)
|
||||
.on_click(cx.listener_for(&self.editor, {
|
||||
move |editor, _, cx| {
|
||||
editor.expand_excerpt(excerpt_id, direction, cx);
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
move |cx| Tooltip::for_action("Expand Excerpt", &ExpandExcerpts { lines: 0 }, cx)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_blocks(
|
||||
&self,
|
||||
@@ -3367,7 +3274,7 @@ impl EditorElement {
|
||||
let end_row_in_current_excerpt = snapshot
|
||||
.blocks_in_range(start_row..end_row)
|
||||
.find_map(|(start_row, block)| {
|
||||
if matches!(block, Block::ExcerptHeader { .. }) {
|
||||
if matches!(block, Block::ExcerptBoundary { .. }) {
|
||||
Some(start_row)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -952,7 +952,7 @@ mod tests {
|
||||
px(14.0),
|
||||
None,
|
||||
true,
|
||||
2,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
FoldPlaceholder::test(),
|
||||
|
||||
@@ -189,6 +189,7 @@ pub struct MultiBufferSnapshot {
|
||||
show_headers: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExcerptInfo {
|
||||
pub id: ExcerptId,
|
||||
pub buffer: BufferSnapshot,
|
||||
@@ -201,6 +202,7 @@ impl std::fmt::Debug for ExcerptInfo {
|
||||
f.debug_struct(type_name::<Self>())
|
||||
.field("id", &self.id)
|
||||
.field("buffer_id", &self.buffer_id)
|
||||
.field("path", &self.buffer.file().map(|f| f.path()))
|
||||
.field("range", &self.range)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -6057,6 +6057,16 @@ impl LspStore {
|
||||
);
|
||||
})?;
|
||||
}
|
||||
"textDocument/rename" => {
|
||||
this.update(&mut cx, |this, _| {
|
||||
if let Some(server) = this.language_server_for_id(server_id)
|
||||
{
|
||||
server.update_capabilities(|capabilities| {
|
||||
capabilities.rename_provider = None
|
||||
})
|
||||
}
|
||||
})?;
|
||||
}
|
||||
"textDocument/rangeFormatting" => {
|
||||
this.update(&mut cx, |this, _| {
|
||||
if let Some(server) = this.language_server_for_id(server_id)
|
||||
|
||||
@@ -1243,10 +1243,6 @@ impl Project {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn ssh_client(&self) -> Option<Model<SshRemoteClient>> {
|
||||
self.ssh_client.clone()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> Model<UserStore> {
|
||||
self.user_store.clone()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ message Envelope {
|
||||
uint32 id = 1;
|
||||
optional uint32 responding_to = 2;
|
||||
optional PeerId original_sender_id = 3;
|
||||
optional uint32 ack_id = 266;
|
||||
|
||||
oneof payload {
|
||||
Hello hello = 4;
|
||||
@@ -296,9 +295,7 @@ message Envelope {
|
||||
OpenServerSettings open_server_settings = 263;
|
||||
|
||||
GetPermalinkToLine get_permalink_to_line = 264;
|
||||
GetPermalinkToLineResponse get_permalink_to_line_response = 265;
|
||||
|
||||
FlushBufferedMessages flush_buffered_messages = 267;
|
||||
GetPermalinkToLineResponse get_permalink_to_line_response = 265; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
@@ -2524,6 +2521,3 @@ message GetPermalinkToLine {
|
||||
message GetPermalinkToLineResponse {
|
||||
string permalink = 1;
|
||||
}
|
||||
|
||||
message FlushBufferedMessages {}
|
||||
message FlushBufferedMessagesResponse {}
|
||||
|
||||
@@ -32,7 +32,6 @@ macro_rules! messages {
|
||||
responding_to,
|
||||
original_sender_id,
|
||||
payload: Some(envelope::Payload::$name(self)),
|
||||
ack_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -372,7 +372,6 @@ messages!(
|
||||
(OpenServerSettings, Foreground),
|
||||
(GetPermalinkToLine, Foreground),
|
||||
(GetPermalinkToLineResponse, Foreground),
|
||||
(FlushBufferedMessages, Foreground),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -499,7 +498,6 @@ request_messages!(
|
||||
(RemoveWorktree, Ack),
|
||||
(OpenServerSettings, OpenBufferResponse),
|
||||
(GetPermalinkToLine, GetPermalinkToLineResponse),
|
||||
(FlushBufferedMessages, Ack),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -19,7 +19,6 @@ test-support = ["fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use prost::Message as _;
|
||||
use rpc::proto::Envelope;
|
||||
use std::mem::size_of;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct MessageId(pub u32);
|
||||
@@ -30,8 +29,10 @@ pub async fn read_message<S: AsyncRead + Unpin>(
|
||||
) -> Result<Envelope> {
|
||||
buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||
stream.read_exact(buffer).await?;
|
||||
|
||||
let len = message_len_from_buffer(buffer);
|
||||
read_message_with_len(stream, buffer, len).await
|
||||
let result = read_message_with_len(stream, buffer, len).await;
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn write_message<S: AsyncWrite + Unpin>(
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::{
|
||||
proxy::ProxyLaunchError,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::{
|
||||
@@ -14,8 +13,7 @@ use futures::{
|
||||
oneshot,
|
||||
},
|
||||
future::BoxFuture,
|
||||
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
|
||||
StreamExt as _,
|
||||
select_biased, AsyncReadExt as _, Future, FutureExt as _, SinkExt, StreamExt as _,
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
|
||||
@@ -32,7 +30,6 @@ use smol::{
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::VecDeque,
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
ops::ControlFlow,
|
||||
@@ -278,7 +275,7 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChannelForwarder {
|
||||
struct ChannelForwarder {
|
||||
quit_tx: UnboundedSender<()>,
|
||||
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
|
||||
}
|
||||
@@ -349,7 +346,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
|
||||
enum State {
|
||||
Connecting,
|
||||
Connected {
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
ssh_connection: SshRemoteConnection,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -359,7 +356,7 @@ enum State {
|
||||
HeartbeatMissed {
|
||||
missed_heartbeats: usize,
|
||||
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
ssh_connection: SshRemoteConnection,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -368,7 +365,7 @@ enum State {
|
||||
},
|
||||
Reconnecting,
|
||||
ReconnectFailed {
|
||||
ssh_connection: Box<dyn SshRemoteProcess>,
|
||||
ssh_connection: SshRemoteConnection,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
forwarder: ChannelForwarder,
|
||||
|
||||
@@ -394,11 +391,11 @@ impl fmt::Display for State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn ssh_connection(&self) -> Option<&dyn SshRemoteProcess> {
|
||||
fn ssh_connection(&self) -> Option<&SshRemoteConnection> {
|
||||
match self {
|
||||
Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
|
||||
Self::Connected { ssh_connection, .. } => Some(ssh_connection),
|
||||
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection),
|
||||
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -532,8 +529,7 @@ impl SshRemoteClient {
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let client =
|
||||
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
|
||||
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
|
||||
let this = cx.new_model(|_| Self {
|
||||
client: client.clone(),
|
||||
unique_identifier: unique_identifier.clone(),
|
||||
@@ -544,19 +540,23 @@ impl SshRemoteClient {
|
||||
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||
|
||||
let (ssh_connection, io_task) = Self::establish_connection(
|
||||
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
|
||||
unique_identifier,
|
||||
false,
|
||||
connection_options,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
|
||||
let multiplex_task = Self::multiplex(
|
||||
this.downgrade(),
|
||||
ssh_proxy_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
log::error!("failed to establish connection: {}", error);
|
||||
@@ -702,24 +702,30 @@ impl SshRemoteClient {
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(error) = ssh_connection.kill().await.context("Failed to kill ssh process") {
|
||||
if let Err(error) = ssh_connection.master_process.kill() {
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
};
|
||||
|
||||
let connection_options = ssh_connection.connection_options();
|
||||
if let Err(error) = ssh_connection
|
||||
.master_process
|
||||
.status()
|
||||
.await
|
||||
.context("Failed to kill ssh process")
|
||||
{
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
}
|
||||
|
||||
let connection_options = ssh_connection.socket.connection_options.clone();
|
||||
|
||||
let (incoming_tx, outgoing_rx) = forwarder.into_channels().await;
|
||||
let (forwarder, proxy_incoming_tx, proxy_outgoing_rx) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
|
||||
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let (ssh_connection, io_task) = match Self::establish_connection(
|
||||
let (ssh_connection, ssh_process) = match Self::establish_connection(
|
||||
identifier,
|
||||
true,
|
||||
connection_options,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
delegate.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
@@ -731,9 +737,16 @@ impl SshRemoteClient {
|
||||
}
|
||||
};
|
||||
|
||||
let multiplex_task = Self::monitor(this.clone(), io_task, &cx);
|
||||
let multiplex_task = Self::multiplex(
|
||||
this.clone(),
|
||||
ssh_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
|
||||
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
|
||||
failed!(error, attempts, ssh_connection, delegate, forwarder);
|
||||
};
|
||||
|
||||
@@ -784,7 +797,7 @@ impl SshRemoteClient {
|
||||
cx.emit(SshRemoteEvent::Disconnected);
|
||||
Ok(())
|
||||
} else {
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
|
||||
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@@ -897,108 +910,101 @@ impl SshRemoteClient {
|
||||
}
|
||||
|
||||
fn multiplex(
|
||||
this: WeakModel<Self>,
|
||||
mut ssh_proxy_process: Child,
|
||||
incoming_tx: UnboundedSender<Envelope>,
|
||||
mut outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Option<i32>>> {
|
||||
) -> Task<Result<()>> {
|
||||
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
|
||||
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
|
||||
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut stdin_buffer = Vec::new();
|
||||
let mut stdout_buffer = Vec::new();
|
||||
let mut stderr_buffer = Vec::new();
|
||||
let mut stderr_offset = 0;
|
||||
let mut stdin_buffer = Vec::new();
|
||||
let mut stdout_buffer = Vec::new();
|
||||
let mut stderr_buffer = Vec::new();
|
||||
let mut stderr_offset = 0;
|
||||
|
||||
loop {
|
||||
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||
stderr_buffer.resize(stderr_offset + 1024, 0);
|
||||
let stdin_task = cx.background_executor().spawn(async move {
|
||||
while let Some(outgoing) = outgoing_rx.next().await {
|
||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
select_biased! {
|
||||
outgoing = outgoing_rx.next().fuse() => {
|
||||
let Some(outgoing) = outgoing else {
|
||||
return anyhow::Ok(None);
|
||||
};
|
||||
let stdout_task = cx.background_executor().spawn({
|
||||
let mut connection_activity_tx = connection_activity_tx.clone();
|
||||
async move {
|
||||
loop {
|
||||
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
|
||||
let len = child_stdout.read(&mut stdout_buffer).await?;
|
||||
|
||||
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
|
||||
if len == 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
result = child_stdout.read(&mut stdout_buffer).fuse() => {
|
||||
match result {
|
||||
Ok(0) => {
|
||||
child_stdin.close().await?;
|
||||
outgoing_rx.close();
|
||||
let status = ssh_proxy_process.status().await?;
|
||||
// If we don't have a code, we assume process
|
||||
// has been killed and treat it as non-zero exit
|
||||
// code
|
||||
return Ok(status.code().or_else(|| Some(1)));
|
||||
}
|
||||
Ok(len) => {
|
||||
if len < stdout_buffer.len() {
|
||||
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
|
||||
}
|
||||
|
||||
let message_len = message_len_from_buffer(&stdout_buffer);
|
||||
match read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len).await {
|
||||
Ok(envelope) => {
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
incoming_tx.unbounded_send(envelope).ok();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("error decoding message {error:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
Err(anyhow!("error reading stdout: {error:?}"))?;
|
||||
}
|
||||
}
|
||||
if len < MESSAGE_LEN_SIZE {
|
||||
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
|
||||
}
|
||||
|
||||
result = child_stderr.read(&mut stderr_buffer[stderr_offset..]).fuse() => {
|
||||
match result {
|
||||
Ok(len) => {
|
||||
stderr_offset += len;
|
||||
let mut start_ix = 0;
|
||||
while let Some(ix) = stderr_buffer[start_ix..stderr_offset].iter().position(|b| b == &b'\n') {
|
||||
let line_ix = start_ix + ix;
|
||||
let content = &stderr_buffer[start_ix..line_ix];
|
||||
start_ix = line_ix + 1;
|
||||
if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
|
||||
record.log(log::logger())
|
||||
} else {
|
||||
eprintln!("(remote) {}", String::from_utf8_lossy(content));
|
||||
}
|
||||
}
|
||||
stderr_buffer.drain(0..start_ix);
|
||||
stderr_offset -= start_ix;
|
||||
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
}
|
||||
Err(error) => {
|
||||
Err(anyhow!("error reading stderr: {error:?}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let message_len = message_len_from_buffer(&stdout_buffer);
|
||||
let envelope =
|
||||
read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len)
|
||||
.await?;
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
incoming_tx.unbounded_send(envelope).ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let stderr_task: Task<anyhow::Result<()>> = cx.background_executor().spawn(async move {
|
||||
loop {
|
||||
stderr_buffer.resize(stderr_offset + 1024, 0);
|
||||
|
||||
let len = child_stderr
|
||||
.read(&mut stderr_buffer[stderr_offset..])
|
||||
.await?;
|
||||
|
||||
stderr_offset += len;
|
||||
let mut start_ix = 0;
|
||||
while let Some(ix) = stderr_buffer[start_ix..stderr_offset]
|
||||
.iter()
|
||||
.position(|b| b == &b'\n')
|
||||
{
|
||||
let line_ix = start_ix + ix;
|
||||
let content = &stderr_buffer[start_ix..line_ix];
|
||||
start_ix = line_ix + 1;
|
||||
if let Ok(record) = serde_json::from_slice::<LogRecord>(content) {
|
||||
record.log(log::logger())
|
||||
} else {
|
||||
eprintln!("(remote) {}", String::from_utf8_lossy(content));
|
||||
}
|
||||
}
|
||||
stderr_buffer.drain(0..start_ix);
|
||||
stderr_offset -= start_ix;
|
||||
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
}
|
||||
});
|
||||
|
||||
fn monitor(
|
||||
this: WeakModel<Self>,
|
||||
io_task: Task<Result<Option<i32>>>,
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let result = io_task.await;
|
||||
let result = futures::select! {
|
||||
result = stdin_task.fuse() => {
|
||||
result.context("stdin")
|
||||
}
|
||||
result = stdout_task.fuse() => {
|
||||
result.context("stdout")
|
||||
}
|
||||
result = stderr_task.fuse() => {
|
||||
result.context("stderr")
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(Some(exit_code)) => {
|
||||
Ok(_) => {
|
||||
let exit_code = ssh_proxy_process.status().await?.code().unwrap_or(1);
|
||||
|
||||
if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) {
|
||||
match error {
|
||||
ProxyLaunchError::ServerNotRunning => {
|
||||
@@ -1016,7 +1022,6 @@ impl SshRemoteClient {
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -1024,6 +1029,7 @@ impl SshRemoteClient {
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -1052,40 +1058,21 @@ impl SshRemoteClient {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn establish_connection(
|
||||
unique_identifier: String,
|
||||
reconnect: bool,
|
||||
connection_options: SshConnectionOptions,
|
||||
proxy_incoming_tx: UnboundedSender<Envelope>,
|
||||
proxy_outgoing_rx: UnboundedReceiver<Envelope>,
|
||||
connection_activity_tx: Sender<()>,
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<(Box<dyn SshRemoteProcess>, Task<Result<Option<i32>>>)> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(fake) = fake::SshRemoteConnection::new(&connection_options) {
|
||||
let io_task = fake::SshRemoteConnection::multiplex(
|
||||
fake.connection_options(),
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
return Ok((fake, io_task));
|
||||
}
|
||||
|
||||
) -> Result<(SshRemoteConnection, Child)> {
|
||||
let ssh_connection =
|
||||
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
|
||||
|
||||
let platform = ssh_connection.query_platform().await?;
|
||||
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
|
||||
if !reconnect {
|
||||
ssh_connection
|
||||
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
|
||||
.await?;
|
||||
}
|
||||
ssh_connection
|
||||
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
|
||||
.await?;
|
||||
|
||||
let socket = ssh_connection.socket.clone();
|
||||
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
|
||||
@@ -1110,15 +1097,7 @@ impl SshRemoteClient {
|
||||
.spawn()
|
||||
.context("failed to spawn remote server")?;
|
||||
|
||||
let io_task = Self::multiplex(
|
||||
ssh_proxy_process,
|
||||
proxy_incoming_tx,
|
||||
proxy_outgoing_rx,
|
||||
connection_activity_tx,
|
||||
&cx,
|
||||
);
|
||||
|
||||
Ok((Box::new(ssh_connection), io_task))
|
||||
Ok((ssh_connection, ssh_proxy_process))
|
||||
}
|
||||
|
||||
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
|
||||
@@ -1130,7 +1109,7 @@ impl SshRemoteClient {
|
||||
.lock()
|
||||
.as_ref()
|
||||
.and_then(|state| state.ssh_connection())
|
||||
.map(|ssh_connection| ssh_connection.ssh_args())
|
||||
.map(|ssh_connection| ssh_connection.socket.ssh_args())
|
||||
}
|
||||
|
||||
pub fn proto_client(&self) -> AnyProtoClient {
|
||||
@@ -1145,6 +1124,7 @@ impl SshRemoteClient {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
self.state
|
||||
.lock()
|
||||
@@ -1153,69 +1133,37 @@ impl SshRemoteClient {
|
||||
.unwrap_or(ConnectionState::Disconnected)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn connection_state(&self) -> ConnectionState {
|
||||
ConnectionState::Connected
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self) -> bool {
|
||||
self.connection_state() == ConnectionState::Disconnected
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn simulate_disconnect(&self, cx: &mut AppContext) -> Task<()> {
|
||||
use gpui::BorrowAppContext;
|
||||
|
||||
let port = self.connection_options().port.unwrap();
|
||||
|
||||
let disconnect =
|
||||
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.take(port).into_channels());
|
||||
cx.spawn(|mut cx| async move {
|
||||
let (input_rx, output_tx) = disconnect.await;
|
||||
let (forwarder, _, _) = ChannelForwarder::new(input_rx, output_tx, &mut cx);
|
||||
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.replace(port, forwarder))
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake_server(
|
||||
server_cx: &mut gpui::TestAppContext,
|
||||
) -> (ChannelForwarder, Arc<ChannelClient>) {
|
||||
server_cx.update(|cx| {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
|
||||
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
|
||||
|
||||
// We use the forwarder on the server side (in production we only use one on the client side)
|
||||
// the idea is that we can simulate a disconnect/reconnect by just messing with the forwarder.
|
||||
let (forwarder, _, _) =
|
||||
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx.to_async());
|
||||
|
||||
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server");
|
||||
(forwarder, client)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_client(
|
||||
forwarder: ChannelForwarder,
|
||||
pub fn fake(
|
||||
client_cx: &mut gpui::TestAppContext,
|
||||
) -> Model<Self> {
|
||||
use gpui::BorrowAppContext;
|
||||
client_cx
|
||||
.update(|cx| {
|
||||
let port = cx.update_default_global(|c: &mut fake::GlobalConnections, _cx| {
|
||||
c.push(forwarder)
|
||||
});
|
||||
server_cx: &mut gpui::TestAppContext,
|
||||
) -> (Model<Self>, Arc<ChannelClient>) {
|
||||
use gpui::Context;
|
||||
|
||||
Self::new(
|
||||
"fake".to_string(),
|
||||
SshConnectionOptions {
|
||||
host: "<fake>".to_string(),
|
||||
port: Some(port),
|
||||
..Default::default()
|
||||
},
|
||||
Arc::new(fake::Delegate),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
|
||||
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
|
||||
|
||||
(
|
||||
client_cx.update(|cx| {
|
||||
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
|
||||
cx.new_model(|_| Self {
|
||||
client,
|
||||
unique_identifier: "fake".to_string(),
|
||||
connection_options: SshConnectionOptions::default(),
|
||||
state: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}),
|
||||
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1225,13 +1173,6 @@ impl From<SshRemoteClient> for AnyProtoClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait SshRemoteProcess: Send + Sync {
|
||||
async fn kill(&mut self) -> Result<()>;
|
||||
fn ssh_args(&self) -> Vec<String>;
|
||||
fn connection_options(&self) -> SshConnectionOptions;
|
||||
}
|
||||
|
||||
struct SshRemoteConnection {
|
||||
socket: SshSocket,
|
||||
master_process: process::Child,
|
||||
@@ -1246,25 +1187,6 @@ impl Drop for SshRemoteConnection {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SshRemoteProcess for SshRemoteConnection {
|
||||
async fn kill(&mut self) -> Result<()> {
|
||||
self.master_process.kill()?;
|
||||
|
||||
self.master_process.status().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
self.socket.ssh_args()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.socket.connection_options.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl SshRemoteConnection {
|
||||
#[cfg(not(unix))]
|
||||
async fn new(
|
||||
@@ -1281,6 +1203,7 @@ impl SshRemoteConnection {
|
||||
delegate: Arc<dyn SshClientDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
use futures::AsyncWriteExt as _;
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
|
||||
use util::ResultExt as _;
|
||||
@@ -1547,10 +1470,8 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
|
||||
pub struct ChannelClient {
|
||||
next_message_id: AtomicU32,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
buffer: Mutex<VecDeque<Envelope>>,
|
||||
response_channels: ResponseChannels,
|
||||
message_handlers: Mutex<ProtoMessageHandlerSet>,
|
||||
max_received: AtomicU32,
|
||||
response_channels: ResponseChannels, // Lock
|
||||
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
@@ -1558,18 +1479,15 @@ impl ChannelClient {
|
||||
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
outgoing_tx: mpsc::UnboundedSender<Envelope>,
|
||||
cx: &AppContext,
|
||||
name: &'static str,
|
||||
) -> Arc<Self> {
|
||||
let this = Arc::new(Self {
|
||||
outgoing_tx,
|
||||
next_message_id: AtomicU32::new(0),
|
||||
max_received: AtomicU32::new(0),
|
||||
response_channels: ResponseChannels::default(),
|
||||
message_handlers: Default::default(),
|
||||
buffer: Mutex::new(VecDeque::new()),
|
||||
});
|
||||
|
||||
Self::start_handling_messages(this.clone(), incoming_rx, cx, name);
|
||||
Self::start_handling_messages(this.clone(), incoming_rx, cx);
|
||||
|
||||
this
|
||||
}
|
||||
@@ -1578,7 +1496,6 @@ impl ChannelClient {
|
||||
this: Arc<Self>,
|
||||
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
cx: &AppContext,
|
||||
name: &'static str,
|
||||
) {
|
||||
cx.spawn(|cx| {
|
||||
let this = Arc::downgrade(&this);
|
||||
@@ -1588,28 +1505,6 @@ impl ChannelClient {
|
||||
let Some(this) = this.upgrade() else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
if let Some(ack_id) = incoming.ack_id {
|
||||
let mut buffer = this.buffer.lock();
|
||||
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
|
||||
buffer.pop_front();
|
||||
}
|
||||
}
|
||||
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload {
|
||||
{
|
||||
let buffer = this.buffer.lock();
|
||||
dbg!(&buffer);
|
||||
for envelope in buffer.iter() {
|
||||
dbg!(("server sending: ", &envelope));
|
||||
this.outgoing_tx.unbounded_send(envelope.clone()).ok();
|
||||
}
|
||||
}
|
||||
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
|
||||
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
|
||||
this.outgoing_tx.unbounded_send(envelope)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.max_received.store(incoming.id, SeqCst);
|
||||
|
||||
if let Some(request_id) = incoming.responding_to {
|
||||
let request_id = MessageId(request_id);
|
||||
@@ -1631,20 +1526,19 @@ impl ChannelClient {
|
||||
this.clone().into(),
|
||||
cx.clone(),
|
||||
) {
|
||||
log::debug!("{name}:ssh message received. name:{type_name}");
|
||||
match future.await {
|
||||
Ok(_) => {
|
||||
log::debug!("{name}:ssh message handled. name:{type_name}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"{name}:error handling message. type:{type_name}, error:{error}",
|
||||
);
|
||||
}
|
||||
log::debug!("ssh message received. name:{type_name}");
|
||||
match future.await {
|
||||
Ok(_) => {
|
||||
log::debug!("ssh message handled. name:{type_name}");
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. type:{type_name}, error:{error}",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("{name}:unhandled ssh message name:{type_name}");
|
||||
log::error!("unhandled ssh message name:{type_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1687,26 +1581,6 @@ impl ChannelClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resync(&self, timeout: Duration) -> Result<()> {
|
||||
smol::future::or(
|
||||
async {
|
||||
dbg!(&self.buffer);
|
||||
self.request(proto::FlushBufferedMessages {}).await?; // ??????
|
||||
dbg!(&self.buffer);
|
||||
for envelope in self.buffer.lock().iter() {
|
||||
dbg!("resening", envelope);
|
||||
self.outgoing_tx.unbounded_send(envelope.clone()).ok();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
async {
|
||||
smol::Timer::after(timeout).await;
|
||||
Err(anyhow!("Timeout detected"))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ping(&self, timeout: Duration) -> Result<()> {
|
||||
smol::future::or(
|
||||
async {
|
||||
@@ -1736,8 +1610,7 @@ impl ChannelClient {
|
||||
let mut response_channels_lock = self.response_channels.lock();
|
||||
response_channels_lock.insert(MessageId(envelope.id), tx);
|
||||
drop(response_channels_lock);
|
||||
|
||||
let result = self.send_buffered(envelope);
|
||||
let result = self.outgoing_tx.unbounded_send(envelope);
|
||||
async move {
|
||||
if let Err(error) = &result {
|
||||
log::error!("failed to send message: {}", error);
|
||||
@@ -1754,13 +1627,6 @@ impl ChannelClient {
|
||||
|
||||
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
|
||||
self.send_buffered(envelope)
|
||||
}
|
||||
|
||||
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
|
||||
dbg!("sending...", &envelope);
|
||||
envelope.ack_id = Some(self.max_received.load(SeqCst));
|
||||
self.buffer.lock().push_back(envelope.clone());
|
||||
self.outgoing_tx.unbounded_send(envelope)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1791,165 +1657,3 @@ impl ProtoClient for ChannelClient {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use futures::{
|
||||
channel::{
|
||||
mpsc::{self, Sender},
|
||||
oneshot,
|
||||
},
|
||||
select_biased, FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{AsyncAppContext, BorrowAppContext, Global, SemanticVersion, Task};
|
||||
use rpc::proto::Envelope;
|
||||
|
||||
use super::{
|
||||
ChannelForwarder, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteProcess,
|
||||
};
|
||||
|
||||
pub(super) struct SshRemoteConnection {
|
||||
connection_options: SshConnectionOptions,
|
||||
}
|
||||
|
||||
impl SshRemoteConnection {
|
||||
pub(super) fn new(
|
||||
connection_options: &SshConnectionOptions,
|
||||
) -> Option<Box<dyn SshRemoteProcess>> {
|
||||
if connection_options.host == "<fake>" {
|
||||
return Some(Box::new(Self {
|
||||
connection_options: connection_options.clone(),
|
||||
}));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
pub(super) async fn multiplex(
|
||||
connection_options: SshConnectionOptions,
|
||||
mut client_tx: mpsc::UnboundedSender<Envelope>,
|
||||
mut client_rx: mpsc::UnboundedReceiver<Envelope>,
|
||||
mut connection_activity_tx: Sender<()>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Task<Result<Option<i32>>> {
|
||||
let (server_tx, server_rx) = cx
|
||||
.update(|cx| {
|
||||
cx.update_global(|conns: &mut GlobalConnections, _| {
|
||||
conns.take(connection_options.port.unwrap())
|
||||
})
|
||||
})
|
||||
.unwrap()
|
||||
.into_channels()
|
||||
.await;
|
||||
|
||||
let (forwarder, mut proxy_tx, mut proxy_rx) =
|
||||
ChannelForwarder::new(server_tx, server_rx, cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|conns: &mut GlobalConnections, _| {
|
||||
conns.replace(connection_options.port.unwrap(), forwarder)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
loop {
|
||||
select_biased! {
|
||||
server_to_client = proxy_rx.next().fuse() => {
|
||||
let Some(server_to_client) = server_to_client else {
|
||||
return Ok(Some(1))
|
||||
};
|
||||
connection_activity_tx.try_send(()).ok();
|
||||
client_tx.send(server_to_client).await.ok();
|
||||
}
|
||||
client_to_server = client_rx.next().fuse() => {
|
||||
let Some(client_to_server) = client_to_server else {
|
||||
return Ok(None)
|
||||
};
|
||||
proxy_tx.send(client_to_server).await.ok();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SshRemoteProcess for SshRemoteConnection {
|
||||
async fn kill(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ssh_args(&self) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> SshConnectionOptions {
|
||||
self.connection_options.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct GlobalConnections(Vec<Option<ChannelForwarder>>);
|
||||
impl Global for GlobalConnections {}
|
||||
|
||||
impl GlobalConnections {
|
||||
pub(super) fn push(&mut self, forwarder: ChannelForwarder) -> u16 {
|
||||
self.0.push(Some(forwarder));
|
||||
self.0.len() as u16 - 1
|
||||
}
|
||||
|
||||
pub(super) fn take(&mut self, port: u16) -> ChannelForwarder {
|
||||
self.0
|
||||
.get_mut(port as usize)
|
||||
.expect("no fake server for port")
|
||||
.take()
|
||||
.expect("fake server is already borrowed")
|
||||
}
|
||||
pub(super) fn replace(&mut self, port: u16, forwarder: ChannelForwarder) {
|
||||
let ret = self
|
||||
.0
|
||||
.get_mut(port as usize)
|
||||
.expect("no fake server for port")
|
||||
.replace(forwarder);
|
||||
if ret.is_some() {
|
||||
panic!("fake server is already replaced");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Delegate;
|
||||
|
||||
impl SshClientDelegate for Delegate {
|
||||
fn ask_password(
|
||||
&self,
|
||||
_: String,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<String>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn remote_server_binary_path(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> Result<PathBuf> {
|
||||
unreachable!()
|
||||
}
|
||||
fn get_server_binary(
|
||||
&self,
|
||||
_: SshPlatform,
|
||||
_: &mut AsyncAppContext,
|
||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
||||
unreachable!()
|
||||
}
|
||||
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,47 +641,6 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 20)]
|
||||
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
let (project, _headless, fs) = init_test(cx, server_cx).await;
|
||||
|
||||
let (worktree, _) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_worktree("/code/project1", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
|
||||
let ix = buffer.text().find('1').unwrap();
|
||||
buffer.edit([(ix..ix + 1, "100")], None, cx);
|
||||
});
|
||||
|
||||
let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
|
||||
client
|
||||
.update(cx, |client, cx| client.simulate_disconnect(cx))
|
||||
.detach();
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
|
||||
"fn one() -> usize { 100 }"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
@@ -692,9 +651,9 @@ async fn init_test(
|
||||
cx: &mut TestAppContext,
|
||||
server_cx: &mut TestAppContext,
|
||||
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
|
||||
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
|
||||
init_logger();
|
||||
|
||||
let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(server_cx);
|
||||
let fs = FakeFs::new(server_cx.executor());
|
||||
fs.insert_tree(
|
||||
"/code",
|
||||
@@ -735,9 +694,8 @@ async fn init_test(
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let project = build_project(ssh_remote_client, cx);
|
||||
|
||||
let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
|
||||
let project = build_project(ssh, cx);
|
||||
project
|
||||
.update(cx, {
|
||||
let headless = headless.clone();
|
||||
|
||||
@@ -12,6 +12,7 @@ use language::LanguageRegistry;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use paths::logs_dir;
|
||||
use project::project_settings::ProjectSettings;
|
||||
|
||||
use remote::proxy::ProxyLaunchError;
|
||||
use remote::ssh_session::ChannelClient;
|
||||
use remote::{
|
||||
@@ -213,19 +214,27 @@ fn start_server(
|
||||
|
||||
let mut input_buffer = Vec::new();
|
||||
let mut output_buffer = Vec::new();
|
||||
|
||||
let (mut stdin_msg_tx, mut stdin_msg_rx) = mpsc::unbounded::<Envelope>();
|
||||
cx.background_executor().spawn(async move {
|
||||
while let Ok(msg) = read_message(&mut stdin_stream, &mut input_buffer).await {
|
||||
if let Err(_) = stdin_msg_tx.send(msg).await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
loop {
|
||||
|
||||
select_biased! {
|
||||
_ = app_quit_rx.next().fuse() => {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
stdin_message = read_message(&mut stdin_stream, &mut input_buffer).fuse() => {
|
||||
let message = match stdin_message {
|
||||
Ok(message) => message,
|
||||
Err(error) => {
|
||||
log::warn!("error reading message on stdin: {}. exiting.", error);
|
||||
break;
|
||||
}
|
||||
stdin_message = stdin_msg_rx.next().fuse() => {
|
||||
let Some(message) = stdin_message else {
|
||||
log::warn!("error reading message on stdin. exiting.");
|
||||
break;
|
||||
};
|
||||
if let Err(error) = incoming_tx.unbounded_send(message) {
|
||||
log::error!("failed to send message to application: {:?}. exiting.", error);
|
||||
@@ -270,7 +279,7 @@ fn start_server(
|
||||
})
|
||||
.detach();
|
||||
|
||||
ChannelClient::new(incoming_rx, outgoing_tx, cx, "server")
|
||||
ChannelClient::new(incoming_rx, outgoing_tx, cx)
|
||||
}
|
||||
|
||||
fn init_paths() -> anyhow::Result<()> {
|
||||
|
||||
@@ -17,8 +17,7 @@ use editor::{
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt as _, 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;
|
||||
@@ -149,23 +148,21 @@ impl EditorBlock {
|
||||
.w(text_line_height)
|
||||
.h(text_line_height)
|
||||
.child(
|
||||
IconButton::new(
|
||||
("close_output_area", EntityId::from(cx.block_id)),
|
||||
IconName::Close,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.shape(IconButtonShape::Square)
|
||||
.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)
|
||||
}
|
||||
}),
|
||||
IconButton::new("close_output_area", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.shape(IconButtonShape::Square)
|
||||
.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)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
div()
|
||||
.id(cx.block_id)
|
||||
.flex()
|
||||
.items_start()
|
||||
.min_h(text_line_height)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_elixir"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "elixir"
|
||||
name = "Elixir"
|
||||
description = "Elixir support."
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
schema_version = 1
|
||||
authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
Reference in New Issue
Block a user