Compare commits

..

7 Commits

Author SHA1 Message Date
Nathan Sobo
139c5c59b7 WIP 2024-10-21 17:52:45 -06:00
Nathan Sobo
781fff220c Fix merging of an update of a symbol with an insert_before operation before the same symbol (#19450)
When we insert before some text and then update that same text, we need
to preserve and concatenate the new text associated with both
operations.

Release Notes:

- N/A
2024-10-19 08:36:21 -06:00
Max Brunsfeld
d209eab058 Combine excerpt footer and header into a single block (#19441)
This simplifies rendering of excerpt headers and footers, and removes
the need to store a `BlockDisposition` on these boundary blocks. It's a
step toward implementing "replace blocks", which we want to use in the
assistant panel.

We've also cleaned up the way heights are specified for headers and
footers and fixed some visual asymmetries between the "expand upward"
and "expand downward" buttons.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
2024-10-18 17:58:07 -07:00
Vitaly Slobodin
3e0c5c10b7 lsp: Handle unregistration "textDocument/rename" from a server (#19427)
Hi. While working on https://github.com/zed-industries/zed/pull/19230 I
noticed that some servers send a request to unregistered the
`textDocument/rename` capability. I thought it would be good to handle
that message in Zed:

```plaintext
[2024-10-18T21:25:07+02:00 WARN  project::lsp_store] unhandled capability unregistration: Unregistration { id: "biome_rename", method: "textDocument/rename" }
```

So this pull request implements that. Thanks.

Release Notes:

- N/A
2024-10-19 00:52:17 +02:00
Mikayla Maki
8a912726d7 Fix flakey SSH connection (#19439)
Fixes a bug due to the `select!` macro tossing futures that had
partially read messages, causing us to desync our message reading with
the input stream.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: conrad <conrad@zed.dev>
2024-10-18 15:41:43 -07:00
Marshall Bowers
30e081b3f7 elixir: Bump to v0.1.1 (#19437)
This PR bumps the Elixir extension to v0.1.1.

Changes:

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

Release Notes:

- N/A
2024-10-18 18:31:08 -04:00
Conrad Irwin
a5492b3ea6 Revert "SSH reconnect reliability (#19398)" (#19440)
This reverts commit 98ecb43b2d.

Tests fail on main?!

Closes #ISSUE

Release Notes:

- N/A
2024-10-18 15:08:56 -07:00
23 changed files with 773 additions and 1088 deletions

3
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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]

View File

@@ -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))
}

View File

@@ -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;

View File

@@ -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))

View File

@@ -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,
}
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -952,7 +952,7 @@ mod tests {
px(14.0),
None,
true,
2,
0,
2,
0,
FoldPlaceholder::test(),

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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 {}

View File

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

View File

@@ -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!(

View File

@@ -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

View File

@@ -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>(

View File

@@ -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!()
}
}
}

View File

@@ -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();

View File

@@ -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<()> {

View File

@@ -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)

View File

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

View File

@@ -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"