Compare commits

..

12 Commits

Author SHA1 Message Date
Conrad Irwin
1ba60d1a8d Undo refactoring that broke things 2024-10-18 16:34:01 -06:00
Conrad Irwin
06f18bc365 Revert "ssh remoting: Undo the spawning of message handlers (#19409)"
This reverts commit ea460014ab.
2024-10-18 16:11:15 -06:00
Conrad Irwin
f5833010aa Merge branch 'main' into ssh-reconnect-reliability 2024-10-18 16:10:25 -06:00
Conrad Irwin
f86f06c81a Revert "Revert "SSH reconnect reliability (#19398)""
This reverts commit 9b0ee7d30b.
2024-10-18 16:05:04 -06:00
Conrad Irwin
9b0ee7d30b Revert "SSH reconnect reliability (#19398)"
This reverts commit 98ecb43b2d.
2024-10-18 15:57:01 -06:00
Conrad Irwin
47a764553d Revert "remote: Fix formatting (#19438)"
This reverts commit 47380001cc.
2024-10-18 15:57:00 -06:00
Conrad Irwin
d7ff85e2a1 Clippity Cloppity 2024-10-17 16:56:11 -06:00
Conrad Irwin
b2b6b1e8a1 Don't rebuild/re-upload binary on reconnect. 2024-10-17 16:19:06 -06:00
Conrad Irwin
023186b7a0 Fix message buffering during reconnect
Co-Authored-By: Nathan <nathan@zed.dev>
2024-10-17 16:07:57 -06:00
Conrad Irwin
e5ec08e8f8 Failing test for edits in parallel with disconnects 2024-10-17 14:29:09 -06:00
Conrad Irwin
2f6c7a2939 Implement the fake connection stuff 2024-10-17 14:10:03 -06:00
Conrad Irwin
2f26a74c83 Refactor Ssh connections to make faking more possible 2024-10-17 12:56:21 -06:00
24 changed files with 1095 additions and 783 deletions

134
Cargo.lock generated
View File

@@ -2510,35 +2510,6 @@ dependencies = [
"objc",
]
[[package]]
name = "code-product"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9feea482d196b435bb8857c2ec1274926993bc3342953515620eb9641337ee01"
dependencies = [
"code-product-lib",
"code-product-macro",
]
[[package]]
name = "code-product-lib"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5938681371198e7a690aa008843d39bd3b3b65f17b0101518c388f453c0aaf4e"
dependencies = [
"proc-macro2",
]
[[package]]
name = "code-product-macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0371403e32bb895fc60f96b3b76a46a17b99a9ff85b3dbff0e1c5ff0f1ec8e13"
dependencies = [
"code-product-lib",
"proc-macro2",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
@@ -2816,12 +2787,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "constptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f6b816ec3d57d4febea032f3ce2bf9dca119f999a865f9f24d7020d9eb5167e"
[[package]]
name = "context_servers"
version = "0.1.0"
@@ -3036,17 +3001,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cowstr"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613de46416b29f30c0c581b5f4cd7dfc01991f59c416b30aa85a0a82af7c791e"
dependencies = [
"code-product",
"constptr",
"mutants",
]
[[package]]
name = "cpal"
version = "0.15.3"
@@ -5805,7 +5759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
dependencies = [
"byteorder-lite",
"quick-error 2.0.1",
"quick-error",
]
[[package]]
@@ -6804,17 +6758,13 @@ dependencies = [
"anyhow",
"async-recursion 1.1.1",
"collections",
"cowstr",
"editor",
"gpui",
"language",
"linkify",
"log",
"pretty_assertions",
"proptest",
"pulldown-cmark 0.12.1",
"pulldown-cmark-to-cmark",
"rand 0.8.5",
"settings",
"theme",
"ui",
@@ -7112,12 +7062,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "mutants"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126"
[[package]]
name = "naga"
version = "22.1.0"
@@ -8603,26 +8547,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "proptest"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d"
dependencies = [
"bit-set 0.5.3",
"bit-vec 0.6.3",
"bitflags 2.6.0",
"lazy_static",
"num-traits",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_xorshift",
"regex-syntax 0.8.4",
"rusty-fork",
"tempfile",
"unarray",
]
[[package]]
name = "prost"
version = "0.9.0"
@@ -8751,15 +8675,6 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
[[package]]
name = "pulldown-cmark-to-cmark"
version = "18.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e02b63adcb49f2eb675b1694b413b3e9fedbf549dfe2cc98727ad97a0c30650"
dependencies = [
"pulldown-cmark 0.12.1",
]
[[package]]
name = "qoi"
version = "0.4.1"
@@ -8769,12 +8684,6 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.1"
@@ -8950,15 +8859,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rangemap"
version = "1.5.1"
@@ -9009,7 +8909,7 @@ dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error 2.0.1",
"quick-error",
"rav1e",
"rgb",
]
@@ -9219,6 +9119,7 @@ name = "remote"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"fs",
"futures 0.3.30",
@@ -9902,18 +9803,6 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error 1.2.3",
"tempfile",
"wait-timeout",
]
[[package]]
name = "rustybuzz"
version = "0.14.1"
@@ -12722,12 +12611,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "unarray"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unicase"
version = "2.7.0"
@@ -13097,15 +12980,6 @@ dependencies = [
"quote",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "waker-fn"
version = "1.2.0"
@@ -14869,7 +14743,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0",
]

View File

@@ -343,7 +343,6 @@ cocoa = "0.26"
convert_case = "0.6.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
cowstr = "1.3.0"
ctor = "0.2.6"
dashmap = "6.0"
derive_more = "0.99.17"
@@ -383,12 +382,10 @@ pathdiff = "0.2"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
profiling = "1"
proptest = "1.5.0"
prost = "0.9"
prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
pulldown-cmark-to-cmark = "18.0.0"
rand = "0.8.5"
regex = "1.5"
repair_json = "0.1.0"

View File

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

View File

@@ -962,6 +962,7 @@ 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>,
@@ -997,7 +998,7 @@ fn editor_blocks(
.ok()?
}
Block::ExcerptBoundary {
Block::ExcerptHeader {
starts_new_buffer, ..
} => {
if *starts_new_buffer {
@@ -1006,6 +1007,7 @@ 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::{Chunk, Patch, Point};
use multi_buffer::{Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, ToPoint as _};
use language::{BufferSnapshot, Chunk, Patch, Point};
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
use parking_lot::Mutex;
use std::{
cell::RefCell,
@@ -128,17 +128,26 @@ pub struct BlockContext<'a, 'b> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlockId {
Custom(CustomBlockId),
ExcerptBoundary(Option<ExcerptId>),
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(),
}
}
}
impl From<BlockId> for ElementId {
fn from(value: BlockId) -> Self {
match value {
BlockId::Custom(CustomBlockId(id)) => ("Block", id).into(),
BlockId::ExcerptBoundary(next_excerpt) => match next_excerpt {
Some(id) => ("ExcerptBoundary", EntityId::from(id)).into(),
None => "LastExcerptBoundary".into(),
},
BlockId::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
BlockId::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
}
}
}
@@ -147,7 +156,8 @@ 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::ExcerptBoundary(id) => write!(f, "ExcerptHeader({id:?})"),
Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
}
}
}
@@ -167,7 +177,8 @@ struct Transform {
pub(crate) enum BlockType {
Custom(CustomBlockId),
ExcerptBoundary,
Header,
Footer,
}
pub(crate) trait BlockLike {
@@ -180,20 +191,27 @@ pub(crate) trait BlockLike {
#[derive(Clone)]
pub enum Block {
Custom(Arc<CustomBlock>),
ExcerptBoundary {
prev_excerpt: Option<ExcerptInfo>,
next_excerpt: Option<ExcerptInfo>,
ExcerptHeader {
id: ExcerptId,
buffer: BufferSnapshot,
range: ExcerptRange<text::Anchor>,
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::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
Block::ExcerptHeader { .. } => BlockType::Header,
Block::ExcerptFooter { .. } => BlockType::Footer,
}
}
@@ -204,7 +222,8 @@ impl BlockLike for Block {
fn priority(&self) -> usize {
match self {
Block::Custom(block) => block.priority,
Block::ExcerptBoundary { .. } => usize::MAX,
Block::ExcerptHeader { .. } => usize::MAX,
Block::ExcerptFooter { .. } => 0,
}
}
}
@@ -213,36 +232,32 @@ impl Block {
pub fn id(&self) -> BlockId {
match self {
Block::Custom(block) => BlockId::Custom(block.id),
Block::ExcerptBoundary { next_excerpt, .. } => {
BlockId::ExcerptBoundary(next_excerpt.as_ref().map(|info| info.id))
}
Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id),
Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id),
}
}
fn disposition(&self) -> BlockDisposition {
match self {
Block::Custom(block) => block.disposition,
Block::ExcerptBoundary { next_excerpt, .. } => {
if next_excerpt.is_some() {
BlockDisposition::Above
} else {
BlockDisposition::Below
}
}
Block::ExcerptHeader { .. } => BlockDisposition::Above,
Block::ExcerptFooter { disposition, .. } => *disposition,
}
}
pub fn height(&self) -> u32 {
match self {
Block::Custom(block) => block.height,
Block::ExcerptBoundary { height, .. } => *height,
Block::ExcerptHeader { height, .. } => *height,
Block::ExcerptFooter { height, .. } => *height,
}
}
pub fn style(&self) -> BlockStyle {
match self {
Block::Custom(block) => block.style,
Block::ExcerptBoundary { .. } => BlockStyle::Sticky,
Block::ExcerptHeader { .. } => BlockStyle::Sticky,
Block::ExcerptFooter { .. } => BlockStyle::Sticky,
}
}
}
@@ -251,17 +266,24 @@ 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::ExcerptBoundary {
Self::ExcerptHeader {
buffer,
starts_new_buffer,
next_excerpt,
prev_excerpt,
id,
..
} => f
.debug_struct("ExcerptBoundary")
.field("prev_excerpt", &prev_excerpt)
.field("next_excerpt", &next_excerpt)
.debug_struct("ExcerptHeader")
.field("id", &id)
.field("path", &buffer.file().map(|f| f.path()))
.field("starts_new_buffer", &starts_new_buffer)
.finish(),
Block::ExcerptFooter {
id, disposition, ..
} => f
.debug_struct("ExcerptFooter")
.field("id", &id)
.field("disposition", &disposition)
.finish(),
}
}
}
@@ -573,62 +595,66 @@ impl BlockMap {
{
buffer
.excerpt_boundaries_in_range(range)
.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,
.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,
},
)
.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)]) {
@@ -639,9 +665,12 @@ impl BlockMap {
.disposition()
.cmp(&block_b.disposition())
.then_with(|| match ((block_a.block_type()), (block_b.block_type())) {
(BlockType::ExcerptBoundary, BlockType::ExcerptBoundary) => Ordering::Equal,
(BlockType::ExcerptBoundary, _) => Ordering::Less,
(_, BlockType::ExcerptBoundary) => Ordering::Greater,
(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::Custom(a_id), BlockType::Custom(b_id)) => block_b
.priority()
.cmp(&block_a.priority())
@@ -1016,19 +1045,33 @@ impl BlockSnapshot {
let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?;
Some(Block::Custom(custom_block.clone()))
}
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);
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(&());
}
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() {
@@ -1425,7 +1468,7 @@ mod tests {
};
use gpui::{div, font, px, AppContext, Context as _, Element};
use language::{Buffer, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use multi_buffer::MultiBuffer;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
@@ -1681,20 +1724,22 @@ mod tests {
// Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline.
assert_eq!(
snapshot.text(),
"\n\nBuff\ner 1\n\n\n\nBuff\ner 2\n\n\n\nBuff\ner 3\n"
"\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n"
);
let blocks: Vec<_> = snapshot
.blocks_in_range(0..u32::MAX)
.map(|(row, block)| (row..row + block.height(), block.id()))
.map(|(row, block)| (row, block.id()))
.collect();
assert_eq!(
blocks,
vec![
(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
(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]))
]
);
}
@@ -2238,10 +2283,13 @@ mod tests {
#[derive(Debug, Eq, PartialEq)]
enum ExpectedBlock {
ExcerptBoundary {
ExcerptHeader {
height: u32,
starts_new_buffer: bool,
is_last: bool,
},
ExcerptFooter {
height: u32,
disposition: BlockDisposition,
},
Custom {
disposition: BlockDisposition,
@@ -2255,7 +2303,8 @@ mod tests {
fn block_type(&self) -> BlockType {
match self {
ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id),
ExpectedBlock::ExcerptBoundary { .. } => BlockType::ExcerptBoundary,
ExpectedBlock::ExcerptHeader { .. } => BlockType::Header,
ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer,
}
}
@@ -2266,7 +2315,8 @@ mod tests {
fn priority(&self) -> usize {
match self {
ExpectedBlock::Custom { priority, .. } => *priority,
ExpectedBlock::ExcerptBoundary { .. } => usize::MAX,
ExpectedBlock::ExcerptHeader { .. } => usize::MAX,
ExpectedBlock::ExcerptFooter { .. } => 0,
}
}
}
@@ -2274,21 +2324,17 @@ mod tests {
impl ExpectedBlock {
fn height(&self) -> u32 {
match self {
ExpectedBlock::ExcerptBoundary { height, .. } => *height,
ExpectedBlock::ExcerptHeader { height, .. } => *height,
ExpectedBlock::Custom { height, .. } => *height,
ExpectedBlock::ExcerptFooter { height, .. } => *height,
}
}
fn disposition(&self) -> BlockDisposition {
match self {
ExpectedBlock::ExcerptBoundary { is_last, .. } => {
if *is_last {
BlockDisposition::Below
} else {
BlockDisposition::Above
}
}
ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
ExpectedBlock::Custom { disposition, .. } => *disposition,
ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition,
}
}
}
@@ -2302,15 +2348,21 @@ mod tests {
height: block.height,
priority: block.priority,
},
Block::ExcerptBoundary {
Block::ExcerptHeader {
height,
starts_new_buffer,
next_excerpt,
..
} => ExpectedBlock::ExcerptBoundary {
} => ExpectedBlock::ExcerptHeader {
height,
starts_new_buffer,
is_last: next_excerpt.is_none(),
},
Block::ExcerptFooter {
height,
disposition,
..
} => ExpectedBlock::ExcerptFooter {
height,
disposition,
},
}
}
@@ -2328,7 +2380,8 @@ mod tests {
fn as_custom(&self) -> Option<&CustomBlock> {
match self {
Block::Custom(block) => Some(block),
Block::ExcerptBoundary { .. } => None,
Block::ExcerptHeader { .. } => None,
Block::ExcerptFooter { .. } => 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, 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, 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,
};
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 = 2;
pub const FILE_HEADER_HEIGHT: u32 = 1;
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,6 +640,7 @@ 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,
@@ -1845,6 +1846,7 @@ 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(),
@@ -1852,7 +1854,7 @@ impl Editor {
font_size,
None,
show_excerpt_controls,
FILE_HEADER_HEIGHT,
file_header_size,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT,
fold_placeholder,
@@ -2036,6 +2038,7 @@ 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),
@@ -12805,7 +12808,7 @@ impl Editor {
}
pub fn file_header_size(&self) -> u32 {
FILE_HEADER_HEIGHT
self.file_header_size
}
pub fn revert(
@@ -14117,7 +14120,7 @@ pub fn diagnostic_block_renderer(
let multi_line_diagnostic = diagnostic.message.contains('\n');
let buttons = |diagnostic: &Diagnostic| {
let buttons = |diagnostic: &Diagnostic, block_id: BlockId| {
if multi_line_diagnostic {
v_flex()
} else {
@@ -14125,7 +14128,7 @@ pub fn diagnostic_block_renderer(
}
.when(allow_closing, |div| {
div.children(diagnostic.is_primary.then(|| {
IconButton::new("close-block", IconName::XCircle)
IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.style(ButtonStyle::Transparent)
@@ -14135,7 +14138,7 @@ pub fn diagnostic_block_renderer(
}))
})
.child(
IconButton::new("copy-block", IconName::Copy)
IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.style(ButtonStyle::Transparent)
@@ -14150,7 +14153,7 @@ pub fn diagnostic_block_renderer(
)
};
let icon_size = buttons(&diagnostic)
let icon_size = buttons(&diagnostic, cx.block_id)
.into_any_element()
.layout_as_root(AvailableSpace::min_size(), cx);
@@ -14167,7 +14170,7 @@ pub fn diagnostic_block_renderer(
.w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
.flex_shrink(),
)
.child(buttons(&diagnostic))
.child(buttons(&diagnostic, cx.block_id))
.child(div().flex().flex_shrink_0().child(
StyledText::new(text_without_backticks.clone()).with_highlights(
&text_style,

View File

@@ -21,8 +21,7 @@ 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, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
CURSORS_VISIBLE_FOR, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
};
use client::ParticipantIndex;
use collections::{BTreeMap, HashMap};
@@ -32,7 +31,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,
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
EntityId, 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,
@@ -47,7 +46,7 @@ use language::{
ChunkRendererContext,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
use project::{
project_settings::{GitGutterSetting, ProjectSettings},
ProjectPath,
@@ -1633,7 +1632,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::ExcerptBoundary { .. }) {
if matches!(block, Block::ExcerptHeader { .. }) {
found_excerpt_header = true;
break;
}
@@ -1650,7 +1649,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::ExcerptBoundary { .. }) {
if matches!(block, Block::ExcerptHeader { .. }) {
found_excerpt_header = true;
}
block_height += block.height();
@@ -2101,14 +2100,23 @@ impl EditorElement {
.into_any_element()
}
Block::ExcerptBoundary {
prev_excerpt,
next_excerpt,
show_excerpt_controls,
Block::ExcerptHeader {
buffer,
range,
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,
@@ -2117,227 +2125,233 @@ 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 header_padding = px(6.0);
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
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(
prev_excerpt.id,
ExpandExcerptDirection::Down,
IconName::ArrowDownFromLine,
cx,
)),
);
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() + "/"));
}
}
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 header_padding = px(6.0);
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()
.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(
v_flex()
.id(("path excerpt header", EntityId::from(block_id)))
.w_full()
.px(header_padding)
.pt(header_padding)
.child(
h_flex()
.id("excerpt header block")
.group("excerpt-jump-action")
.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()
.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(
div()
.flex()
.v_flex()
.justify_start()
.w_full()
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
.relative()
.id("jump to collapsed context")
.w(relative(1.0))
.h_full()
.child(
div()
.top(px(0.))
.absolute()
.w_full()
.h_px()
.w_full()
.bg(cx.theme().colors().border_variant)
.group_hover("excerpt-jump-action", |style| {
style.bg(cx.theme().colors().border)
}),
)
.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(
h_flex()
.justify_end()
.flex_none()
.w(icon_offset)
.h_full()
.child(
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 {
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(|| {
ButtonLike::new("jump-icon")
.style(ButtonStyle::Transparent)
.child(
@@ -2347,6 +2361,7 @@ impl EditorElement {
.text_color(
cx.theme().colors().border_variant,
)
.group("excerpt-jump-action")
.group_hover(
"excerpt-jump-action",
|style| {
@@ -2356,13 +2371,118 @@ 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();
result.into_any()
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()
}
};
@@ -2389,33 +2509,6 @@ 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,
@@ -3274,7 +3367,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::ExcerptBoundary { .. }) {
if matches!(block, Block::ExcerptHeader { .. }) {
Some(start_row)
} else {
None

View File

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

View File

@@ -15,7 +15,6 @@ path = "src/markdown_preview.rs"
test-support = []
[dependencies]
rand.workspace = true
anyhow.workspace = true
async-recursion.workspace = true
collections.workspace = true
@@ -32,7 +31,4 @@ ui.workspace = true
workspace.workspace = true
[dev-dependencies]
cowstr.workspace = true
editor = { workspace = true, features = ["test-support"] }
proptest.workspace = true
pulldown-cmark-to-cmark.workspace = true

View File

@@ -6,13 +6,6 @@ use language::LanguageRegistry;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
use std::{ops::Range, path::PathBuf, sync::Arc};
#[cfg(test)]
use cowstr::CowStr;
#[cfg(test)]
use proptest;
#[cfg(test)]
use proptest::prelude::*;
pub async fn parse_markdown(
markdown_input: &str,
file_location_directory: Option<PathBuf>,
@@ -109,8 +102,6 @@ impl<'a> MarkdownParser<'a> {
while !self.eof() {
if let Some(block) = self.parse_block().await {
self.parsed.extend(block);
} else {
self.cursor += 1;
}
}
self
@@ -172,14 +163,20 @@ impl<'a> MarkdownParser<'a> {
let code_block = self.parse_code_block(language).await;
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
}
_ => None,
_ => {
self.cursor += 1;
None
}
},
Event::Rule => {
let source_range = source_range.clone();
self.cursor += 1;
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
}
_ => None,
_ => {
self.cursor += 1;
None
}
}
}
@@ -1003,8 +1000,6 @@ Some other content
- Inner
- Inner
2. Goodbyte
- Next item empty
-
* Last
",
)
@@ -1026,10 +1021,8 @@ Some other content
list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
list_item(186..190, 3, Unordered, vec![]),
list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]),
]
);
}
@@ -1230,35 +1223,6 @@ fn main() {
);
}
fn arbitrary_events<'a>() -> impl Strategy<Value = Vec<Event<'static>>> {
proptest::collection::vec(arbitrary_event(), 1..100)
}
fn arbitrary_event() -> impl Strategy<Value = Event<'static>> {
prop_oneof![
arbitrary_tag().prop_map(Event::Start),
arbitrary_tag_end().prop_map(Event::End),
any::<CowStr>().prop_map(Event::Text),
any::<CowStr>().prop_map(Event::Code),
any::<CowStr>().prop_map(Event::InlineMath),
any::<CowStr>().prop_map(Event::DisplayMath),
any::<CowStr>().prop_map(Event::Html),
any::<CowStr>().prop_map(Event::InlineHtml),
any::<CowStr>().prop_map(Event::FootnoteReference),
Just(Event::SoftBreak),
Just(Event::HardBreak),
Just(Event::Rule),
any::<bool>().prop_map(Event::TaskListMarker),
]
}
proptest! {
#[test]
fn test_text_preservation_property(events in arbitrary_events()) {
assert_eq!(events, vec![])
}
}
fn rust_lang() -> Arc<Language> {
Arc::new(Language::new(
LanguageConfig {

View File

@@ -189,7 +189,6 @@ pub struct MultiBufferSnapshot {
show_headers: bool,
}
#[derive(Clone)]
pub struct ExcerptInfo {
pub id: ExcerptId,
pub buffer: BufferSnapshot,
@@ -202,7 +201,6 @@ 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,16 +6057,6 @@ 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,6 +1243,10 @@ impl Project {
self.client.clone()
}
pub fn ssh_client(&self) -> Option<Model<SshRemoteClient>> {
self.ssh_client.clone()
}
pub fn user_store(&self) -> Model<UserStore> {
self.user_store.clone()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ 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);
@@ -29,10 +30,8 @@ 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);
let result = read_message_with_len(stream, buffer, len).await;
result
read_message_with_len(stream, buffer, len).await
}
pub async fn write_message<S: AsyncWrite + Unpin>(

View File

@@ -6,6 +6,7 @@ use crate::{
proxy::ProxyLaunchError,
};
use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::{
channel::{
@@ -13,7 +14,8 @@ use futures::{
oneshot,
},
future::BoxFuture,
select_biased, AsyncReadExt as _, Future, FutureExt as _, SinkExt, StreamExt as _,
select_biased, AsyncReadExt as _, AsyncWriteExt as _, Future, FutureExt as _, SinkExt,
StreamExt as _,
};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
@@ -30,6 +32,7 @@ use smol::{
};
use std::{
any::TypeId,
collections::VecDeque,
ffi::OsStr,
fmt,
ops::ControlFlow,
@@ -275,7 +278,7 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
}
}
struct ChannelForwarder {
pub struct ChannelForwarder {
quit_tx: UnboundedSender<()>,
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
}
@@ -346,7 +349,7 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
enum State {
Connecting,
Connected {
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -356,7 +359,7 @@ enum State {
HeartbeatMissed {
missed_heartbeats: usize,
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -365,7 +368,7 @@ enum State {
},
Reconnecting,
ReconnectFailed {
ssh_connection: SshRemoteConnection,
ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
@@ -391,11 +394,11 @@ impl fmt::Display for State {
}
impl State {
fn ssh_connection(&self) -> Option<&SshRemoteConnection> {
fn ssh_connection(&self) -> Option<&dyn SshRemoteProcess> {
match self {
Self::Connected { ssh_connection, .. } => Some(ssh_connection),
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection),
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection),
Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
_ => None,
}
}
@@ -529,7 +532,8 @@ impl SshRemoteClient {
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?;
let client =
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
let this = cx.new_model(|_| Self {
client: client.clone(),
unique_identifier: unique_identifier.clone(),
@@ -540,23 +544,19 @@ impl SshRemoteClient {
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
let (ssh_connection, io_task) = Self::establish_connection(
unique_identifier,
false,
connection_options,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
delegate.clone(),
&mut cx,
)
.await?;
let multiplex_task = Self::multiplex(
this.downgrade(),
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
log::error!("failed to establish connection: {}", error);
@@ -702,30 +702,24 @@ impl SshRemoteClient {
};
}
if let Err(error) = ssh_connection.master_process.kill() {
if let Err(error) = ssh_connection.kill().await.context("Failed to kill ssh process") {
failed!(error, attempts, ssh_connection, delegate, forwarder);
};
if let Err(error) = ssh_connection
.master_process
.status()
.await
.context("Failed to kill ssh process")
{
failed!(error, attempts, ssh_connection, delegate, forwarder);
}
let connection_options = ssh_connection.socket.connection_options.clone();
let connection_options = ssh_connection.connection_options();
let (incoming_tx, outgoing_rx) = forwarder.into_channels().await;
let (forwarder, proxy_incoming_tx, proxy_outgoing_rx) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let (ssh_connection, ssh_process) = match Self::establish_connection(
let (ssh_connection, io_task) = match Self::establish_connection(
identifier,
true,
connection_options,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
delegate.clone(),
&mut cx,
)
@@ -737,16 +731,9 @@ impl SshRemoteClient {
}
};
let multiplex_task = Self::multiplex(
this.clone(),
ssh_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
let multiplex_task = Self::monitor(this.clone(), io_task, &cx);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
failed!(error, attempts, ssh_connection, delegate, forwarder);
};
@@ -797,7 +784,7 @@ impl SshRemoteClient {
cx.emit(SshRemoteEvent::Disconnected);
Ok(())
} else {
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state.");
log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
Ok(())
}
})
@@ -910,101 +897,108 @@ impl SshRemoteClient {
}
fn multiplex(
this: WeakModel<Self>,
mut ssh_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
) -> Task<Result<Option<i32>>> {
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
let mut stdin_buffer = Vec::new();
let mut stdout_buffer = Vec::new();
let mut stderr_buffer = Vec::new();
let mut stderr_offset = 0;
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 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(())
});
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?;
if len == 0 {
return anyhow::Ok(());
}
if len < MESSAGE_LEN_SIZE {
child_stdout.read_exact(&mut stdout_buffer[len..]).await?;
}
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 {
stdout_buffer.resize(MESSAGE_LEN_SIZE, 0);
stderr_buffer.resize(stderr_offset + 1024, 0);
let len = child_stderr
.read(&mut stderr_buffer[stderr_offset..])
.await?;
select_biased! {
outgoing = outgoing_rx.next().fuse() => {
let Some(outgoing) = outgoing else {
return anyhow::Ok(None);
};
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));
write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?;
}
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:?}"))?;
}
}
}
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:?}"))?;
}
}
}
}
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 = futures::select! {
result = stdin_task.fuse() => {
result.context("stdin")
}
result = stdout_task.fuse() => {
result.context("stdout")
}
result = stderr_task.fuse() => {
result.context("stderr")
}
};
let result = io_task.await;
match result {
Ok(_) => {
let exit_code = ssh_proxy_process.status().await?.code().unwrap_or(1);
Ok(Some(exit_code)) => {
if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) {
match error {
ProxyLaunchError::ServerNotRunning => {
@@ -1022,6 +1016,7 @@ impl SshRemoteClient {
})?;
}
}
Ok(None) => {}
Err(error) => {
log::warn!("ssh io task died with error: {:?}. reconnecting...", error);
this.update(&mut cx, |this, cx| {
@@ -1029,7 +1024,6 @@ impl SshRemoteClient {
})?;
}
}
Ok(())
})
}
@@ -1058,21 +1052,40 @@ impl SshRemoteClient {
cx.notify();
}
#[allow(clippy::too_many_arguments)]
async fn establish_connection(
unique_identifier: String,
reconnect: bool,
connection_options: SshConnectionOptions,
proxy_incoming_tx: UnboundedSender<Envelope>,
proxy_outgoing_rx: UnboundedReceiver<Envelope>,
connection_activity_tx: Sender<()>,
delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext,
) -> Result<(SshRemoteConnection, Child)> {
) -> Result<(Box<dyn SshRemoteProcess>, Task<Result<Option<i32>>>)> {
#[cfg(any(test, feature = "test-support"))]
if let Some(fake) = fake::SshRemoteConnection::new(&connection_options) {
let io_task = fake::SshRemoteConnection::multiplex(
fake.connection_options(),
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
cx,
)
.await;
return Ok((fake, io_task));
}
let ssh_connection =
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
let platform = ssh_connection.query_platform().await?;
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
ssh_connection
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
if !reconnect {
ssh_connection
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
}
let socket = ssh_connection.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
@@ -1097,7 +1110,15 @@ impl SshRemoteClient {
.spawn()
.context("failed to spawn remote server")?;
Ok((ssh_connection, ssh_proxy_process))
let io_task = Self::multiplex(
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&cx,
);
Ok((Box::new(ssh_connection), io_task))
}
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
@@ -1109,7 +1130,7 @@ impl SshRemoteClient {
.lock()
.as_ref()
.and_then(|state| state.ssh_connection())
.map(|ssh_connection| ssh_connection.socket.ssh_args())
.map(|ssh_connection| ssh_connection.ssh_args())
}
pub fn proto_client(&self) -> AnyProtoClient {
@@ -1124,7 +1145,6 @@ impl SshRemoteClient {
self.connection_options.clone()
}
#[cfg(not(any(test, feature = "test-support")))]
pub fn connection_state(&self) -> ConnectionState {
self.state
.lock()
@@ -1133,37 +1153,69 @@ impl SshRemoteClient {
.unwrap_or(ConnectionState::Disconnected)
}
#[cfg(any(test, feature = "test-support"))]
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::Connected
}
pub fn is_disconnected(&self) -> bool {
self.connection_state() == ConnectionState::Disconnected
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake(
client_cx: &mut gpui::TestAppContext,
pub fn simulate_disconnect(&self, cx: &mut AppContext) -> Task<()> {
use gpui::BorrowAppContext;
let port = self.connection_options().port.unwrap();
let disconnect =
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.take(port).into_channels());
cx.spawn(|mut cx| async move {
let (input_rx, output_tx) = disconnect.await;
let (forwarder, _, _) = ChannelForwarder::new(input_rx, output_tx, &mut cx);
cx.update_global(|c: &mut fake::GlobalConnections, _cx| c.replace(port, forwarder))
.unwrap()
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_server(
server_cx: &mut gpui::TestAppContext,
) -> (Model<Self>, Arc<ChannelClient>) {
use gpui::Context;
) -> (ChannelForwarder, Arc<ChannelClient>) {
server_cx.update(|cx| {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded();
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded();
// We use the forwarder on the server side (in production we only use one on the client side)
// the idea is that we can simulate a disconnect/reconnect by just messing with the forwarder.
let (forwarder, _, _) =
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx.to_async());
(
client_cx.update(|cx| {
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx);
cx.new_model(|_| Self {
client,
unique_identifier: "fake".to_string(),
connection_options: SshConnectionOptions::default(),
state: Arc::new(Mutex::new(None)),
})
}),
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)),
)
let client = ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server");
(forwarder, client)
})
}
#[cfg(any(test, feature = "test-support"))]
pub async fn fake_client(
forwarder: ChannelForwarder,
client_cx: &mut gpui::TestAppContext,
) -> Model<Self> {
use gpui::BorrowAppContext;
client_cx
.update(|cx| {
let port = cx.update_default_global(|c: &mut fake::GlobalConnections, _cx| {
c.push(forwarder)
});
Self::new(
"fake".to_string(),
SshConnectionOptions {
host: "<fake>".to_string(),
port: Some(port),
..Default::default()
},
Arc::new(fake::Delegate),
cx,
)
})
.await
.unwrap()
}
}
@@ -1173,6 +1225,13 @@ impl From<SshRemoteClient> for AnyProtoClient {
}
}
#[async_trait]
trait SshRemoteProcess: Send + Sync {
async fn kill(&mut self) -> Result<()>;
fn ssh_args(&self) -> Vec<String>;
fn connection_options(&self) -> SshConnectionOptions;
}
struct SshRemoteConnection {
socket: SshSocket,
master_process: process::Child,
@@ -1187,6 +1246,25 @@ impl Drop for SshRemoteConnection {
}
}
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
self.master_process.kill()?;
self.master_process.status().await?;
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
self.socket.ssh_args()
}
fn connection_options(&self) -> SshConnectionOptions {
self.socket.connection_options.clone()
}
}
impl SshRemoteConnection {
#[cfg(not(unix))]
async fn new(
@@ -1203,7 +1281,6 @@ 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 _;
@@ -1470,8 +1547,10 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
pub struct ChannelClient {
next_message_id: AtomicU32,
outgoing_tx: mpsc::UnboundedSender<Envelope>,
response_channels: ResponseChannels, // Lock
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock
buffer: Mutex<VecDeque<Envelope>>,
response_channels: ResponseChannels,
message_handlers: Mutex<ProtoMessageHandlerSet>,
max_received: AtomicU32,
}
impl ChannelClient {
@@ -1479,15 +1558,18 @@ impl ChannelClient {
incoming_rx: mpsc::UnboundedReceiver<Envelope>,
outgoing_tx: mpsc::UnboundedSender<Envelope>,
cx: &AppContext,
name: &'static str,
) -> Arc<Self> {
let this = Arc::new(Self {
outgoing_tx,
next_message_id: AtomicU32::new(0),
max_received: AtomicU32::new(0),
response_channels: ResponseChannels::default(),
message_handlers: Default::default(),
buffer: Mutex::new(VecDeque::new()),
});
Self::start_handling_messages(this.clone(), incoming_rx, cx);
Self::start_handling_messages(this.clone(), incoming_rx, cx, name);
this
}
@@ -1496,6 +1578,7 @@ impl ChannelClient {
this: Arc<Self>,
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
cx: &AppContext,
name: &'static str,
) {
cx.spawn(|cx| {
let this = Arc::downgrade(&this);
@@ -1505,6 +1588,26 @@ impl ChannelClient {
let Some(this) = this.upgrade() else {
return anyhow::Ok(());
};
if let Some(ack_id) = incoming.ack_id {
let mut buffer = this.buffer.lock();
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
buffer.pop_front();
}
}
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload {
{
let buffer = this.buffer.lock();
for envelope in buffer.iter() {
this.outgoing_tx.unbounded_send(envelope.clone()).ok();
}
}
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
this.outgoing_tx.unbounded_send(envelope)?;
continue;
}
this.max_received.store(incoming.id, SeqCst);
if let Some(request_id) = incoming.responding_to {
let request_id = MessageId(request_id);
@@ -1526,19 +1629,22 @@ impl ChannelClient {
this.clone().into(),
cx.clone(),
) {
log::debug!("ssh message received. name:{type_name}");
match future.await {
Ok(_) => {
log::debug!("ssh message handled. name:{type_name}");
log::debug!("{name}:ssh message received. name:{type_name}");
cx.foreground_executor().spawn(async move {
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}",
);
}
}
Err(error) => {
log::error!(
"error handling message. type:{type_name}, error:{error}",
);
}
}
}).detach();
} else {
log::error!("unhandled ssh message name:{type_name}");
log::error!("{name}:unhandled ssh message name:{type_name}");
}
}
}
@@ -1581,6 +1687,23 @@ impl ChannelClient {
}
}
pub async fn resync(&self, timeout: Duration) -> Result<()> {
smol::future::or(
async {
self.request(proto::FlushBufferedMessages {}).await?;
for envelope in self.buffer.lock().iter() {
self.outgoing_tx.unbounded_send(envelope.clone()).ok();
}
Ok(())
},
async {
smol::Timer::after(timeout).await;
Err(anyhow!("Timeout detected"))
},
)
.await
}
pub async fn ping(&self, timeout: Duration) -> Result<()> {
smol::future::or(
async {
@@ -1610,7 +1733,8 @@ impl ChannelClient {
let mut response_channels_lock = self.response_channels.lock();
response_channels_lock.insert(MessageId(envelope.id), tx);
drop(response_channels_lock);
let result = self.outgoing_tx.unbounded_send(envelope);
let result = self.send_buffered(envelope);
async move {
if let Err(error) = &result {
log::error!("failed to send message: {}", error);
@@ -1627,6 +1751,12 @@ impl ChannelClient {
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.id = self.next_message_id.fetch_add(1, SeqCst);
self.send_buffered(envelope)
}
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.ack_id = Some(self.max_received.load(SeqCst));
self.buffer.lock().push_back(envelope.clone());
self.outgoing_tx.unbounded_send(envelope)?;
Ok(())
}
@@ -1657,3 +1787,165 @@ impl ProtoClient for ChannelClient {
false
}
}
#[cfg(any(test, feature = "test-support"))]
mod fake {
use std::path::PathBuf;
use anyhow::Result;
use async_trait::async_trait;
use futures::{
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, BorrowAppContext, Global, SemanticVersion, Task};
use rpc::proto::Envelope;
use super::{
ChannelForwarder, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteProcess,
};
pub(super) struct SshRemoteConnection {
connection_options: SshConnectionOptions,
}
impl SshRemoteConnection {
pub(super) fn new(
connection_options: &SshConnectionOptions,
) -> Option<Box<dyn SshRemoteProcess>> {
if connection_options.host == "<fake>" {
return Some(Box::new(Self {
connection_options: connection_options.clone(),
}));
}
return None;
}
pub(super) async fn multiplex(
connection_options: SshConnectionOptions,
mut client_tx: mpsc::UnboundedSender<Envelope>,
mut client_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &mut AsyncAppContext,
) -> Task<Result<Option<i32>>> {
let (server_tx, server_rx) = cx
.update(|cx| {
cx.update_global(|conns: &mut GlobalConnections, _| {
conns.take(connection_options.port.unwrap())
})
})
.unwrap()
.into_channels()
.await;
let (forwarder, mut proxy_tx, mut proxy_rx) =
ChannelForwarder::new(server_tx, server_rx, cx);
cx.update(|cx| {
cx.update_global(|conns: &mut GlobalConnections, _| {
conns.replace(connection_options.port.unwrap(), forwarder)
})
})
.unwrap();
cx.background_executor().spawn(async move {
loop {
select_biased! {
server_to_client = proxy_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(Some(1))
};
connection_activity_tx.try_send(()).ok();
client_tx.send(server_to_client).await.ok();
}
client_to_server = client_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(None)
};
proxy_tx.send(client_to_server).await.ok();
}
}
}
})
}
}
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
Vec::new()
}
fn connection_options(&self) -> SshConnectionOptions {
self.connection_options.clone()
}
}
#[derive(Default)]
pub(super) struct GlobalConnections(Vec<Option<ChannelForwarder>>);
impl Global for GlobalConnections {}
impl GlobalConnections {
pub(super) fn push(&mut self, forwarder: ChannelForwarder) -> u16 {
self.0.push(Some(forwarder));
self.0.len() as u16 - 1
}
pub(super) fn take(&mut self, port: u16) -> ChannelForwarder {
self.0
.get_mut(port as usize)
.expect("no fake server for port")
.take()
.expect("fake server is already borrowed")
}
pub(super) fn replace(&mut self, port: u16, forwarder: ChannelForwarder) {
let ret = self
.0
.get_mut(port as usize)
.expect("no fake server for port")
.replace(forwarder);
if ret.is_some() {
panic!("fake server is already replaced");
}
}
}
pub(super) struct Delegate;
impl SshClientDelegate for Delegate {
fn ask_password(
&self,
_: String,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<String>> {
unreachable!()
}
fn remote_server_binary_path(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> Result<PathBuf> {
unreachable!()
}
fn get_server_binary(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
unreachable!()
}
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
unreachable!()
}
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
unreachable!()
}
}
}

View File

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

View File

@@ -12,7 +12,6 @@ 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::{
@@ -214,27 +213,19 @@ 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 = stdin_msg_rx.next().fuse() => {
let Some(message) = stdin_message else {
log::warn!("error reading message on stdin. exiting.");
break;
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;
}
};
if let Err(error) = incoming_tx.unbounded_send(message) {
log::error!("failed to send message to application: {:?}. exiting.", error);
@@ -279,7 +270,7 @@ fn start_server(
})
.detach();
ChannelClient::new(incoming_rx, outgoing_tx, cx)
ChannelClient::new(incoming_rx, outgoing_tx, cx, "server")
}
fn init_paths() -> anyhow::Result<()> {

View File

@@ -17,7 +17,8 @@ use editor::{
use futures::io::BufReader;
use futures::{AsyncBufReadExt as _, FutureExt as _, StreamExt as _};
use gpui::{
div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
div, prelude::*, EntityId, EventEmitter, Model, Render, Subscription, Task, View, ViewContext,
WeakView,
};
use language::Point;
use project::Fs;
@@ -148,21 +149,23 @@ impl EditorBlock {
.w(text_line_height)
.h(text_line_height)
.child(
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)
}
}),
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)
}
}),
);
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.1"
version = "0.1.0"
edition = "2021"
publish = false
license = "Apache-2.0"

View File

@@ -1,7 +1,7 @@
id = "elixir"
name = "Elixir"
description = "Elixir support."
version = "0.1.1"
version = "0.1.0"
schema_version = 1
authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
repository = "https://github.com/zed-industries/zed"