Support CSV preview in project-less set-ups (#63350)
Closes https://github.com/zed-industries/zed/issues/63265 Release Notes: - Fixed CSV preview not showing in project-less set-ups
This commit is contained in:
1 parent
75917d2f73
commit
7ea8eb3f2e
9 files changed
+184
-62
No files matched your search
Generated
+3
@@ -18009,9 +18009,12 @@ dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
"picker",
|
||||
"project",
|
||||
"proptest",
|
||||
"serde_json",
|
||||
"text",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
|
||||
@@ -212,23 +212,32 @@ impl RelatedExcerptStore {
|
||||
position: Anchor,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let (project, snapshot, identifier_line_count) = this.read_with(cx, |this, cx| {
|
||||
(
|
||||
this.project.upgrade(),
|
||||
buffer.read(cx).snapshot(),
|
||||
this.identifier_line_count,
|
||||
)
|
||||
})?;
|
||||
let (project, snapshot, file_extension, identifier_line_count) =
|
||||
this.read_with(cx, |this, cx| {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let file_extension = snapshot
|
||||
.file()
|
||||
.and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(
|
||||
this.project.upgrade(),
|
||||
snapshot,
|
||||
file_extension,
|
||||
this.identifier_line_count,
|
||||
)
|
||||
})?;
|
||||
let Some(project) = project else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let file = snapshot.file().cloned();
|
||||
let file_extension = file
|
||||
.as_ref()
|
||||
.and_then(|file| file.path().extension())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if let Some(file) = &file {
|
||||
log::debug!("retrieving_context buffer:{}", file.path().as_unix_str());
|
||||
}
|
||||
|
||||
@@ -1638,10 +1638,14 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let extension = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| Some(file.path().extension()?.to_string()));
|
||||
let extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
std::path::Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
)
|
||||
});
|
||||
|
||||
let event_type = match accepted {
|
||||
true => "Edit Prediction Accepted",
|
||||
|
||||
@@ -8913,7 +8913,7 @@ impl Editor {
|
||||
) {
|
||||
if let Some(file_stem) = self.active_buffer(cx).and_then(|buffer| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
file.path().file_stem()
|
||||
Path::new(file.file_name(cx)).file_stem()?.to_str()
|
||||
}) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(file_stem.to_string()));
|
||||
}
|
||||
|
||||
@@ -10648,6 +10648,47 @@ async fn test_copy_file_location_from_multibuffer(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_file_name_for_external_file(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/root"), json!({ "a.txt": "" })).await;
|
||||
fs.insert_tree(path!("/elsewhere"), json!({ "external.csv": "a,b\n" }))
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/elsewhere/external.csv"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.copy_file_name(&CopyFileName, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard().and_then(|item| item.text()),
|
||||
Some("external.csv".to_string()),
|
||||
"copy_file_name should work for a file outside the project"
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard().and_then(|item| item.text()),
|
||||
Some("external".to_string()),
|
||||
"copy_file_name_without_extension should work for a file outside the project"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copy_file_location_across_buffers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -2357,7 +2357,7 @@ impl OutlinePanel {
|
||||
let color =
|
||||
entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
|
||||
let icon = if settings.file_icons {
|
||||
FileIcons::get_icon(entry.path.as_std_path(), cx)
|
||||
FileIcons::get_icon(Path::new(&name), cx)
|
||||
.map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
|
||||
} else {
|
||||
None
|
||||
@@ -2415,15 +2415,15 @@ impl OutlinePanel {
|
||||
let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
|
||||
Some(buffer_snapshot) => match buffer_snapshot.file() {
|
||||
Some(file) => {
|
||||
let path = file.path();
|
||||
let name = file.file_name(cx).to_string();
|
||||
let icon = if settings.file_icons {
|
||||
FileIcons::get_icon(path.as_std_path(), cx)
|
||||
FileIcons::get_icon(Path::new(&name), cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(color).into_any_element());
|
||||
(icon, file_name(path.as_std_path()))
|
||||
(icon, name)
|
||||
}
|
||||
None => (None, "Untitled".to_string()),
|
||||
},
|
||||
@@ -2722,6 +2722,26 @@ impl OutlinePanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn fs_entry_label(&self, fs_entry: &FsEntry, cx: &App) -> Option<String> {
|
||||
match fs_entry {
|
||||
FsEntry::ExternalFile(external) => Some(
|
||||
self.buffer_snapshot_for_id(external.buffer_id, cx)?
|
||||
.file()?
|
||||
.file_name(cx)
|
||||
.to_string(),
|
||||
),
|
||||
FsEntry::Directory(FsEntryDirectory {
|
||||
worktree_id, entry, ..
|
||||
})
|
||||
| FsEntry::File(FsEntryFile {
|
||||
worktree_id, entry, ..
|
||||
}) => match entry.path.file_name() {
|
||||
Some(name) => Some(name.to_string()),
|
||||
None => Some(self.entry_name(worktree_id, entry, cx)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update_fs_entries(
|
||||
&mut self,
|
||||
active_editor: Entity<Editor>,
|
||||
@@ -4128,10 +4148,13 @@ impl OutlinePanel {
|
||||
let id = state.entries.len();
|
||||
match &entry {
|
||||
PanelEntry::Fs(fs_entry) => {
|
||||
if let Some(file_name) = self
|
||||
.relative_path(fs_entry, cx)
|
||||
.and_then(|path| Some(path.file_name()?.to_string()))
|
||||
{
|
||||
let candidate_label = match fs_entry {
|
||||
FsEntry::Directory(directory) => {
|
||||
directory.entry.path.file_name().map(str::to_string)
|
||||
}
|
||||
_ => self.fs_entry_label(fs_entry, cx),
|
||||
};
|
||||
if let Some(file_name) = candidate_label {
|
||||
state
|
||||
.match_candidates
|
||||
.push(StringMatchCandidate::new(id, &file_name));
|
||||
@@ -4569,21 +4592,9 @@ impl OutlinePanel {
|
||||
|
||||
fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
|
||||
let item_text_chars = match entry {
|
||||
PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
|
||||
.buffer_snapshot_for_id(external.buffer_id, cx)
|
||||
.and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
|
||||
.unwrap_or_default(),
|
||||
PanelEntry::Fs(FsEntry::Directory(directory)) => directory
|
||||
.entry
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.len())
|
||||
.unwrap_or_default(),
|
||||
PanelEntry::Fs(FsEntry::File(file)) => file
|
||||
.entry
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.len())
|
||||
PanelEntry::Fs(fs_entry) => self
|
||||
.fs_entry_label(fs_entry, cx)
|
||||
.map(|label| label.len())
|
||||
.unwrap_or_default(),
|
||||
PanelEntry::FoldedDirs(folded_dirs) => {
|
||||
folded_dirs
|
||||
|
||||
@@ -19,7 +19,12 @@ log.workspace = true
|
||||
text.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
proptest.workspace = true
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[features]
|
||||
dev-tools = []
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::{
|
||||
types::{LineNumber, TableCell},
|
||||
};
|
||||
use editor::Editor;
|
||||
use gpui::{AppContext, Context, Entity, Subscription, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
use text::BufferSnapshot;
|
||||
use ui::{SharedString, table_row::TableRow};
|
||||
@@ -34,11 +35,14 @@ const TABULAR_FORMATS: &[(&str, TabularFormat)] = &[
|
||||
];
|
||||
|
||||
impl TabularFormat {
|
||||
pub(crate) fn from_editor(editor: &Entity<Editor>, cx: &App) -> Option<Self> {
|
||||
Self::from_extension(editor_file_extension(editor, cx)?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_extension(ext: &str) -> Option<Self> {
|
||||
let lower = ext.to_lowercase();
|
||||
TABULAR_FORMATS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == lower)
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(ext))
|
||||
.map(|(_, format)| *format)
|
||||
}
|
||||
|
||||
@@ -52,6 +56,12 @@ impl TabularFormat {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn editor_file_extension<'a>(editor: &Entity<Editor>, cx: &'a App) -> Option<&'a str> {
|
||||
let buffer = editor.read(cx).buffer().read(cx).as_singleton()?;
|
||||
let file = buffer.read(cx).file()?;
|
||||
Path::new(file.file_name(cx)).extension()?.to_str()
|
||||
}
|
||||
|
||||
impl TabularDataPreviewPane {
|
||||
pub(crate) fn parse_from_active_editor(
|
||||
&mut self,
|
||||
@@ -99,22 +109,14 @@ impl TabularDataPreviewPane {
|
||||
.as_singleton()
|
||||
.map(|b| b.read(cx).text_snapshot());
|
||||
|
||||
let extension = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.and_then(|file| file.path().extension().map(ToOwned::to_owned));
|
||||
let extension = editor_file_extension(&editor, cx);
|
||||
|
||||
let delimiter = extension
|
||||
.as_deref()
|
||||
.and_then(TabularFormat::from_extension)
|
||||
.map(TabularFormat::delimiter)
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"unrecognized tabular data extension {:?}, defaulting to comma delimiter",
|
||||
extension
|
||||
"unrecognized tabular data extension {extension:?}, defaulting to comma delimiter"
|
||||
);
|
||||
','
|
||||
});
|
||||
|
||||
@@ -265,15 +265,7 @@ impl TabularDataPreviewPane {
|
||||
}
|
||||
|
||||
pub fn is_tabular_data_file(editor: &Entity<Editor>, cx: &App) -> bool {
|
||||
editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.and_then(|file| file.path().extension())
|
||||
.and_then(parser::TabularFormat::from_extension)
|
||||
.is_some()
|
||||
parser::TabularFormat::from_editor(editor, cx).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,3 +361,58 @@ impl ColumnWidths {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use util::path;
|
||||
use workspace::AppState;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_detects_tabular_files_outside_the_project(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({ "inside.csv": "a,b\n1,2\n", "inside.txt": "plain" }),
|
||||
)
|
||||
.await;
|
||||
fs.insert_tree(
|
||||
path!("/elsewhere"),
|
||||
json!({ "outside.csv": "a,b\n1,2\n", "outside.txt": "plain" }),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||
|
||||
for (abs_path, expected) in [
|
||||
(path!("/project/inside.csv"), true),
|
||||
(path!("/project/inside.txt"), false),
|
||||
(path!("/elsewhere/outside.csv"), true),
|
||||
(path!("/elsewhere/outside.txt"), false),
|
||||
] {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer(abs_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (editor, _) = cx.add_window_view(|window, cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let is_tabular =
|
||||
cx.update(|cx| TabularDataPreviewPane::is_tabular_data_file(&editor, cx));
|
||||
assert_eq!(is_tabular, expected, "{abs_path}");
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
AppState::test(cx);
|
||||
editor::init(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user