Detailed changes
@@ -92,6 +92,7 @@
"g y": "editor::GoToTypeDefinition",
"g shift-i": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
+ "g f": "editor::OpenFile",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
@@ -262,6 +262,7 @@ gpui::actions!(
OpenExcerptsSplit,
OpenPermalinkToLine,
OpenUrl,
+ OpenFile,
Outdent,
PageDown,
PageUp,
@@ -99,7 +99,7 @@ use language::{point_to_lsp, BufferRow, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
-use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
+use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
pub use lsp::CompletionContext;
use lsp::{
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
@@ -9179,6 +9179,38 @@ impl Editor {
.detach();
}
+ pub fn open_file(&mut self, _: &OpenFile, cx: &mut ViewContext<Self>) {
+ let Some(workspace) = self.workspace() else {
+ return;
+ };
+
+ let position = self.selections.newest_anchor().head();
+
+ let Some((buffer, buffer_position)) =
+ self.buffer.read(cx).text_anchor_for_position(position, cx)
+ else {
+ return;
+ };
+
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+
+ cx.spawn(|_, mut cx| async move {
+ let result = find_file(&buffer, project, buffer_position, &mut cx).await;
+
+ if let Some((_, path)) = result {
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_resolved_path(path, cx)
+ })?
+ .await?;
+ }
+ anyhow::Ok(())
+ })
+ .detach();
+ }
+
pub(crate) fn navigate_to_hover_links(
&mut self,
kind: Option<GotoDefinitionKind>,
@@ -9189,21 +9221,49 @@ impl Editor {
// If there is one definition, just open it directly
if definitions.len() == 1 {
let definition = definitions.pop().unwrap();
+
+ enum TargetTaskResult {
+ Location(Option<Location>),
+ AlreadyNavigated,
+ }
+
let target_task = match definition {
- HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+ HoverLink::Text(link) => {
+ Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target))))
+ }
HoverLink::InlayHint(lsp_location, server_id) => {
- self.compute_target_location(lsp_location, server_id, cx)
+ let computation = self.compute_target_location(lsp_location, server_id, cx);
+ cx.background_executor().spawn(async move {
+ let location = computation.await?;
+ Ok(TargetTaskResult::Location(location))
+ })
}
HoverLink::Url(url) => {
cx.open_url(&url);
- Task::ready(Ok(None))
+ Task::ready(Ok(TargetTaskResult::AlreadyNavigated))
+ }
+ HoverLink::File(path) => {
+ if let Some(workspace) = self.workspace() {
+ cx.spawn(|_, mut cx| async move {
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace.open_resolved_path(path, cx)
+ })?
+ .await
+ .map(|_| TargetTaskResult::AlreadyNavigated)
+ })
+ } else {
+ Task::ready(Ok(TargetTaskResult::Location(None)))
+ }
}
};
cx.spawn(|editor, mut cx| async move {
- let target = target_task.await.context("target resolution task")?;
- let Some(target) = target else {
- return Ok(Navigated::No);
+ let target = match target_task.await.context("target resolution task")? {
+ TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes),
+ TargetTaskResult::Location(None) => return Ok(Navigated::No),
+ TargetTaskResult::Location(Some(target)) => target,
};
+
editor.update(&mut cx, |editor, cx| {
let Some(workspace) = editor.workspace() else {
return Navigated::No;
@@ -9281,6 +9341,7 @@ impl Editor {
}),
HoverLink::InlayHint(_, _) => None,
HoverLink::Url(_) => None,
+ HoverLink::File(_) => None,
})
.unwrap_or(tab_kind.to_string());
let location_tasks = definitions
@@ -9291,6 +9352,7 @@ impl Editor {
editor.compute_target_location(lsp_location, server_id, cx)
}
HoverLink::Url(_) => Task::ready(Ok(None)),
+ HoverLink::File(_) => Task::ready(Ok(None)),
})
.collect::<Vec<_>>();
(title, location_tasks, editor.workspace().clone())
@@ -331,6 +331,7 @@ impl EditorElement {
.detach_and_log_err(cx);
});
register_action(view, cx, Editor::open_url);
+ register_action(view, cx, Editor::open_file);
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::unfold_lines);
@@ -9,8 +9,8 @@ use language::{Bias, ToOffset};
use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
use project::{
- HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
- ResolveState,
+ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project,
+ ResolveState, ResolvedPath,
};
use std::ops::Range;
use theme::ActiveTheme as _;
@@ -63,6 +63,7 @@ impl RangeInEditor {
#[derive(Debug, Clone)]
pub enum HoverLink {
Url(String),
+ File(ResolvedPath),
Text(LocationLink),
InlayHint(lsp::Location, LanguageServerId),
}
@@ -522,35 +523,54 @@ pub fn show_link_definition(
})
.ok()
} else if let Some(project) = project {
- // query the LSP for definition info
- project
- .update(&mut cx, |project, cx| match preferred_kind {
- LinkDefinitionKind::Symbol => {
- project.definition(&buffer, buffer_position, cx)
- }
+ if let Some((filename_range, filename)) =
+ find_file(&buffer, project.clone(), buffer_position, &mut cx).await
+ {
+ let range = maybe!({
+ let start =
+ snapshot.anchor_in_excerpt(excerpt_id, filename_range.start)?;
+ let end =
+ snapshot.anchor_in_excerpt(excerpt_id, filename_range.end)?;
+ Some(RangeInEditor::Text(start..end))
+ });
- LinkDefinitionKind::Type => {
- project.type_definition(&buffer, buffer_position, cx)
- }
- })?
- .await
- .ok()
- .map(|definition_result| {
- (
- definition_result.iter().find_map(|link| {
- link.origin.as_ref().and_then(|origin| {
- let start = snapshot.anchor_in_excerpt(
- excerpt_id,
- origin.range.start,
- )?;
- let end = snapshot
- .anchor_in_excerpt(excerpt_id, origin.range.end)?;
- Some(RangeInEditor::Text(start..end))
- })
- }),
- definition_result.into_iter().map(HoverLink::Text).collect(),
- )
- })
+ Some((range, vec![HoverLink::File(filename)]))
+ } else {
+ // query the LSP for definition info
+ project
+ .update(&mut cx, |project, cx| match preferred_kind {
+ LinkDefinitionKind::Symbol => {
+ project.definition(&buffer, buffer_position, cx)
+ }
+
+ LinkDefinitionKind::Type => {
+ project.type_definition(&buffer, buffer_position, cx)
+ }
+ })?
+ .await
+ .ok()
+ .map(|definition_result| {
+ (
+ definition_result.iter().find_map(|link| {
+ link.origin.as_ref().and_then(|origin| {
+ let start = snapshot.anchor_in_excerpt(
+ excerpt_id,
+ origin.range.start,
+ )?;
+ let end = snapshot.anchor_in_excerpt(
+ excerpt_id,
+ origin.range.end,
+ )?;
+ Some(RangeInEditor::Text(start..end))
+ })
+ }),
+ definition_result
+ .into_iter()
+ .map(HoverLink::Text)
+ .collect(),
+ )
+ })
+ }
} else {
None
}
@@ -686,6 +706,116 @@ pub(crate) fn find_url(
None
}
+pub(crate) async fn find_file(
+ buffer: &Model<language::Buffer>,
+ project: Model<Project>,
+ position: text::Anchor,
+ cx: &mut AsyncWindowContext,
+) -> Option<(Range<text::Anchor>, ResolvedPath)> {
+ let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()).ok()?;
+
+ let (range, candidate_file_path) = surrounding_filename(snapshot, position)?;
+
+ let existing_path = project
+ .update(cx, |project, cx| {
+ project.resolve_existing_file_path(&candidate_file_path, &buffer, cx)
+ })
+ .ok()?
+ .await?;
+
+ Some((range, existing_path))
+}
+
+fn surrounding_filename(
+ snapshot: language::BufferSnapshot,
+ position: text::Anchor,
+) -> Option<(Range<text::Anchor>, String)> {
+ const LIMIT: usize = 2048;
+
+ let offset = position.to_offset(&snapshot);
+ let mut token_start = offset;
+ let mut token_end = offset;
+ let mut found_start = false;
+ let mut found_end = false;
+ let mut inside_quotes = false;
+
+ let mut filename = String::new();
+
+ let mut backwards = snapshot.reversed_chars_at(offset).take(LIMIT).peekable();
+ while let Some(ch) = backwards.next() {
+ // Escaped whitespace
+ if ch.is_whitespace() && backwards.peek() == Some(&'\\') {
+ filename.push(ch);
+ token_start -= ch.len_utf8();
+ backwards.next();
+ token_start -= '\\'.len_utf8();
+ continue;
+ }
+ if ch.is_whitespace() {
+ found_start = true;
+ break;
+ }
+ if (ch == '"' || ch == '\'') && !inside_quotes {
+ found_start = true;
+ inside_quotes = true;
+ break;
+ }
+
+ filename.push(ch);
+ token_start -= ch.len_utf8();
+ }
+ if !found_start && token_start != 0 {
+ return None;
+ }
+
+ filename = filename.chars().rev().collect();
+
+ let mut forwards = snapshot
+ .chars_at(offset)
+ .take(LIMIT - (offset - token_start))
+ .peekable();
+ while let Some(ch) = forwards.next() {
+ // Skip escaped whitespace
+ if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
+ token_end += ch.len_utf8();
+ let whitespace = forwards.next().unwrap();
+ token_end += whitespace.len_utf8();
+ filename.push(whitespace);
+ continue;
+ }
+
+ if ch.is_whitespace() {
+ found_end = true;
+ break;
+ }
+ if ch == '"' || ch == '\'' {
+ // If we're inside quotes, we stop when we come across the next quote
+ if inside_quotes {
+ found_end = true;
+ break;
+ } else {
+ // Otherwise, we skip the quote
+ inside_quotes = true;
+ continue;
+ }
+ }
+ filename.push(ch);
+ token_end += ch.len_utf8();
+ }
+
+ if !found_end && (token_end - token_start >= LIMIT) {
+ return None;
+ }
+
+ if filename.is_empty() {
+ return None;
+ }
+
+ let range = snapshot.anchor_before(token_start)..snapshot.anchor_after(token_end);
+
+ Some((range, filename))
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -1268,4 +1398,184 @@ mod tests {
cx.simulate_click(screen_coord, Modifiers::secondary_key());
assert_eq!(cx.opened_url(), Some("https://zed.dev/releases".into()));
}
+
+ #[gpui::test]
+ async fn test_surrounding_filename(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ let test_cases = [
+ ("file ˇ name", None),
+ ("ˇfile name", Some("file")),
+ ("file ˇname", Some("name")),
+ ("fiˇle name", Some("file")),
+ ("filenˇame", Some("filename")),
+ // Absolute path
+ ("foobar ˇ/home/user/f.txt", Some("/home/user/f.txt")),
+ ("foobar /home/useˇr/f.txt", Some("/home/user/f.txt")),
+ // Windows
+ ("C:\\Useˇrs\\user\\f.txt", Some("C:\\Users\\user\\f.txt")),
+ // Whitespace
+ ("ˇfile\\ -\\ name.txt", Some("file - name.txt")),
+ ("file\\ -\\ naˇme.txt", Some("file - name.txt")),
+ // Tilde
+ ("ˇ~/file.txt", Some("~/file.txt")),
+ ("~/fiˇle.txt", Some("~/file.txt")),
+ // Double quotes
+ ("\"fˇile.txt\"", Some("file.txt")),
+ ("ˇ\"file.txt\"", Some("file.txt")),
+ ("ˇ\"fi\\ le.txt\"", Some("fi le.txt")),
+ // Single quotes
+ ("'fˇile.txt'", Some("file.txt")),
+ ("ˇ'file.txt'", Some("file.txt")),
+ ("ˇ'fi\\ le.txt'", Some("fi le.txt")),
+ ];
+
+ for (input, expected) in test_cases {
+ cx.set_state(input);
+
+ let (position, snapshot) = cx.editor(|editor, cx| {
+ let positions = editor.selections.newest_anchor().head().text_anchor;
+ let snapshot = editor
+ .buffer()
+ .clone()
+ .read(cx)
+ .as_singleton()
+ .unwrap()
+ .read(cx)
+ .snapshot();
+ (positions, snapshot)
+ });
+
+ let result = surrounding_filename(snapshot, position);
+
+ if let Some(expected) = expected {
+ assert!(result.is_some(), "Failed to find file path: {}", input);
+ let (_, path) = result.unwrap();
+ assert_eq!(&path, expected, "Incorrect file path for input: {}", input);
+ } else {
+ assert!(
+ result.is_none(),
+ "Expected no result, but got one: {:?}",
+ result
+ );
+ }
+ }
+ }
+
+ #[gpui::test]
+ async fn test_hover_filenames(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Insert a new file
+ let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
+ fs.as_fake()
+ .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
+ .await;
+
+ cx.set_state(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to /root/dir/file2.rs if project is local.ˇ
+ "});
+
+ // File does not exist
+ let screen_coord = cx.pixel_position(indoc! {"
+ You can't go to a file that dˇoes_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to /root/dir/file2.rs if project is local.
+ "});
+ cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+ // No highlight
+ cx.update_editor(|editor, cx| {
+ assert!(editor
+ .snapshot(cx)
+ .text_highlight_ranges::<HoveredLinkState>()
+ .unwrap_or_default()
+ .1
+ .is_empty());
+ });
+
+ // Moving the mouse over a file that does exist should highlight it.
+ let screen_coord = cx.pixel_position(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to fˇile2.rs if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to /root/dir/file2.rs if project is local.
+ "});
+
+ cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to «file2.rsˇ» if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to /root/dir/file2.rs if project is local.
+ "});
+
+ // Moving the mouse over a relative path that does exist should highlight it
+ let screen_coord = cx.pixel_position(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to ../dir/fˇile2.rs if you want.
+ Or go to /root/dir/file2.rs if project is local.
+ "});
+
+ cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to «../dir/file2.rsˇ» if you want.
+ Or go to /root/dir/file2.rs if project is local.
+ "});
+
+ // Moving the mouse over an absolute path that does exist should highlight it
+ let screen_coord = cx.pixel_position(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to /root/diˇr/file2.rs if project is local.
+ "});
+
+ cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
+ cx.assert_editor_text_highlights::<HoveredLinkState>(indoc! {"
+ You can't go to a file that does_not_exist.txt.
+ Go to file2.rs if you want.
+ Or go to ../dir/file2.rs if you want.
+ Or go to «/root/dir/file2.rsˇ» if project is local.
+ "});
+
+ cx.simulate_click(screen_coord, Modifiers::secondary_key());
+
+ cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+ cx.update_workspace(|workspace, cx| {
+ let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
+
+ let buffer = active_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap();
+
+ let file = buffer.read(cx).file().unwrap();
+ let file_path = file.as_local().unwrap().abs_path(cx);
+
+ assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
+ });
+ }
}
@@ -383,7 +383,7 @@ pub trait File: Send + Sync {
/// The file associated with a buffer, in the case where the file is on the local disk.
pub trait LocalFile: File {
- /// Returns the absolute path of this file.
+ /// Returns the absolute path of this file
fn abs_path(&self, cx: &AppContext) -> PathBuf;
/// Loads the file's contents from disk.
@@ -649,16 +649,17 @@ impl DirectoryLister {
};
"~/".to_string()
}
- pub fn list_directory(&self, query: String, cx: &mut AppContext) -> Task<Result<Vec<PathBuf>>> {
+
+ pub fn list_directory(&self, path: String, cx: &mut AppContext) -> Task<Result<Vec<PathBuf>>> {
match self {
DirectoryLister::Project(project) => {
- project.update(cx, |project, cx| project.list_directory(query, cx))
+ project.update(cx, |project, cx| project.list_directory(path, cx))
}
DirectoryLister::Local(fs) => {
let fs = fs.clone();
cx.background_executor().spawn(async move {
let mut results = vec![];
- let expanded = shellexpand::tilde(&query);
+ let expanded = shellexpand::tilde(&path);
let query = Path::new(expanded.as_ref());
let mut response = fs.read_dir(query).await?;
while let Some(path) = response.next().await {
@@ -7769,6 +7770,88 @@ impl Project {
}
}
+ // Returns the resolved version of `path`, that was found in `buffer`, if it exists.
+ pub fn resolve_existing_file_path(
+ &self,
+ path: &str,
+ buffer: &Model<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Option<ResolvedPath>> {
+ // TODO: ssh based remoting.
+ if self.ssh_session.is_some() {
+ return Task::ready(None);
+ }
+
+ if self.is_local() {
+ let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned());
+
+ if expanded.is_absolute() {
+ let fs = self.fs.clone();
+ cx.background_executor().spawn(async move {
+ let path = expanded.as_path();
+ let exists = fs.is_file(path).await;
+
+ exists.then(|| ResolvedPath::AbsPath(expanded))
+ })
+ } else {
+ self.resolve_path_in_worktrees(expanded, buffer, cx)
+ }
+ } else {
+ let path = PathBuf::from(path);
+ if path.is_absolute() || path.starts_with("~") {
+ return Task::ready(None);
+ }
+
+ self.resolve_path_in_worktrees(path, buffer, cx)
+ }
+ }
+
+ fn resolve_path_in_worktrees(
+ &self,
+ path: PathBuf,
+ buffer: &Model<Buffer>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Option<ResolvedPath>> {
+ let mut candidates = vec![path.clone()];
+
+ if let Some(file) = buffer.read(cx).file() {
+ if let Some(dir) = file.path().parent() {
+ let joined = dir.to_path_buf().join(path);
+ candidates.push(joined);
+ }
+ }
+
+ let worktrees = self.worktrees(cx).collect::<Vec<_>>();
+ cx.spawn(|_, mut cx| async move {
+ for worktree in worktrees {
+ for candidate in candidates.iter() {
+ let path = worktree
+ .update(&mut cx, |worktree, _| {
+ let root_entry_path = &worktree.root_entry().unwrap().path;
+
+ let resolved = resolve_path(&root_entry_path, candidate);
+
+ let stripped =
+ resolved.strip_prefix(&root_entry_path).unwrap_or(&resolved);
+
+ worktree.entry_for_path(stripped).map(|entry| {
+ ResolvedPath::ProjectPath(ProjectPath {
+ worktree_id: worktree.id(),
+ path: entry.path.clone(),
+ })
+ })
+ })
+ .ok()?;
+
+ if path.is_some() {
+ return path;
+ }
+ }
+ }
+ None
+ })
+ }
+
pub fn list_directory(
&self,
query: String,
@@ -11230,6 +11313,14 @@ fn resolve_path(base: &Path, path: &Path) -> PathBuf {
result
}
+/// ResolvedPath is a path that has been resolved to either a ProjectPath
+/// or an AbsPath and that *exists*.
+#[derive(Debug, Clone)]
+pub enum ResolvedPath {
+ ProjectPath(ProjectPath),
+ AbsPath(PathBuf),
+}
+
impl Item for Buffer {
fn try_open(
project: &Model<Project>,
@@ -742,9 +742,15 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
mod test {
use std::path::Path;
- use crate::test::{NeovimBackedTestContext, VimTestContext};
+ use crate::{
+ state::Mode,
+ test::{NeovimBackedTestContext, VimTestContext},
+ };
+ use editor::Editor;
use gpui::TestAppContext;
use indoc::indoc;
+ use ui::ViewContext;
+ use workspace::Workspace;
#[gpui::test]
async fn test_command_basics(cx: &mut TestAppContext) {
@@ -923,4 +929,55 @@ mod test {
.await;
cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1");
}
+
+ fn assert_active_item(
+ workspace: &mut Workspace,
+ expected_path: &str,
+ expected_text: &str,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let active_editor = workspace.active_item_as::<Editor>(cx).unwrap();
+
+ let buffer = active_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .unwrap();
+
+ let text = buffer.read(cx).text();
+ let file = buffer.read(cx).file().unwrap();
+ let file_path = file.as_local().unwrap().abs_path(cx);
+
+ assert_eq!(text, expected_text);
+ assert_eq!(file_path.to_str().unwrap(), expected_path);
+ }
+
+ #[gpui::test]
+ async fn test_command_gf(cx: &mut TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ // Assert base state, that we're in /root/dir/file.rs
+ cx.workspace(|workspace, cx| {
+ assert_active_item(workspace, "/root/dir/file.rs", "", cx);
+ });
+
+ // Insert a new file
+ let fs = cx.workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
+ fs.as_fake()
+ .insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
+ .await;
+
+ // Put the path to the second file into the currently open buffer
+ cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal);
+
+ // Go to file2.rs
+ cx.simulate_keystrokes("g f");
+
+ // We now have two items
+ cx.workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 2));
+ cx.workspace(|workspace, cx| {
+ assert_active_item(workspace, "/root/dir/file2.rs", "This is file2.rs", cx);
+ });
+ }
}
@@ -55,7 +55,9 @@ pub use persistence::{
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::stream::Stream;
-use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use project::{
+ DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
+};
use serde::Deserialize;
use session::AppSession;
use settings::Settings;
@@ -2015,6 +2017,17 @@ impl Workspace {
})
}
+ pub fn open_resolved_path(
+ &mut self,
+ path: ResolvedPath,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+ match path {
+ ResolvedPath::ProjectPath(project_path) => self.open_path(project_path, None, true, cx),
+ ResolvedPath::AbsPath(path) => self.open_abs_path(path, false, cx),
+ }
+ }
+
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
let project = self.project.read(cx);
if project.is_remote() && project.dev_server_project_id().is_none() {