mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
vim: Add 'gf' command, make files cmd-clickable (#16534)
Release Notes: - vim: Added `gf` command to open files under the cursor. - Filenames can now be `cmd`/`ctrl`-clicked, which opens them. TODOs: - [x] `main_test.go` <-- works - [x] `./my-pkg/my_pkg.go` <-- works - [x] `../go.mod` <-- works - [x] `my-pkg/my_pkg.go` <-- works - [x] `my-pkg/subpkg/subpkg_test.go` <-- works - [x] `file\ with\ space\ in\ it.txt` <-- works - [x] `"file\ with\ space\ in\ it.txt"` <-- works - [x] `"main_test.go"` <-- works - [x] `/Users/thorstenball/.vimrc` <-- works, but only locally - [x] `~/.vimrc` <--works, but only locally - [x] Get it working over collab - [x] Get hover links working Demo: https://github.com/user-attachments/assets/26af7f3b-c392-4aaf-849a-95d6c3b00067 Collab demo: https://github.com/user-attachments/assets/272598bd-0e82-4556-8f9c-ba53d3a95682
This commit is contained in:
parent
1e39d407c2
commit
db0c1fd592
@ -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,6 +523,19 @@ pub fn show_link_definition(
|
||||
})
|
||||
.ok()
|
||||
} else if let Some(project) = project {
|
||||
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))
|
||||
});
|
||||
|
||||
Some((range, vec![HoverLink::File(filename)]))
|
||||
} else {
|
||||
// query the LSP for definition info
|
||||
project
|
||||
.update(&mut cx, |project, cx| match preferred_kind {
|
||||
@ -543,14 +557,20 @@ pub fn show_link_definition(
|
||||
excerpt_id,
|
||||
origin.range.start,
|
||||
)?;
|
||||
let end = snapshot
|
||||
.anchor_in_excerpt(excerpt_id, origin.range.end)?;
|
||||
let end = snapshot.anchor_in_excerpt(
|
||||
excerpt_id,
|
||||
origin.range.end,
|
||||
)?;
|
||||
Some(RangeInEditor::Text(start..end))
|
||||
})
|
||||
}),
|
||||
definition_result.into_iter().map(HoverLink::Text).collect(),
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user