Compare commits
15 Commits
remote-ima
...
v0.165.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8199d4362 | ||
|
|
ce6de20db5 | ||
|
|
f0a1a4b600 | ||
|
|
316186b6dc | ||
|
|
78b36a466b | ||
|
|
f58bb04bf7 | ||
|
|
2b3d06d3ac | ||
|
|
ec23cbd30a | ||
|
|
9ba6e1b869 | ||
|
|
76ba59ad95 | ||
|
|
258fb338bd | ||
|
|
6ac691b3e9 | ||
|
|
7cd2f71392 | ||
|
|
771ee7a9f6 | ||
|
|
14c0a9d587 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -15594,7 +15594,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.165.0"
|
||||
version = "0.165.4"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -134,27 +134,16 @@ impl ProjectDiagnosticsEditor {
|
||||
language_server_id,
|
||||
path,
|
||||
} => {
|
||||
let max_severity = this.max_severity();
|
||||
let has_diagnostics_to_display = project.read(cx).lsp_store().read(cx).diagnostics_for_buffer(path)
|
||||
.into_iter().flatten()
|
||||
.filter(|(server_id, _)| language_server_id == server_id)
|
||||
.flat_map(|(_, diagnostics)| diagnostics)
|
||||
.any(|diagnostic| diagnostic.diagnostic.severity <= max_severity);
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
if has_diagnostics_to_display {
|
||||
this.paths_to_update
|
||||
.insert((path.clone(), Some(*language_server_id)));
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
}
|
||||
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. no diagnostics to display");
|
||||
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
|
||||
this.update_stale_excerpts(cx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -340,12 +329,16 @@ impl ProjectDiagnosticsEditor {
|
||||
ExcerptId::min()
|
||||
};
|
||||
|
||||
let max_severity = self.max_severity();
|
||||
let path_state = &mut self.path_states[path_ix];
|
||||
let mut new_group_ixs = Vec::new();
|
||||
let mut blocks_to_add = Vec::new();
|
||||
let mut blocks_to_remove = HashSet::default();
|
||||
let mut first_excerpt_id = None;
|
||||
let max_severity = if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
};
|
||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, cx| {
|
||||
let mut old_groups = mem::take(&mut path_state.diagnostic_groups)
|
||||
.into_iter()
|
||||
@@ -634,14 +627,6 @@ impl ProjectDiagnosticsEditor {
|
||||
prev_path = Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
fn max_severity(&self) -> DiagnosticSeverity {
|
||||
if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for ProjectDiagnosticsEditor {
|
||||
|
||||
@@ -4452,19 +4452,10 @@ impl Editor {
|
||||
};
|
||||
|
||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
|
||||
let is_followup_invoke = {
|
||||
let context_menu_state = self.context_menu.read();
|
||||
matches!(
|
||||
context_menu_state.deref(),
|
||||
Some(ContextMenu::Completions(_))
|
||||
)
|
||||
};
|
||||
let trigger_kind = match (&options.trigger, is_followup_invoke) {
|
||||
(_, true) => CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
|
||||
(Some(trigger), _) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||
let trigger_kind = match &options.trigger {
|
||||
Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => {
|
||||
CompletionTriggerKind::TRIGGER_CHARACTER
|
||||
}
|
||||
|
||||
_ => CompletionTriggerKind::INVOKED,
|
||||
};
|
||||
let completion_context = CompletionContext {
|
||||
@@ -12901,7 +12892,6 @@ impl Editor {
|
||||
None => Autoscroll::newest(),
|
||||
};
|
||||
let nav_history = editor.nav_history.take();
|
||||
editor.unfold_ranges(&ranges, false, true, cx);
|
||||
editor.change_selections(Some(autoscroll), cx, |s| {
|
||||
s.select_ranges(ranges);
|
||||
});
|
||||
@@ -13836,6 +13826,11 @@ fn snippet_completions(
|
||||
.take_while(|c| classifier.is_word(*c))
|
||||
.collect::<String>();
|
||||
last_word = last_word.chars().rev().collect();
|
||||
|
||||
if last_word.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
|
||||
let to_lsp = |point: &text::Anchor| {
|
||||
let end = text::ToPointUtf16::to_point_utf16(point, &snapshot);
|
||||
|
||||
@@ -11567,7 +11567,7 @@ async fn test_multibuffer_reverts(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let cols = 4;
|
||||
@@ -11856,74 +11856,6 @@ async fn test_multibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer_unfold_on_jump(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let texts = ["{\n\tx\n}".to_owned(), "y".to_owned()];
|
||||
let buffers = texts
|
||||
.clone()
|
||||
.map(|txt| cx.new_model(|cx| Buffer::local(txt, cx)));
|
||||
let multi_buffer = cx.new_model(|cx| {
|
||||
let mut multi_buffer = MultiBuffer::new(ReadWrite);
|
||||
for i in 0..2 {
|
||||
multi_buffer.push_excerpts(
|
||||
buffers[i].clone(),
|
||||
[ExcerptRange {
|
||||
context: 0..texts[i].len(),
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
multi_buffer
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"x": &texts[0],
|
||||
"y": &texts[1],
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
|
||||
let multi_buffer_editor = cx.new_view(|cx| {
|
||||
Editor::for_multibuffer(multi_buffer.clone(), Some(project.clone()), true, cx)
|
||||
});
|
||||
let buffer_editor =
|
||||
cx.new_view(|cx| Editor::for_buffer(buffers[0].clone(), Some(project.clone()), cx));
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(multi_buffer_editor.clone()),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
workspace.add_item_to_active_pane(Box::new(buffer_editor.clone()), None, false, cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
buffer_editor.update(cx, |buffer_editor, cx| {
|
||||
buffer_editor.fold_at_level(&FoldAtLevel { level: 1 }, cx);
|
||||
assert!(buffer_editor.snapshot(cx).fold_count() == 1);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
multi_buffer_editor.update(cx, |multi_buffer_editor, cx| {
|
||||
multi_buffer_editor.change_selections(None, cx, |s| s.select_ranges([3..4]));
|
||||
multi_buffer_editor.open_excerpts(&OpenExcerpts, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
buffer_editor.update(cx, |buffer_editor, cx| {
|
||||
assert!(buffer_editor.snapshot(cx).fold_count() == 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_hunk_diff(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::panic::Location;
|
||||
use std::panic::{AssertUnwindSafe, Location};
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
@@ -23,7 +23,7 @@ use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::channel::oneshot;
|
||||
use futures::{channel::oneshot, future::FutureExt};
|
||||
use parking_lot::Mutex;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -481,7 +481,12 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let username = attributes
|
||||
.get("username")
|
||||
.ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
|
||||
let secret = item.secret().await?;
|
||||
// oo7 panics if the retrieved secret can't be decrypted due to
|
||||
// unexpected padding.
|
||||
let secret = AssertUnwindSafe(item.secret())
|
||||
.catch_unwind()
|
||||
.await
|
||||
.map_err(|_| anyhow!("oo7 panicked while trying to read credentials"))??;
|
||||
|
||||
// we lose the zeroizing capabilities at this boundary,
|
||||
// a current limitation GPUI's credentials api
|
||||
|
||||
@@ -104,14 +104,15 @@ impl LanguageSelectorDelegate {
|
||||
let candidates = language_registry
|
||||
.language_names()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(candidate_id, name)| {
|
||||
.filter_map(|name| {
|
||||
language_registry
|
||||
.available_language_for_name(&name)?
|
||||
.hidden()
|
||||
.not()
|
||||
.then(|| StringMatchCandidate::new(candidate_id, name))
|
||||
.then_some(name)
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
|
||||
@@ -18,22 +18,19 @@ pub enum ParsedMarkdownElement {
|
||||
}
|
||||
|
||||
impl ParsedMarkdownElement {
|
||||
pub fn source_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
pub fn source_range(&self) -> Option<Range<usize>> {
|
||||
Some(match self {
|
||||
Self::Heading(heading) => heading.source_range.clone(),
|
||||
Self::ListItem(list_item) => list_item.source_range.clone(),
|
||||
Self::Table(table) => table.source_range.clone(),
|
||||
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
|
||||
Self::CodeBlock(code_block) => code_block.source_range.clone(),
|
||||
Self::Paragraph(text) => match &text[0] {
|
||||
Self::Paragraph(text) => match text.get(0)? {
|
||||
MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
|
||||
MarkdownParagraphChunk::Image(image) => match image {
|
||||
Image::Web { source_range, .. } => source_range.clone(),
|
||||
Image::Path { source_range, .. } => source_range.clone(),
|
||||
},
|
||||
MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
|
||||
},
|
||||
Self::HorizontalRule(range) => range.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_list_item(&self) -> bool {
|
||||
@@ -289,104 +286,27 @@ impl Display for Link {
|
||||
/// A Markdown Image
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub enum Image {
|
||||
Web {
|
||||
source_range: Range<usize>,
|
||||
/// The URL of the Image.
|
||||
url: String,
|
||||
/// Link URL if exists.
|
||||
link: Option<Link>,
|
||||
/// alt text if it exists
|
||||
alt_text: Option<ParsedMarkdownText>,
|
||||
},
|
||||
/// Image path on the filesystem.
|
||||
Path {
|
||||
source_range: Range<usize>,
|
||||
/// The path as provided in the Markdown document.
|
||||
display_path: PathBuf,
|
||||
/// The absolute path to the item.
|
||||
path: PathBuf,
|
||||
/// Link URL if exists.
|
||||
link: Option<Link>,
|
||||
/// alt text if it exists
|
||||
alt_text: Option<ParsedMarkdownText>,
|
||||
},
|
||||
pub struct Image {
|
||||
pub link: Link,
|
||||
pub source_range: Range<usize>,
|
||||
pub alt_text: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
pub fn identify(
|
||||
text: String,
|
||||
source_range: Range<usize>,
|
||||
file_location_directory: Option<PathBuf>,
|
||||
text: String,
|
||||
link: Option<Link>,
|
||||
) -> Option<Image> {
|
||||
if text.starts_with("http") {
|
||||
return Some(Image::Web {
|
||||
source_range,
|
||||
url: text,
|
||||
link,
|
||||
alt_text: None,
|
||||
});
|
||||
}
|
||||
let path = PathBuf::from(&text);
|
||||
if path.is_absolute() {
|
||||
return Some(Image::Path {
|
||||
source_range,
|
||||
display_path: path.clone(),
|
||||
path,
|
||||
link,
|
||||
alt_text: None,
|
||||
});
|
||||
}
|
||||
if let Some(file_location_directory) = file_location_directory {
|
||||
let display_path = path;
|
||||
let path = file_location_directory.join(text);
|
||||
return Some(Image::Path {
|
||||
source_range,
|
||||
display_path,
|
||||
path,
|
||||
link,
|
||||
alt_text: None,
|
||||
});
|
||||
}
|
||||
None
|
||||
) -> Option<Self> {
|
||||
let link = Link::identify(file_location_directory, text)?;
|
||||
Some(Self {
|
||||
source_range,
|
||||
link,
|
||||
alt_text: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self {
|
||||
match self {
|
||||
Image::Web {
|
||||
ref source_range,
|
||||
ref url,
|
||||
ref link,
|
||||
..
|
||||
} => Image::Web {
|
||||
source_range: source_range.clone(),
|
||||
url: url.clone(),
|
||||
link: link.clone(),
|
||||
alt_text: Some(alt_text),
|
||||
},
|
||||
Image::Path {
|
||||
ref source_range,
|
||||
ref display_path,
|
||||
ref path,
|
||||
ref link,
|
||||
..
|
||||
} => Image::Path {
|
||||
source_range: source_range.clone(),
|
||||
display_path: display_path.clone(),
|
||||
path: path.clone(),
|
||||
link: link.clone(),
|
||||
alt_text: Some(alt_text),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Image {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Image::Web { url, .. } => write!(f, "{}", url),
|
||||
Image::Path { display_path, .. } => write!(f, "{}", display_path.display()),
|
||||
}
|
||||
pub fn set_alt_text(&mut self, alt_text: SharedString) {
|
||||
self.alt_text = Some(alt_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ impl<'a> MarkdownParser<'a> {
|
||||
break;
|
||||
}
|
||||
|
||||
let (current, _source_range) = self.current().unwrap();
|
||||
let (current, _) = self.current().unwrap();
|
||||
let prev_len = text.len();
|
||||
match current {
|
||||
Event::SoftBreak => {
|
||||
@@ -314,56 +314,29 @@ impl<'a> MarkdownParser<'a> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(mut image) = image.clone() {
|
||||
let is_valid_image = match image.clone() {
|
||||
Image::Path { display_path, .. } => {
|
||||
gpui::ImageSource::try_from(display_path).is_ok()
|
||||
}
|
||||
Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(),
|
||||
};
|
||||
if is_valid_image {
|
||||
text.truncate(text.len() - t.len());
|
||||
if !t.is_empty() {
|
||||
let alt_text = ParsedMarkdownText {
|
||||
source_range: source_range.clone(),
|
||||
contents: t.to_string(),
|
||||
highlights: highlights.clone(),
|
||||
region_ranges: region_ranges.clone(),
|
||||
regions: regions.clone(),
|
||||
};
|
||||
image = image.with_alt_text(alt_text);
|
||||
} else {
|
||||
let alt_text = ParsedMarkdownText {
|
||||
source_range: source_range.clone(),
|
||||
contents: "img".to_string(),
|
||||
highlights: highlights.clone(),
|
||||
region_ranges: region_ranges.clone(),
|
||||
regions: regions.clone(),
|
||||
};
|
||||
image = image.with_alt_text(alt_text);
|
||||
}
|
||||
if !text.is_empty() {
|
||||
let parsed_regions =
|
||||
MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||
source_range: source_range.clone(),
|
||||
contents: text.clone(),
|
||||
highlights: highlights.clone(),
|
||||
region_ranges: region_ranges.clone(),
|
||||
regions: regions.clone(),
|
||||
});
|
||||
text = String::new();
|
||||
highlights = vec![];
|
||||
region_ranges = vec![];
|
||||
regions = vec![];
|
||||
markdown_text_like.push(parsed_regions);
|
||||
}
|
||||
|
||||
let parsed_image = MarkdownParagraphChunk::Image(image.clone());
|
||||
markdown_text_like.push(parsed_image);
|
||||
style = MarkdownHighlightStyle::default();
|
||||
if let Some(image) = image.as_mut() {
|
||||
text.truncate(text.len() - t.len());
|
||||
image.set_alt_text(t.to_string().into());
|
||||
if !text.is_empty() {
|
||||
let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText {
|
||||
source_range: source_range.clone(),
|
||||
contents: text.clone(),
|
||||
highlights: highlights.clone(),
|
||||
region_ranges: region_ranges.clone(),
|
||||
regions: regions.clone(),
|
||||
});
|
||||
text = String::new();
|
||||
highlights = vec![];
|
||||
region_ranges = vec![];
|
||||
regions = vec![];
|
||||
markdown_text_like.push(parsed_regions);
|
||||
}
|
||||
|
||||
let parsed_image = MarkdownParagraphChunk::Image(image.clone());
|
||||
markdown_text_like.push(parsed_image);
|
||||
style = MarkdownHighlightStyle::default();
|
||||
style.underline = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
@@ -395,10 +368,9 @@ impl<'a> MarkdownParser<'a> {
|
||||
}
|
||||
Tag::Image { dest_url, .. } => {
|
||||
image = Image::identify(
|
||||
dest_url.to_string(),
|
||||
source_range.clone(),
|
||||
self.file_location_directory.clone(),
|
||||
dest_url.to_string(),
|
||||
link.clone(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
@@ -926,6 +898,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_empty_image() {
|
||||
let parsed = parse("![]()").await;
|
||||
|
||||
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
|
||||
text
|
||||
} else {
|
||||
panic!("Expected a paragraph");
|
||||
};
|
||||
assert_eq!(paragraph.len(), 0);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_image_links_detection() {
|
||||
let parsed = parse("").await;
|
||||
@@ -937,19 +921,12 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
paragraph[0],
|
||||
MarkdownParagraphChunk::Image(Image::Web {
|
||||
MarkdownParagraphChunk::Image(Image {
|
||||
source_range: 0..111,
|
||||
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||
link: None,
|
||||
alt_text: Some(
|
||||
ParsedMarkdownText {
|
||||
source_range: 0..111,
|
||||
contents: "test".to_string(),
|
||||
highlights: vec![],
|
||||
region_ranges: vec![],
|
||||
regions: vec![],
|
||||
},
|
||||
),
|
||||
link: Link::Web {
|
||||
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
|
||||
},
|
||||
alt_text: Some("test".into()),
|
||||
},)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -192,11 +192,16 @@ impl MarkdownPreviewView {
|
||||
.group("markdown-block")
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
|
||||
if event.down.click_count == 2 {
|
||||
if let Some(block) =
|
||||
this.contents.as_ref().and_then(|c| c.children.get(ix))
|
||||
if let Some(source_range) = this
|
||||
.contents
|
||||
.as_ref()
|
||||
.and_then(|c| c.children.get(ix))
|
||||
.and_then(|block| block.source_range())
|
||||
{
|
||||
let start = block.source_range().start;
|
||||
this.move_cursor_to_block(cx, start..start);
|
||||
this.move_cursor_to_block(
|
||||
cx,
|
||||
source_range.start..source_range.start,
|
||||
);
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -410,7 +415,9 @@ impl MarkdownPreviewView {
|
||||
let mut last_end = 0;
|
||||
if let Some(content) = &self.contents {
|
||||
for (i, block) in content.children.iter().enumerate() {
|
||||
let Range { start, end } = block.source_range();
|
||||
let Some(Range { start, end }) = block.source_range() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Check if the cursor is between the last block and the current block
|
||||
if last_end <= cursor && cursor < start {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::markdown_elements::{
|
||||
HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
|
||||
HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
|
||||
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
|
||||
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
|
||||
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
|
||||
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
|
||||
};
|
||||
use gpui::{
|
||||
div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
|
||||
@@ -13,7 +13,6 @@ use gpui::{
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
ops::{Mul, Range},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
vec,
|
||||
};
|
||||
@@ -505,103 +504,41 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
|
||||
}
|
||||
|
||||
MarkdownParagraphChunk::Image(image) => {
|
||||
let (link, source_range, image_source, alt_text) = match image {
|
||||
Image::Web {
|
||||
link,
|
||||
source_range,
|
||||
url,
|
||||
alt_text,
|
||||
} => (
|
||||
link,
|
||||
source_range,
|
||||
Resource::Uri(url.clone().into()),
|
||||
alt_text,
|
||||
),
|
||||
Image::Path {
|
||||
link,
|
||||
source_range,
|
||||
path,
|
||||
alt_text,
|
||||
..
|
||||
} => {
|
||||
let image_path = Path::new(path.to_str().unwrap());
|
||||
(
|
||||
link,
|
||||
source_range,
|
||||
Resource::Path(Arc::from(image_path)),
|
||||
alt_text,
|
||||
)
|
||||
}
|
||||
let image_resource = match image.link.clone() {
|
||||
Link::Web { url } => Resource::Uri(url.into()),
|
||||
Link::Path { path, .. } => Resource::Path(Arc::from(path)),
|
||||
};
|
||||
|
||||
let element_id = cx.next_id(source_range);
|
||||
let element_id = cx.next_id(&image.source_range);
|
||||
|
||||
match link {
|
||||
None => {
|
||||
let fallback_workspace = workspace_clone.clone();
|
||||
let fallback_syntax_theme = syntax_theme.clone();
|
||||
let fallback_text_style = text_style.clone();
|
||||
let fallback_alt_text = alt_text.clone();
|
||||
let element_id_new = element_id.clone();
|
||||
let element = div()
|
||||
.child(img(ImageSource::Resource(image_source)).with_fallback({
|
||||
move || {
|
||||
fallback_text(
|
||||
fallback_alt_text.clone().unwrap(),
|
||||
element_id.clone(),
|
||||
&fallback_syntax_theme,
|
||||
code_span_bg_color,
|
||||
fallback_workspace.clone(),
|
||||
&fallback_text_style,
|
||||
)
|
||||
let image_element = div()
|
||||
.id(element_id)
|
||||
.child(img(ImageSource::Resource(image_resource)).with_fallback({
|
||||
let alt_text = image.alt_text.clone();
|
||||
{
|
||||
move || div().children(alt_text.clone()).into_any_element()
|
||||
}
|
||||
}))
|
||||
.tooltip({
|
||||
let link = image.link.clone();
|
||||
move |cx| LinkPreview::new(&link.to_string(), cx)
|
||||
})
|
||||
.on_click({
|
||||
let workspace = workspace_clone.clone();
|
||||
let link = image.link.clone();
|
||||
move |_event, window_cx| match &link {
|
||||
Link::Web { url } => window_cx.open_url(url),
|
||||
Link::Path { path, .. } => {
|
||||
if let Some(workspace) = &workspace {
|
||||
_ = workspace.update(window_cx, |workspace, cx| {
|
||||
workspace.open_abs_path(path.clone(), false, cx).detach();
|
||||
});
|
||||
}
|
||||
}))
|
||||
.id(element_id_new)
|
||||
.into_any();
|
||||
any_element.push(element);
|
||||
}
|
||||
Some(link) => {
|
||||
let link_click = link.clone();
|
||||
let link_tooltip = link.clone();
|
||||
let fallback_workspace = workspace_clone.clone();
|
||||
let fallback_syntax_theme = syntax_theme.clone();
|
||||
let fallback_text_style = text_style.clone();
|
||||
let fallback_alt_text = alt_text.clone();
|
||||
let element_id_new = element_id.clone();
|
||||
let image_element = div()
|
||||
.child(img(ImageSource::Resource(image_source)).with_fallback({
|
||||
move || {
|
||||
fallback_text(
|
||||
fallback_alt_text.clone().unwrap(),
|
||||
element_id.clone(),
|
||||
&fallback_syntax_theme,
|
||||
code_span_bg_color,
|
||||
fallback_workspace.clone(),
|
||||
&fallback_text_style,
|
||||
)
|
||||
}
|
||||
}))
|
||||
.id(element_id_new)
|
||||
.tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx))
|
||||
.on_click({
|
||||
let workspace = workspace_clone.clone();
|
||||
move |_event, window_cx| match &link_click {
|
||||
Link::Web { url } => window_cx.open_url(url),
|
||||
Link::Path { path, .. } => {
|
||||
if let Some(workspace) = &workspace {
|
||||
_ = workspace.update(window_cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(path.clone(), false, cx)
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any();
|
||||
any_element.push(image_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any();
|
||||
any_element.push(image_element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -613,80 +550,3 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
|
||||
let rule = div().w_full().h(px(2.)).bg(cx.border_color);
|
||||
div().pt_3().pb_3().child(rule).into_any()
|
||||
}
|
||||
|
||||
fn fallback_text(
|
||||
parsed: ParsedMarkdownText,
|
||||
source_range: ElementId,
|
||||
syntax_theme: &theme::SyntaxTheme,
|
||||
code_span_bg_color: Hsla,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
text_style: &TextStyle,
|
||||
) -> AnyElement {
|
||||
let element_id = source_range;
|
||||
|
||||
let highlights = gpui::combine_highlights(
|
||||
parsed.highlights.iter().filter_map(|(range, highlight)| {
|
||||
let highlight = highlight.to_highlight_style(syntax_theme)?;
|
||||
Some((range.clone(), highlight))
|
||||
}),
|
||||
parsed
|
||||
.regions
|
||||
.iter()
|
||||
.zip(&parsed.region_ranges)
|
||||
.filter_map(|(region, range)| {
|
||||
if region.code {
|
||||
Some((
|
||||
range.clone(),
|
||||
HighlightStyle {
|
||||
background_color: Some(code_span_bg_color),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
);
|
||||
let mut links = Vec::new();
|
||||
let mut link_ranges = Vec::new();
|
||||
for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
|
||||
if let Some(link) = region.link.clone() {
|
||||
links.push(link);
|
||||
link_ranges.push(range.clone());
|
||||
}
|
||||
}
|
||||
let element = div()
|
||||
.child(
|
||||
InteractiveText::new(
|
||||
element_id,
|
||||
StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights),
|
||||
)
|
||||
.tooltip({
|
||||
let links = links.clone();
|
||||
let link_ranges = link_ranges.clone();
|
||||
move |idx, cx| {
|
||||
for (ix, range) in link_ranges.iter().enumerate() {
|
||||
if range.contains(&idx) {
|
||||
return Some(LinkPreview::new(&links[ix].to_string(), cx));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
.on_click(
|
||||
link_ranges,
|
||||
move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
|
||||
Link::Web { url } => window_cx.open_url(url),
|
||||
Link::Path { path, .. } => {
|
||||
if let Some(workspace) = &workspace {
|
||||
_ = workspace.update(window_cx, |workspace, cx| {
|
||||
workspace.open_abs_path(path.clone(), false, cx).detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.into_any();
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -2919,21 +2919,6 @@ impl LspStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn diagnostics_for_buffer(
|
||||
&self,
|
||||
path: &ProjectPath,
|
||||
) -> Option<
|
||||
&[(
|
||||
LanguageServerId,
|
||||
Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
|
||||
)],
|
||||
> {
|
||||
self.diagnostics
|
||||
.get(&path.worktree_id)?
|
||||
.get(&path.path)
|
||||
.map(|diagnostics| diagnostics.as_slice())
|
||||
}
|
||||
|
||||
pub fn started_language_servers(&self) -> Vec<(WorktreeId, LanguageServerName)> {
|
||||
self.language_server_ids.keys().cloned().collect()
|
||||
}
|
||||
|
||||
@@ -214,8 +214,13 @@ async fn deserialize_pane_group(
|
||||
.await;
|
||||
|
||||
let pane = panel
|
||||
.update(cx, |_, cx| {
|
||||
new_terminal_pane(workspace.clone(), project.clone(), cx)
|
||||
.update(cx, |terminal_panel, cx| {
|
||||
new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
terminal_panel.active_pane.read(cx).is_zoomed(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()?;
|
||||
let active_item = serialized_pane.active_item;
|
||||
|
||||
@@ -84,9 +84,10 @@ pub struct TerminalPanel {
|
||||
impl TerminalPanel {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), cx);
|
||||
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
|
||||
let center = PaneGroup::new(pane.clone());
|
||||
let enabled = project.read(cx).supports_terminal(cx);
|
||||
cx.focus_view(&pane);
|
||||
let terminal_panel = Self {
|
||||
center,
|
||||
active_pane: pane,
|
||||
@@ -299,6 +300,9 @@ impl TerminalPanel {
|
||||
let pane_count_before_removal = self.center.panes().len();
|
||||
let _removal_result = self.center.remove(&pane);
|
||||
if pane_count_before_removal == 1 {
|
||||
self.center.first_pane().update(cx, |pane, cx| {
|
||||
pane.set_zoomed(false, cx);
|
||||
});
|
||||
cx.emit(PanelEvent::Close);
|
||||
} else {
|
||||
if let Some(focus_on_pane) =
|
||||
@@ -308,27 +312,49 @@ impl TerminalPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
|
||||
pane::Event::ZoomIn => {
|
||||
for pane in self.center.panes() {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_zoomed(true, cx);
|
||||
})
|
||||
}
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
cx.notify();
|
||||
}
|
||||
pane::Event::ZoomOut => {
|
||||
for pane in self.center.panes() {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_zoomed(false, cx);
|
||||
})
|
||||
}
|
||||
cx.emit(PanelEvent::ZoomOut);
|
||||
cx.notify();
|
||||
}
|
||||
pane::Event::AddItem { item } => {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
item.added_to_pane(workspace, pane.clone(), cx)
|
||||
})
|
||||
}
|
||||
self.serialize(cx);
|
||||
}
|
||||
pane::Event::Split(direction) => {
|
||||
let new_pane = self.new_pane_with_cloned_active_terminal(cx);
|
||||
let pane = pane.clone();
|
||||
let direction = *direction;
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
cx.spawn(move |terminal_panel, mut cx| async move {
|
||||
let Some(new_pane) = new_pane.await else {
|
||||
return;
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.center.split(&pane, &new_pane, direction).log_err();
|
||||
})
|
||||
.ok();
|
||||
terminal_panel
|
||||
.update(&mut cx, |terminal_panel, cx| {
|
||||
terminal_panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction)
|
||||
.log_err();
|
||||
cx.focus_view(&new_pane);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -365,7 +391,7 @@ impl TerminalPanel {
|
||||
.or_else(|| default_working_directory(workspace.read(cx), cx));
|
||||
let kind = TerminalKind::Shell(working_directory);
|
||||
let window = cx.window_handle();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
cx.spawn(move |terminal_panel, mut cx| async move {
|
||||
let terminal = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.create_terminal(kind, window, cx)
|
||||
@@ -380,10 +406,15 @@ impl TerminalPanel {
|
||||
})
|
||||
.ok()?,
|
||||
);
|
||||
let pane = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let pane = new_terminal_pane(weak_workspace, project, cx);
|
||||
this.apply_tab_bar_buttons(&pane, cx);
|
||||
let pane = terminal_panel
|
||||
.update(&mut cx, |terminal_panel, cx| {
|
||||
let pane = new_terminal_pane(
|
||||
weak_workspace,
|
||||
project,
|
||||
terminal_panel.active_pane.read(cx).is_zoomed(),
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&pane, cx);
|
||||
pane
|
||||
})
|
||||
.ok()?;
|
||||
@@ -392,7 +423,6 @@ impl TerminalPanel {
|
||||
pane.add_item(terminal_view, true, true, None, cx);
|
||||
})
|
||||
.ok()?;
|
||||
cx.focus_view(&pane).ok()?;
|
||||
|
||||
Some(pane)
|
||||
})
|
||||
@@ -814,6 +844,7 @@ impl TerminalPanel {
|
||||
pub fn new_terminal_pane(
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
zoomed: bool,
|
||||
cx: &mut ViewContext<TerminalPanel>,
|
||||
) -> View<Pane> {
|
||||
let is_local = project.read(cx).is_local();
|
||||
@@ -827,9 +858,11 @@ pub fn new_terminal_pane(
|
||||
NewTerminal.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
pane.set_zoomed(zoomed, cx);
|
||||
pane.set_can_navigate(false, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_| true);
|
||||
pane.set_zoom_out_on_close(false);
|
||||
|
||||
let terminal_panel_for_split_check = terminal_panel.clone();
|
||||
pane.set_can_split(Some(Arc::new(move |pane, dragged_item, cx| {
|
||||
@@ -879,8 +912,17 @@ pub fn new_terminal_pane(
|
||||
|
||||
let new_pane = pane.drag_split_direction().and_then(|split_direction| {
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let new_pane =
|
||||
new_terminal_pane(workspace.clone(), project.clone(), cx);
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel
|
||||
.center
|
||||
@@ -1062,14 +1104,21 @@ impl Render for TerminalPanel {
|
||||
cx.focus_view(&pane);
|
||||
} else {
|
||||
let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn(|terminal_panel, mut cx| async move {
|
||||
if let Some(new_pane) = new_pane.await {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.center
|
||||
.split(&this.active_pane, &new_pane, SplitDirection::Right)
|
||||
.log_err();
|
||||
})
|
||||
.ok();
|
||||
terminal_panel
|
||||
.update(&mut cx, |terminal_panel, cx| {
|
||||
terminal_panel
|
||||
.center
|
||||
.split(
|
||||
&terminal_panel.active_pane,
|
||||
&new_pane,
|
||||
SplitDirection::Right,
|
||||
)
|
||||
.log_err();
|
||||
cx.focus_view(&new_pane);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -1152,8 +1201,12 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
|
||||
self.active_pane
|
||||
.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
|
||||
for pane in self.center.panes() {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.set_zoomed(zoomed, cx);
|
||||
})
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -305,6 +305,7 @@ pub struct Pane {
|
||||
pub new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub split_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pinned_tab_count: usize,
|
||||
zoom_out_on_close: bool,
|
||||
}
|
||||
|
||||
pub struct ActivationHistoryEntry {
|
||||
@@ -504,6 +505,7 @@ impl Pane {
|
||||
split_item_context_menu_handle: Default::default(),
|
||||
new_item_context_menu_handle: Default::default(),
|
||||
pinned_tab_count: 0,
|
||||
zoom_out_on_close: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1541,7 +1543,7 @@ impl Pane {
|
||||
.remove(&item.item_id());
|
||||
}
|
||||
|
||||
if self.items.is_empty() && close_pane_if_empty && self.zoomed {
|
||||
if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed {
|
||||
cx.emit(Event::ZoomOut);
|
||||
}
|
||||
|
||||
@@ -2717,6 +2719,10 @@ impl Pane {
|
||||
pub fn drag_split_direction(&self) -> Option<SplitDirection> {
|
||||
self.drag_split_direction
|
||||
}
|
||||
|
||||
pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) {
|
||||
self.zoom_out_on_close = zoom_out_on_close;
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for Pane {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.165.0"
|
||||
version = "0.165.4"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
Reference in New Issue
Block a user