mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-15 10:52:25 +00:00
Compare commits
22 commits
collab-sta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bfd2d7cf0 | ||
|
|
3565c49dad | ||
|
|
9eb5fa98f1 | ||
|
|
fa2c3794d0 | ||
|
|
c7b43b20b4 | ||
|
|
c28e37687d | ||
|
|
f181a2f47b | ||
|
|
febe242c0b | ||
|
|
b76e4db15d | ||
|
|
166f044fd0 | ||
|
|
88ce2d526e | ||
|
|
4a3e0af532 | ||
|
|
6b9f448ffc | ||
|
|
e3dd7b462e | ||
|
|
fca4016aef | ||
|
|
2838ea3f59 | ||
|
|
75ac36d92a | ||
|
|
48a0cbcfd8 | ||
|
|
3e76813825 | ||
|
|
ba0eea3f49 | ||
|
|
ef2430c8f3 | ||
|
|
72656afa6d |
136 changed files with 5192 additions and 2049 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -954,6 +954,7 @@ dependencies = [
|
|||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"which 6.0.3",
|
||||
"windows 0.61.3",
|
||||
"zeroize",
|
||||
]
|
||||
|
|
@ -4347,6 +4348,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"async-process",
|
||||
"crash-handler",
|
||||
"libc",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
"minidumper",
|
||||
|
|
@ -6318,6 +6320,7 @@ dependencies = [
|
|||
"log",
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"path",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"semver",
|
||||
|
|
@ -6907,6 +6910,7 @@ dependencies = [
|
|||
"log",
|
||||
"notify 9.0.0-rc.4",
|
||||
"parking_lot",
|
||||
"path",
|
||||
"paths",
|
||||
"proto",
|
||||
"rope",
|
||||
|
|
@ -9751,15 +9755,14 @@ dependencies = [
|
|||
"gpui_shared_string",
|
||||
"gpui_util",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"parking_lot",
|
||||
"path",
|
||||
"regex",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.23",
|
||||
"tree-sitter",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -12704,6 +12707,16 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "path"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dunce",
|
||||
"serde",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.3"
|
||||
|
|
@ -14039,6 +14052,7 @@ dependencies = [
|
|||
"markdown",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"path",
|
||||
"paths",
|
||||
"percent-encoding",
|
||||
"postage",
|
||||
|
|
@ -19696,9 +19710,11 @@ dependencies = [
|
|||
"num-format",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum 0.27.2",
|
||||
"theme",
|
||||
"theme_settings",
|
||||
"ui_macros",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
|
@ -19951,6 +19967,7 @@ dependencies = [
|
|||
"log",
|
||||
"mach2 0.5.0",
|
||||
"nix 0.29.0",
|
||||
"path",
|
||||
"percent-encoding",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.4",
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ members = [
|
|||
"crates/outline",
|
||||
"crates/outline_panel",
|
||||
"crates/panel",
|
||||
"crates/path",
|
||||
"crates/paths",
|
||||
"crates/picker",
|
||||
"crates/picker_preview",
|
||||
|
|
@ -414,6 +415,7 @@ outline = { path = "crates/outline" }
|
|||
outline_panel = { path = "crates/outline_panel" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
path = { path = "crates/path" }
|
||||
perf = { path = "tooling/perf" }
|
||||
picker = { path = "crates/picker" }
|
||||
picker_preview = { path = "crates/picker_preview" }
|
||||
|
|
|
|||
|
|
@ -1095,8 +1095,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_posix_paths_are_not_rewritten_as_windows_drives() {
|
||||
let parsed =
|
||||
MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse_hyperlink("/c/Projects/AGENTS.md", PathStyle::Unix).unwrap();
|
||||
match parsed {
|
||||
MentionUri::File { abs_path } => {
|
||||
assert_eq!(abs_path, PathBuf::from("/c/Projects/AGENTS.md"));
|
||||
|
|
@ -1107,7 +1106,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_hyperlink_percent_escapes_are_decoded() {
|
||||
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse_hyperlink("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
MentionUri::File {
|
||||
|
|
@ -1126,15 +1125,14 @@ mod tests {
|
|||
);
|
||||
|
||||
// Separator escapes stay encoded (no introduced path traversal).
|
||||
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse_hyperlink("/tmp/a%2Fb.rs", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
MentionUri::File {
|
||||
abs_path: PathBuf::from("/tmp/a%2Fb.rs")
|
||||
}
|
||||
);
|
||||
let parsed =
|
||||
MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse_hyperlink("/tmp/..%2F..%2Fsecret", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
MentionUri::File {
|
||||
|
|
@ -1145,7 +1143,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_keeps_bare_path_targets_verbatim() {
|
||||
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
MentionUri::File {
|
||||
|
|
@ -1165,7 +1163,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_hyperlink_literal_keeps_percent_escapes() {
|
||||
let literal =
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Posix).unwrap();
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
literal,
|
||||
MentionUri::File {
|
||||
|
|
@ -1175,7 +1173,7 @@ mod tests {
|
|||
|
||||
// Line suffixes still parse.
|
||||
let literal =
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Posix).unwrap();
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%20b.rs:42", PathStyle::Unix).unwrap();
|
||||
assert_eq!(
|
||||
literal,
|
||||
MentionUri::Selection {
|
||||
|
|
@ -1200,27 +1198,27 @@ mod tests {
|
|||
fn test_parse_hyperlink_literal_returns_none_when_unambiguous() {
|
||||
// No percent escapes: identical to `parse_hyperlink`.
|
||||
assert_eq!(
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Posix),
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a b.rs", PathStyle::Unix),
|
||||
None
|
||||
);
|
||||
// Invalid escape sequences are also left alone by `parse_hyperlink`.
|
||||
assert_eq!(
|
||||
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Posix),
|
||||
MentionUri::parse_hyperlink_literal("/tmp/100%_done.txt", PathStyle::Unix),
|
||||
None
|
||||
);
|
||||
// Separator escapes are never decoded, so they're not ambiguous.
|
||||
assert_eq!(
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Posix),
|
||||
MentionUri::parse_hyperlink_literal("/tmp/a%2Fb.rs", PathStyle::Unix),
|
||||
None
|
||||
);
|
||||
// URLs are spec-encoded, not ambiguous.
|
||||
assert_eq!(
|
||||
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Posix),
|
||||
MentionUri::parse_hyperlink_literal("file:///tmp/a%20b.rs", PathStyle::Unix),
|
||||
None
|
||||
);
|
||||
// Relative paths are not bare-path mentions.
|
||||
assert_eq!(
|
||||
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Posix),
|
||||
MentionUri::parse_hyperlink_literal("tmp/a%20b.rs", PathStyle::Unix),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
|
@ -1476,7 +1474,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_absolute_file_path_with_row() {
|
||||
let file_path = "/path/to/file.rs:42";
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
|
|
@ -1494,7 +1492,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_absolute_file_path_with_row_and_column() {
|
||||
let file_path = "/path/to/file.rs:42:5";
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
|
|
@ -1506,7 +1504,7 @@ mod tests {
|
|||
assert_eq!(line_range.end(), &41);
|
||||
assert_eq!(column, &Some(4));
|
||||
|
||||
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Posix)
|
||||
let parsed_again = MentionUri::parse(parsed.to_uri().as_ref(), PathStyle::Unix)
|
||||
.expect("selection URI with column should parse");
|
||||
assert_eq!(parsed_again, parsed.clone());
|
||||
}
|
||||
|
|
@ -1517,7 +1515,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_absolute_file_path_with_fragment_line() {
|
||||
let file_path = "/path/to/file.rs#L42";
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
|
|
@ -1589,7 +1587,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_backticked_absolute_file_path() {
|
||||
let file_path = "`/path/to/file.rs`";
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::File { abs_path } => {
|
||||
assert_eq!(abs_path, Path::new("/path/to/file.rs"));
|
||||
|
|
@ -1601,7 +1599,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_parse_backticked_absolute_file_path_with_fragment_line() {
|
||||
let file_path = "`/path/to/file.rs#L42`";
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Posix).unwrap();
|
||||
let parsed = MentionUri::parse(file_path, PathStyle::Unix).unwrap();
|
||||
match &parsed {
|
||||
MentionUri::Selection {
|
||||
abs_path: path,
|
||||
|
|
|
|||
|
|
@ -446,18 +446,22 @@ impl gpui::EventEmitter<SkillLoadingIssuesUpdated> for NativeAgent {}
|
|||
static RULES_FILE_REL_PATHS: LazyLock<Vec<Arc<RelPath>>> = LazyLock::new(|| {
|
||||
RULES_FILE_NAMES
|
||||
.iter()
|
||||
.filter_map(|name| RelPath::unix(name).ok().map(|path| path.into_arc()))
|
||||
.filter_map(|name| {
|
||||
RelPath::from_unix_str(name)
|
||||
.ok()
|
||||
.map(|path| path.into_arc())
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
|
||||
static AGENTS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
|
||||
RelPath::unix(AGENTS_DIR_NAME)
|
||||
RelPath::from_unix_str(AGENTS_DIR_NAME)
|
||||
.ok()
|
||||
.map(|path| path.into_arc())
|
||||
});
|
||||
|
||||
static SKILLS_PREFIX: LazyLock<Option<Arc<RelPath>>> = LazyLock::new(|| {
|
||||
RelPath::unix(project_skills_relative_path())
|
||||
RelPath::from_unix_str(project_skills_relative_path())
|
||||
.ok()
|
||||
.map(|path| path.into_arc())
|
||||
});
|
||||
|
|
@ -492,7 +496,7 @@ async fn expand_project_skills_directories(
|
|||
worktree: &Entity<Worktree>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let agents_dir = RelPath::unix(AGENTS_DIR_NAME)?;
|
||||
let agents_dir = RelPath::from_unix_str(AGENTS_DIR_NAME)?;
|
||||
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
|
@ -518,7 +522,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
|
|||
let Some(skills_prefix) = SKILLS_PREFIX.as_ref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Ok(skill_file_name) = RelPath::unix(SKILL_FILE_NAME) else {
|
||||
let Ok(skill_file_name) = RelPath::from_unix_str(SKILL_FILE_NAME) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
|
|
@ -538,7 +542,7 @@ fn project_skill_files_from_worktree(worktree: &Worktree) -> Vec<ProjectSkillFil
|
|||
|
||||
skill_files.push(ProjectSkillFile {
|
||||
display_path: worktree.absolutize(&relative_path),
|
||||
relative_path,
|
||||
relative_path: relative_path.into(),
|
||||
size: skill_file.size,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ mod tests {
|
|||
root_name: "my-project".to_string(),
|
||||
abs_path: std::path::Path::new("/tmp/my-project").into(),
|
||||
rules_file: Some(RulesFileContext {
|
||||
path_in_worktree: RelPath::unix("AGENTS.md").unwrap().into(),
|
||||
path_in_worktree: RelPath::from_unix_str("AGENTS.md").unwrap().into(),
|
||||
text: "project-specific guidance".to_string(),
|
||||
project_entry_id: 1,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1245,11 +1245,11 @@ fn resolve_path(
|
|||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|file_name| file_name.to_str())
|
||||
.and_then(|file_name| RelPath::unix(file_name).ok())
|
||||
.and_then(|file_name| RelPath::from_unix_str(file_name).ok())
|
||||
.ok_or_else(|| "Can't create file: invalid filename".to_string())?;
|
||||
|
||||
let new_file_path = parent_project_path.map(|parent| ProjectPath {
|
||||
path: parent.path.join(file_name),
|
||||
path: parent.path.join(file_name).into(),
|
||||
..parent
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ fn project_agents_md_path(
|
|||
require_existing_file: bool,
|
||||
cx: &App,
|
||||
) -> Option<PathBuf> {
|
||||
let rel_path = util::rel_path::RelPath::unix("AGENTS.md").ok()?;
|
||||
let rel_path = util::rel_path::RelPath::from_unix_str("AGENTS.md").ok()?;
|
||||
project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
|
|
|
|||
|
|
@ -8057,7 +8057,7 @@ impl ThreadView {
|
|||
|
||||
let tool_output_display = if is_open {
|
||||
match &tool_call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { options, .. } => {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => {
|
||||
let confirmation_content = v_flex()
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|
|
@ -8167,37 +8167,7 @@ impl ThreadView {
|
|||
)
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if layout == ToolCallLayout::Floating {
|
||||
// Cap the content (e.g. a full plan awaiting
|
||||
// approval) so the floating row can never
|
||||
// consume the entire panel and squeeze the
|
||||
// conversation list to zero height, while the
|
||||
// permission buttons below stay visible.
|
||||
this.child(
|
||||
div()
|
||||
.id(("floating-confirmation-content", entry_ix))
|
||||
.max_h_40()
|
||||
.overflow_y_scroll()
|
||||
.child(confirmation_content),
|
||||
)
|
||||
} else {
|
||||
this.child(confirmation_content)
|
||||
}
|
||||
})
|
||||
.child(self.render_permission_buttons(
|
||||
self.thread.read(cx).session_id().clone(),
|
||||
self.is_first_tool_call(active_session_id, &tool_call.id, cx),
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
focus_handle,
|
||||
self.sandbox_confusables_block_allow(tool_call, cx),
|
||||
cx,
|
||||
))
|
||||
.into_any()
|
||||
confirmation_content.into_any()
|
||||
}
|
||||
ToolCallStatus::Pending | ToolCallStatus::InProgress
|
||||
if is_edit
|
||||
|
|
@ -8295,35 +8265,23 @@ impl ThreadView {
|
|||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.map(|this| {
|
||||
if matches!(
|
||||
layout,
|
||||
ToolCallLayout::Embedded | ToolCallLayout::Floating
|
||||
) {
|
||||
this
|
||||
} else if use_card_layout {
|
||||
this.my_1p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.when(failed_or_canceled, |this| this.border_dashed())
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_hidden()
|
||||
} else {
|
||||
this.my_1()
|
||||
}
|
||||
})
|
||||
.when(layout == ToolCallLayout::Standalone, |this| {
|
||||
this.map(|this| {
|
||||
if has_location && !use_card_layout {
|
||||
this.ml_4()
|
||||
} else {
|
||||
this.ml_5()
|
||||
}
|
||||
})
|
||||
.mr_5()
|
||||
})
|
||||
let permission_buttons =
|
||||
if let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status {
|
||||
Some(self.render_permission_buttons(
|
||||
self.thread.read(cx).session_id().clone(),
|
||||
self.is_first_tool_call(active_session_id, &tool_call.id, cx),
|
||||
options,
|
||||
entry_ix,
|
||||
tool_call.id.clone(),
|
||||
focus_handle,
|
||||
self.sandbox_confusables_block_allow(tool_call, cx),
|
||||
cx,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let body = v_flex()
|
||||
.map(|this| {
|
||||
if is_terminal_tool {
|
||||
this.child(self.render_collapsible_command(
|
||||
|
|
@ -8498,7 +8456,48 @@ impl ThreadView {
|
|||
)
|
||||
}
|
||||
})
|
||||
.children(tool_output_display)
|
||||
.children(tool_output_display);
|
||||
|
||||
v_flex()
|
||||
.map(|this| {
|
||||
if matches!(layout, ToolCallLayout::Embedded | ToolCallLayout::Floating) {
|
||||
this
|
||||
} else if use_card_layout {
|
||||
this.my_1p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.when(failed_or_canceled, |this| this.border_dashed())
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.overflow_hidden()
|
||||
} else {
|
||||
this.my_1()
|
||||
}
|
||||
})
|
||||
.when(layout == ToolCallLayout::Standalone, |this| {
|
||||
this.map(|this| {
|
||||
if has_location && !use_card_layout {
|
||||
this.ml_4()
|
||||
} else {
|
||||
this.ml_5()
|
||||
}
|
||||
})
|
||||
.mr_5()
|
||||
})
|
||||
.map(|this| {
|
||||
if layout == ToolCallLayout::Floating {
|
||||
this.child(
|
||||
div()
|
||||
.id(("floating-tool-call-body", entry_ix))
|
||||
.max_h_40()
|
||||
.overflow_y_scroll()
|
||||
.child(body),
|
||||
)
|
||||
} else {
|
||||
this.child(body)
|
||||
}
|
||||
})
|
||||
.children(permission_buttons)
|
||||
}
|
||||
|
||||
/// A small "Learn more" link to the sandboxing docs, deep-linked to
|
||||
|
|
|
|||
|
|
@ -1248,8 +1248,7 @@ fn full_mention_for_directory(
|
|||
|(worktree_path, full_path): (Arc<RelPath>, String)| {
|
||||
let rel_path = worktree_path
|
||||
.strip_prefix(&directory_path)
|
||||
.log_err()
|
||||
.map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
|
||||
.map_or_else(|_| worktree_path.clone(), |rel_path| rel_path.into());
|
||||
|
||||
let open_task = project.update(cx, |project, cx| {
|
||||
project.buffer_store().update(cx, |buffer_store, cx| {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ tempfile.workspace = true
|
|||
util.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
which.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,9 @@ pub struct AskPassSession {
|
|||
#[cfg(not(target_os = "windows"))]
|
||||
const ASKPASS_SCRIPT_NAME: &str = "askpass.sh";
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const GPG_WRAPPER_SCRIPT_NAME: &str = "gpg-wrapper.sh";
|
||||
|
||||
impl AskPassSession {
|
||||
/// This will create a new AskPassSession.
|
||||
/// You must retain this session until the master process exits.
|
||||
|
|
@ -189,6 +192,15 @@ impl AskPassSession {
|
|||
self.askpass_task.script_path()
|
||||
}
|
||||
|
||||
/// Path to a script suitable for git's `gpg.program`, routing GnuPG
|
||||
/// passphrase prompts through Zed's askpass UI. `None` if unavailable.
|
||||
pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return self.askpass_task.gpg_wrapper_path();
|
||||
#[cfg(target_os = "windows")]
|
||||
return None;
|
||||
}
|
||||
|
||||
/// Returns the socket path to set as ZED_ASKPASS_SOCKET.
|
||||
///
|
||||
/// On Windows, SSH_ASKPASS points directly to cli.exe. SSH passes only
|
||||
|
|
@ -206,6 +218,8 @@ pub struct PasswordProxy {
|
|||
/// On Unix: path to the generated .sh askpass script (set as SSH_ASKPASS).
|
||||
/// On Windows: path to cli.exe (set as SSH_ASKPASS directly — no script needed).
|
||||
askpass_script_path: std::path::PathBuf,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
gpg_wrapper_script_path: Option<std::path::PathBuf>,
|
||||
/// On Windows only: path to the Unix socket, passed as ZED_ASKPASS_SOCKET
|
||||
/// so cli.exe can find it without --askpass argument parsing.
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
@ -238,6 +252,24 @@ impl PasswordProxy {
|
|||
|
||||
let askpass_socket_path = askpass_socket.clone();
|
||||
|
||||
// Create a gpg wrapper script that routes GnuPG passphrase prompts through
|
||||
// the same socket (and thus through Zed's askpass UI). This only works on
|
||||
// Unix where we control the pinentry via loopback mode. We compute the path
|
||||
// before the socket task takes ownership of `temp_dir`, and write the file
|
||||
// afterwards.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let (gpg_wrapper_script_path, gpg_wrapper_script) =
|
||||
match generate_gpg_wrapper_script(askpass_program, &askpass_socket_path) {
|
||||
Ok(script) => (
|
||||
Some(temp_dir.path().join(GPG_WRAPPER_SCRIPT_NAME)),
|
||||
Some(script),
|
||||
),
|
||||
Err(err) => {
|
||||
log::warn!("could not create gpg askpass wrapper: {err:#}");
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
let _task = executor.spawn(async move {
|
||||
maybe!(async move {
|
||||
let listener =
|
||||
|
|
@ -291,9 +323,36 @@ impl PasswordProxy {
|
|||
})?;
|
||||
}
|
||||
|
||||
// Write the gpg wrapper script (computed above) and mark it executable.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let gpg_wrapper_script_path =
|
||||
if let Some((path, script)) = gpg_wrapper_script_path.zip(gpg_wrapper_script) {
|
||||
match async {
|
||||
fs::write(&path, script)
|
||||
.await
|
||||
.with_context(|| format!("creating gpg wrapper script at {path:?}"))?;
|
||||
make_file_executable(&path).await.with_context(|| {
|
||||
format!("marking gpg wrapper script executable at {path:?}")
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(()) => Some(path),
|
||||
Err(err) => {
|
||||
log::warn!("could not write gpg askpass wrapper: {err:#}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
_task,
|
||||
askpass_script_path,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
gpg_wrapper_script_path,
|
||||
#[cfg(target_os = "windows")]
|
||||
askpass_socket_path,
|
||||
})
|
||||
|
|
@ -307,6 +366,11 @@ impl PasswordProxy {
|
|||
pub fn socket_path(&self) -> impl AsRef<OsStr> {
|
||||
&self.askpass_socket_path
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn gpg_wrapper_path(&self) -> Option<&std::path::Path> {
|
||||
self.gpg_wrapper_script_path.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs Zed in netcat mode for use in askpass.
|
||||
|
|
@ -398,3 +462,75 @@ fn generate_askpass_script(
|
|||
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_gpg_wrapper_script(
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let shell_kind = ShellKind::Posix;
|
||||
let gpg_program = find_gpg_program().context("could not find a gpg binary on PATH")?;
|
||||
let gpg_program = gpg_program
|
||||
.to_str()
|
||||
.context("gpg program is on a non-utf8 path")?;
|
||||
let gpg_program = shell_kind
|
||||
.try_quote_prefix_aware(gpg_program)
|
||||
.context("Failed to shell-escape gpg program path")?;
|
||||
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
|
||||
let prompt = shell_kind
|
||||
.try_quote_prefix_aware("Enter passphrase for your Git signing key:")
|
||||
.context("Failed to shell-escape gpg passphrase prompt")?;
|
||||
|
||||
// The wrapper inspects gpg's arguments and only prompts for a passphrase when
|
||||
// git asks it to *sign* (e.g. `gpg -bsau <key>`). Other invocations such as
|
||||
// signature verification (`--verify`) run gpg unchanged so we never pop a
|
||||
// spurious modal. When signing, we feed the passphrase to gpg on fd 3 via
|
||||
// `--passphrase-fd 3` in loopback mode, so no pinentry/terminal is required.
|
||||
Ok(format!(
|
||||
r#"#!/bin/sh
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
# Long-form signing options.
|
||||
--sign|--detach-sign|--clearsign|--clear-sign) is_signing=1 ;;
|
||||
# Skip other long options so flags like `--status-fd` don't match below.
|
||||
--*) ;;
|
||||
# Short-flag clusters containing `s`, e.g. git's `-bsau`.
|
||||
-*s*) is_signing=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Not a signing request: run gpg as-is, leaving its prompting untouched.
|
||||
if [ -z "${{is_signing}}" ]; then
|
||||
exec {gpg_program} "$@"
|
||||
fi
|
||||
|
||||
# Signing: ask Zed for the passphrase, then hand it to gpg on fd 3.
|
||||
passphrase=$(printf '%s\0' {prompt} | {askpass_program} --askpass={askpass_socket} 2>/dev/null)
|
||||
exec {gpg_program} --pinentry-mode loopback --passphrase-fd 3 "$@" 3<<EOF
|
||||
${{passphrase}}
|
||||
EOF
|
||||
"#,
|
||||
))
|
||||
}
|
||||
|
||||
/// Finds the real `gpg` (or `gpg2`) executable on `PATH`.
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn find_gpg_program() -> Option<std::path::PathBuf> {
|
||||
["gpg", "gpg2"]
|
||||
.into_iter()
|
||||
.find_map(|candidate| which::which(candidate).ok())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1077,7 +1077,7 @@ mod tests {
|
|||
.enumerate()
|
||||
.filter_map(|(i, path)| {
|
||||
Some((
|
||||
Arc::from(RelPath::unix(path).ok()?),
|
||||
Arc::from(RelPath::from_unix_str(path).ok()?),
|
||||
ProjectEntryId::from_proto(i as u64 + 1),
|
||||
PathChange::Added,
|
||||
))
|
||||
|
|
|
|||
|
|
@ -961,7 +961,7 @@ impl Database {
|
|||
let path_style = if project.windows_paths {
|
||||
PathStyle::Windows
|
||||
} else {
|
||||
PathStyle::Posix
|
||||
PathStyle::Unix
|
||||
};
|
||||
let features: Vec<String> = serde_json::from_str(&project.features).unwrap_or_default();
|
||||
|
||||
|
|
|
|||
|
|
@ -5139,6 +5139,109 @@ async fn test_definition(
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_prediction_definition(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"a.rs": "const ONE: usize = TWO;",
|
||||
"b.rs": "const TWO: usize = 2;",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
|
||||
let (buffer_b, _handle) = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.open_buffer_with_lsp((worktree_id, rel_path("a.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
fake_language_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(
|
||||
|_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
),
|
||||
)))
|
||||
},
|
||||
);
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let definitions = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.edit_prediction_definitions(&buffer_b, 19, false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_b.read(|cx| {
|
||||
assert_eq!(definitions.len(), 1);
|
||||
assert_eq!(
|
||||
definitions[0].path,
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path("b.rs").into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
definitions[0].range.start.0,
|
||||
language::PointUtf16::new(0, 6)
|
||||
);
|
||||
assert_eq!(definitions[0].range.end.0, language::PointUtf16::new(0, 9));
|
||||
assert!(
|
||||
project_b
|
||||
.read(cx)
|
||||
.get_open_buffer(&definitions[0].path, cx)
|
||||
.is_none()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_references(
|
||||
executor: BackgroundExecutor,
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
|||
.choose(rng)
|
||||
.unwrap();
|
||||
if entry.path.as_ref().is_empty() {
|
||||
worktree.root_name().into()
|
||||
worktree.root_name().to_rel_path_buf()
|
||||
} else {
|
||||
worktree.root_name().join(&entry.path)
|
||||
}
|
||||
|
|
@ -1524,7 +1524,7 @@ fn buffer_for_full_path(
|
|||
else {
|
||||
return false;
|
||||
};
|
||||
worktree.read(cx).root_name().join(&file.path()).as_ref() == full_path
|
||||
worktree.read(cx).root_name().join(&file.path()) == *full_path
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ serde_json.workspace = true
|
|||
system_specs.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
mach2.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,17 @@ where
|
|||
info!("crash signal handlers installed");
|
||||
send_crash_server_message(&client, CrashServerMessage::Init(crash_init));
|
||||
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
if let Some(address) = abort_message_address() {
|
||||
send_crash_server_message(
|
||||
&client,
|
||||
CrashServerMessage::AbortMessageLocation(AbortMessageLocation {
|
||||
pid: process::id(),
|
||||
address,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
handler.set_ptracer(Some(_crash_handler.id()));
|
||||
|
||||
|
|
@ -170,6 +181,7 @@ pub struct CrashServer {
|
|||
panic_info: Mutex<Option<CrashPanic>>,
|
||||
active_gpu: Mutex<Option<system_specs::GpuSpecs>>,
|
||||
user_info: Mutex<Option<UserInfo>>,
|
||||
abort_message_location: Mutex<Option<AbortMessageLocation>>,
|
||||
has_connection: Arc<AtomicBool>,
|
||||
logs_dir: PathBuf,
|
||||
}
|
||||
|
|
@ -179,11 +191,27 @@ pub struct CrashInfo {
|
|||
pub init: InitCrashHandler,
|
||||
pub panic: Option<CrashPanic>,
|
||||
pub minidump_error: Option<String>,
|
||||
/// The diagnostic the C runtime recorded before aborting the process, e.g.
|
||||
/// glibc's "free(): invalid pointer". Only present when the crash was a
|
||||
/// runtime-initiated abort rather than a signal like SIGSEGV or a panic.
|
||||
#[serde(default)]
|
||||
pub abort_message: Option<String>,
|
||||
pub gpus: Vec<system_specs::GpuInfo>,
|
||||
pub active_gpu: Option<system_specs::GpuSpecs>,
|
||||
pub user_info: Option<UserInfo>,
|
||||
}
|
||||
|
||||
/// Where to find the C runtime's abort diagnostic in the crashed process's
|
||||
/// memory. Sent by the client at startup so that after a crash the server can
|
||||
/// recover the message with `process_vm_readv`; the crashed process itself
|
||||
/// can't safely do this work, since its heap may be corrupt and its allocator
|
||||
/// locks may be held by the crashed thread.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
|
||||
pub struct AbortMessageLocation {
|
||||
pub pid: u32,
|
||||
pub address: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct InitCrashHandler {
|
||||
pub session_id: String,
|
||||
|
|
@ -233,6 +261,110 @@ enum CrashServerMessage {
|
|||
Panic(CrashPanic),
|
||||
GPUInfo(GpuSpecs),
|
||||
UserInfo(UserInfo),
|
||||
AbortMessageLocation(AbortMessageLocation),
|
||||
}
|
||||
|
||||
/// glibc records the diagnostic it prints just before aborting (malloc integrity
|
||||
/// failures like "free(): invalid pointer", assertion failures, stack-smashing
|
||||
/// reports) in the private global `__abort_msg`, specifically so it can be
|
||||
/// recovered post-mortem. Resolve its address here, in a safe context at startup.
|
||||
/// The symbol is only exported at the GLIBC_PRIVATE version, which plain `dlsym`
|
||||
/// won't resolve, and it has no stability guarantee, so a null result (e.g. musl,
|
||||
/// or a future glibc removing it) just disables this diagnostic.
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
fn abort_message_address() -> Option<u64> {
|
||||
let ptr = unsafe {
|
||||
libc::dlvsym(
|
||||
libc::RTLD_DEFAULT,
|
||||
c"__abort_msg".as_ptr(),
|
||||
c"GLIBC_PRIVATE".as_ptr(),
|
||||
)
|
||||
};
|
||||
std::ptr::NonNull::new(ptr).map(|ptr| ptr.as_ptr() as u64)
|
||||
}
|
||||
|
||||
/// Read the crashed process's abort diagnostic. `__abort_msg` points to a
|
||||
/// `struct abort_msg_s { unsigned int size; char msg[]; }` that glibc allocates
|
||||
/// with mmap so that it stays intact even when the heap is corrupt. `size` is
|
||||
/// the total byte size of that mapping (header included, rounded up to whole
|
||||
/// pages), not the message length; the message itself is NUL-terminated.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_abort_message(location: AbortMessageLocation) -> Option<String> {
|
||||
let pointer_bytes = read_process_memory(location.pid, location.address, size_of::<usize>())?;
|
||||
let message_address = usize::from_ne_bytes(pointer_bytes.try_into().ok()?) as u64;
|
||||
if message_address == 0 {
|
||||
return None;
|
||||
}
|
||||
let size_bytes = read_process_memory(location.pid, message_address, size_of::<u32>())?;
|
||||
let size = u32::from_ne_bytes(size_bytes.try_into().ok()?);
|
||||
let message_bytes = read_process_memory(
|
||||
location.pid,
|
||||
message_address + size_of::<u32>() as u64,
|
||||
abort_message_read_len(size)?,
|
||||
)?;
|
||||
parse_abort_message(&message_bytes)
|
||||
}
|
||||
|
||||
/// How many message bytes to read given the `size` field of glibc's
|
||||
/// `abort_msg_s`. `size` holds the total size of the mmap'd allocation, so a
|
||||
/// value that isn't a whole number of pages means the layout has changed and
|
||||
/// we shouldn't trust it. Reading is capped at (one page minus the header),
|
||||
/// which both bounds the work and ensures the read never extends past the end
|
||||
/// of the mapping.
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn abort_message_read_len(size: u32) -> Option<usize> {
|
||||
// Every Linux page size (4 KiB, 16 KiB, 64 KiB, ...) is a multiple of 4 KiB.
|
||||
const PAGE_MULTIPLE: usize = 4096;
|
||||
const MAX_READ: usize = 4096;
|
||||
|
||||
let size = size as usize;
|
||||
if size == 0 || !size.is_multiple_of(PAGE_MULTIPLE) {
|
||||
log::warn!("__abort_msg size field {size} is not page-rounded; layout may have changed");
|
||||
return None;
|
||||
}
|
||||
Some(size.min(MAX_READ) - size_of::<u32>())
|
||||
}
|
||||
|
||||
/// The message is NUL-terminated inside a zero-filled mapping, so truncate at
|
||||
/// the first NUL; `trim` alone would keep the padding, since NUL is not
|
||||
/// whitespace.
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn parse_abort_message(bytes: &[u8]) -> Option<String> {
|
||||
let len = bytes
|
||||
.iter()
|
||||
.position(|&byte| byte == 0)
|
||||
.unwrap_or(bytes.len());
|
||||
let message = String::from_utf8_lossy(&bytes[..len]).trim().to_string();
|
||||
(!message.is_empty()).then_some(message)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_process_memory(pid: u32, address: u64, len: usize) -> Option<Vec<u8>> {
|
||||
let mut buffer = vec![0u8; len];
|
||||
let local = libc::iovec {
|
||||
iov_base: buffer.as_mut_ptr().cast(),
|
||||
iov_len: len,
|
||||
};
|
||||
let remote = libc::iovec {
|
||||
iov_base: address as *mut libc::c_void,
|
||||
iov_len: len,
|
||||
};
|
||||
let bytes_read =
|
||||
unsafe { libc::process_vm_readv(pid as libc::pid_t, &local, 1, &remote, 1, 0) };
|
||||
if bytes_read < 0 {
|
||||
log::warn!(
|
||||
"process_vm_readv of {len} bytes at {address:#x} in pid {pid} failed: {}",
|
||||
io::Error::last_os_error()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if bytes_read as usize != len {
|
||||
log::warn!(
|
||||
"process_vm_readv short read at {address:#x} in pid {pid}: {bytes_read} of {len} bytes"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(buffer)
|
||||
}
|
||||
|
||||
impl minidumper::ServerHandler for CrashServer {
|
||||
|
|
@ -281,6 +413,14 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
}
|
||||
};
|
||||
|
||||
// The crashed process is still alive at this point: it stays parked in
|
||||
// its signal handler until the server acknowledges the dump request,
|
||||
// which happens after this callback returns.
|
||||
#[cfg(target_os = "linux")]
|
||||
let abort_message = (*self.abort_message_location.lock()).and_then(read_abort_message);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let abort_message = None;
|
||||
|
||||
let crash_info = CrashInfo {
|
||||
init: self
|
||||
.initialization_params
|
||||
|
|
@ -289,6 +429,7 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
.expect("not initialized"),
|
||||
panic: self.panic_info.lock().clone(),
|
||||
minidump_error,
|
||||
abort_message,
|
||||
active_gpu: self.active_gpu.lock().clone(),
|
||||
gpus,
|
||||
user_info: self.user_info.lock().clone(),
|
||||
|
|
@ -320,6 +461,9 @@ impl minidumper::ServerHandler for CrashServer {
|
|||
CrashServerMessage::UserInfo(user_info) => {
|
||||
self.user_info.lock().replace(user_info);
|
||||
}
|
||||
CrashServerMessage::AbortMessageLocation(location) => {
|
||||
self.abort_message_location.lock().replace(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -523,6 +667,7 @@ pub fn crash_server(socket: &Path, logs_dir: PathBuf) {
|
|||
initialization_params: Mutex::default(),
|
||||
panic_info: Mutex::default(),
|
||||
user_info: Mutex::default(),
|
||||
abort_message_location: Mutex::default(),
|
||||
has_connection,
|
||||
active_gpu: Mutex::default(),
|
||||
logs_dir,
|
||||
|
|
@ -532,3 +677,99 @@ pub fn crash_server(socket: &Path, logs_dir: PathBuf) {
|
|||
)
|
||||
.expect("failed to run server");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn abort_message_read_len_requires_page_rounded_total() {
|
||||
assert_eq!(abort_message_read_len(0), None);
|
||||
// A message length rather than a mapping total means the glibc layout
|
||||
// has changed out from under us.
|
||||
assert_eq!(abort_message_read_len(23), None);
|
||||
assert_eq!(abort_message_read_len(4097), None);
|
||||
// The read must stay within the mapping: one page minus the header.
|
||||
assert_eq!(abort_message_read_len(4096), Some(4092));
|
||||
// Larger totals (long messages, larger page sizes) are clamped.
|
||||
assert_eq!(abort_message_read_len(8192), Some(4092));
|
||||
assert_eq!(abort_message_read_len(65536), Some(4092));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_abort_message_truncates_at_nul() {
|
||||
let mut buffer = b"free(): invalid pointer\n\0".to_vec();
|
||||
buffer.resize(4092, 0);
|
||||
assert_eq!(
|
||||
parse_abort_message(&buffer),
|
||||
Some("free(): invalid pointer".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_abort_message_handles_missing_nul() {
|
||||
assert_eq!(
|
||||
parse_abort_message(b"double free or corruption (out)"),
|
||||
Some("double free or corruption (out)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_abort_message_rejects_empty() {
|
||||
assert_eq!(parse_abort_message(&[]), None);
|
||||
assert_eq!(parse_abort_message(&[0; 16]), None);
|
||||
assert_eq!(parse_abort_message(b"\n \0garbage after nul"), None);
|
||||
}
|
||||
|
||||
/// End-to-end check of `read_abort_message` against a synthetic
|
||||
/// `abort_msg_s` in this very process (`process_vm_readv` may always read
|
||||
/// one's own memory). The message page is followed by a `PROT_NONE` guard
|
||||
/// page so the test fails if the read ever extends past the mapping glibc
|
||||
/// would have allocated.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn read_abort_message_reads_glibc_layout_from_a_live_process() {
|
||||
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
|
||||
unsafe {
|
||||
let mapping = libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
2 * page_size,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_ANON | libc::MAP_PRIVATE,
|
||||
-1,
|
||||
0,
|
||||
);
|
||||
assert_ne!(mapping, libc::MAP_FAILED);
|
||||
assert_eq!(
|
||||
libc::mprotect(
|
||||
mapping.cast::<u8>().add(page_size).cast(),
|
||||
page_size,
|
||||
libc::PROT_NONE
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
mapping.cast::<u32>().write(page_size as u32);
|
||||
let message = b"free(): invalid pointer\n\0";
|
||||
std::ptr::copy_nonoverlapping(
|
||||
message.as_ptr(),
|
||||
mapping.cast::<u8>().add(size_of::<u32>()),
|
||||
message.len(),
|
||||
);
|
||||
|
||||
// Stands in for the `__abort_msg` global: a pointer variable whose
|
||||
// address we hand to the reader.
|
||||
let abort_msg: *mut libc::c_void = mapping;
|
||||
let location = AbortMessageLocation {
|
||||
pid: process::id(),
|
||||
address: (&raw const abort_msg) as u64,
|
||||
};
|
||||
assert_eq!(
|
||||
read_abort_message(location),
|
||||
Some("free(): invalid pointer".to_string())
|
||||
);
|
||||
|
||||
libc::munmap(mapping, 2 * page_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1090,14 +1090,14 @@ impl DebugPanel {
|
|||
directory_in_worktree: dir,
|
||||
..
|
||||
} => {
|
||||
let relative_path = if dir.ends_with(RelPath::unix(".vscode").unwrap()) {
|
||||
dir.join(RelPath::unix("launch.json").unwrap())
|
||||
let relative_path = if dir.ends_with(RelPath::from_unix_str(".vscode").unwrap()) {
|
||||
dir.join(RelPath::from_unix_str("launch.json").unwrap())
|
||||
} else {
|
||||
dir.join(RelPath::unix("debug.json").unwrap())
|
||||
dir.join(RelPath::from_unix_str("debug.json").unwrap())
|
||||
};
|
||||
ProjectPath {
|
||||
worktree_id: id,
|
||||
path: relative_path,
|
||||
path: relative_path.into(),
|
||||
}
|
||||
}
|
||||
_ => return self.save_scenario(scenario, worktree_id, window, cx),
|
||||
|
|
|
|||
|
|
@ -1069,10 +1069,10 @@ impl DebugDelegate {
|
|||
|
||||
match path.components().next_back() {
|
||||
Some(".zed") => {
|
||||
path.push(RelPath::unix("debug.json").unwrap());
|
||||
path.push(RelPath::from_unix_str("debug.json").unwrap());
|
||||
}
|
||||
Some(".vscode") => {
|
||||
path.push(RelPath::unix("launch.json").unwrap());
|
||||
path.push(RelPath::from_unix_str("launch.json").unwrap());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -1165,7 +1165,7 @@ impl DebugDelegate {
|
|||
id: _,
|
||||
directory_in_worktree: dir,
|
||||
id_base: _,
|
||||
} => dir.ends_with(RelPath::unix(".zed").unwrap()),
|
||||
} => dir.ends_with(RelPath::from_unix_str(".zed").unwrap()),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
|
|
@ -1186,7 +1186,8 @@ impl DebugDelegate {
|
|||
id_base: _,
|
||||
} => {
|
||||
!(hide_vscode
|
||||
&& dir.ends_with(RelPath::unix(".vscode").unwrap()))
|
||||
&& dir
|
||||
.ends_with(RelPath::from_unix_str(".vscode").unwrap()))
|
||||
}
|
||||
_ => true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ impl Render for RunningState {
|
|||
} else if let Some(active) = active {
|
||||
self.panes
|
||||
.render(
|
||||
None,
|
||||
None,
|
||||
&ActivePaneDecorator::new(active, &self.workspace),
|
||||
window,
|
||||
|
|
|
|||
|
|
@ -524,7 +524,7 @@ impl StackFrameList {
|
|||
.filter(|path| {
|
||||
// Since we do not know if we are debugging on the host or (a remote/WSL) target,
|
||||
// we need to check if either the path is absolute as Posix or Windows.
|
||||
is_absolute(path, PathStyle::Posix) || is_absolute(path, PathStyle::Windows)
|
||||
is_absolute(path, PathStyle::Unix) || is_absolute(path, PathStyle::Windows)
|
||||
})
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -172,13 +172,13 @@ pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<D
|
|||
pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig> {
|
||||
let mut configs = Vec::new();
|
||||
|
||||
let devcontainer_dir_path = RelPath::unix(".devcontainer").expect("valid path");
|
||||
let devcontainer_dir_path = RelPath::from_unix_str(".devcontainer").expect("valid path");
|
||||
|
||||
if let Some(devcontainer_entry) = snapshot.entry_for_path(devcontainer_dir_path) {
|
||||
if devcontainer_entry.is_dir() {
|
||||
log::debug!("find_configs_in_snapshot: Scanning .devcontainer directory");
|
||||
let devcontainer_json_path =
|
||||
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
|
||||
RelPath::from_unix_str(".devcontainer/devcontainer.json").expect("valid path");
|
||||
for entry in snapshot.child_entries(devcontainer_dir_path) {
|
||||
log::debug!(
|
||||
"find_configs_in_snapshot: Found entry: {:?}, is_file: {}, is_dir: {}",
|
||||
|
|
@ -199,7 +199,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
|
|||
|
||||
let config_json_path =
|
||||
format!("{}/devcontainer.json", entry.path.as_unix_str());
|
||||
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
|
||||
if let Ok(rel_config_path) = RelPath::from_unix_str(&config_json_path) {
|
||||
if snapshot.entry_for_path(rel_config_path).is_some() {
|
||||
log::debug!(
|
||||
"find_configs_in_snapshot: Found config in subfolder: {}",
|
||||
|
|
@ -223,7 +223,7 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
|
|||
|
||||
// Always include `.devcontainer.json` so the user can pick it from the UI
|
||||
// even when `.devcontainer/devcontainer.json` also exists.
|
||||
let root_config_path = RelPath::unix(".devcontainer.json").expect("valid path");
|
||||
let root_config_path = RelPath::from_unix_str(".devcontainer.json").expect("valid path");
|
||||
if snapshot
|
||||
.entry_for_path(root_config_path)
|
||||
.is_some_and(|entry| entry.is_file())
|
||||
|
|
@ -400,7 +400,7 @@ pub(crate) async fn apply_devcontainer_template(
|
|||
log::error!("Can't create relative path: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
})?;
|
||||
let rel_path = RelPath::unix(relative_path)
|
||||
let rel_path = RelPath::from_unix_str(relative_path)
|
||||
.map_err(|e| {
|
||||
log::error!("Can't create relative path: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
|
|
|
|||
|
|
@ -1579,11 +1579,12 @@ fn dispatch_apply_templates(
|
|||
};
|
||||
|
||||
if files.project_files.contains(&Arc::from(
|
||||
RelPath::unix(".devcontainer/devcontainer.json").unwrap(),
|
||||
RelPath::from_unix_str(".devcontainer/devcontainer.json").unwrap(),
|
||||
)) {
|
||||
let Some(workspace_task) = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let Ok(path) = RelPath::unix(".devcontainer/devcontainer.json") else {
|
||||
let Ok(path) = RelPath::from_unix_str(".devcontainer/devcontainer.json")
|
||||
else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Couldn't create path for .devcontainer/devcontainer.json"
|
||||
)));
|
||||
|
|
|
|||
|
|
@ -309,12 +309,12 @@ pub async fn refresh_worktree_entries(
|
|||
) -> Result<()> {
|
||||
let mut rel_paths = Vec::new();
|
||||
for path in paths {
|
||||
if let Ok(rel_path) = RelPath::new(path, PathStyle::Posix) {
|
||||
if let Ok(rel_path) = RelPath::new(path, PathStyle::Unix) {
|
||||
rel_paths.push(rel_path.into_arc());
|
||||
}
|
||||
|
||||
let path_without_root: PathBuf = path.components().skip(1).collect();
|
||||
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Posix) {
|
||||
if let Ok(rel_path) = RelPath::new(&path_without_root, PathStyle::Unix) {
|
||||
rel_paths.push(rel_path.into_arc());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ use futures::{FutureExt, StreamExt as _, channel::mpsc, future};
|
|||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, TaskExt, WeakEntity,
|
||||
};
|
||||
use language::{Anchor, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, ToOffset as _};
|
||||
use project::{LocationLink, Project, ProjectPath};
|
||||
use language::{Anchor, Bias, Buffer, BufferSnapshot, OffsetRangeExt as _, Point, ToOffset as _};
|
||||
use project::{EditPredictionDefinition, Project, ProjectPath};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
|
|
@ -77,20 +77,15 @@ struct Identifier {
|
|||
|
||||
enum DefinitionTask {
|
||||
CacheHit(Arc<CacheEntry>),
|
||||
CacheMiss(
|
||||
Task<
|
||||
Option<(
|
||||
Task<Result<Option<Vec<LocationLink>>>>,
|
||||
Task<Result<Option<Vec<LocationLink>>>>,
|
||||
)>,
|
||||
>,
|
||||
),
|
||||
CacheMiss {
|
||||
project: WeakEntity<Project>,
|
||||
task: Task<Result<Vec<EditPredictionDefinition>>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CacheEntry {
|
||||
definitions: SmallVec<[CachedDefinition; 1]>,
|
||||
type_definitions: SmallVec<[CachedDefinition; 1]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -183,7 +178,7 @@ impl RelatedExcerptStore {
|
|||
.path
|
||||
.strip_prefix(worktree.root_name().as_unix_str())
|
||||
.ok()?;
|
||||
let relative_path = RelPath::new(relative_path, PathStyle::Posix).ok()?;
|
||||
let relative_path = RelPath::new(relative_path, PathStyle::Unix).ok()?;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: relative_path.into_owned().into(),
|
||||
|
|
@ -312,34 +307,21 @@ impl RelatedExcerptStore {
|
|||
DefinitionTask::CacheHit(entry.clone())
|
||||
} else {
|
||||
let project = this.project.clone();
|
||||
let buffer = buffer.downgrade();
|
||||
DefinitionTask::CacheMiss(cx.spawn(async move |_, cx| {
|
||||
let buffer = buffer.upgrade()?;
|
||||
let definitions = project
|
||||
.update(cx, |project, cx| {
|
||||
project.workspace_definitions(
|
||||
&buffer,
|
||||
identifier.range.start,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
let type_definitions = project
|
||||
.update(cx, |project, cx| {
|
||||
// tombi LSP for toml will open a scratch buffer with the JSON schema of
|
||||
// the toml file when a goto type definition is requested
|
||||
if is_tombi_lsp_in_toml(project, &buffer, cx) {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
project.workspace_type_definitions(
|
||||
&buffer,
|
||||
identifier.range.start,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
Some((definitions, type_definitions))
|
||||
}))
|
||||
let task = project
|
||||
.update(cx, |project, cx| {
|
||||
// tombi LSP for toml will open a scratch buffer with the JSON schema of
|
||||
// the toml file when a goto type definition is requested
|
||||
let include_type_definitions =
|
||||
!is_tombi_lsp_in_toml(project, &buffer, cx);
|
||||
project.edit_prediction_definitions(
|
||||
&buffer,
|
||||
identifier.range.start,
|
||||
include_type_definitions,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|_| Task::ready(Ok(Vec::new())));
|
||||
DefinitionTask::CacheMiss { project, task }
|
||||
};
|
||||
|
||||
let cx = async_cx.clone();
|
||||
|
|
@ -348,50 +330,29 @@ impl RelatedExcerptStore {
|
|||
DefinitionTask::CacheHit(cache_entry) => {
|
||||
Some((identifier, cache_entry, None))
|
||||
}
|
||||
DefinitionTask::CacheMiss(task) => {
|
||||
let (definitions, type_definitions) = task.await?;
|
||||
let (definition_locations, type_definition_locations) =
|
||||
futures::join!(definitions, type_definitions);
|
||||
DefinitionTask::CacheMiss { project, task } => {
|
||||
let definition_locations = task.await.log_err().unwrap_or_default();
|
||||
let duration = start_time.elapsed();
|
||||
|
||||
let definition_locations =
|
||||
definition_locations.log_err().flatten().unwrap_or_default();
|
||||
let type_definition_locations = type_definition_locations
|
||||
.log_err()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
let definitions: SmallVec<[CachedDefinition; 1]> =
|
||||
definition_locations
|
||||
.into_iter()
|
||||
.filter_map(|location| {
|
||||
future::join_all(definition_locations.into_iter().map(
|
||||
|definition| {
|
||||
let project = project.clone();
|
||||
let mut cx = cx.clone();
|
||||
process_definition(location, &mut cx)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let type_definitions: SmallVec<[CachedDefinition; 1]> =
|
||||
type_definition_locations
|
||||
.into_iter()
|
||||
.filter_map(|location| {
|
||||
let mut cx = cx.clone();
|
||||
process_definition(location, &mut cx)
|
||||
})
|
||||
.filter(|type_def| {
|
||||
!definitions.iter().any(|def| {
|
||||
def.buffer.entity_id()
|
||||
== type_def.buffer.entity_id()
|
||||
&& def.anchor_range == type_def.anchor_range
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
async move {
|
||||
process_definition(definition, &project, &mut cx)
|
||||
.await
|
||||
}
|
||||
},
|
||||
))
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
Some((
|
||||
identifier,
|
||||
Arc::new(CacheEntry {
|
||||
definitions,
|
||||
type_definitions,
|
||||
}),
|
||||
Arc::new(CacheEntry { definitions }),
|
||||
Some(duration),
|
||||
))
|
||||
}
|
||||
|
|
@ -469,11 +430,7 @@ async fn rebuild_related_files(
|
|||
let mut snapshots = HashMap::default();
|
||||
let mut worktree_root_names = HashMap::default();
|
||||
for entry in new_entries.values() {
|
||||
for definition in entry
|
||||
.definitions
|
||||
.iter()
|
||||
.chain(entry.type_definitions.iter())
|
||||
{
|
||||
for definition in entry.definitions.iter() {
|
||||
if let hash_map::Entry::Vacant(e) = snapshots.entry(definition.buffer.entity_id()) {
|
||||
definition
|
||||
.buffer
|
||||
|
|
@ -510,11 +467,7 @@ async fn rebuild_related_files(
|
|||
.get(identifier)
|
||||
.copied()
|
||||
.unwrap_or(usize::MAX);
|
||||
for definition in entry
|
||||
.definitions
|
||||
.iter()
|
||||
.chain(entry.type_definitions.iter())
|
||||
{
|
||||
for definition in entry.definitions.iter() {
|
||||
let Some(snapshot) = snapshots.get(&definition.buffer.entity_id()) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -635,16 +588,24 @@ use language::ToPoint as _;
|
|||
|
||||
const MAX_TARGET_LEN: usize = 128;
|
||||
|
||||
fn process_definition(location: LocationLink, cx: &mut AsyncApp) -> Option<CachedDefinition> {
|
||||
async fn process_definition(
|
||||
definition: EditPredictionDefinition,
|
||||
project: &WeakEntity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<CachedDefinition> {
|
||||
let EditPredictionDefinition { path, range } = definition;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(path.clone(), cx))
|
||||
.ok()?
|
||||
.await
|
||||
.log_err()?;
|
||||
|
||||
cx.update(|cx| {
|
||||
let buffer = location.target.buffer;
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let file = buffer_snapshot.file()?;
|
||||
let path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
};
|
||||
let anchor_range = location.target.range;
|
||||
let target_start = buffer_snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let target_end = buffer_snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
let anchor_range =
|
||||
buffer_snapshot.anchor_after(target_start)..buffer_snapshot.anchor_before(target_end);
|
||||
|
||||
// If the target range is large, it likely means we requested the definition of an entire module.
|
||||
// For individual definitions, the target range should be small as it only covers the symbol.
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ use gpui::TestAppContext;
|
|||
use indoc::indoc;
|
||||
use language::{Point, ToPoint as _, rust_lang};
|
||||
use lsp::FakeLanguageServer;
|
||||
use project::{FakeFs, LocationLink, Project};
|
||||
use project::{FakeFs, LocationLink, Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::fmt::Write as _;
|
||||
use util::rel_path::rel_path;
|
||||
use util::{path, test::marked_text_ranges};
|
||||
|
||||
#[gpui::test]
|
||||
|
|
@ -561,9 +562,10 @@ async fn test_type_definition_deduplication(cx: &mut TestAppContext) {
|
|||
|
||||
// In this project the only identifier near the cursor whose type definition
|
||||
// resolves is `TypeA`, and its GotoTypeDefinition returns the exact same
|
||||
// location as GotoDefinition. After deduplication the CacheEntry for `TypeA`
|
||||
// should have an empty `type_definitions` vec, meaning the type-definition
|
||||
// path contributes nothing extra to the related-file output.
|
||||
// location as GotoDefinition. After the definitions and type definitions are
|
||||
// merged and deduped, the type-definition location is dropped, so it
|
||||
// contributes nothing extra to the related-file output (the target location
|
||||
// appears only once).
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
|
|
@ -638,6 +640,85 @@ async fn test_type_definition_deduplication(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_prediction_filters_raw_definitions_before_opening_buffers(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": indoc! {"
|
||||
// fake-definition-lsp-extra target /root/src/large.rs 0 0 0 129
|
||||
// fake-definition-lsp-extra target /root/src/valid.rs 0 3 0 9
|
||||
// fake-definition-lsp-extra target /outside.rs 0 3 0 10
|
||||
fn main() {
|
||||
target();
|
||||
}
|
||||
"},
|
||||
"valid.rs": "fn target() {}\n",
|
||||
"large.rs": format!("{}\n", "a".repeat(MAX_TARGET_LEN + 16)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.insert_file(path!("/outside.rs"), "fn outside() {}\n".into())
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let mut servers = setup_fake_lsp(&project, cx);
|
||||
|
||||
let (buffer, _handle) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _fake_language_server = servers.next().await.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
|
||||
related_excerpt_store.update(cx, |store, cx| {
|
||||
let position = {
|
||||
let buffer = buffer.read(cx);
|
||||
let offset = buffer
|
||||
.text()
|
||||
.find("target();")
|
||||
.expect("target call not found");
|
||||
buffer.anchor_before(offset)
|
||||
};
|
||||
|
||||
store.set_identifier_line_count(0);
|
||||
store.refresh(buffer.clone(), position, cx);
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(DEBOUNCE_DURATION);
|
||||
related_excerpt_store.update(cx, |store, cx| {
|
||||
assert_related_files(
|
||||
&store.related_files(cx),
|
||||
&[
|
||||
("root/src/valid.rs", &["fn target() {}"]),
|
||||
("root/src/main.rs", &["fn main() {\n target();\n}"]),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
let worktree_id = buffer.read_with(cx, |buffer, cx| {
|
||||
buffer.file().expect("buffer has file").worktree_id(cx)
|
||||
});
|
||||
let valid_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path("src/valid.rs").into(),
|
||||
};
|
||||
project.read_with(cx, |project, cx| {
|
||||
assert!(project.get_open_buffer(&valid_path, cx).is_some());
|
||||
assert_eq!(project.worktrees(cx).count(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_definitions_ranked_by_cursor_proximity(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
|
|
|||
|
|
@ -652,7 +652,7 @@ async fn collect_git_log_context(
|
|||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Posix) else {
|
||||
let Ok(related_path) = RelPath::new(&related_path, PathStyle::Unix) else {
|
||||
continue;
|
||||
};
|
||||
let project_path = ProjectPath {
|
||||
|
|
|
|||
|
|
@ -243,6 +243,12 @@ impl DefinitionIndex {
|
|||
.or_insert_with(Vec::new)
|
||||
.push(location);
|
||||
}
|
||||
for (name, location) in extract_extra_definition_locations(content) {
|
||||
self.definitions
|
||||
.entry(name)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(location);
|
||||
}
|
||||
|
||||
let type_annotations = extract_type_annotations(content)
|
||||
.into_iter()
|
||||
|
|
@ -303,6 +309,34 @@ impl DefinitionIndex {
|
|||
}
|
||||
}
|
||||
|
||||
fn extract_extra_definition_locations(content: &str) -> Vec<(String, lsp::Location)> {
|
||||
content
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line
|
||||
.trim()
|
||||
.strip_prefix("// fake-definition-lsp-extra ")?
|
||||
.split_whitespace();
|
||||
let name = parts.next()?.to_string();
|
||||
let path = PathBuf::from(parts.next()?);
|
||||
let start_row = parts.next()?.parse().ok()?;
|
||||
let start_column = parts.next()?.parse().ok()?;
|
||||
let end_row = parts.next()?.parse().ok()?;
|
||||
let end_column = parts.next()?.parse().ok()?;
|
||||
Some((
|
||||
name,
|
||||
lsp::Location::new(
|
||||
Uri::from_file_path(path).ok()?,
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(start_row, start_column),
|
||||
lsp::Position::new(end_row, end_column),
|
||||
),
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extracts `identifier_name -> type_name` mappings from field declarations
|
||||
/// and function parameters. For example, `owner: Arc<Person>` produces
|
||||
/// `"owner" -> "Person"` by unwrapping common generic wrappers.
|
||||
|
|
|
|||
|
|
@ -740,9 +740,9 @@ fn unused_image_path(
|
|||
let mut filename = format!("image.{extension}");
|
||||
let mut counter = 1u32;
|
||||
loop {
|
||||
let candidate = dir_rel_path.join(RelPath::unix(&filename).ok()?);
|
||||
let candidate = dir_rel_path.join(RelPath::from_unix_str(&filename).ok()?);
|
||||
if !exists(&candidate) {
|
||||
return Some((filename, candidate));
|
||||
return Some((filename, candidate.into()));
|
||||
}
|
||||
filename = format!("image_{counter}.{extension}");
|
||||
counter += 1;
|
||||
|
|
|
|||
|
|
@ -331,13 +331,12 @@ impl Editor {
|
|||
self.set_max_diagnostics_severity(new_severity, cx);
|
||||
if self.diagnostics_enabled {
|
||||
self.active_diagnostics = ActiveDiagnostic::None;
|
||||
self.refresh_inline_diagnostics(false, window, cx);
|
||||
} else {
|
||||
self.inline_diagnostics_update = Task::ready(());
|
||||
self.inline_diagnostics.clear();
|
||||
} else {
|
||||
self.refresh_inline_diagnostics(false, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn all_diagnostics_active(&self) -> bool {
|
||||
|
|
@ -464,6 +463,7 @@ impl Editor {
|
|||
{
|
||||
self.inline_diagnostics_update = Task::ready(());
|
||||
self.inline_diagnostics.clear();
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -596,3 +596,158 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
actions::{ToggleDiagnostics, ToggleInlineDiagnostics},
|
||||
editor_tests::init_test,
|
||||
test::editor_test_context::EditorTestContext,
|
||||
};
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use indoc::indoc;
|
||||
use language::DiagnosticSourceKind;
|
||||
use lsp::LanguageServerId;
|
||||
use settings::{DelayMs, SettingsStore};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{self, AtomicUsize},
|
||||
};
|
||||
use util::path;
|
||||
|
||||
fn setup_inline_diagnostics(cx: &mut EditorTestContext) {
|
||||
cx.update(|_, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let inline = settings
|
||||
.diagnostics
|
||||
.get_or_insert_default()
|
||||
.inline
|
||||
.get_or_insert_default();
|
||||
inline.enabled = Some(true);
|
||||
inline.update_debounce_ms = Some(DelayMs(0));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
fn func(abc dˇef: i32) -> u32 {
|
||||
}
|
||||
"});
|
||||
|
||||
let lsp_store =
|
||||
cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
|
||||
cx.update(|_, cx| {
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store
|
||||
.update_diagnostics(
|
||||
LanguageServerId(0),
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 12),
|
||||
lsp::Position::new(0, 15),
|
||||
),
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
message: "cannot find value `def`".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
None,
|
||||
DiagnosticSourceKind::Pushed,
|
||||
&[],
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(
|
||||
editor.inline_diagnostics.len(),
|
||||
1,
|
||||
"inline diagnostics should appear after the language server publishes them"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_diagnostics_refreshes_inline_diagnostics(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
setup_inline_diagnostics(&mut cx);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(
|
||||
editor.inline_diagnostics.len(),
|
||||
0,
|
||||
"inline diagnostics should be cleared after disabling diagnostics"
|
||||
);
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_diagnostics(&ToggleDiagnostics, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(
|
||||
editor.inline_diagnostics.len(),
|
||||
1,
|
||||
"inline diagnostics should reappear after re-enabling diagnostics, without further editor events"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggle_inline_diagnostics_notifies_on_hide(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
setup_inline_diagnostics(&mut cx);
|
||||
|
||||
let notify_count = Arc::new(AtomicUsize::new(0));
|
||||
let editor = cx.editor.clone();
|
||||
let _subscription = cx.update({
|
||||
let notify_count = notify_count.clone();
|
||||
move |_, cx| {
|
||||
cx.observe(&editor, move |_, _| {
|
||||
notify_count.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
|
||||
});
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(
|
||||
editor.inline_diagnostics.len(),
|
||||
0,
|
||||
"inline diagnostics should be cleared after toggling them off"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
notify_count.load(atomic::Ordering::SeqCst),
|
||||
1,
|
||||
"toggling inline diagnostics off should notify to repaint the editor"
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_inline_diagnostics(&ToggleInlineDiagnostics, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, _, _| {
|
||||
assert_eq!(
|
||||
editor.inline_diagnostics.len(),
|
||||
1,
|
||||
"inline diagnostics should reappear after toggling them back on"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38855,7 +38855,7 @@ fn test_hunk_key(file_path: &str) -> DiffHunkKey {
|
|||
file_path: if file_path.is_empty() {
|
||||
Arc::from(util::rel_path::RelPath::empty())
|
||||
} else {
|
||||
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
|
||||
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
|
||||
},
|
||||
hunk_start_anchor: Anchor::Min,
|
||||
}
|
||||
|
|
@ -38867,7 +38867,7 @@ fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
|
|||
file_path: if file_path.is_empty() {
|
||||
Arc::from(util::rel_path::RelPath::empty())
|
||||
} else {
|
||||
Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
|
||||
Arc::from(util::rel_path::RelPath::from_unix_str(file_path).unwrap())
|
||||
},
|
||||
hunk_start_anchor: anchor,
|
||||
}
|
||||
|
|
@ -39306,11 +39306,11 @@ fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
|
|||
// Create two different hunk keys (simulating two different files)
|
||||
let anchor = snapshot.anchor_before(Point::new(0, 0));
|
||||
let key1 = DiffHunkKey {
|
||||
file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
|
||||
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file1.rs").unwrap()),
|
||||
hunk_start_anchor: anchor,
|
||||
};
|
||||
let key2 = DiffHunkKey {
|
||||
file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
|
||||
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file2.rs").unwrap()),
|
||||
hunk_start_anchor: anchor,
|
||||
};
|
||||
|
||||
|
|
@ -39378,11 +39378,11 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
|
|||
|
||||
// Create two keys with the same file path and anchor
|
||||
let key1 = DiffHunkKey {
|
||||
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
|
||||
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
|
||||
hunk_start_anchor: anchor,
|
||||
};
|
||||
let key2 = DiffHunkKey {
|
||||
file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
|
||||
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("file.rs").unwrap()),
|
||||
hunk_start_anchor: anchor,
|
||||
};
|
||||
|
||||
|
|
@ -39399,7 +39399,7 @@ fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
|
|||
|
||||
// Create a key with different file path
|
||||
let different_file_key = DiffHunkKey {
|
||||
file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
|
||||
file_path: Arc::from(util::rel_path::RelPath::from_unix_str("other.rs").unwrap()),
|
||||
hunk_start_anchor: anchor,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ use std::{
|
|||
};
|
||||
use text::{BufferId, BufferSnapshot, OffsetRangeExt, Selection, ToPoint as _};
|
||||
use ui::{IconDecorationKind, prelude::*};
|
||||
use util::{ResultExt, TryFutureExt, paths::PathExt, rel_path::RelPath};
|
||||
use util::{ResultExt, TryFutureExt, debug_panic, paths::PathExt, rel_path::RelPath};
|
||||
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||
use workspace::{
|
||||
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||
|
|
@ -2100,6 +2100,75 @@ pub fn active_match_index(
|
|||
}
|
||||
}
|
||||
|
||||
/// Opens a path-like target (e.g. `items.rs:100:5`) in the workspace, moving the cursor
|
||||
/// to the one-based row/column if present. Returns whether the target was opened.
|
||||
pub async fn open_resolved_target(
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
open_target: &workspace::path_link::OpenTarget,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<bool> {
|
||||
let path_to_open = open_target.path();
|
||||
let mut opened_items = workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_paths(
|
||||
vec![path_to_open.path.clone()],
|
||||
workspace::OpenOptions {
|
||||
visible: Some(workspace::OpenVisible::OnlyDirectories),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.context("workspace update")?
|
||||
.await;
|
||||
if opened_items.len() != 1 {
|
||||
debug_panic!(
|
||||
"Received {} items for one path {path_to_open:?}",
|
||||
opened_items.len(),
|
||||
);
|
||||
}
|
||||
let Some(opened_item) = opened_items.pop() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if open_target.is_file() {
|
||||
let Some(opened_item) = opened_item else {
|
||||
return Ok(false);
|
||||
};
|
||||
let opened_item =
|
||||
opened_item.with_context(|| format!("opening {:?}", path_to_open.path))?;
|
||||
if let Some(row) = path_to_open.row
|
||||
&& let Some(editor) = opened_item.downcast::<Editor>()
|
||||
{
|
||||
let column = path_to_open.column.unwrap_or(0);
|
||||
editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||
let point = buffer.read(cx).snapshot().point_from_external_input(
|
||||
row.saturating_sub(1),
|
||||
column.saturating_sub(1),
|
||||
);
|
||||
editor.go_to_singleton_buffer_point(point, window, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
Ok(true)
|
||||
} else if open_target.is_dir() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |_, cx| {
|
||||
cx.emit(project::Event::ActivateProjectPanel);
|
||||
})
|
||||
})?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entry_label_color(selected: bool) -> Color {
|
||||
if selected {
|
||||
Color::Default
|
||||
|
|
@ -2221,14 +2290,14 @@ fn restore_serialized_buffer_contents(
|
|||
fn serialize_path_key(path_key: &PathKey) -> proto::PathKey {
|
||||
proto::PathKey {
|
||||
sort_prefix: path_key.sort_prefix,
|
||||
path: path_key.path.to_proto(),
|
||||
path: path_key.path.as_unix_str().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_path_key(path_key: proto::PathKey) -> Option<PathKey> {
|
||||
Some(PathKey {
|
||||
sort_prefix: path_key.sort_prefix,
|
||||
path: RelPath::from_proto(&path_key.path).ok()?,
|
||||
path: RelPath::from_unix_str(&path_key.path).ok()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -2396,7 +2465,8 @@ mod tests {
|
|||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
use util::{path, rel_path::RelPath};
|
||||
use util::{path, paths::PathWithPosition, rel_path::RelPath};
|
||||
use workspace::path_link::{OpenTarget, OpenTargetFoundBy};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut App) {
|
||||
|
|
@ -3038,6 +3108,62 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_resolved_target_at_non_ascii_column(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "first\naéøbc\n",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
|
||||
|
||||
let open_target = OpenTarget::Path(
|
||||
PathWithPosition {
|
||||
path: PathBuf::from(path!("/root/src/main.rs")),
|
||||
row: Some(2),
|
||||
column: Some(4),
|
||||
},
|
||||
false,
|
||||
OpenTargetFoundBy::BackgroundPathResolution,
|
||||
);
|
||||
|
||||
let opened = workspace
|
||||
.update_in(cx, |_, window, cx| {
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
open_resolved_target(&workspace, &open_target, cx).await
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("opening the target should succeed");
|
||||
assert!(opened, "target should open as a file");
|
||||
|
||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
.expect("active item should be an editor")
|
||||
});
|
||||
let cursor = editor.update_in(cx, |editor, _, cx| {
|
||||
editor
|
||||
.selections
|
||||
.newest::<language::Point>(&editor.display_snapshot(cx))
|
||||
.head()
|
||||
});
|
||||
// Column 4 is the fourth character of `aéøbc` (the `b`), which starts at byte 5.
|
||||
assert_eq!(cursor, language::Point::new(1, 5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_compute_modified_ranges_git_diff(cx: &mut gpui::TestAppContext) {
|
||||
let base_text = "line0\nline1\nline2\nline3\nline4\nline5\nline6\n";
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ language.workspace = true
|
|||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
path.workspace = true
|
||||
proto.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ pub trait Extension: Send + Sync + 'static {
|
|||
|
||||
/// Returns a path relative to this extension's working directory.
|
||||
fn path_from_extension(&self, path: &Path) -> PathBuf {
|
||||
util::normalize_path(&self.work_dir().join(path))
|
||||
path::normalize_path(&self.work_dir().join(path))
|
||||
}
|
||||
|
||||
async fn language_server_command(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use futures::{
|
|||
use heck::ToSnakeCase;
|
||||
use http_client::{self, AsyncBody, HttpClient};
|
||||
use language::LanguageConfig;
|
||||
use path::PathExt;
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
|
|
@ -20,7 +21,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, command::Stdio, rel_path::PathExt};
|
||||
use util::{ResultExt, command::Stdio};
|
||||
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
|
||||
use wasmparser::Parser;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
@ -11,7 +12,8 @@ use language::LanguageName;
|
|||
use lsp::LanguageServerName;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::rel_path::{PathExt, RelPathBuf};
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::{RelPath, RelPathBuf};
|
||||
|
||||
use crate::ExtensionCapability;
|
||||
|
||||
|
|
@ -211,9 +213,12 @@ pub fn build_debug_adapter_schema_path(
|
|||
) -> anyhow::Result<RelPathBuf> {
|
||||
match &meta.schema_path {
|
||||
Some(path) => Ok(path.clone()),
|
||||
None => Path::new("debug_adapter_schemas")
|
||||
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
|
||||
.to_rel_path_buf(),
|
||||
None => RelPath::new(
|
||||
&Path::new("debug_adapter_schemas")
|
||||
.join(Path::new(adapter_name.as_ref()).with_extension("json")),
|
||||
PathStyle::local(),
|
||||
)
|
||||
.map(Cow::into_owned),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ use std::{
|
|||
};
|
||||
use task::TaskTemplates;
|
||||
use url::Url;
|
||||
use util::{ResultExt, paths::RemotePathBuf, rel_path::PathExt};
|
||||
use util::{PathExt, ResultExt, paths::RemotePathBuf};
|
||||
use wasm_host::{
|
||||
WasmExtension, WasmHost,
|
||||
wit::{is_supported_wasm_api_version, wasm_api_version_range},
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ impl ExtensionImports for WasmState {
|
|||
self.on_main_thread(|cx| {
|
||||
async move {
|
||||
let path = location.as_ref().and_then(|location| {
|
||||
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
|
||||
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
|
||||
});
|
||||
let location = path
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@ impl HostWorktree for WasmState {
|
|||
) -> wasmtime::Result<Result<String, String>> {
|
||||
let delegate = self.table.get(&delegate)?;
|
||||
Ok(delegate
|
||||
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?)
|
||||
.read_text_file(&RelPath::new(Path::new(&path), PathStyle::Unix)?)
|
||||
.await
|
||||
.map_err(|error| error.to_string()))
|
||||
}
|
||||
|
|
@ -948,7 +948,7 @@ impl ExtensionImports for WasmState {
|
|||
self.on_main_thread(|cx| {
|
||||
async move {
|
||||
let path = location.as_ref().and_then(|location| {
|
||||
RelPath::new(Path::new(&location.path), PathStyle::Posix).ok()
|
||||
RelPath::new(Path::new(&location.path), PathStyle::Unix).ok()
|
||||
});
|
||||
let location = path
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -1272,7 +1272,11 @@ impl FileFinderDelegate {
|
|||
let full_path = if should_hide_root_in_entry_path(&worktree_store, cx) {
|
||||
entry_path.project.path.clone()
|
||||
} else {
|
||||
worktree.read(cx).root_name().join(&entry_path.project.path)
|
||||
worktree
|
||||
.read(cx)
|
||||
.root_name()
|
||||
.join(&entry_path.project.path)
|
||||
.into()
|
||||
};
|
||||
let mut components = full_path.components();
|
||||
let filename = components.next_back().unwrap_or("");
|
||||
|
|
@ -1841,7 +1845,7 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
.all(|worktree| {
|
||||
worktree
|
||||
.read(cx)
|
||||
.entry_for_path(RelPath::unix(prefix.split_at(1).0).unwrap())
|
||||
.entry_for_path(RelPath::from_unix_str(prefix.split_at(1).0).unwrap())
|
||||
.is_none_or(|entry| !entry.is_dir())
|
||||
})
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4584,7 +4584,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
|
|||
if let Some(path_match) = path_match.as_ref() {
|
||||
search_entries
|
||||
.history
|
||||
.push(path_match.0.path_prefix.join(&path_match.0.path));
|
||||
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
|
||||
} else {
|
||||
// This occurs when the query is empty and we show history matches
|
||||
// that are outside the project.
|
||||
|
|
@ -4597,7 +4597,7 @@ fn collect_search_matches(picker: &Picker<FileFinderDelegate>) -> SearchEntries
|
|||
Match::Search(path_match) => {
|
||||
search_entries
|
||||
.search
|
||||
.push(path_match.0.path_prefix.join(&path_match.0.path));
|
||||
.push(path_match.0.path_prefix.join(&path_match.0.path).into());
|
||||
search_entries.search_matches.push(path_match.0.clone());
|
||||
}
|
||||
Match::CreateNew(_) => {}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ tempfile.workspace = true
|
|||
text.workspace = true
|
||||
time.workspace = true
|
||||
util.workspace = true
|
||||
path.workspace = true
|
||||
is_executable = "1.0.5"
|
||||
notify = "9.0.0-rc.4"
|
||||
trash = { git = "https://github.com/zed-industries/trash-rs", rev = "47761739192828a66b11a94ba5420b82d63c03c5" }
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ use git::{
|
|||
status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||
};
|
||||
#[cfg(feature = "test-support")]
|
||||
use util::normalize_path;
|
||||
use path::normalize_path;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
use smol::io::AsyncReadExt;
|
||||
|
|
|
|||
|
|
@ -233,7 +233,11 @@ fn generate_nucleo_path_candidates(
|
|||
paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
fuzzy_nucleo::PathMatchCandidate::new(RelPath::unix(path).unwrap(), false, None)
|
||||
fuzzy_nucleo::PathMatchCandidate::new(
|
||||
RelPath::from_unix_str(path).unwrap(),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -245,7 +249,7 @@ fn generate_fuzzy_path_candidates(
|
|||
.iter()
|
||||
.map(|path| fuzzy::PathMatchCandidate {
|
||||
is_dir: false,
|
||||
path: RelPath::unix(path).unwrap(),
|
||||
path: RelPath::from_unix_str(path).unwrap(),
|
||||
char_bag: CharBag::from(path.as_str()),
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -302,7 +306,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
|
|||
query,
|
||||
case,
|
||||
size,
|
||||
PathStyle::Posix,
|
||||
PathStyle::Unix,
|
||||
)
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
|
|
@ -325,7 +329,7 @@ fn bench_path_matching(criterion: &mut Criterion) {
|
|||
query,
|
||||
false,
|
||||
size,
|
||||
PathStyle::Posix,
|
||||
PathStyle::Unix,
|
||||
)
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
|
|
|
|||
|
|
@ -1517,7 +1517,7 @@ impl GitRepository for RealGitRepository {
|
|||
let change = change?;
|
||||
let path = change.path;
|
||||
// git-show outputs `/`-delimited paths even on Windows.
|
||||
let Some(rel_path) = RelPath::unix(path).log_err() else {
|
||||
let Some(rel_path) = RelPath::from_unix_str(path).log_err() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
|
@ -3744,6 +3744,16 @@ async fn run_git_command(
|
|||
.env("GIT_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS", ask_pass.script_path())
|
||||
.env("SSH_ASKPASS_REQUIRE", "force");
|
||||
|
||||
if !env.contains_key("GIT_CONFIG_COUNT")
|
||||
&& let Some(gpg_wrapper) = ask_pass.gpg_wrapper_path()
|
||||
{
|
||||
command
|
||||
.env("GIT_CONFIG_COUNT", "1")
|
||||
.env("GIT_CONFIG_KEY_0", "gpg.program")
|
||||
.env("GIT_CONFIG_VALUE_0", gpg_wrapper);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
command.env("ZED_ASKPASS_SOCKET", ask_pass.socket_path());
|
||||
let git_process = command.spawn()?;
|
||||
|
|
@ -3793,7 +3803,7 @@ impl std::fmt::Debug for RepoPath {
|
|||
|
||||
impl RepoPath {
|
||||
pub fn new<S: AsRef<str> + ?Sized>(s: &S) -> Result<Self> {
|
||||
let rel_path = RelPath::unix(s.as_ref())?;
|
||||
let rel_path = RelPath::from_unix_str(s.as_ref())?;
|
||||
Ok(Self::from_rel_path(rel_path))
|
||||
}
|
||||
|
||||
|
|
@ -3803,7 +3813,7 @@ impl RepoPath {
|
|||
}
|
||||
|
||||
pub fn from_proto(proto: &str) -> Result<Self> {
|
||||
let rel_path = RelPath::from_proto(proto)?;
|
||||
let rel_path = RelPath::from_unix_str(proto)?.into();
|
||||
Ok(Self(rel_path))
|
||||
}
|
||||
|
||||
|
|
@ -3822,7 +3832,7 @@ impl RepoPath {
|
|||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn repo_path<S: AsRef<str> + ?Sized>(s: &S) -> RepoPath {
|
||||
RepoPath(RelPath::unix(s.as_ref()).unwrap().into())
|
||||
RepoPath(RelPath::from_unix_str(s.as_ref()).unwrap().into())
|
||||
}
|
||||
|
||||
impl AsRef<Arc<RelPath>> for RepoPath {
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ impl FromStr for GitStatus {
|
|||
let status = entry.as_bytes()[0..2].try_into().unwrap();
|
||||
let status = FileStatus::from_bytes(status).log_err()?;
|
||||
// git-status outputs `/`-delimited repo paths, even on Windows.
|
||||
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
|
||||
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path).log_err()?);
|
||||
Some((path, status))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
|
@ -544,7 +544,7 @@ impl FromStr for TreeDiff {
|
|||
let mut fields = s.split('\0');
|
||||
let mut parsed = HashMap::default();
|
||||
while let Some((status, path)) = fields.next().zip(fields.next()) {
|
||||
let path = RepoPath::from_rel_path(RelPath::unix(path)?);
|
||||
let path = RepoPath::from_rel_path(RelPath::from_unix_str(path)?);
|
||||
|
||||
let mut fields = status.split(" ").skip(2);
|
||||
let old_sha = fields
|
||||
|
|
|
|||
243
crates/git_ui/src/commit_context_menu.rs
Normal file
243
crates/git_ui/src/commit_context_menu.rs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
use crate::commit_view::CommitView;
|
||||
use git::Oid;
|
||||
use gpui::{Action, ClipboardItem, Entity, FocusHandle, SharedString, WeakEntity, Window, actions};
|
||||
use project::{GIT_COMMAND_TASK_TAG, git_store::Repository};
|
||||
|
||||
use task::{TaskContext, TaskVariables, VariableName};
|
||||
use ui::{Color, ContextMenu, ContextMenuEntry, IconName, IconPosition, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
git_graph,
|
||||
[
|
||||
/// Copies the SHA of the selected commit to the clipboard.
|
||||
CopyCommitSha,
|
||||
/// Copies a tag from the selected commit to the clipboard.
|
||||
CopyCommitTag,
|
||||
/// Opens the commit view for the selected commit.
|
||||
OpenCommitView,
|
||||
]
|
||||
);
|
||||
|
||||
const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.);
|
||||
const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands";
|
||||
|
||||
pub(crate) struct CommitContextMenuData {
|
||||
pub(crate) sha: Oid,
|
||||
pub(crate) tag_names: Vec<SharedString>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CommitContextMenuSource {
|
||||
GitGraph,
|
||||
GitPanel,
|
||||
}
|
||||
|
||||
pub(crate) fn commit_context_menu(
|
||||
commit: CommitContextMenuData,
|
||||
source: CommitContextMenuSource,
|
||||
ref_name: Option<SharedString>,
|
||||
focus_handle: FocusHandle,
|
||||
repository: Option<WeakEntity<Repository>>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<ContextMenu> {
|
||||
let sha = commit.sha;
|
||||
let sha_short = sha.display_short();
|
||||
let git_tasks = git_context_menu_tasks(
|
||||
git_task_context(&repository, sha, ref_name.as_deref(), cx),
|
||||
&workspace,
|
||||
cx,
|
||||
);
|
||||
let header = match &ref_name {
|
||||
Some(ref_name) => format!("Ref {ref_name}"),
|
||||
None => format!("Commit {sha_short}"),
|
||||
};
|
||||
|
||||
ContextMenu::build(window, cx, move |context_menu, _, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.header(header)
|
||||
.entry("View Commit", Some(OpenCommitView.boxed_clone()), {
|
||||
let repository = repository.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
let Some(repository) = repository.clone() else {
|
||||
return;
|
||||
};
|
||||
CommitView::open(
|
||||
sha.to_string(),
|
||||
repository,
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.entry(
|
||||
"Copy SHA",
|
||||
Some(CopyCommitSha.boxed_clone()),
|
||||
move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(sha.to_string()));
|
||||
},
|
||||
)
|
||||
.when_some(ref_name.clone(), |menu, ref_name| {
|
||||
menu.entry("Copy Ref Name", None, move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(ref_name.to_string()));
|
||||
})
|
||||
})
|
||||
.when(ref_name.is_none(), |menu| {
|
||||
menu.map(|menu| {
|
||||
let tag_names = commit.tag_names.clone();
|
||||
let copy_tag_label = "Copy Tag";
|
||||
|
||||
match tag_names.as_slice() {
|
||||
[] => menu.item(
|
||||
ContextMenuEntry::new(copy_tag_label)
|
||||
.action(CopyCommitTag.boxed_clone())
|
||||
.disabled(true),
|
||||
),
|
||||
[tag_name] => {
|
||||
let tag_name = tag_name.clone();
|
||||
let label = format!("{copy_tag_label}: {tag_name}");
|
||||
menu.entry(
|
||||
label,
|
||||
Some(CopyCommitTag.boxed_clone()),
|
||||
move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
tag_name.to_string(),
|
||||
));
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => menu.submenu(copy_tag_label, move |menu, _window, _cx| {
|
||||
let mut menu = menu.fixed_width(COMMIT_TAG_LIST_WIDTH_IN_REMS.into());
|
||||
|
||||
for tag_name in tag_names.clone() {
|
||||
let tag_name_to_copy = tag_name.clone();
|
||||
menu = menu.entry(tag_name, None, move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
tag_name_to_copy.to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
menu
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(source == CommitContextMenuSource::GitPanel, |menu| {
|
||||
menu.entry("Show in Git Graph", None, move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(crate::git_graph::OpenAtCommit {
|
||||
sha: sha.to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.map(|mut menu| {
|
||||
menu = menu.separator().header("Custom Commands");
|
||||
|
||||
if git_tasks.is_empty() {
|
||||
return menu.item(
|
||||
ContextMenuEntry::new("Learn More")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::End)
|
||||
.handler(|_window, cx| {
|
||||
let docs_url =
|
||||
release_channel::docs_url(CUSTOM_GIT_COMMANDS_DOCS_SLUG, cx);
|
||||
cx.open_url(&docs_url);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (task_source_kind, resolved_task) in git_tasks {
|
||||
let label = resolved_task.display_label().to_string();
|
||||
let workspace = workspace.clone();
|
||||
menu = menu.entry(label, None, move |window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.schedule_resolved_task(
|
||||
task_source_kind.clone(),
|
||||
resolved_task.clone(),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn git_task_context(
|
||||
repository: &Option<WeakEntity<Repository>>,
|
||||
commit_sha: git::Oid,
|
||||
ref_name: Option<&str>,
|
||||
cx: &App,
|
||||
) -> Option<TaskContext> {
|
||||
let repository_path = repository
|
||||
.as_ref()?
|
||||
.upgrade()?
|
||||
.read(cx)
|
||||
.work_directory_abs_path
|
||||
.to_path_buf();
|
||||
let repository_name = repository_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(ToString::to_string);
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::GitSha, commit_sha.to_string()),
|
||||
(VariableName::GitShaShort, commit_sha.display_short()),
|
||||
(
|
||||
VariableName::GitRepositoryPath,
|
||||
repository_path.to_string_lossy().into_owned(),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(repository_name) = repository_name {
|
||||
task_variables.insert(VariableName::GitRepositoryName, repository_name);
|
||||
}
|
||||
if let Some(ref_name) = ref_name {
|
||||
task_variables.insert(VariableName::GitRef, ref_name.to_string());
|
||||
}
|
||||
|
||||
Some(TaskContext {
|
||||
cwd: Some(repository_path),
|
||||
task_variables,
|
||||
..TaskContext::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn git_context_menu_tasks(
|
||||
task_context: Option<TaskContext>,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Vec<(project::TaskSourceKind, task::ResolvedTask)> {
|
||||
let Some(task_context) = task_context else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(workspace) = workspace.upgrade() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let task_inventory = project.read_with(cx, |project, cx| {
|
||||
project.task_store().read(cx).task_inventory().cloned()
|
||||
});
|
||||
let Some(task_inventory) = task_inventory else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
task_inventory
|
||||
.read(cx)
|
||||
.resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, &task_context)
|
||||
}
|
||||
|
|
@ -1007,7 +1007,7 @@ fn name_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
|
|||
return repo_path.into_arc();
|
||||
};
|
||||
let synthetic = format!("{}/{}", file_name, repo_path.as_unix_str());
|
||||
RelPath::unix(&synthetic)
|
||||
RelPath::from_unix_str(&synthetic)
|
||||
.map(|path| path.into_arc())
|
||||
.unwrap_or_else(|_| repo_path.into_arc())
|
||||
}
|
||||
|
|
@ -1032,7 +1032,7 @@ fn tree_sort_path(repo_path: &RelPath) -> Arc<RelPath> {
|
|||
}
|
||||
synthetic.push_str(component);
|
||||
}
|
||||
RelPath::unix(&synthetic)
|
||||
RelPath::from_unix_str(&synthetic)
|
||||
.map(|path| path.into_arc())
|
||||
.unwrap_or_else(|_| repo_path.into_arc())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
pub use crate::commit_context_menu::{CopyCommitSha, CopyCommitTag, OpenCommitView};
|
||||
use crate::{
|
||||
commit_context_menu::{CommitContextMenuData, CommitContextMenuSource, commit_context_menu},
|
||||
commit_tooltip::{CommitAvatar, CommitDetails, CommitTooltip},
|
||||
commit_view::CommitView,
|
||||
git_status_icon,
|
||||
|
|
@ -17,9 +19,9 @@ use git::{
|
|||
status::{FileStatus, StatusCode, TrackedStatus},
|
||||
};
|
||||
use gpui::{
|
||||
Action, Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength,
|
||||
DismissEvent, DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy,
|
||||
Anchor, AnyElement, App, Bounds, ClickEvent, ClipboardItem, DefiniteLength, DismissEvent,
|
||||
DragMoveEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
|
||||
MouseButton, MouseDownEvent, PathBuilder, Pixels, Point, ScrollHandle, ScrollStrategy,
|
||||
ScrollWheelEvent, SharedString, Subscription, Task, TextStyleRefinement,
|
||||
UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, point, prelude::*,
|
||||
px, uniform_list,
|
||||
|
|
@ -29,7 +31,7 @@ use markdown::{Markdown, MarkdownElement};
|
|||
use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
GIT_COMMAND_TASK_TAG, ProjectPath, TaskSourceKind,
|
||||
ProjectPath,
|
||||
git_store::{
|
||||
CommitDataState, GitGraphEvent, GitStore, GitStoreEvent, GraphDataResponse, Repository,
|
||||
RepositoryEvent, RepositoryId,
|
||||
|
|
@ -47,12 +49,12 @@ use std::{
|
|||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use task::{ResolvedTask, TaskContext, TaskVariables, VariableName};
|
||||
|
||||
use theme::AccentColors;
|
||||
use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem};
|
||||
use ui::{
|
||||
Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, ContextMenuEntry, DiffStat,
|
||||
Divider, HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing,
|
||||
Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider,
|
||||
HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing,
|
||||
RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState,
|
||||
TableRenderContext, TableResizeBehavior, Tooltip, WithScrollbar, bind_redistributable_columns,
|
||||
prelude::*, redistribute_hidden_fractions, redistribute_hidden_widths,
|
||||
|
|
@ -72,7 +74,6 @@ const LINE_WIDTH: Pixels = px(1.5);
|
|||
const RESIZE_HANDLE_WIDTH: f32 = 8.0;
|
||||
const COPIED_STATE_DURATION: Duration = Duration::from_secs(2);
|
||||
const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.);
|
||||
const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands";
|
||||
const TREE_INDENT: f32 = 20.0;
|
||||
const TABLE_COLUMN_COUNT: usize = 4;
|
||||
const ROW_VERTICAL_PADDING: Pixels = px(4.0);
|
||||
|
|
@ -580,12 +581,6 @@ actions!(
|
|||
[
|
||||
/// Opens the Git Graph Tab.
|
||||
Open,
|
||||
/// Copies the SHA of the selected commit to the clipboard.
|
||||
CopyCommitSha,
|
||||
/// Copies a tag from the selected commit to the clipboard.
|
||||
CopyCommitTag,
|
||||
/// Opens the commit view for the selected commit.
|
||||
OpenCommitView,
|
||||
/// Focuses the search field.
|
||||
FocusSearch,
|
||||
/// Focuses the next git graph tab stop.
|
||||
|
|
@ -2467,91 +2462,6 @@ impl GitGraph {
|
|||
self.copy_commit_tag(selected_entry_index, window, cx);
|
||||
}
|
||||
|
||||
fn git_task_context(
|
||||
&self,
|
||||
commit_sha: Oid,
|
||||
ref_name: Option<&str>,
|
||||
cx: &App,
|
||||
) -> Option<TaskContext> {
|
||||
let repository_path = self
|
||||
.get_repository(cx)?
|
||||
.read(cx)
|
||||
.work_directory_abs_path
|
||||
.to_path_buf();
|
||||
|
||||
let repository_name = repository_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(ToString::to_string);
|
||||
|
||||
let mut task_variables = TaskVariables::from_iter([
|
||||
(VariableName::GitSha, commit_sha.to_string()),
|
||||
(VariableName::GitShaShort, commit_sha.display_short()),
|
||||
(
|
||||
VariableName::GitRepositoryPath,
|
||||
repository_path.to_string_lossy().into_owned(),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(repository_name) = repository_name {
|
||||
task_variables.insert(VariableName::GitRepositoryName, repository_name);
|
||||
}
|
||||
|
||||
if let Some(ref_name) = ref_name {
|
||||
task_variables.insert(VariableName::GitRef, ref_name.to_string());
|
||||
}
|
||||
|
||||
Some(TaskContext {
|
||||
cwd: Some(repository_path),
|
||||
task_variables,
|
||||
..TaskContext::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn git_context_menu_tasks(
|
||||
&self,
|
||||
task_context: &TaskContext,
|
||||
cx: &App,
|
||||
) -> Vec<(TaskSourceKind, ResolvedTask)> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
|
||||
let task_inventory = project.read_with(cx, |project, cx| {
|
||||
project.task_store().read(cx).task_inventory().cloned()
|
||||
});
|
||||
|
||||
let Some(task_inventory) = task_inventory else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
task_inventory
|
||||
.read(cx)
|
||||
.resolve_global_tasks_with_tag(GIT_COMMAND_TASK_TAG, task_context)
|
||||
}
|
||||
|
||||
fn schedule_git_task(
|
||||
&mut self,
|
||||
task_source_kind: TaskSourceKind,
|
||||
resolved_task: ResolvedTask,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.schedule_resolved_task(
|
||||
task_source_kind,
|
||||
resolved_task,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn deploy_entry_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
|
|
@ -2563,129 +2473,27 @@ impl GitGraph {
|
|||
let Some(commit) = self.graph_data.commits.get(index) else {
|
||||
return;
|
||||
};
|
||||
let sha = commit.data.sha;
|
||||
let sha_short = sha.display_short();
|
||||
let git_tasks = self
|
||||
.git_task_context(sha, ref_name.as_deref(), cx)
|
||||
.map(|task_context| self.git_context_menu_tasks(&task_context, cx))
|
||||
.unwrap_or_default();
|
||||
|
||||
let header = match &ref_name {
|
||||
Some(ref_name) => format!("Ref {ref_name}"),
|
||||
None => format!("Commit {sha_short}"),
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let git_graph = cx.entity();
|
||||
let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
|
||||
context_menu
|
||||
.context(focus_handle)
|
||||
.header(header)
|
||||
.entry(
|
||||
"View Commit",
|
||||
Some(OpenCommitView.boxed_clone()),
|
||||
window.handler_for(&git_graph, move |this, window, cx| {
|
||||
this.open_commit_view(index, window, cx);
|
||||
}),
|
||||
)
|
||||
.entry(
|
||||
"Copy SHA",
|
||||
Some(CopyCommitSha.boxed_clone()),
|
||||
window.handler_for(&git_graph, move |this, _window, cx| {
|
||||
this.copy_commit_sha(index, cx);
|
||||
}),
|
||||
)
|
||||
.when_some(ref_name.clone(), |menu, ref_name| {
|
||||
menu.entry("Copy Ref Name", None, move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(ref_name.to_string()));
|
||||
})
|
||||
})
|
||||
.when(ref_name.is_none(), |menu| {
|
||||
menu.map(|menu| {
|
||||
let tag_names = commit
|
||||
.data
|
||||
.tag_names()
|
||||
.into_iter()
|
||||
.map(|tag_name| SharedString::from(tag_name.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let copy_tag_label = "Copy Tag";
|
||||
|
||||
match tag_names.as_slice() {
|
||||
[] => menu.item(
|
||||
ContextMenuEntry::new(copy_tag_label)
|
||||
.action(CopyCommitTag.boxed_clone())
|
||||
.disabled(true),
|
||||
),
|
||||
[tag_name] => {
|
||||
let tag_name = tag_name.clone();
|
||||
let label = format!("{copy_tag_label}: {tag_name}");
|
||||
menu.entry(
|
||||
label,
|
||||
Some(CopyCommitTag.boxed_clone()),
|
||||
move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
tag_name.to_string(),
|
||||
));
|
||||
},
|
||||
)
|
||||
}
|
||||
_ => menu.submenu(copy_tag_label, move |menu, _window, _cx| {
|
||||
let mut menu =
|
||||
menu.fixed_width(COMMIT_TAG_LIST_WIDTH_IN_REMS.into());
|
||||
|
||||
for tag_name in tag_names.clone() {
|
||||
let tag_name_to_copy = tag_name.clone();
|
||||
|
||||
menu = menu.entry(tag_name, None, move |_window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
tag_name_to_copy.to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
menu
|
||||
}),
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|mut menu| {
|
||||
menu = menu.separator().header("Custom Commands");
|
||||
|
||||
if git_tasks.is_empty() {
|
||||
return menu.item(
|
||||
ContextMenuEntry::new("Learn More")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::End)
|
||||
.handler(|_window, cx| {
|
||||
let docs_url = release_channel::docs_url(
|
||||
CUSTOM_GIT_COMMANDS_DOCS_SLUG,
|
||||
cx,
|
||||
);
|
||||
cx.open_url(&docs_url);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (task_source_kind, resolved_task) in git_tasks {
|
||||
let label = resolved_task.display_label().to_string();
|
||||
|
||||
menu = menu.entry(
|
||||
label,
|
||||
None,
|
||||
window.handler_for(&git_graph, move |this, window, cx| {
|
||||
this.schedule_git_task(
|
||||
task_source_kind.clone(),
|
||||
resolved_task.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
});
|
||||
let repository = self
|
||||
.get_repository(cx)
|
||||
.map(|repository| repository.downgrade());
|
||||
let context_menu = commit_context_menu(
|
||||
CommitContextMenuData {
|
||||
sha: commit.data.sha,
|
||||
tag_names: commit
|
||||
.data
|
||||
.tag_names()
|
||||
.into_iter()
|
||||
.map(|tag_name| SharedString::from(tag_name.to_string()))
|
||||
.collect(),
|
||||
},
|
||||
CommitContextMenuSource::GitGraph,
|
||||
ref_name,
|
||||
self.focus_handle.clone(),
|
||||
repository,
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.set_context_menu(context_menu, position, Some(index), window, cx);
|
||||
}
|
||||
|
||||
|
|
@ -4946,7 +4754,9 @@ mod tests {
|
|||
use git::repository::{CommitData, InitialGraphCommitData};
|
||||
use gpui::{TestAppContext, UpdateGlobal};
|
||||
use project::git_store::{GitStoreEvent, RepositoryEvent};
|
||||
use project::{Project, TaskSourceKind, task_store::TaskSettingsLocation};
|
||||
use project::{
|
||||
GIT_COMMAND_TASK_TAG, Project, TaskSourceKind, task_store::TaskSettingsLocation,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{SettingsStore, ThemeSettingsContent};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -36,6 +36,7 @@ use crate::{
|
|||
mod askpass_modal;
|
||||
pub mod branch_diff;
|
||||
pub mod branch_picker;
|
||||
mod commit_context_menu;
|
||||
mod commit_modal;
|
||||
pub mod commit_tooltip;
|
||||
pub mod commit_view;
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ fn register_entry(
|
|||
RelPath::new(rel, PathStyle::local())
|
||||
.map(|r| r.into_owned().into())
|
||||
.unwrap_or_else(|_| {
|
||||
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
|
||||
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
|
||||
.unwrap()
|
||||
.into_owned()
|
||||
.into()
|
||||
|
|
@ -105,10 +105,10 @@ fn register_entry(
|
|||
.new_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok())
|
||||
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Unix).ok())
|
||||
.map(|r| r.into_owned().into())
|
||||
.unwrap_or_else(|| {
|
||||
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Posix)
|
||||
RelPath::new(Path::new(MultiBuffer::DEFAULT_TITLE), PathStyle::Unix)
|
||||
.unwrap()
|
||||
.into_owned()
|
||||
.into()
|
||||
|
|
|
|||
|
|
@ -800,6 +800,9 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
|||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
fn start_window_move(&self) {}
|
||||
fn start_window_resize(&self, _edge: ResizeEdge) {}
|
||||
fn set_exclusive_zone(&self, _zone: Pixels) {}
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
fn set_exclusive_edge(&self, _edge: layer_shell::Anchor) {}
|
||||
fn set_input_region(&self, _region: Option<&[Bounds<Pixels>]>) {}
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
Decorations::Server
|
||||
|
|
|
|||
|
|
@ -1997,6 +1997,24 @@ impl Window {
|
|||
self.platform_window.request_decorations(decorations);
|
||||
}
|
||||
|
||||
/// Set the exclusive zone for a layer-shell surface: how much screen space it
|
||||
/// reserves so other surfaces avoid occluding it (e.g. a panel reserving space).
|
||||
/// Positive values reserve that distance from the anchored edge, 0 lets the
|
||||
/// surface be moved out of others' exclusive zones, and -1 ignores reserved
|
||||
/// space and may extend under other surfaces. (Wayland layer-shell windows only)
|
||||
pub fn set_exclusive_zone(&self, zone: Pixels) {
|
||||
self.platform_window.set_exclusive_zone(zone);
|
||||
}
|
||||
|
||||
/// Set which anchored edge a layer-shell surface's exclusive zone applies to.
|
||||
/// This is only needed to disambiguate a corner-anchored surface; otherwise the
|
||||
/// edge is deduced from the anchor. The edge must be a single edge the surface
|
||||
/// is anchored to, or it is ignored. (Wayland layer-shell windows only)
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
pub fn set_exclusive_edge(&self, edge: crate::layer_shell::Anchor) {
|
||||
self.platform_window.set_exclusive_edge(edge);
|
||||
}
|
||||
|
||||
/// Start a window resize operation (Wayland)
|
||||
pub fn start_window_resize(&self, edge: ResizeEdge) {
|
||||
self.platform_window.start_window_resize(edge);
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ use gpui::{
|
|||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, Scene, Size, Tiling,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls,
|
||||
WindowDecorations, WindowKind, WindowParams, layer_shell::LayerShellNotSupportedError,
|
||||
popup::PopupOptions, px, size,
|
||||
WindowDecorations, WindowKind, WindowParams,
|
||||
layer_shell::{Anchor, LayerShellNotSupportedError},
|
||||
popup::PopupOptions,
|
||||
px, size,
|
||||
};
|
||||
use gpui_wgpu::{CompositorGpuHint, WgpuRenderer, WgpuSurfaceConfig, wgpu};
|
||||
|
||||
|
|
@ -183,12 +185,12 @@ impl WaylandSurfaceState {
|
|||
}
|
||||
|
||||
if let Some(exclusive_edge) = options.exclusive_edge {
|
||||
layer_surface
|
||||
.set_exclusive_edge(super::layer_shell::wayland_anchor(exclusive_edge));
|
||||
Self::apply_exclusive_edge(&layer_surface, options.anchor, exclusive_edge);
|
||||
}
|
||||
|
||||
return Ok(WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState {
|
||||
layer_surface,
|
||||
anchor: options.anchor,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -298,6 +300,7 @@ pub struct WaylandXdgSurfaceState {
|
|||
|
||||
pub struct WaylandLayerSurfaceState {
|
||||
layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
anchor: Anchor,
|
||||
}
|
||||
|
||||
pub struct WaylandPopupSurfaceState {
|
||||
|
|
@ -452,6 +455,49 @@ impl WaylandSurfaceState {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_exclusive_zone(&self, zone: i32) -> bool {
|
||||
if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) =
|
||||
self
|
||||
{
|
||||
layer_surface.set_exclusive_zone(zone);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// An exclusive edge must be a single edge that the surface is anchored to,
|
||||
/// otherwise the compositor raises a fatal `invalid_exclusive_edge` protocol
|
||||
/// error. An invalid edge is logged and ignored. Returns whether it applied.
|
||||
fn apply_exclusive_edge(
|
||||
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
anchor: Anchor,
|
||||
edge: Anchor,
|
||||
) -> bool {
|
||||
if edge.bits().count_ones() == 1 && anchor.contains(edge) {
|
||||
layer_surface.set_exclusive_edge(super::layer_shell::wayland_anchor(edge));
|
||||
true
|
||||
} else {
|
||||
log::warn!(
|
||||
"ignoring exclusive edge {edge:?}: must be a single edge of the surface anchor {anchor:?}"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn set_exclusive_edge(&self, edge: Anchor) -> bool {
|
||||
if let WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState {
|
||||
layer_surface,
|
||||
anchor,
|
||||
..
|
||||
}) = self
|
||||
{
|
||||
Self::apply_exclusive_edge(layer_surface, *anchor, edge)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
|
||||
|
|
@ -470,7 +516,7 @@ impl WaylandSurfaceState {
|
|||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => {
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
|
||||
layer_surface.destroy();
|
||||
}
|
||||
WaylandSurfaceState::Popup(WaylandPopupSurfaceState {
|
||||
|
|
@ -1739,6 +1785,27 @@ impl PlatformWindow for WaylandWindow {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_exclusive_zone(&self, zone: Pixels) {
|
||||
let state = self.borrow();
|
||||
if state
|
||||
.surface_state
|
||||
.set_exclusive_zone(f32::from(zone) as i32)
|
||||
{
|
||||
// Commit to apply it immediately, otherwise it only takes effect
|
||||
// on the next frame.
|
||||
state.surface.commit();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_exclusive_edge(&self, edge: Anchor) {
|
||||
let state = self.borrow();
|
||||
if state.surface_state.set_exclusive_edge(edge) {
|
||||
// Commit to apply it immediately, otherwise it only takes effect
|
||||
// on the next frame.
|
||||
state.surface.commit();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_input_region(&self, region: Option<&[Bounds<Pixels>]>) {
|
||||
let state = self.borrow();
|
||||
match region {
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ use windows::Win32::{
|
|||
Foundation::{FILETIME, LPARAM, WPARAM},
|
||||
Media::{timeBeginPeriod, timeEndPeriod},
|
||||
System::Threading::{
|
||||
CloseThreadpoolTimer, CloseThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWork,
|
||||
GetCurrentThread, PTP_CALLBACK_INSTANCE, PTP_TIMER, PTP_WORK, SetThreadPriority,
|
||||
SetThreadpoolTimer, SubmitThreadpoolWork, THREAD_PRIORITY_TIME_CRITICAL,
|
||||
CloseThreadpoolTimer, CreateThreadpoolTimer, GetCurrentThread, PTP_CALLBACK_INSTANCE,
|
||||
PTP_TIMER, SetThreadPriority, SetThreadpoolTimer, THREAD_PRIORITY_TIME_CRITICAL,
|
||||
TP_CALLBACK_ENVIRON_V3, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH,
|
||||
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL,
|
||||
TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL, TrySubmitThreadpoolCallback,
|
||||
},
|
||||
UI::WindowsAndMessaging::PostMessageW,
|
||||
};
|
||||
|
|
@ -66,11 +65,8 @@ impl WindowsDispatcher {
|
|||
let context = runnable.into_raw().as_ptr() as *mut c_void;
|
||||
|
||||
unsafe {
|
||||
if let Ok(work) =
|
||||
CreateThreadpoolWork(Some(run_work_callback), Some(context), Some(&environ))
|
||||
{
|
||||
SubmitThreadpoolWork(work);
|
||||
}
|
||||
TrySubmitThreadpoolCallback(Some(run_work_callback), Some(context), Some(&environ))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,11 +175,9 @@ impl PlatformDispatcher for WindowsDispatcher {
|
|||
unsafe extern "system" fn run_work_callback(
|
||||
_instance: PTP_CALLBACK_INSTANCE,
|
||||
context: *mut c_void,
|
||||
work: PTP_WORK,
|
||||
) {
|
||||
let runnable = unsafe { RunnableVariant::from_raw(NonNull::new_unchecked(context as *mut ())) };
|
||||
WindowsDispatcher::execute_runnable(runnable);
|
||||
unsafe { CloseThreadpoolWork(work) };
|
||||
}
|
||||
|
||||
unsafe extern "system" fn run_timer_callback(
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ impl Item for ImageView {
|
|||
}
|
||||
|
||||
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
|
||||
let mut path = image.file.path().clone();
|
||||
let mut path = image.file.path().to_rel_path_buf();
|
||||
if project.visible_worktrees(cx).count() > 1
|
||||
&& let Some(worktree) = project.worktree_for_id(image.project_path(cx).worktree_id, cx)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1 +1,76 @@
|
|||
pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};
|
||||
use gpui::SharedString;
|
||||
use lsp::{DiagnosticSeverity, NumberOrString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// A diagnostic associated with a certain range of a buffer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Diagnostic {
|
||||
/// The name of the service that produced this diagnostic.
|
||||
pub source: Option<String>,
|
||||
/// The ID provided by the dynamic registration that produced this diagnostic.
|
||||
pub registration_id: Option<SharedString>,
|
||||
/// A machine-readable code that identifies this diagnostic.
|
||||
pub code: Option<NumberOrString>,
|
||||
pub code_description: Option<lsp::Uri>,
|
||||
/// Whether this diagnostic is a hint, warning, or error.
|
||||
pub severity: DiagnosticSeverity,
|
||||
/// The human-readable message associated with this diagnostic.
|
||||
pub message: String,
|
||||
/// The human-readable message (in markdown format)
|
||||
pub markdown: Option<String>,
|
||||
/// An id that identifies the group to which this diagnostic belongs.
|
||||
///
|
||||
/// When a language server produces a diagnostic with
|
||||
/// one or more associated diagnostics, those diagnostics are all
|
||||
/// assigned a single group ID.
|
||||
pub group_id: usize,
|
||||
/// Whether this diagnostic is the primary diagnostic for its group.
|
||||
///
|
||||
/// In a given group, the primary diagnostic is the top-level diagnostic
|
||||
/// returned by the language server. The non-primary diagnostics are the
|
||||
/// associated diagnostics.
|
||||
pub is_primary: bool,
|
||||
/// Whether this diagnostic is considered to originate from an analysis of
|
||||
/// files on disk, as opposed to any unsaved buffer contents. This is a
|
||||
/// property of a given diagnostic source, and is configured for a given
|
||||
/// language server via the `LspAdapter::disk_based_diagnostic_sources` method
|
||||
/// for the language server.
|
||||
pub is_disk_based: bool,
|
||||
/// Whether this diagnostic marks unnecessary code.
|
||||
pub is_unnecessary: bool,
|
||||
/// Quick separation of diagnostics groups based by their source.
|
||||
pub source_kind: DiagnosticSourceKind,
|
||||
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
|
||||
pub data: Option<Value>,
|
||||
/// Whether to underline the corresponding text range in the editor.
|
||||
pub underline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DiagnosticSourceKind {
|
||||
Pulled,
|
||||
Pushed,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Default for Diagnostic {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: Default::default(),
|
||||
source_kind: DiagnosticSourceKind::Other,
|
||||
code: None,
|
||||
code_description: None,
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: Default::default(),
|
||||
markdown: None,
|
||||
group_id: 0,
|
||||
is_primary: false,
|
||||
is_disk_based: false,
|
||||
is_unnecessary: false,
|
||||
underline: true,
|
||||
data: None,
|
||||
registration_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ use futures::lock::OwnedMutexGuard;
|
|||
use gpui::{App, AsyncApp, Entity};
|
||||
use http_client::HttpClient;
|
||||
|
||||
pub use language_core::highlight_map::{HighlightId, HighlightMap};
|
||||
pub use language_core::{
|
||||
SymbolKind,
|
||||
highlight_map::{HighlightId, HighlightMap},
|
||||
};
|
||||
|
||||
use futures::future::FutureExt as _;
|
||||
pub use language_core::{
|
||||
|
|
@ -48,10 +51,10 @@ pub use language_core::{
|
|||
DecreaseIndentConfig, Grammar, GrammarId, HighlightsConfig, IndentConfig, InjectionConfig,
|
||||
InjectionPatternConfig, JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride,
|
||||
LanguageId, LanguageMatcher, OrderedListConfig, OutlineConfig, Override, OverrideConfig,
|
||||
OverrideEntry, PromptResponseContext, RedactionConfig, RunnableCapture, RunnableConfig,
|
||||
SoftWrap, Symbol, TaskListConfig, TextObject, TextObjectConfig, ToLspPosition,
|
||||
WrapCharactersConfig, auto_indent_using_last_non_empty_line_default, deserialize_regex,
|
||||
deserialize_regex_vec, regex_json_schema, regex_vec_json_schema, serialize_regex,
|
||||
OverrideEntry, RedactionConfig, RunnableCapture, RunnableConfig, SoftWrap, Symbol,
|
||||
TaskListConfig, TextObject, TextObjectConfig, WrapCharactersConfig, default_true,
|
||||
deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema,
|
||||
serialize_regex,
|
||||
};
|
||||
pub use language_registry::{
|
||||
LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,
|
||||
|
|
@ -216,6 +219,69 @@ pub static PLAIN_TEXT: LazyLock<Arc<Language>> = LazyLock::new(|| {
|
|||
))
|
||||
});
|
||||
|
||||
pub fn symbol_kind_to_lsp(kind: SymbolKind) -> lsp::SymbolKind {
|
||||
match kind {
|
||||
SymbolKind::File => lsp::SymbolKind::FILE,
|
||||
SymbolKind::Module => lsp::SymbolKind::MODULE,
|
||||
SymbolKind::Namespace => lsp::SymbolKind::NAMESPACE,
|
||||
SymbolKind::Package => lsp::SymbolKind::PACKAGE,
|
||||
SymbolKind::Class => lsp::SymbolKind::CLASS,
|
||||
SymbolKind::Method => lsp::SymbolKind::METHOD,
|
||||
SymbolKind::Property => lsp::SymbolKind::PROPERTY,
|
||||
SymbolKind::Field => lsp::SymbolKind::FIELD,
|
||||
SymbolKind::Constructor => lsp::SymbolKind::CONSTRUCTOR,
|
||||
SymbolKind::Enum => lsp::SymbolKind::ENUM,
|
||||
SymbolKind::Interface => lsp::SymbolKind::INTERFACE,
|
||||
SymbolKind::Function => lsp::SymbolKind::FUNCTION,
|
||||
SymbolKind::Variable => lsp::SymbolKind::VARIABLE,
|
||||
SymbolKind::Constant => lsp::SymbolKind::CONSTANT,
|
||||
SymbolKind::String => lsp::SymbolKind::STRING,
|
||||
SymbolKind::Number => lsp::SymbolKind::NUMBER,
|
||||
SymbolKind::Boolean => lsp::SymbolKind::BOOLEAN,
|
||||
SymbolKind::Array => lsp::SymbolKind::ARRAY,
|
||||
SymbolKind::Object => lsp::SymbolKind::OBJECT,
|
||||
SymbolKind::Key => lsp::SymbolKind::KEY,
|
||||
SymbolKind::Null => lsp::SymbolKind::NULL,
|
||||
SymbolKind::EnumMember => lsp::SymbolKind::ENUM_MEMBER,
|
||||
SymbolKind::Struct => lsp::SymbolKind::STRUCT,
|
||||
SymbolKind::Event => lsp::SymbolKind::EVENT,
|
||||
SymbolKind::Operator => lsp::SymbolKind::OPERATOR,
|
||||
SymbolKind::TypeParameter => lsp::SymbolKind::TYPE_PARAMETER,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lsp_to_symbol_kind(kind: lsp::SymbolKind) -> SymbolKind {
|
||||
match kind {
|
||||
lsp::SymbolKind::FILE => SymbolKind::File,
|
||||
lsp::SymbolKind::MODULE => SymbolKind::Module,
|
||||
lsp::SymbolKind::NAMESPACE => SymbolKind::Namespace,
|
||||
lsp::SymbolKind::PACKAGE => SymbolKind::Package,
|
||||
lsp::SymbolKind::CLASS => SymbolKind::Class,
|
||||
lsp::SymbolKind::METHOD => SymbolKind::Method,
|
||||
lsp::SymbolKind::PROPERTY => SymbolKind::Property,
|
||||
lsp::SymbolKind::FIELD => SymbolKind::Field,
|
||||
lsp::SymbolKind::CONSTRUCTOR => SymbolKind::Constructor,
|
||||
lsp::SymbolKind::ENUM => SymbolKind::Enum,
|
||||
lsp::SymbolKind::INTERFACE => SymbolKind::Interface,
|
||||
lsp::SymbolKind::FUNCTION => SymbolKind::Function,
|
||||
lsp::SymbolKind::VARIABLE => SymbolKind::Variable,
|
||||
lsp::SymbolKind::CONSTANT => SymbolKind::Constant,
|
||||
lsp::SymbolKind::STRING => SymbolKind::String,
|
||||
lsp::SymbolKind::NUMBER => SymbolKind::Number,
|
||||
lsp::SymbolKind::BOOLEAN => SymbolKind::Boolean,
|
||||
lsp::SymbolKind::ARRAY => SymbolKind::Array,
|
||||
lsp::SymbolKind::OBJECT => SymbolKind::Object,
|
||||
lsp::SymbolKind::KEY => SymbolKind::Key,
|
||||
lsp::SymbolKind::NULL => SymbolKind::Null,
|
||||
lsp::SymbolKind::ENUM_MEMBER => SymbolKind::EnumMember,
|
||||
lsp::SymbolKind::STRUCT => SymbolKind::Struct,
|
||||
lsp::SymbolKind::EVENT => SymbolKind::Event,
|
||||
lsp::SymbolKind::OPERATOR => SymbolKind::Operator,
|
||||
lsp::SymbolKind::TYPE_PARAMETER => SymbolKind::TypeParameter,
|
||||
_ => SymbolKind::Null,
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands that the client (editor) handles locally rather than forwarding
|
||||
/// to the language server. Servers embed these in code lens and code action
|
||||
/// responses when they want the editor to perform a well-known UI action.
|
||||
|
|
@ -233,6 +299,17 @@ pub struct Location {
|
|||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
|
||||
/// This allows adapters to intercept preference selections (like "Always" or "Never")
|
||||
/// and potentially persist them to Zed's settings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptResponseContext {
|
||||
/// The original message shown to the user
|
||||
pub message: String,
|
||||
/// The action (button) the user selected
|
||||
pub selected_action: lsp::MessageActionItem,
|
||||
}
|
||||
|
||||
type ServerBinaryCache = futures::lock::Mutex<Option<(bool, LanguageServerBinary)>>;
|
||||
type DownloadableLanguageServerBinary = LocalBoxFuture<'static, Result<LanguageServerBinary>>;
|
||||
pub type LanguageServerBinaryLocations = LocalBoxFuture<
|
||||
|
|
|
|||
|
|
@ -11,17 +11,16 @@ path = "src/language_core.rs"
|
|||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
gpui_shared_string.workspace = true
|
||||
gpui_util.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
path.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
util.workspace = true
|
||||
gpui_util.workspace = true
|
||||
lsp-types.workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
|
|
|||
|
|
@ -1,10 +1,74 @@
|
|||
use crate::highlight_map::HighlightId;
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum SymbolKind {
|
||||
File,
|
||||
Module,
|
||||
Namespace,
|
||||
Package,
|
||||
Class,
|
||||
Method,
|
||||
Property,
|
||||
Field,
|
||||
Constructor,
|
||||
Enum,
|
||||
Interface,
|
||||
Function,
|
||||
Variable,
|
||||
Constant,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
Key,
|
||||
Null,
|
||||
EnumMember,
|
||||
Struct,
|
||||
Event,
|
||||
Operator,
|
||||
TypeParameter,
|
||||
}
|
||||
|
||||
impl SymbolKind {
|
||||
pub fn from_proto(i32: i32) -> Self {
|
||||
match i32 {
|
||||
1 => SymbolKind::File,
|
||||
2 => SymbolKind::Module,
|
||||
3 => SymbolKind::Namespace,
|
||||
4 => SymbolKind::Package,
|
||||
5 => SymbolKind::Class,
|
||||
6 => SymbolKind::Method,
|
||||
7 => SymbolKind::Property,
|
||||
8 => SymbolKind::Field,
|
||||
9 => SymbolKind::Constructor,
|
||||
10 => SymbolKind::Enum,
|
||||
11 => SymbolKind::Interface,
|
||||
12 => SymbolKind::Function,
|
||||
13 => SymbolKind::Variable,
|
||||
14 => SymbolKind::Constant,
|
||||
15 => SymbolKind::String,
|
||||
16 => SymbolKind::Number,
|
||||
17 => SymbolKind::Boolean,
|
||||
18 => SymbolKind::Array,
|
||||
19 => SymbolKind::Object,
|
||||
20 => SymbolKind::Key,
|
||||
21 => SymbolKind::Null,
|
||||
22 => SymbolKind::EnumMember,
|
||||
23 => SymbolKind::Struct,
|
||||
24 => SymbolKind::Event,
|
||||
25 => SymbolKind::Operator,
|
||||
26 => SymbolKind::TypeParameter,
|
||||
_ => SymbolKind::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Symbol {
|
||||
pub name: String,
|
||||
pub kind: lsp_types::SymbolKind,
|
||||
pub kind: SymbolKind,
|
||||
pub container_name: Option<String>,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
use gpui_shared_string::SharedString;
|
||||
use lsp_types::{DiagnosticSeverity, NumberOrString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// A diagnostic associated with a certain range of a buffer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Diagnostic {
|
||||
/// The name of the service that produced this diagnostic.
|
||||
pub source: Option<String>,
|
||||
/// The ID provided by the dynamic registration that produced this diagnostic.
|
||||
pub registration_id: Option<SharedString>,
|
||||
/// A machine-readable code that identifies this diagnostic.
|
||||
pub code: Option<NumberOrString>,
|
||||
pub code_description: Option<lsp_types::Uri>,
|
||||
/// Whether this diagnostic is a hint, warning, or error.
|
||||
pub severity: DiagnosticSeverity,
|
||||
/// The human-readable message associated with this diagnostic.
|
||||
pub message: String,
|
||||
/// The human-readable message (in markdown format)
|
||||
pub markdown: Option<String>,
|
||||
/// An id that identifies the group to which this diagnostic belongs.
|
||||
///
|
||||
/// When a language server produces a diagnostic with
|
||||
/// one or more associated diagnostics, those diagnostics are all
|
||||
/// assigned a single group ID.
|
||||
pub group_id: usize,
|
||||
/// Whether this diagnostic is the primary diagnostic for its group.
|
||||
///
|
||||
/// In a given group, the primary diagnostic is the top-level diagnostic
|
||||
/// returned by the language server. The non-primary diagnostics are the
|
||||
/// associated diagnostics.
|
||||
pub is_primary: bool,
|
||||
/// Whether this diagnostic is considered to originate from an analysis of
|
||||
/// files on disk, as opposed to any unsaved buffer contents. This is a
|
||||
/// property of a given diagnostic source, and is configured for a given
|
||||
/// language server via the `LspAdapter::disk_based_diagnostic_sources` method
|
||||
/// for the language server.
|
||||
pub is_disk_based: bool,
|
||||
/// Whether this diagnostic marks unnecessary code.
|
||||
pub is_unnecessary: bool,
|
||||
/// Quick separation of diagnostics groups based by their source.
|
||||
pub source_kind: DiagnosticSourceKind,
|
||||
/// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
|
||||
pub data: Option<Value>,
|
||||
/// Whether to underline the corresponding text range in the editor.
|
||||
pub underline: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DiagnosticSourceKind {
|
||||
Pulled,
|
||||
Pushed,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Default for Diagnostic {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
source: Default::default(),
|
||||
source_kind: DiagnosticSourceKind::Other,
|
||||
code: None,
|
||||
code_description: None,
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
message: Default::default(),
|
||||
markdown: None,
|
||||
group_id: 0,
|
||||
is_primary: false,
|
||||
is_disk_based: false,
|
||||
is_unnecessary: false,
|
||||
underline: true,
|
||||
data: None,
|
||||
registration_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ use regex::Regex;
|
|||
use schemars::{JsonSchema, SchemaGenerator, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::{num::NonZeroU32, path::Path, sync::Arc};
|
||||
use util::serde::default_true;
|
||||
|
||||
/// Controls the soft-wrapping behavior in the editor.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
|
|
@ -46,7 +45,7 @@ pub struct LanguageConfig {
|
|||
pub brackets: BracketPairConfig,
|
||||
/// If set to true, auto indentation uses last non empty line to determine
|
||||
/// the indentation level for a new line.
|
||||
#[serde(default = "auto_indent_using_last_non_empty_line_default")]
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_indent_using_last_non_empty_line: bool,
|
||||
// Whether indentation of pasted content should be adjusted based on the context.
|
||||
#[serde(default)]
|
||||
|
|
@ -166,7 +165,7 @@ impl Default for LanguageConfig {
|
|||
grammar: None,
|
||||
matcher: LanguageMatcher::default(),
|
||||
brackets: Default::default(),
|
||||
auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
|
||||
auto_indent_using_last_non_empty_line: default_true(),
|
||||
auto_indent_on_paste: None,
|
||||
increase_indent_pattern: Default::default(),
|
||||
decrease_indent_pattern: Default::default(),
|
||||
|
|
@ -480,7 +479,7 @@ pub struct WrapCharactersConfig {
|
|||
pub end_suffix: String,
|
||||
}
|
||||
|
||||
pub fn auto_indent_using_last_non_empty_line_default() -> bool {
|
||||
pub fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
// language_core: tree-sitter grammar infrastructure, LSP adapter traits,
|
||||
// language configuration, and highlight mapping.
|
||||
|
||||
pub mod diagnostic;
|
||||
pub mod grammar;
|
||||
pub mod highlight_map;
|
||||
pub mod language_config;
|
||||
|
||||
pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
|
||||
pub use grammar::{
|
||||
BracketsConfig, BracketsPatternConfig, DebugVariablesConfig, DebuggerTextObject, Grammar,
|
||||
GrammarId, HighlightsConfig, IndentConfig, InjectionConfig, InjectionPatternConfig,
|
||||
|
|
@ -17,9 +15,9 @@ pub use highlight_map::{HighlightId, HighlightMap};
|
|||
pub use language_config::{
|
||||
BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, DecreaseIndentConfig,
|
||||
JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
|
||||
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig,
|
||||
auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec,
|
||||
regex_json_schema, regex_vec_json_schema, serialize_regex,
|
||||
OrderedListConfig, Override, SoftWrap, TaskListConfig, WrapCharactersConfig, default_true,
|
||||
deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema,
|
||||
serialize_regex,
|
||||
};
|
||||
|
||||
pub mod code_label;
|
||||
|
|
@ -29,11 +27,9 @@ pub mod manifest;
|
|||
pub mod queries;
|
||||
pub mod toolchain;
|
||||
|
||||
pub use code_label::{CodeLabel, CodeLabelBuilder, Symbol};
|
||||
pub use code_label::{CodeLabel, CodeLabelBuilder, Symbol, SymbolKind};
|
||||
pub use language_name::{LanguageId, LanguageName};
|
||||
pub use lsp_adapter::{
|
||||
BinaryStatus, LanguageServerStatusUpdate, PromptResponseContext, ServerHealth, ToLspPosition,
|
||||
};
|
||||
pub use lsp_adapter::{BinaryStatus, LanguageServerStatusUpdate, ServerHealth};
|
||||
pub use manifest::ManifestName;
|
||||
pub use queries::{LanguageQueries, QUERY_FILENAME_PREFIXES};
|
||||
pub use toolchain::{Toolchain, ToolchainList, ToolchainMetadata, ToolchainScope};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,6 @@
|
|||
use gpui_shared_string::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Converts a value into an LSP position.
|
||||
pub trait ToLspPosition {
|
||||
/// Converts the value into an LSP position.
|
||||
fn to_lsp_position(self) -> lsp_types::Position;
|
||||
}
|
||||
|
||||
/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
|
||||
/// This allows adapters to intercept preference selections (like "Always" or "Never")
|
||||
/// and potentially persist them to Zed's settings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PromptResponseContext {
|
||||
/// The original message shown to the user
|
||||
pub message: String,
|
||||
/// The action (button) the user selected
|
||||
pub selected_action: lsp_types::MessageActionItem,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LanguageServerStatusUpdate {
|
||||
Binary(BinaryStatus),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use gpui_shared_string::SharedString;
|
||||
use util::rel_path::RelPath;
|
||||
use path::rel_path::RelPath;
|
||||
|
||||
use crate::{LanguageName, ManifestName};
|
||||
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ impl LspAdapter for ExtensionLspAdapter {
|
|||
container_name,
|
||||
}| extension::Symbol {
|
||||
name,
|
||||
kind: lsp_symbol_kind_to_extension(kind),
|
||||
kind: symbol_kind_to_extension(kind),
|
||||
container_name,
|
||||
},
|
||||
)
|
||||
|
|
@ -636,35 +636,34 @@ fn lsp_insert_text_format_to_extension(
|
|||
}
|
||||
}
|
||||
|
||||
fn lsp_symbol_kind_to_extension(value: lsp::SymbolKind) -> extension::SymbolKind {
|
||||
fn symbol_kind_to_extension(value: language::SymbolKind) -> extension::SymbolKind {
|
||||
match value {
|
||||
lsp::SymbolKind::FILE => extension::SymbolKind::File,
|
||||
lsp::SymbolKind::MODULE => extension::SymbolKind::Module,
|
||||
lsp::SymbolKind::NAMESPACE => extension::SymbolKind::Namespace,
|
||||
lsp::SymbolKind::PACKAGE => extension::SymbolKind::Package,
|
||||
lsp::SymbolKind::CLASS => extension::SymbolKind::Class,
|
||||
lsp::SymbolKind::METHOD => extension::SymbolKind::Method,
|
||||
lsp::SymbolKind::PROPERTY => extension::SymbolKind::Property,
|
||||
lsp::SymbolKind::FIELD => extension::SymbolKind::Field,
|
||||
lsp::SymbolKind::CONSTRUCTOR => extension::SymbolKind::Constructor,
|
||||
lsp::SymbolKind::ENUM => extension::SymbolKind::Enum,
|
||||
lsp::SymbolKind::INTERFACE => extension::SymbolKind::Interface,
|
||||
lsp::SymbolKind::FUNCTION => extension::SymbolKind::Function,
|
||||
lsp::SymbolKind::VARIABLE => extension::SymbolKind::Variable,
|
||||
lsp::SymbolKind::CONSTANT => extension::SymbolKind::Constant,
|
||||
lsp::SymbolKind::STRING => extension::SymbolKind::String,
|
||||
lsp::SymbolKind::NUMBER => extension::SymbolKind::Number,
|
||||
lsp::SymbolKind::BOOLEAN => extension::SymbolKind::Boolean,
|
||||
lsp::SymbolKind::ARRAY => extension::SymbolKind::Array,
|
||||
lsp::SymbolKind::OBJECT => extension::SymbolKind::Object,
|
||||
lsp::SymbolKind::KEY => extension::SymbolKind::Key,
|
||||
lsp::SymbolKind::NULL => extension::SymbolKind::Null,
|
||||
lsp::SymbolKind::ENUM_MEMBER => extension::SymbolKind::EnumMember,
|
||||
lsp::SymbolKind::STRUCT => extension::SymbolKind::Struct,
|
||||
lsp::SymbolKind::EVENT => extension::SymbolKind::Event,
|
||||
lsp::SymbolKind::OPERATOR => extension::SymbolKind::Operator,
|
||||
lsp::SymbolKind::TYPE_PARAMETER => extension::SymbolKind::TypeParameter,
|
||||
_ => extension::SymbolKind::Other(extract_int(value)),
|
||||
language::SymbolKind::File => extension::SymbolKind::File,
|
||||
language::SymbolKind::Module => extension::SymbolKind::Module,
|
||||
language::SymbolKind::Namespace => extension::SymbolKind::Namespace,
|
||||
language::SymbolKind::Package => extension::SymbolKind::Package,
|
||||
language::SymbolKind::Class => extension::SymbolKind::Class,
|
||||
language::SymbolKind::Method => extension::SymbolKind::Method,
|
||||
language::SymbolKind::Property => extension::SymbolKind::Property,
|
||||
language::SymbolKind::Field => extension::SymbolKind::Field,
|
||||
language::SymbolKind::Constructor => extension::SymbolKind::Constructor,
|
||||
language::SymbolKind::Enum => extension::SymbolKind::Enum,
|
||||
language::SymbolKind::Interface => extension::SymbolKind::Interface,
|
||||
language::SymbolKind::Function => extension::SymbolKind::Function,
|
||||
language::SymbolKind::Variable => extension::SymbolKind::Variable,
|
||||
language::SymbolKind::Constant => extension::SymbolKind::Constant,
|
||||
language::SymbolKind::String => extension::SymbolKind::String,
|
||||
language::SymbolKind::Number => extension::SymbolKind::Number,
|
||||
language::SymbolKind::Boolean => extension::SymbolKind::Boolean,
|
||||
language::SymbolKind::Array => extension::SymbolKind::Array,
|
||||
language::SymbolKind::Object => extension::SymbolKind::Object,
|
||||
language::SymbolKind::Key => extension::SymbolKind::Key,
|
||||
language::SymbolKind::Null => extension::SymbolKind::Null,
|
||||
language::SymbolKind::EnumMember => extension::SymbolKind::EnumMember,
|
||||
language::SymbolKind::Struct => extension::SymbolKind::Struct,
|
||||
language::SymbolKind::Event => extension::SymbolKind::Event,
|
||||
language::SymbolKind::Operator => extension::SymbolKind::Operator,
|
||||
language::SymbolKind::TypeParameter => extension::SymbolKind::TypeParameter,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -300,43 +300,43 @@ impl super::LspAdapter for CLspAdapter {
|
|||
) -> Option<CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
let (text, filter_range, display_range) = match symbol.kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
language::SymbolKind::Method | language::SymbolKind::Function => {
|
||||
let text = format!("void {} () {{}}", name);
|
||||
let filter_range = 0..name.len();
|
||||
let display_range = 5..5 + name.len();
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::STRUCT => {
|
||||
language::SymbolKind::Struct => {
|
||||
let text = format!("struct {} {{}}", name);
|
||||
let filter_range = 7..7 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::ENUM => {
|
||||
language::SymbolKind::Enum => {
|
||||
let text = format!("enum {} {{}}", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::INTERFACE | lsp::SymbolKind::CLASS => {
|
||||
language::SymbolKind::Interface | language::SymbolKind::Class => {
|
||||
let text = format!("class {} {{}}", name);
|
||||
let filter_range = 6..6 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
language::SymbolKind::Constant => {
|
||||
let text = format!("const int {} = 0;", name);
|
||||
let filter_range = 10..10 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::MODULE => {
|
||||
language::SymbolKind::Module => {
|
||||
let text = format!("namespace {} {{}}", name);
|
||||
let filter_range = 10..10 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::TYPE_PARAMETER => {
|
||||
language::SymbolKind::TypeParameter => {
|
||||
let text = format!("typename {} {{}};", name);
|
||||
let filter_range = 9..9 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ impl LspAdapter for EsLintLspAdapter {
|
|||
let file_path = requested_file_path
|
||||
.as_ref()
|
||||
.and_then(|abs_path| abs_path.strip_prefix(worktree_root).ok())
|
||||
.and_then(|p| RelPath::unix(&p).ok().map(ToOwned::to_owned))
|
||||
.and_then(|p| RelPath::from_unix_str(&p).ok().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| RelPath::empty().to_owned());
|
||||
let override_options = cx.update(|cx| {
|
||||
language_server_settings_for(
|
||||
|
|
|
|||
|
|
@ -394,43 +394,43 @@ impl LspAdapter for GoLspAdapter {
|
|||
) -> Option<CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
let (text, filter_range, display_range) = match symbol.kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
language::SymbolKind::Method | language::SymbolKind::Function => {
|
||||
let text = format!("func {} () {{}}", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::STRUCT => {
|
||||
language::SymbolKind::Struct => {
|
||||
let text = format!("type {} struct {{}}", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..text.len();
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::INTERFACE => {
|
||||
language::SymbolKind::Interface => {
|
||||
let text = format!("type {} interface {{}}", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..text.len();
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CLASS => {
|
||||
language::SymbolKind::Class => {
|
||||
let text = format!("type {} T", name);
|
||||
let filter_range = 5..5 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
language::SymbolKind::Constant => {
|
||||
let text = format!("const {} = nil", name);
|
||||
let filter_range = 6..6 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::VARIABLE => {
|
||||
language::SymbolKind::Variable => {
|
||||
let text = format!("var {} = nil", name);
|
||||
let filter_range = 4..4 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::MODULE => {
|
||||
language::SymbolKind::Module => {
|
||||
let text = format!("package {}", name);
|
||||
let filter_range = 8..8 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
|
|
|
|||
|
|
@ -52,8 +52,12 @@ impl ContextProvider for JsonTaskProvider {
|
|||
let Some(file) = project::File::from_dyn(file).cloned() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap());
|
||||
let is_composer_json = file.path.ends_with(RelPath::unix("composer.json").unwrap());
|
||||
let is_package_json = file
|
||||
.path
|
||||
.ends_with(RelPath::from_unix_str("package.json").unwrap());
|
||||
let is_composer_json = file
|
||||
.path
|
||||
.ends_with(RelPath::from_unix_str("composer.json").unwrap());
|
||||
if !is_package_json && !is_composer_json {
|
||||
return Task::ready(None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,14 +90,14 @@ impl ManifestProvider for PyprojectTomlManifestProvider {
|
|||
let mut outermost_workspace_root = None;
|
||||
|
||||
for path in path.ancestors().take(depth) {
|
||||
let pyproject_path = path.join(RelPath::unix("pyproject.toml").unwrap());
|
||||
let pyproject_path = path.join(RelPath::from_unix_str("pyproject.toml").unwrap());
|
||||
if delegate.exists(&pyproject_path, Some(false)) {
|
||||
if innermost_pyproject.is_none() {
|
||||
innermost_pyproject = Some(Arc::from(path));
|
||||
}
|
||||
|
||||
let has_lockfile = WORKSPACE_LOCKFILES.iter().any(|lockfile| {
|
||||
let lockfile_path = path.join(RelPath::unix(lockfile).unwrap());
|
||||
let lockfile_path = path.join(RelPath::from_unix_str(lockfile).unwrap());
|
||||
delegate.exists(&lockfile_path, Some(false))
|
||||
});
|
||||
if has_lockfile {
|
||||
|
|
@ -220,19 +220,19 @@ fn label_for_python_symbol(
|
|||
) -> Option<language::CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
let (text, filter_range, display_range) = match symbol.kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
language::SymbolKind::Method | language::SymbolKind::Function => {
|
||||
let text = format!("def {}():\n", name);
|
||||
let filter_range = 4..4 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CLASS => {
|
||||
language::SymbolKind::Class => {
|
||||
let text = format!("class {}:", name);
|
||||
let filter_range = 6..6 + name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
(text, filter_range, display_range)
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
language::SymbolKind::Constant => {
|
||||
let text = format!("{} = 0", name);
|
||||
let filter_range = 0..name.len();
|
||||
let display_range = 0..filter_range.end;
|
||||
|
|
@ -669,7 +669,7 @@ impl LspAdapter for PyrightLspAdapter {
|
|||
// If we have a detected toolchain, configure Pyright to use it - unless the user sets it themselves.
|
||||
let should_insert_toolchain = || {
|
||||
user_settings.as_object().is_none_or(|object| {
|
||||
[
|
||||
![
|
||||
"venvPath",
|
||||
"venv",
|
||||
"python",
|
||||
|
|
@ -1114,7 +1114,7 @@ impl PythonContextProvider {
|
|||
|
||||
fn python_module_name_from_relative_path(relative_path: &str) -> Option<String> {
|
||||
let rel_path = RelPath::new(relative_path.as_ref(), PathStyle::local()).ok()?;
|
||||
let path_with_dots = rel_path.display(PathStyle::Posix).replace('/', ".");
|
||||
let path_with_dots = rel_path.display(PathStyle::Unix).replace('/', ".");
|
||||
Some(
|
||||
path_with_dots
|
||||
.strip_suffix(".py")
|
||||
|
|
@ -2112,10 +2112,10 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// If we have a detected toolchain, configure Pyright to use it
|
||||
// If we have a detected toolchain, configure BasedPyright to use it - unless the user sets it themselves.
|
||||
let should_insert_toolchain = || {
|
||||
user_settings.as_object().is_none_or(|object| {
|
||||
[
|
||||
![
|
||||
"venvPath",
|
||||
"venv",
|
||||
"python",
|
||||
|
|
@ -3265,7 +3265,7 @@ mod tests {
|
|||
});
|
||||
let provider = PyprojectTomlManifestProvider;
|
||||
provider.search(ManifestQuery {
|
||||
path: RelPath::unix(query_path).unwrap().into(),
|
||||
path: RelPath::from_unix_str(query_path).unwrap().into(),
|
||||
depth: 10,
|
||||
delegate,
|
||||
})
|
||||
|
|
@ -3274,7 +3274,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_simple_project_no_lockfile() {
|
||||
let result = search(&["project/pyproject.toml"], "project/src/main.py");
|
||||
assert_eq!(result.as_deref(), RelPath::unix("project").ok());
|
||||
assert_eq!(result.as_deref(), RelPath::from_unix_str("project").ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3287,7 +3287,7 @@ mod tests {
|
|||
],
|
||||
"packages/subproject/src/main.py",
|
||||
);
|
||||
assert_eq!(result.as_deref(), RelPath::unix("").ok());
|
||||
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3296,7 +3296,7 @@ mod tests {
|
|||
&["pyproject.toml", "poetry.lock", "libs/mylib/pyproject.toml"],
|
||||
"libs/mylib/src/main.py",
|
||||
);
|
||||
assert_eq!(result.as_deref(), RelPath::unix("").ok());
|
||||
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3309,7 +3309,7 @@ mod tests {
|
|||
],
|
||||
"packages/mypackage/src/main.py",
|
||||
);
|
||||
assert_eq!(result.as_deref(), RelPath::unix("").ok());
|
||||
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3318,13 +3318,19 @@ mod tests {
|
|||
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
|
||||
"project-a/src/main.py",
|
||||
);
|
||||
assert_eq!(result_a.as_deref(), RelPath::unix("project-a").ok());
|
||||
assert_eq!(
|
||||
result_a.as_deref(),
|
||||
RelPath::from_unix_str("project-a").ok()
|
||||
);
|
||||
|
||||
let result_b = search(
|
||||
&["project-a/pyproject.toml", "project-b/pyproject.toml"],
|
||||
"project-b/src/main.py",
|
||||
);
|
||||
assert_eq!(result_b.as_deref(), RelPath::unix("project-b").ok());
|
||||
assert_eq!(
|
||||
result_b.as_deref(),
|
||||
RelPath::from_unix_str("project-b").ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3345,7 +3351,7 @@ mod tests {
|
|||
],
|
||||
"packages/sub/src/main.py",
|
||||
);
|
||||
assert_eq!(result.as_deref(), RelPath::unix("").ok());
|
||||
assert_eq!(result.as_deref(), RelPath::from_unix_str("").ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -3360,11 +3366,16 @@ mod tests {
|
|||
// "deep/nested/src/main.py", "deep/nested/src", and "deep/nested"
|
||||
// It won't reach "deep" or root ""
|
||||
let result = provider.search(ManifestQuery {
|
||||
path: RelPath::unix("deep/nested/src/main.py").unwrap().into(),
|
||||
path: RelPath::from_unix_str("deep/nested/src/main.py")
|
||||
.unwrap()
|
||||
.into(),
|
||||
depth: 3,
|
||||
delegate,
|
||||
});
|
||||
assert_eq!(result.as_deref(), RelPath::unix("deep/nested").ok());
|
||||
assert_eq!(
|
||||
result.as_deref(),
|
||||
RelPath::from_unix_str("deep/nested").ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ impl ManifestProvider for CargoManifestProvider {
|
|||
) -> Option<Arc<RelPath>> {
|
||||
let mut outermost_cargo_toml = None;
|
||||
for path in path.ancestors().take(depth) {
|
||||
let p = path.join(RelPath::unix("Cargo.toml").unwrap());
|
||||
let p = path.join(RelPath::from_unix_str("Cargo.toml").unwrap());
|
||||
if delegate.exists(&p, Some(false)) {
|
||||
outermost_cargo_toml = Some(Arc::from(path));
|
||||
}
|
||||
|
|
@ -622,15 +622,15 @@ impl LspAdapter for RustLspAdapter {
|
|||
) -> Option<CodeLabel> {
|
||||
let name = &symbol.name;
|
||||
let (prefix, suffix) = match symbol.kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => ("fn ", "();"),
|
||||
lsp::SymbolKind::STRUCT => ("struct ", ";"),
|
||||
lsp::SymbolKind::ENUM => ("enum ", "{}"),
|
||||
lsp::SymbolKind::INTERFACE => ("trait ", "{}"),
|
||||
lsp::SymbolKind::CONSTANT => ("const ", ":()=();"),
|
||||
lsp::SymbolKind::MODULE => ("mod ", ";"),
|
||||
lsp::SymbolKind::PACKAGE => ("extern crate ", ";"),
|
||||
lsp::SymbolKind::TYPE_PARAMETER => ("type ", "=();"),
|
||||
lsp::SymbolKind::ENUM_MEMBER => {
|
||||
language::SymbolKind::Method | language::SymbolKind::Function => ("fn ", "();"),
|
||||
language::SymbolKind::Struct => ("struct ", ";"),
|
||||
language::SymbolKind::Enum => ("enum ", "{}"),
|
||||
language::SymbolKind::Interface => ("trait ", "{}"),
|
||||
language::SymbolKind::Constant => ("const ", ":()=();"),
|
||||
language::SymbolKind::Module => ("mod ", ";"),
|
||||
language::SymbolKind::Package => ("extern crate ", ";"),
|
||||
language::SymbolKind::TypeParameter => ("type ", "=();"),
|
||||
language::SymbolKind::EnumMember => {
|
||||
let prefix = "enum E {";
|
||||
return Some(CodeLabel::new(
|
||||
name.to_string(),
|
||||
|
|
@ -1972,7 +1972,7 @@ mod tests {
|
|||
.label_for_symbol(
|
||||
&language::Symbol {
|
||||
name: "hello".to_string(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
kind: language::SymbolKind::Function,
|
||||
container_name: None,
|
||||
},
|
||||
&language
|
||||
|
|
@ -1990,7 +1990,7 @@ mod tests {
|
|||
.label_for_symbol(
|
||||
&language::Symbol {
|
||||
name: "World".to_string(),
|
||||
kind: lsp::SymbolKind::TYPE_PARAMETER,
|
||||
kind: language::SymbolKind::TypeParameter,
|
||||
container_name: None,
|
||||
},
|
||||
&language
|
||||
|
|
@ -2008,7 +2008,7 @@ mod tests {
|
|||
.label_for_symbol(
|
||||
&language::Symbol {
|
||||
name: "zed".to_string(),
|
||||
kind: lsp::SymbolKind::PACKAGE,
|
||||
kind: language::SymbolKind::Package,
|
||||
container_name: None,
|
||||
},
|
||||
&language
|
||||
|
|
@ -2026,7 +2026,7 @@ mod tests {
|
|||
.label_for_symbol(
|
||||
&language::Symbol {
|
||||
name: "Variant".to_string(),
|
||||
kind: lsp::SymbolKind::ENUM_MEMBER,
|
||||
kind: language::SymbolKind::EnumMember,
|
||||
container_name: None,
|
||||
},
|
||||
&language
|
||||
|
|
|
|||
|
|
@ -625,7 +625,9 @@ impl TypeScriptLspAdapter {
|
|||
|
||||
async fn tsdk_path(&self, adapter: &Arc<dyn LspAdapterDelegate>) -> Option<&'static str> {
|
||||
let is_yarn = adapter
|
||||
.read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap())
|
||||
.read_text_file(
|
||||
RelPath::from_unix_str(".yarn/sdks/typescript/lib/typescript.js").unwrap(),
|
||||
)
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ impl PathKey {
|
|||
} else {
|
||||
Self {
|
||||
sort_prefix: None,
|
||||
path: RelPath::unix(&buffer.entity_id().to_string())
|
||||
path: RelPath::from_unix_str(&buffer.entity_id().to_string())
|
||||
.unwrap()
|
||||
.into_arc(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ impl OpenPathDelegate {
|
|||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||
should_dismiss: true,
|
||||
prompt_root: match path_style {
|
||||
PathStyle::Posix => "/".to_string(),
|
||||
PathStyle::Unix => "/".to_string(),
|
||||
PathStyle::Windows => "C:\\".to_string(),
|
||||
},
|
||||
path_style,
|
||||
|
|
@ -158,7 +158,7 @@ impl OpenPathDelegate {
|
|||
|
||||
fn current_dir(&self) -> &'static str {
|
||||
match self.path_style {
|
||||
PathStyle::Posix => "./",
|
||||
PathStyle::Unix => "./",
|
||||
PathStyle::Windows => ".\\",
|
||||
}
|
||||
}
|
||||
|
|
@ -929,7 +929,7 @@ fn path_candidates(
|
|||
|
||||
fn get_dir_and_suffix(query: String, path_style: PathStyle) -> (String, String) {
|
||||
match path_style {
|
||||
PathStyle::Posix => {
|
||||
PathStyle::Unix => {
|
||||
let (mut dir, suffix) = if let Some(index) = query.rfind('/') {
|
||||
(query[..index].to_string(), query[index + 1..].to_string())
|
||||
} else {
|
||||
|
|
@ -1028,39 +1028,39 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_get_dir_and_suffix_with_posix_style() {
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/Use".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/");
|
||||
assert_eq!(suffix, "Use");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Docum".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Docum");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/Users/Junkui/");
|
||||
assert_eq!(suffix, "Documents");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/Users/Junkui/Documents/".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/Users/Junkui/Documents/");
|
||||
assert_eq!(suffix, "");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/.".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/root/");
|
||||
assert_eq!(suffix, ".");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/..".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/root/");
|
||||
assert_eq!(suffix, "..");
|
||||
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Posix);
|
||||
let (dir, suffix) = get_dir_and_suffix("/root/.hidden".into(), PathStyle::Unix);
|
||||
assert_eq!(dir, "/root/");
|
||||
assert_eq!(suffix, ".hidden");
|
||||
}
|
||||
|
|
|
|||
23
crates/path/Cargo.toml
Normal file
23
crates/path/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "path"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
path = "src/path.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dunce.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/path/LICENSE-APACHE
Symbolic link
1
crates/path/LICENSE-APACHE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
||||
286
crates/path/src/abs_path.rs
Normal file
286
crates/path/src/abs_path.rs
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt, io,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::{PathStyle, rel_path::RelPath};
|
||||
|
||||
// An absolute path on the user's local filesystem.
|
||||
// Requires paths to be valid utf-8
|
||||
#[derive(PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
#[repr(transparent)]
|
||||
pub struct AbsPath(Path);
|
||||
|
||||
impl AbsPath {
|
||||
pub fn new(path: &Path) -> anyhow::Result<&Self> {
|
||||
if !path.is_absolute() {
|
||||
return Err(anyhow::anyhow!("Path is not absolute: {:?}", path));
|
||||
}
|
||||
if path.to_str().is_none() {
|
||||
return Err(anyhow::anyhow!("Path is not valid utf-8: {:?}", path));
|
||||
}
|
||||
Ok(Self::new_unchecked(path))
|
||||
}
|
||||
|
||||
fn new_unchecked(path: &Path) -> &Self {
|
||||
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
||||
unsafe { &*(path as *const Path as *const Self) }
|
||||
}
|
||||
|
||||
pub fn to_abs_path_buf(&self) -> AbsPathBuf {
|
||||
AbsPathBuf(self.0.to_owned())
|
||||
}
|
||||
|
||||
pub fn join(&self, name: impl AsRef<str>) -> AbsPathBuf {
|
||||
AbsPathBuf(self.0.join(name.as_ref()))
|
||||
}
|
||||
|
||||
pub fn join_rel_path(&self, relative_path: &RelPath) -> AbsPathBuf {
|
||||
AbsPathBuf(self.0.join(relative_path.as_std_path()))
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<&AbsPath> {
|
||||
let parent = self.0.parent()?;
|
||||
Some(AbsPath::new_unchecked(parent))
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: &AbsPath) -> bool {
|
||||
self.0.starts_with(&other.0)
|
||||
}
|
||||
|
||||
pub fn ends_with(&self, other: &RelPath) -> bool {
|
||||
self.0.ends_with(other.as_std_path())
|
||||
}
|
||||
|
||||
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
|
||||
if self == ancestor {
|
||||
return false;
|
||||
}
|
||||
self.starts_with(ancestor)
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.0.file_name()?.to_str()
|
||||
}
|
||||
|
||||
pub fn display(&self) -> impl fmt::Display + '_ {
|
||||
self.0.display()
|
||||
}
|
||||
|
||||
pub fn as_std_path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0
|
||||
.to_str()
|
||||
.expect("valid UTF-8 enforced in constructor")
|
||||
}
|
||||
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = &AbsPath> {
|
||||
self.0.ancestors().map(|p| AbsPath::new_unchecked(p))
|
||||
}
|
||||
|
||||
pub fn strip_prefix<'a>(&'a self, prefix: &AbsPath) -> Option<Cow<'a, RelPath>> {
|
||||
let prefix = self.0.strip_prefix(&prefix.0).ok()?;
|
||||
RelPath::new(prefix, PathStyle::local()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for AbsPath {
|
||||
type Owned = AbsPathBuf;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
self.to_abs_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for AbsPath {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AbsPath> for AbsPath {
|
||||
fn as_ref(&self) -> &AbsPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AbsPath> for Arc<AbsPath> {
|
||||
fn from(path: &AbsPath) -> Self {
|
||||
let arc: Arc<Path> = Arc::from(&path.0);
|
||||
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
||||
unsafe { Arc::from_raw(Arc::into_raw(arc) as *const AbsPath) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&AbsPath> for Rc<AbsPath> {
|
||||
fn from(path: &AbsPath) -> Self {
|
||||
let arc: Rc<Path> = Rc::from(&path.0);
|
||||
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
||||
unsafe { Rc::from_raw(Rc::into_raw(arc) as *const AbsPath) }
|
||||
}
|
||||
}
|
||||
|
||||
// An absolute path on the user's local filesystem.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct AbsPathBuf(PathBuf);
|
||||
|
||||
impl AbsPathBuf {
|
||||
pub fn new(path: impl Into<PathBuf>) -> anyhow::Result<Self> {
|
||||
let path = path.into();
|
||||
if let Err(e) = AbsPath::new(&path) {
|
||||
return Err(e);
|
||||
}
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
pub fn home_dir() -> anyhow::Result<Self> {
|
||||
let home = std::env::home_dir().context("no home dir available")?;
|
||||
Self::new(home)
|
||||
}
|
||||
|
||||
/// Resolves `path` to its canonical on-disk spelling: symlinks and `..`
|
||||
/// are resolved, relative input is anchored on the current directory, and
|
||||
/// on case-insensitive filesystems each component takes the casing stored
|
||||
/// on disk. Unlike [`std::fs::canonicalize`], the result never uses
|
||||
/// Windows extended-length (`\\?\`) syntax, which chokes tools the path
|
||||
/// is later handed to (e.g. `git`).
|
||||
///
|
||||
/// Paths act as identity in several places (lock keys, watch-target
|
||||
/// comparisons, persisted repository records), so canonicalize a path
|
||||
/// where it enters the system whenever it may have been spelled by a user
|
||||
/// or an external tool.
|
||||
pub fn canonicalize(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let canonical = dunce::canonicalize(path.as_ref())?;
|
||||
Self::new(canonical).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, name: &str) {
|
||||
self.0.push(name);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn new_test(path: &'static str) -> Self {
|
||||
if cfg!(windows) {
|
||||
Self::new(format!("C:{path}")).unwrap()
|
||||
} else {
|
||||
Self::new(path).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn abs_path(path: &str) -> AbsPathBuf {
|
||||
if cfg!(windows) {
|
||||
AbsPathBuf::new(format!("C:{path}")).unwrap()
|
||||
} else {
|
||||
AbsPathBuf::new(path).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AbsPathBuf {
|
||||
type Target = AbsPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
AbsPath::new_unchecked(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<AbsPath> for AbsPathBuf {
|
||||
fn borrow(&self) -> &AbsPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for AbsPathBuf {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AbsPath> for AbsPathBuf {
|
||||
fn as_ref(&self) -> &AbsPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AbsPathBuf> for PathBuf {
|
||||
fn from(path: AbsPathBuf) -> PathBuf {
|
||||
path.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AbsPathBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<AbsPath> for AbsPathBuf {
|
||||
fn eq(&self, other: &AbsPath) -> bool {
|
||||
**self == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<AbsPathBuf> for AbsPath {
|
||||
fn eq(&self, other: &AbsPathBuf) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_canonicalize_restores_on_disk_spelling() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = dunce::canonicalize(temp.path()).unwrap();
|
||||
let dir = root.join("CamelCase");
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
|
||||
let canonical = AbsPathBuf::canonicalize(&dir).unwrap();
|
||||
assert_eq!(canonical.as_std_path(), dir);
|
||||
assert!(
|
||||
!canonical.as_str().starts_with(r"\\?\"),
|
||||
"canonical paths must not use Windows extended-length syntax: {canonical}"
|
||||
);
|
||||
|
||||
// A differently-cased spelling addresses the same directory only on a
|
||||
// case-insensitive filesystem; when it does, canonicalization must
|
||||
// restore the stored casing.
|
||||
let lowercased = root.join("camelcase");
|
||||
if std::fs::metadata(&lowercased).is_ok() {
|
||||
assert_eq!(
|
||||
AbsPathBuf::canonicalize(&lowercased).unwrap().as_std_path(),
|
||||
dir,
|
||||
"canonicalization should restore the on-disk casing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_test_normalizes_rooted_paths() {
|
||||
if cfg!(windows) {
|
||||
assert_eq!(AbsPathBuf::new_test("/").as_str(), "C:/");
|
||||
assert_eq!(
|
||||
AbsPathBuf::new_test("/test/project").as_str(),
|
||||
"C:/test/project"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(AbsPathBuf::new_test("/").as_str(), "/");
|
||||
assert_eq!(
|
||||
AbsPathBuf::new_test("/test/project").as_str(),
|
||||
"/test/project"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
263
crates/path/src/path.rs
Normal file
263
crates/path/src/path.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
//! Relative path types for deltadb.
|
||||
//!
|
||||
//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be
|
||||
//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)
|
||||
//! format regardless of host platform.
|
||||
//!
|
||||
//! Adapted from Zed's `util::rel_path` module.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::rel_path::RelPath;
|
||||
|
||||
pub mod abs_path;
|
||||
pub mod rel_path;
|
||||
|
||||
pub trait PathExt {
|
||||
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
|
||||
}
|
||||
|
||||
impl<T: AsRef<Path> + ?Sized> PathExt for T {
|
||||
fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
|
||||
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum PathStyle {
|
||||
Unix,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PathStyle {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const fn local() -> Self {
|
||||
PathStyle::Windows
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const fn local() -> Self {
|
||||
PathStyle::Unix
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn primary_separator(&self) -> &'static str {
|
||||
match self {
|
||||
PathStyle::Unix => "/",
|
||||
PathStyle::Windows => "\\",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn separators(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
PathStyle::Unix => &["/"],
|
||||
PathStyle::Windows => &["\\", "/"],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn separators_ch(&self) -> &'static [char] {
|
||||
match self {
|
||||
PathStyle::Unix => &['/'],
|
||||
PathStyle::Windows => &['\\', '/'],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_absolute(&self, path_like: &str) -> bool {
|
||||
path_like.starts_with('/')
|
||||
|| *self == PathStyle::Windows
|
||||
&& (path_like.starts_with('\\')
|
||||
|| path_like
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic())
|
||||
&& path_like[1..]
|
||||
.strip_prefix(':')
|
||||
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
|
||||
}
|
||||
|
||||
pub fn is_windows(&self) -> bool {
|
||||
*self == PathStyle::Windows
|
||||
}
|
||||
|
||||
pub fn is_posix(&self) -> bool {
|
||||
*self == PathStyle::Unix
|
||||
}
|
||||
|
||||
pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
|
||||
let right = right.as_ref().to_str()?;
|
||||
if is_absolute(right, self) {
|
||||
return None;
|
||||
}
|
||||
let left = left.as_ref().to_str()?;
|
||||
if left.is_empty() {
|
||||
Some(right.into())
|
||||
} else {
|
||||
Some(format!(
|
||||
"{left}{}{right}",
|
||||
if left.ends_with(self.primary_separator()) {
|
||||
""
|
||||
} else {
|
||||
self.primary_separator()
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join_path(
|
||||
self,
|
||||
left: impl AsRef<Path>,
|
||||
right: impl AsRef<Path>,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
let left = left
|
||||
.as_ref()
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
|
||||
let right = right.as_ref();
|
||||
let right_string = right
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
|
||||
let joined = self
|
||||
.join(left, right_string)
|
||||
.ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
|
||||
Ok(PathBuf::from(self.normalize(&joined)))
|
||||
}
|
||||
|
||||
pub fn normalize(self, path_like: &str) -> String {
|
||||
match self {
|
||||
PathStyle::Windows => crate::normalize_path(Path::new(path_like))
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
PathStyle::Unix => {
|
||||
let is_absolute = path_like.starts_with('/');
|
||||
let remainder = if is_absolute {
|
||||
path_like.trim_start_matches('/')
|
||||
} else {
|
||||
path_like
|
||||
};
|
||||
|
||||
let mut components = Vec::new();
|
||||
for component in remainder.split(self.separators_ch()) {
|
||||
match component {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
if components
|
||||
.last()
|
||||
.is_some_and(|component| *component != "..")
|
||||
{
|
||||
components.pop();
|
||||
} else if !is_absolute {
|
||||
components.push(component);
|
||||
}
|
||||
}
|
||||
component => components.push(component),
|
||||
}
|
||||
}
|
||||
|
||||
let normalized = components.join(self.primary_separator());
|
||||
if is_absolute && normalized.is_empty() {
|
||||
"/".to_string()
|
||||
} else if is_absolute {
|
||||
format!("/{normalized}")
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
|
||||
let Some(pos) = path_like.rfind(self.primary_separator()) else {
|
||||
return (None, path_like);
|
||||
};
|
||||
let filename_start = pos + self.primary_separator().len();
|
||||
(
|
||||
Some(&path_like[..filename_start]),
|
||||
&path_like[filename_start..],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn strip_prefix<'a>(
|
||||
&self,
|
||||
child: &'a Path,
|
||||
parent: &'a Path,
|
||||
) -> Option<std::borrow::Cow<'a, RelPath>> {
|
||||
let parent = parent.to_str()?;
|
||||
if parent.is_empty() {
|
||||
return RelPath::new(child, *self).ok();
|
||||
}
|
||||
let parent = self
|
||||
.separators()
|
||||
.iter()
|
||||
.find_map(|sep| parent.strip_suffix(sep))
|
||||
.unwrap_or(parent);
|
||||
let child = child.to_str()?;
|
||||
|
||||
// Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
|
||||
let stripped = if self.is_windows()
|
||||
&& child.as_bytes().get(1) == Some(&b':')
|
||||
&& parent.as_bytes().get(1) == Some(&b':')
|
||||
&& child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
|
||||
{
|
||||
child[2..].strip_prefix(&parent[2..])?
|
||||
} else {
|
||||
child.strip_prefix(parent)?
|
||||
};
|
||||
if let Some(relative) = self
|
||||
.separators()
|
||||
.iter()
|
||||
.find_map(|sep| stripped.strip_prefix(sep))
|
||||
{
|
||||
RelPath::new(relative.as_ref(), *self).ok()
|
||||
} else if stripped.is_empty() {
|
||||
Some(Cow::Borrowed(RelPath::empty()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
|
||||
path_like.starts_with('/')
|
||||
|| path_style == PathStyle::Windows
|
||||
&& (path_like.starts_with('\\')
|
||||
|| path_like
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_ascii_alphabetic())
|
||||
&& path_like[1..]
|
||||
.strip_prefix(':')
|
||||
.is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
|
||||
}
|
||||
|
||||
/// Normalizes a path by resolving `.` and `..` components without
|
||||
/// requiring the path to exist on disk (unlike `canonicalize`).
|
||||
pub fn normalize_path(path: &Path) -> PathBuf {
|
||||
use std::path::Component;
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
Component::Prefix(..) => unreachable!(),
|
||||
Component::RootDir => {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
use crate::paths::{PathStyle, is_absolute};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt,
|
||||
|
|
@ -9,6 +7,12 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PathStyle,
|
||||
abs_path::{AbsPath, AbsPathBuf},
|
||||
is_absolute,
|
||||
};
|
||||
|
||||
/// A file system path that is guaranteed to be relative and normalized.
|
||||
///
|
||||
/// This type can be used to represent paths in a uniform way, regardless of
|
||||
|
|
@ -20,20 +24,22 @@ use std::{
|
|||
///
|
||||
/// Relative paths are also guaranteed to be valid unicode.
|
||||
#[repr(transparent)]
|
||||
#[derive(PartialEq, Eq, Hash, Serialize)]
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct RelPath(str);
|
||||
|
||||
/// An owned representation of a file system path that is guaranteed to be
|
||||
/// relative and normalized.
|
||||
///
|
||||
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
|
||||
#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
|
||||
pub struct RelPathBuf(String);
|
||||
|
||||
impl RelPath {
|
||||
/// Creates an empty [`RelPath`].
|
||||
pub fn empty() -> &'static Self {
|
||||
Self::new_unchecked("")
|
||||
Self::from_str("")
|
||||
}
|
||||
|
||||
/// Creates an empty [`RelPath`].
|
||||
|
|
@ -54,7 +60,7 @@ impl RelPath {
|
|||
let mut path = path.to_str().context("non utf-8 path")?;
|
||||
|
||||
let (prefixes, suffixes): (&[_], &[_]) = match path_style {
|
||||
PathStyle::Posix => (&["./"], &['/']),
|
||||
PathStyle::Unix => (&["./"], &['/']),
|
||||
PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
|
||||
};
|
||||
|
||||
|
|
@ -77,7 +83,7 @@ impl RelPath {
|
|||
}
|
||||
|
||||
let mut result = match string {
|
||||
Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
|
||||
Cow::Borrowed(string) => Cow::Borrowed(Self::from_str(string)),
|
||||
Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
|
||||
};
|
||||
|
||||
|
|
@ -95,7 +101,7 @@ impl RelPath {
|
|||
return Err(anyhow!("path is not relative: {result:?}"));
|
||||
}
|
||||
}
|
||||
other => normalized.push(RelPath::new_unchecked(other)),
|
||||
other => normalized.push(RelPath::from_str(other)),
|
||||
}
|
||||
}
|
||||
result = Cow::Owned(normalized)
|
||||
|
|
@ -104,20 +110,25 @@ impl RelPath {
|
|||
Ok(result)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn new_test<'a>(path: &'a str) -> Cow<'a, Self> {
|
||||
Self::new(Path::new(path), PathStyle::Unix).unwrap()
|
||||
}
|
||||
|
||||
/// Converts a path that is already normalized and uses '/' separators
|
||||
/// into a [`RelPath`] .
|
||||
///
|
||||
/// Returns an error if the path is not already in the correct format.
|
||||
#[track_caller]
|
||||
pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
|
||||
pub fn from_unix_str<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
|
||||
let path = path.as_ref();
|
||||
match Self::new(path, PathStyle::Posix)? {
|
||||
match Self::new(path, PathStyle::Unix)? {
|
||||
Cow::Borrowed(path) => Ok(path),
|
||||
Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_unchecked(s: &str) -> &Self {
|
||||
fn from_str(s: &str) -> &Self {
|
||||
// Safety: `RelPath` is a transparent wrapper around `str`.
|
||||
unsafe { &*(s as *const str as *const Self) }
|
||||
}
|
||||
|
|
@ -131,7 +142,12 @@ impl RelPath {
|
|||
}
|
||||
|
||||
pub fn ancestors(&self) -> RelPathAncestors<'_> {
|
||||
RelPathAncestors(Some(&self.0))
|
||||
RelPathAncestors {
|
||||
full: &self.0,
|
||||
front: self.0.len(),
|
||||
back: 0,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
|
|
@ -156,7 +172,22 @@ impl RelPath {
|
|||
self.strip_prefix(other).is_ok()
|
||||
}
|
||||
|
||||
/// Returns true if this path is a strict descendant of `ancestor`.
|
||||
///
|
||||
/// Unlike `starts_with`, this returns false when `self == ancestor`
|
||||
/// and false when `ancestor` is empty (since every path trivially
|
||||
/// starts with the empty prefix).
|
||||
pub fn is_descendant_of(&self, ancestor: &Self) -> bool {
|
||||
if ancestor.is_empty() || self == ancestor {
|
||||
return false;
|
||||
}
|
||||
self.starts_with(ancestor)
|
||||
}
|
||||
|
||||
pub fn ends_with(&self, other: &Self) -> bool {
|
||||
if other.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if let Some(suffix) = self.0.strip_suffix(&other.0) {
|
||||
if suffix.ends_with('/') {
|
||||
return true;
|
||||
|
|
@ -173,7 +204,7 @@ impl RelPath {
|
|||
}
|
||||
if let Some(suffix) = self.0.strip_prefix(&other.0) {
|
||||
if let Some(suffix) = suffix.strip_prefix('/') {
|
||||
return Ok(Self::new_unchecked(suffix));
|
||||
return Ok(Self::from_str(suffix));
|
||||
} else if suffix.is_empty() {
|
||||
return Ok(Self::empty());
|
||||
}
|
||||
|
|
@ -182,7 +213,11 @@ impl RelPath {
|
|||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.matches('/').count() + 1
|
||||
if self.0.is_empty() {
|
||||
0
|
||||
} else {
|
||||
self.0.matches('/').count() + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_n_components(&self, count: usize) -> Option<&Self> {
|
||||
|
|
@ -198,15 +233,14 @@ impl RelPath {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn join(&self, other: &Self) -> Arc<Self> {
|
||||
let result = if self.0.is_empty() {
|
||||
Cow::Borrowed(&other.0)
|
||||
pub fn join(&self, other: &Self) -> RelPathBuf {
|
||||
if self.0.is_empty() {
|
||||
other.to_rel_path_buf()
|
||||
} else if other.0.is_empty() {
|
||||
Cow::Borrowed(&self.0)
|
||||
self.to_rel_path_buf()
|
||||
} else {
|
||||
Cow::Owned(format!("{}/{}", &self.0, &other.0))
|
||||
};
|
||||
Arc::from(Self::new_unchecked(result.as_ref()))
|
||||
RelPathBuf(format!("{}/{}", &self.0, &other.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_rel_path_buf(&self) -> RelPathBuf {
|
||||
|
|
@ -217,23 +251,13 @@ impl RelPath {
|
|||
Arc::from(self)
|
||||
}
|
||||
|
||||
/// Convert the path into the wire representation.
|
||||
pub fn to_proto(&self) -> String {
|
||||
self.as_unix_str().to_owned()
|
||||
}
|
||||
|
||||
/// Load the path from its wire representation.
|
||||
pub fn from_proto(path: &str) -> Result<Arc<Self>> {
|
||||
Ok(Arc::from(Self::unix(path)?))
|
||||
}
|
||||
|
||||
/// Convert the path into a string with the given path style.
|
||||
///
|
||||
/// Whenever a path is presented to the user, it should be converted to
|
||||
/// a string via this method.
|
||||
pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
|
||||
match style {
|
||||
PathStyle::Posix => Cow::Borrowed(&self.0),
|
||||
PathStyle::Unix => Cow::Borrowed(&self.0),
|
||||
PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
|
||||
PathStyle::Windows => Cow::Borrowed(&self.0),
|
||||
}
|
||||
|
|
@ -255,19 +279,16 @@ impl RelPath {
|
|||
pub fn as_std_path(&self) -> &Path {
|
||||
Path::new(&self.0)
|
||||
}
|
||||
|
||||
/// Resolves this relative path against an absolute base path.
|
||||
pub fn absolutize(&self, base: impl AsRef<AbsPath>) -> AbsPathBuf {
|
||||
base.as_ref().join(self.as_unix_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StripPrefixError;
|
||||
|
||||
impl std::fmt::Display for StripPrefixError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("prefix not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for StripPrefixError {}
|
||||
|
||||
impl ToOwned for RelPath {
|
||||
type Owned = RelPathBuf;
|
||||
|
||||
|
|
@ -324,14 +345,33 @@ impl RelPathBuf {
|
|||
}
|
||||
|
||||
pub fn push(&mut self, path: &RelPath) {
|
||||
if path.is_empty() {
|
||||
return;
|
||||
}
|
||||
if !self.is_empty() {
|
||||
self.0.push('/');
|
||||
}
|
||||
self.0.push_str(&path.0);
|
||||
}
|
||||
|
||||
pub fn push_component(&mut self, component: &str) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
!component.is_empty()
|
||||
&& !component.contains('/')
|
||||
&& component != "."
|
||||
&& component != "..",
|
||||
"invalid relative path component: {component:?}"
|
||||
);
|
||||
|
||||
if !self.is_empty() {
|
||||
self.0.push('/');
|
||||
}
|
||||
self.0.push_str(component);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn as_rel_path(&self) -> &RelPath {
|
||||
RelPath::new_unchecked(self.0.as_str())
|
||||
RelPath::from_str(self.0.as_str())
|
||||
}
|
||||
|
||||
pub fn set_extension(&mut self, extension: &str) -> bool {
|
||||
|
|
@ -339,7 +379,7 @@ impl RelPathBuf {
|
|||
let mut filename = PathBuf::from(filename);
|
||||
filename.set_extension(extension);
|
||||
self.pop();
|
||||
self.0.push_str(filename.to_str().unwrap());
|
||||
self.push(RelPath::from_str(filename.to_str().unwrap()));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
@ -347,33 +387,21 @@ impl RelPathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RelPathBuf {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let path = String::deserialize(deserializer)?;
|
||||
let rel_path =
|
||||
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
|
||||
Ok(rel_path.into_owned())
|
||||
impl PartialOrd for RelPathBuf {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Arc<RelPath>> for RelPathBuf {
|
||||
fn into(self) -> Arc<RelPath> {
|
||||
Arc::from(self.as_rel_path())
|
||||
impl Ord for RelPathBuf {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.as_rel_path().cmp(other.as_rel_path())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for RelPathBuf {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_std_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for RelPath {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_std_path()
|
||||
impl From<RelPathBuf> for Arc<RelPath> {
|
||||
fn from(value: RelPathBuf) -> Self {
|
||||
Arc::from(value.as_rel_path())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -383,12 +411,24 @@ impl AsRef<RelPath> for RelPathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::path::Path> for RelPathBuf {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_std_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RelPath> for RelPath {
|
||||
fn as_ref(&self) -> &RelPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<std::path::Path> for RelPath {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.as_std_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RelPathBuf {
|
||||
type Target = RelPath;
|
||||
|
||||
|
|
@ -406,20 +446,37 @@ impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
|
|||
impl From<&RelPath> for Arc<RelPath> {
|
||||
fn from(rel_path: &RelPath) -> Self {
|
||||
let bytes: Arc<str> = Arc::from(&rel_path.0);
|
||||
// SAFETY: `AbsPath` is a `repr(transparent)` wrapper around `Path`.
|
||||
unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a RelPath {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<Self> {
|
||||
RelPath::from_unix_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RelPathBuf {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self> {
|
||||
RelPath::new(Path::new(s), PathStyle::Unix).map(|cow| cow.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn rel_path(path: &str) -> &RelPath {
|
||||
RelPath::unix(path).unwrap()
|
||||
RelPath::from_unix_str(path).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn rel_path_buf(path: &str) -> RelPathBuf {
|
||||
RelPath::unix(path).unwrap().to_rel_path_buf()
|
||||
rel_path(path).to_owned()
|
||||
}
|
||||
|
||||
impl PartialEq<str> for RelPath {
|
||||
|
|
@ -428,26 +485,45 @@ impl PartialEq<str> for RelPath {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait PathExt {
|
||||
fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
|
||||
impl PartialEq<RelPath> for RelPathBuf {
|
||||
fn eq(&self, other: &RelPath) -> bool {
|
||||
self.as_rel_path() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<Path> + ?Sized> PathExt for T {
|
||||
fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
|
||||
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
|
||||
impl PartialEq<RelPathBuf> for RelPath {
|
||||
fn eq(&self, other: &RelPathBuf) -> bool {
|
||||
other.as_rel_path() == self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RelPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RelPathBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RelPathComponents<'a>(&'a str);
|
||||
|
||||
pub struct RelPathAncestors<'a>(Option<&'a str>);
|
||||
pub struct RelPathAncestors<'a> {
|
||||
full: &'a str,
|
||||
front: usize,
|
||||
back: usize,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
const SEPARATOR: char = '/';
|
||||
|
||||
impl<'a> RelPathComponents<'a> {
|
||||
pub fn rest(&self) -> &'a RelPath {
|
||||
RelPath::new_unchecked(self.0)
|
||||
RelPath::from_str(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -473,15 +549,35 @@ impl<'a> Iterator for RelPathAncestors<'a> {
|
|||
type Item = &'a RelPath;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.0?;
|
||||
if let Some(sep_ix) = result.rfind(SEPARATOR) {
|
||||
self.0 = Some(&result[..sep_ix]);
|
||||
} else if !result.is_empty() {
|
||||
self.0 = Some("");
|
||||
} else {
|
||||
self.0 = None;
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
Some(RelPath::new_unchecked(result))
|
||||
let result = &self.full[..self.front];
|
||||
if self.front == self.back {
|
||||
self.done = true;
|
||||
} else {
|
||||
self.front = result.rfind(SEPARATOR).unwrap_or_default();
|
||||
}
|
||||
Some(RelPath::from_str(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for RelPathAncestors<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.done {
|
||||
return None;
|
||||
}
|
||||
let result = &self.full[..self.back];
|
||||
if self.front == self.back {
|
||||
self.done = true;
|
||||
} else {
|
||||
let search_start = if self.back == 0 { 0 } else { self.back + 1 };
|
||||
self.back = match self.full[search_start..].find(SEPARATOR) {
|
||||
Some(sep_ix) => search_start + sep_ix,
|
||||
None => self.full.len(),
|
||||
};
|
||||
}
|
||||
Some(RelPath::from_str(result))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -501,11 +597,21 @@ impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for RelPathBuf {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let path = String::deserialize(deserializer)?;
|
||||
let rel_path =
|
||||
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
|
||||
Ok(rel_path.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::assert_matches;
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_new() {
|
||||
|
|
@ -515,11 +621,11 @@ mod tests {
|
|||
|
||||
let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
assert_eq!(
|
||||
RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
|
||||
|
|
@ -528,29 +634,29 @@ mod tests {
|
|||
rel_path("foo/baz/quux")
|
||||
);
|
||||
|
||||
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
|
||||
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Unix).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
|
||||
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Unix).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Owned(_));
|
||||
assert!(matches!(path, Cow::Owned(_)));
|
||||
|
||||
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
assert!(matches!(path, Cow::Borrowed(_)));
|
||||
|
||||
let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Owned(_));
|
||||
assert!(matches!(path, Cow::Owned(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -590,6 +696,41 @@ mod tests {
|
|||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next(), None);
|
||||
|
||||
let path = rel_path("foo/bar/baz");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("foo/bar/baz")));
|
||||
assert_eq!(ancestors.next_back(), None);
|
||||
|
||||
let path = rel_path("foo/bar/baz");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("")));
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
|
||||
assert_eq!(ancestors.next(), None);
|
||||
assert_eq!(ancestors.next_back(), None);
|
||||
|
||||
let path = rel_path("foo");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("foo")));
|
||||
assert_eq!(ancestors.next_back(), None);
|
||||
|
||||
let path = RelPath::empty();
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next_back(), None);
|
||||
|
||||
let path = rel_path("über/x");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next_back(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("über")));
|
||||
assert_eq!(ancestors.next_back(), Some(rel_path("über/x")));
|
||||
assert_eq!(ancestors.next_back(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -602,13 +743,18 @@ mod tests {
|
|||
#[test]
|
||||
fn test_rel_path_partial_ord_is_compatible_with_std() {
|
||||
let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
|
||||
for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
|
||||
assert_eq!(
|
||||
Path::new(lhs).cmp(Path::new(rhs)),
|
||||
RelPath::unix(lhs)
|
||||
.unwrap()
|
||||
.cmp(&RelPath::unix(rhs).unwrap())
|
||||
);
|
||||
for (i, lhs) in test_cases.iter().enumerate() {
|
||||
for rhs in &test_cases[i + 1..] {
|
||||
assert_eq!(
|
||||
Path::new(lhs).cmp(Path::new(rhs)),
|
||||
RelPath::from_unix_str(lhs)
|
||||
.unwrap()
|
||||
.cmp(RelPath::from_unix_str(rhs).unwrap()),
|
||||
"ordering mismatch for {:?} vs {:?}",
|
||||
lhs,
|
||||
rhs,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -621,19 +767,28 @@ mod tests {
|
|||
assert_eq!(child.strip_prefix(parent).unwrap(), child);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ends_with() {
|
||||
assert!(rel_path("foo/bar").ends_with(rel_path("bar")));
|
||||
assert!(rel_path("foo/bar").ends_with(rel_path("foo/bar")));
|
||||
assert!(rel_path("foo/bar").ends_with(RelPath::empty()));
|
||||
assert!(RelPath::empty().ends_with(RelPath::empty()));
|
||||
assert!(!rel_path("foobar").ends_with(rel_path("bar")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_constructors_absolute_path() {
|
||||
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
|
||||
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Unix).is_err());
|
||||
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
|
||||
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Unix).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pop() {
|
||||
let mut path = rel_path("a/b").to_rel_path_buf();
|
||||
let mut path = rel_path_buf("a/b");
|
||||
path.pop();
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "a");
|
||||
path.pop();
|
||||
|
|
@ -641,4 +796,27 @@ mod tests {
|
|||
path.pop();
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
assert_eq!(RelPath::empty().len(), 0);
|
||||
assert_eq!(rel_path("a").len(), 1);
|
||||
assert_eq!(rel_path("a/b").len(), 2);
|
||||
assert_eq!(rel_path("a/b/c").len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_extension() {
|
||||
let mut path = rel_path_buf("a/b/c.txt");
|
||||
assert!(path.set_extension("rs"));
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "a/b/c.rs");
|
||||
|
||||
let mut single = rel_path_buf("file.txt");
|
||||
assert!(single.set_extension("md"));
|
||||
assert_eq!(single.as_rel_path().as_unix_str(), "file.md");
|
||||
|
||||
let mut no_ext = rel_path_buf("a/b/c");
|
||||
assert!(no_ext.set_extension("rs"));
|
||||
assert_eq!(no_ext.as_rel_path().as_unix_str(), "a/b/c.rs");
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
|||
/// Returns the relative path to the zed_server directory on the ssh host.
|
||||
pub fn remote_server_dir_relative() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".zed_server").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".zed_server").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ pub fn remote_server_dir_relative() -> &'static RelPath {
|
|||
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
|
||||
pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".zed_wsl_server").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
|
|
@ -496,21 +496,21 @@ pub fn local_vscode_folder_name() -> &'static str {
|
|||
/// Returns the relative path to a `settings.json` file within a project.
|
||||
pub fn local_settings_file_relative_path() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".zed/settings.json").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `tasks.json` file within a project.
|
||||
pub fn local_tasks_file_relative_path() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".zed/tasks.json").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
|
||||
pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".vscode/tasks.json").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
|
|
@ -526,14 +526,14 @@ pub fn task_file_name() -> &'static str {
|
|||
/// .zed/debug.json
|
||||
pub fn local_debug_file_relative_path() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".zed/debug.json").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `.vscode/launch.json` file within a project.
|
||||
pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
|
||||
static CACHED: LazyLock<&'static RelPath> =
|
||||
LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap());
|
||||
LazyLock::new(|| RelPath::from_unix_str(".vscode/launch.json").unwrap());
|
||||
*CACHED
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,8 +76,6 @@ actions!(
|
|||
SetPreviewHidden,
|
||||
/// Opens the footer's actions menu.
|
||||
ToggleActionsMenu,
|
||||
/// Take the picker's content and open it in a multibuffer
|
||||
ToMultiBuffer,
|
||||
/// Toggles multi-select mode, in which clicking items adds them to
|
||||
/// the selection instead of opening them
|
||||
ToggleMultiSelect,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText,
|
||||
Task, Window, px,
|
||||
AnyElement, App, AppContext as _, Context, Entity, IntoElement, Pixels, StyledText, Task,
|
||||
Window, px,
|
||||
};
|
||||
use language::{Bias, Buffer, HighlightedText, HighlightedTextBuilder, ToPoint};
|
||||
use picker::{
|
||||
MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate, ToMultiBuffer,
|
||||
};
|
||||
use picker::{MatchLocation, PreviewBackend, PreviewLayout, PreviewSource, PreviewUpdate};
|
||||
use project::{Project, Symbol};
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
|
|
@ -335,7 +333,7 @@ impl EditorPreview {
|
|||
div()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(self.editor_as_giant_button())
|
||||
.child(self.occluded_editor())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
|
@ -357,7 +355,7 @@ impl EditorPreview {
|
|||
.child(content)
|
||||
}
|
||||
|
||||
fn editor_as_giant_button(&self) -> impl IntoElement {
|
||||
fn occluded_editor(&self) -> impl IntoElement {
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
|
|
@ -367,10 +365,7 @@ impl EditorPreview {
|
|||
.id("picker-preview-editor")
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.occlude()
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToMultiBuffer.boxed_clone(), cx);
|
||||
}),
|
||||
.occlude(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ test-support = [
|
|||
aho-corasick.workspace = true
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
async-trait.workspace = true
|
||||
async-channel.workspace = true
|
||||
async-trait.workspace = true
|
||||
base64.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
circular-buffer.workspace = true
|
||||
|
|
@ -60,15 +60,17 @@ globset.workspace = true
|
|||
gpui.workspace = true
|
||||
http_client = { workspace = true, features = ["github-download"] }
|
||||
image.workspace = true
|
||||
itertools.workspace = true
|
||||
indexmap.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
path.workspace = true
|
||||
paths.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
postage.workspace = true
|
||||
prettier.workspace = true
|
||||
rand.workspace = true
|
||||
|
|
@ -93,8 +95,8 @@ tempfile.workspace = true
|
|||
terminal.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
wax.workspace = true
|
||||
|
|
@ -104,7 +106,6 @@ zed_credentials_provider.workspace = true
|
|||
zeroize.workspace = true
|
||||
zlog.workspace = true
|
||||
ztracing.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ impl RemoteBufferStore {
|
|||
.request(proto::OpenBufferByPath {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path.to_proto(),
|
||||
path: path.as_unix_str().to_owned(),
|
||||
})
|
||||
.await?;
|
||||
let buffer_id = BufferId::new(response.buffer_id)?;
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ impl language::File for IndexTextFile {
|
|||
rpc::proto::File {
|
||||
worktree_id: self.worktree_id.to_proto(),
|
||||
entry_id: None,
|
||||
path: self.path.as_ref().to_proto(),
|
||||
path: self.path.as_ref().as_unix_str().to_owned(),
|
||||
mtime: None,
|
||||
is_deleted: false,
|
||||
is_historic: true,
|
||||
|
|
@ -336,7 +336,7 @@ impl StatusEntry {
|
|||
};
|
||||
|
||||
proto::StatusEntry {
|
||||
repo_path: self.repo_path.to_proto(),
|
||||
repo_path: self.repo_path.as_unix_str().to_owned(),
|
||||
simple_status,
|
||||
status: Some(status_to_proto(self.status)),
|
||||
diff_stat_added: self.diff_stat.map(|ds| ds.added),
|
||||
|
|
@ -3825,7 +3825,7 @@ impl GitStore {
|
|||
.files
|
||||
.into_iter()
|
||||
.map(|file| proto::CommitFile {
|
||||
path: file.path.to_proto(),
|
||||
path: file.path.as_unix_str().to_owned(),
|
||||
old_text: file.old_text,
|
||||
new_text: file.new_text,
|
||||
is_binary: file.is_binary,
|
||||
|
|
@ -4023,7 +4023,7 @@ impl GitStore {
|
|||
.entries
|
||||
.into_iter()
|
||||
.map(|(path, status)| proto::TreeDiffStatus {
|
||||
path: path.as_ref().to_proto(),
|
||||
path: path.as_ref().as_unix_str().to_owned(),
|
||||
status: match status {
|
||||
TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(),
|
||||
TreeDiffStatus::Modified { .. } => {
|
||||
|
|
@ -5109,7 +5109,7 @@ impl RepositorySnapshot {
|
|||
.merge
|
||||
.merge_heads_by_conflicted_path
|
||||
.iter()
|
||||
.map(|(repo_path, _)| repo_path.to_proto())
|
||||
.map(|(repo_path, _)| repo_path.as_unix_str().to_owned())
|
||||
.collect(),
|
||||
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
|
||||
project_id,
|
||||
|
|
@ -5165,13 +5165,13 @@ impl RepositorySnapshot {
|
|||
current_new_entry = new_statuses.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
removed_statuses.push(old_entry.repo_path.to_proto());
|
||||
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
|
||||
current_old_entry = old_statuses.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(old_entry)) => {
|
||||
removed_statuses.push(old_entry.repo_path.to_proto());
|
||||
removed_statuses.push(old_entry.repo_path.as_unix_str().to_owned());
|
||||
current_old_entry = old_statuses.next();
|
||||
}
|
||||
(Some(new_entry), None) => {
|
||||
|
|
@ -5196,7 +5196,7 @@ impl RepositorySnapshot {
|
|||
.merge
|
||||
.merge_heads_by_conflicted_path
|
||||
.iter()
|
||||
.map(|(path, _)| path.to_proto())
|
||||
.map(|(path, _)| path.as_unix_str().to_owned())
|
||||
.collect(),
|
||||
merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()),
|
||||
project_id,
|
||||
|
|
@ -6051,7 +6051,7 @@ impl Repository {
|
|||
commit,
|
||||
paths: paths
|
||||
.into_iter()
|
||||
.map(|p| p.to_proto())
|
||||
.map(|p| p.as_unix_str().to_owned())
|
||||
.collect(),
|
||||
})
|
||||
.await?;
|
||||
|
|
@ -7013,7 +7013,9 @@ impl Repository {
|
|||
repository_id: id.to_proto(),
|
||||
paths: entries
|
||||
.into_iter()
|
||||
.map(|repo_path| repo_path.to_proto())
|
||||
.map(|repo_path| {
|
||||
repo_path.as_unix_str().to_owned()
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.await
|
||||
|
|
@ -7026,7 +7028,9 @@ impl Repository {
|
|||
repository_id: id.to_proto(),
|
||||
paths: entries
|
||||
.into_iter()
|
||||
.map(|repo_path| repo_path.to_proto())
|
||||
.map(|repo_path| {
|
||||
repo_path.as_unix_str().to_owned()
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.await
|
||||
|
|
@ -7157,7 +7161,7 @@ impl Repository {
|
|||
repository_id: id.to_proto(),
|
||||
paths: entries
|
||||
.into_iter()
|
||||
.map(|repo_path| repo_path.to_proto())
|
||||
.map(|repo_path| repo_path.as_unix_str().to_owned())
|
||||
.collect(),
|
||||
})
|
||||
.await?;
|
||||
|
|
@ -7245,7 +7249,7 @@ impl Repository {
|
|||
is_dir: bool,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let work_dir = self.snapshot.work_directory_abs_path.clone();
|
||||
let path_display = repo_path.as_ref().display(PathStyle::Posix);
|
||||
let path_display = repo_path.as_ref().display(PathStyle::Unix);
|
||||
let file_path_str = if is_dir {
|
||||
format!("{}/", path_display)
|
||||
} else {
|
||||
|
|
@ -7279,7 +7283,7 @@ impl Repository {
|
|||
is_dir: bool,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let repository_dir = self.snapshot.repository_dir_abs_path.clone();
|
||||
let path_display = repo_path.as_ref().display(PathStyle::Posix);
|
||||
let path_display = repo_path.as_ref().display(PathStyle::Unix);
|
||||
let file_path_str = if is_dir {
|
||||
format!("{}/", path_display)
|
||||
} else {
|
||||
|
|
@ -7722,7 +7726,7 @@ impl Repository {
|
|||
.request(proto::SetIndexText {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
path: path.to_proto(),
|
||||
path: path.as_unix_str().to_owned(),
|
||||
text: content,
|
||||
})
|
||||
.await?;
|
||||
|
|
@ -8406,7 +8410,7 @@ impl Repository {
|
|||
};
|
||||
Some((
|
||||
RepoPath::from_rel_path(
|
||||
&RelPath::from_proto(&entry.path).log_err()?,
|
||||
RelPath::from_unix_str(&entry.path).log_err()?,
|
||||
),
|
||||
status,
|
||||
))
|
||||
|
|
@ -8735,7 +8739,7 @@ impl Repository {
|
|||
.into_iter()
|
||||
.filter_map(|path| {
|
||||
Some(sum_tree::Edit::Remove(PathKey(
|
||||
RelPath::from_proto(&path).log_err()?,
|
||||
RelPath::from_unix_str(&path).log_err()?.into(),
|
||||
)))
|
||||
})
|
||||
.chain(
|
||||
|
|
@ -9326,7 +9330,7 @@ fn format_job_key(key: &GitJobKey) -> SharedString {
|
|||
.iter()
|
||||
.map(|p| {
|
||||
let rel: &RelPath = p;
|
||||
format!("{}", AsRef::<Path>::as_ref(rel).display())
|
||||
rel.display(PathStyle::local())
|
||||
})
|
||||
.collect();
|
||||
format!("WriteIndex({})", paths_str.join(", ")).into()
|
||||
|
|
@ -9405,7 +9409,7 @@ pub fn worktrees_directory_for_repo(
|
|||
let resolved = if path_style.is_posix() {
|
||||
joined
|
||||
} else {
|
||||
util::normalize_path(&joined)
|
||||
path::normalize_path(&joined)
|
||||
};
|
||||
let resolved = if resolved.starts_with(repository_anchor_path) {
|
||||
resolved
|
||||
|
|
@ -9662,7 +9666,9 @@ fn log_source_to_proto(log_source: &LogSource) -> proto::GitLogSource {
|
|||
LogSource::All => proto::git_log_source::Source::All(proto::GitLogSourceAll {}),
|
||||
LogSource::Branch(branch) => proto::git_log_source::Source::Branch(branch.to_string()),
|
||||
LogSource::Sha(sha) => proto::git_log_source::Source::Sha(sha.to_string()),
|
||||
LogSource::Path(path) => proto::git_log_source::Source::Path(path.to_proto()),
|
||||
LogSource::Path(path) => {
|
||||
proto::git_log_source::Source::Path(path.as_unix_str().to_owned())
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -10066,11 +10072,9 @@ mod tests {
|
|||
fn test_new_worktree_path_uses_posix_style_for_remote_paths() {
|
||||
let work_dir = Path::new("/home/user/dev/lsp-tests");
|
||||
let directory =
|
||||
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
|
||||
let directory = PathStyle::Posix
|
||||
.join_path(&directory, "nimble-sky")
|
||||
.unwrap();
|
||||
let path = PathStyle::Posix.join_path(&directory, "lsp-tests").unwrap();
|
||||
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
|
||||
let directory = PathStyle::Unix.join_path(&directory, "nimble-sky").unwrap();
|
||||
let path = PathStyle::Unix.join_path(&directory, "lsp-tests").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
path,
|
||||
|
|
|
|||
|
|
@ -698,7 +698,7 @@ impl ImageStoreImpl for Entity<RemoteImageStore> {
|
|||
.request(rpc::proto::OpenImageByPath {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path.to_proto(),
|
||||
path: path.as_unix_str().to_owned(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor,
|
||||
DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint,
|
||||
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
|
||||
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
|
||||
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectPath,
|
||||
ProjectTransaction, PulledDiagnostics, ResolveState,
|
||||
lsp_store::{LocalLspStore, LspDocumentLink, LspFoldingRange, LspStore},
|
||||
};
|
||||
|
|
@ -19,7 +19,7 @@ use language::{
|
|||
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext,
|
||||
OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
language_settings::{InlayHintKind, LanguageSettings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
lsp_to_symbol_kind, point_from_lsp, point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
|
||||
serialize_anchor_range, serialize_version,
|
||||
|
|
@ -35,10 +35,9 @@ use lsp::{
|
|||
use serde_json::Value;
|
||||
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
use std::{
|
||||
cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
|
||||
};
|
||||
use std::{cmp::Reverse, collections::hash_map, ops::Range, path::Path, str::FromStr, sync::Arc};
|
||||
use text::{BufferId, LineEnding};
|
||||
use util::rel_path::RelPath;
|
||||
use util::{ResultExt as _, debug_panic};
|
||||
|
||||
pub use signature_help::SignatureHelp;
|
||||
|
|
@ -189,7 +188,17 @@ pub(crate) struct PerformRename {
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct GetDefinitions {
|
||||
pub position: PointUtf16,
|
||||
pub workspace_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct EditPredictionDefinition {
|
||||
pub path: ProjectPath,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct GetEditPredictionDefinitions {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -200,7 +209,11 @@ pub(crate) struct GetDeclarations {
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct GetTypeDefinitions {
|
||||
pub position: PointUtf16,
|
||||
pub workspace_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct GetEditPredictionTypeDefinitions {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -694,15 +707,7 @@ impl LspCommand for GetDefinitions {
|
|||
server_id: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_lsp(
|
||||
message,
|
||||
lsp_store,
|
||||
buffer,
|
||||
server_id,
|
||||
self.workspace_only,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
|
||||
|
|
@ -713,7 +718,6 @@ impl LspCommand for GetDefinitions {
|
|||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
workspace_only: self.workspace_only,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -734,7 +738,6 @@ impl LspCommand for GetDefinitions {
|
|||
.await?;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
workspace_only: message.workspace_only,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -764,6 +767,106 @@ impl LspCommand for GetDefinitions {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetEditPredictionDefinitions {
|
||||
type Response = Vec<EditPredictionDefinition>;
|
||||
type LspRequest = lsp::request::GotoDefinition;
|
||||
type ProtoRequest = proto::GetEditPredictionDefinition;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"Get edit prediction definition"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
||||
capabilities
|
||||
.server_capabilities
|
||||
.definition_provider
|
||||
.is_some_and(|capability| match capability {
|
||||
OneOf::Left(supported) => supported,
|
||||
OneOf::Right(_options) => true,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<lsp::GotoDefinitionParams> {
|
||||
Ok(lsp::GotoDefinitionParams {
|
||||
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<lsp::GotoDefinitionResponse>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
edit_prediction_definitions_from_lsp(message, lsp_store, cx)
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetEditPredictionDefinition {
|
||||
proto::GetEditPredictionDefinition {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::GetEditPredictionDefinition,
|
||||
_: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
position: edit_prediction_position_from_proto(
|
||||
message.position,
|
||||
message.version,
|
||||
buffer,
|
||||
&mut cx,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Vec<EditPredictionDefinition>,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::GetEditPredictionDefinitionResponse {
|
||||
proto::GetEditPredictionDefinitionResponse {
|
||||
definitions: edit_prediction_definitions_to_proto(response),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::GetEditPredictionDefinitionResponse,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
edit_prediction_definitions_from_proto(message.definitions)
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetEditPredictionDefinition) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetDeclarations {
|
||||
type Response = Vec<LocationLink>;
|
||||
|
|
@ -807,7 +910,7 @@ impl LspCommand for GetDeclarations {
|
|||
server_id: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_lsp(message, lsp_store, buffer, server_id, false, cx).await
|
||||
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration {
|
||||
|
|
@ -909,7 +1012,7 @@ impl LspCommand for GetImplementations {
|
|||
server_id: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_lsp(message, lsp_store, buffer, server_id, false, cx).await
|
||||
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation {
|
||||
|
|
@ -1008,7 +1111,7 @@ impl LspCommand for GetTypeDefinitions {
|
|||
server_id: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
location_links_from_lsp(message, project, buffer, server_id, self.workspace_only, cx).await
|
||||
location_links_from_lsp(message, project, buffer, server_id, cx).await
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
|
||||
|
|
@ -1019,7 +1122,6 @@ impl LspCommand for GetTypeDefinitions {
|
|||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
workspace_only: self.workspace_only,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1040,7 +1142,6 @@ impl LspCommand for GetTypeDefinitions {
|
|||
.await?;
|
||||
Ok(Self {
|
||||
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer)),
|
||||
workspace_only: message.workspace_only,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1070,6 +1171,103 @@ impl LspCommand for GetTypeDefinitions {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetEditPredictionTypeDefinitions {
|
||||
type Response = Vec<EditPredictionDefinition>;
|
||||
type LspRequest = lsp::request::GotoTypeDefinition;
|
||||
type ProtoRequest = proto::GetEditPredictionTypeDefinition;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"Get edit prediction type definition"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
||||
!matches!(
|
||||
&capabilities.server_capabilities.type_definition_provider,
|
||||
None | Some(lsp::TypeDefinitionProviderCapability::Simple(false))
|
||||
)
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<lsp::GotoTypeDefinitionParams> {
|
||||
Ok(lsp::GotoTypeDefinitionParams {
|
||||
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<lsp::GotoDefinitionResponse>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: LanguageServerId,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
edit_prediction_definitions_from_lsp(message, lsp_store, cx)
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetEditPredictionTypeDefinition {
|
||||
proto::GetEditPredictionTypeDefinition {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
position: Some(language::proto::serialize_anchor(
|
||||
&buffer.anchor_before(self.position),
|
||||
)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::GetEditPredictionTypeDefinition,
|
||||
_: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
position: edit_prediction_position_from_proto(
|
||||
message.position,
|
||||
message.version,
|
||||
buffer,
|
||||
&mut cx,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Vec<EditPredictionDefinition>,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::GetEditPredictionTypeDefinitionResponse {
|
||||
proto::GetEditPredictionTypeDefinitionResponse {
|
||||
definitions: edit_prediction_definitions_to_proto(response),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::GetEditPredictionTypeDefinitionResponse,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
edit_prediction_definitions_from_proto(message.definitions)
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetEditPredictionTypeDefinition) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_for_buffer(
|
||||
lsp_store: &Entity<LspStore>,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
|
@ -1165,57 +1363,13 @@ pub async fn location_links_from_lsp(
|
|||
lsp_store: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
workspace_only: bool,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Vec<LocationLink>> {
|
||||
let message = match message {
|
||||
Some(message) => message,
|
||||
None => return Ok(Vec::new()),
|
||||
};
|
||||
|
||||
let mut unresolved_links = Vec::new();
|
||||
match message {
|
||||
lsp::GotoDefinitionResponse::Scalar(loc) => {
|
||||
unresolved_links.push((None, loc.uri, loc.range));
|
||||
}
|
||||
|
||||
lsp::GotoDefinitionResponse::Array(locs) => {
|
||||
unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
|
||||
}
|
||||
|
||||
lsp::GotoDefinitionResponse::Link(links) => {
|
||||
unresolved_links.extend(links.into_iter().map(|l| {
|
||||
(
|
||||
l.origin_selection_range,
|
||||
l.target_uri,
|
||||
l.target_selection_range,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
let unresolved_links = definition_locations_from_lsp(message);
|
||||
|
||||
let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
||||
let mut definitions = Vec::new();
|
||||
for (origin_range, target_uri, target_range) in unresolved_links {
|
||||
if workspace_only
|
||||
&& !lsp_store.update(&mut cx, |this, cx| {
|
||||
use util::paths::UrlExt as _;
|
||||
let worktree_store = this.worktree_store().read(cx);
|
||||
let path_style = worktree_store.path_style();
|
||||
let Ok(abs_path) = target_uri.clone().to_file_path_ext(path_style) else {
|
||||
return false;
|
||||
};
|
||||
worktree_store
|
||||
.find_worktree(&abs_path, cx)
|
||||
.is_some_and(|(worktree, _)| {
|
||||
let worktree = worktree.read(cx);
|
||||
worktree.is_visible() && !worktree.is_single_file()
|
||||
})
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let target_buffer_handle = lsp_store
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
|
||||
|
|
@ -1256,6 +1410,94 @@ pub async fn location_links_from_lsp(
|
|||
Ok(definitions)
|
||||
}
|
||||
|
||||
fn definition_locations_from_lsp(
|
||||
message: Option<lsp::GotoDefinitionResponse>,
|
||||
) -> Vec<(Option<lsp::Range>, lsp::Uri, lsp::Range)> {
|
||||
let Some(message) = message else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut locations = Vec::new();
|
||||
match message {
|
||||
lsp::GotoDefinitionResponse::Scalar(location) => {
|
||||
locations.push((None, location.uri, location.range));
|
||||
}
|
||||
|
||||
lsp::GotoDefinitionResponse::Array(locations_from_lsp) => {
|
||||
locations.extend(
|
||||
locations_from_lsp
|
||||
.into_iter()
|
||||
.map(|location| (None, location.uri, location.range)),
|
||||
);
|
||||
}
|
||||
|
||||
lsp::GotoDefinitionResponse::Link(links) => {
|
||||
locations.extend(links.into_iter().map(|link| {
|
||||
(
|
||||
link.origin_selection_range,
|
||||
link.target_uri,
|
||||
link.target_selection_range,
|
||||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
locations
|
||||
}
|
||||
|
||||
fn edit_prediction_definitions_from_lsp(
|
||||
message: Option<lsp::GotoDefinitionResponse>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
let unresolved_locations = definition_locations_from_lsp(message);
|
||||
lsp_store.update(&mut cx, |lsp_store, cx| {
|
||||
use util::paths::UrlExt as _;
|
||||
let mut definitions = Vec::new();
|
||||
let worktree_store = lsp_store.worktree_store().read(cx);
|
||||
let path_style = worktree_store.path_style();
|
||||
|
||||
for (_, uri, range) in unresolved_locations {
|
||||
let Ok(abs_path) = uri.to_file_path_ext(path_style) else {
|
||||
continue;
|
||||
};
|
||||
let Some((worktree, relative_path)) = worktree_store.find_worktree(&abs_path, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let worktree = worktree.read(cx);
|
||||
if !worktree.is_visible() || worktree.is_single_file() {
|
||||
continue;
|
||||
}
|
||||
definitions.push(EditPredictionDefinition {
|
||||
path: ProjectPath {
|
||||
worktree_id: worktree.id(),
|
||||
path: relative_path,
|
||||
},
|
||||
range: point_from_lsp(range.start)..point_from_lsp(range.end),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(definitions)
|
||||
})
|
||||
}
|
||||
|
||||
async fn edit_prediction_position_from_proto(
|
||||
position: Option<proto::Anchor>,
|
||||
version: Vec<proto::VectorClockEntry>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<PointUtf16> {
|
||||
let position = position
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid position")?;
|
||||
buffer
|
||||
.update(cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&version))
|
||||
})
|
||||
.await?;
|
||||
Ok(buffer.read_with(cx, |buffer, _| position.to_point_utf16(buffer)))
|
||||
}
|
||||
|
||||
pub async fn location_link_from_lsp(
|
||||
link: lsp::LocationLink,
|
||||
lsp_store: &Entity<LspStore>,
|
||||
|
|
@ -1363,6 +1605,48 @@ pub fn location_link_to_proto(
|
|||
}
|
||||
}
|
||||
|
||||
fn edit_prediction_definitions_to_proto(
|
||||
definitions: Vec<EditPredictionDefinition>,
|
||||
) -> Vec<proto::EditPredictionDefinition> {
|
||||
definitions
|
||||
.into_iter()
|
||||
.map(|definition| proto::EditPredictionDefinition {
|
||||
worktree_id: definition.path.worktree_id.to_proto(),
|
||||
path: definition.path.path.as_ref().as_unix_str().to_owned(),
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: definition.range.start.0.row,
|
||||
column: definition.range.start.0.column,
|
||||
}),
|
||||
end: Some(proto::PointUtf16 {
|
||||
row: definition.range.end.0.row,
|
||||
column: definition.range.end.0.column,
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn edit_prediction_definitions_from_proto(
|
||||
definitions: Vec<proto::EditPredictionDefinition>,
|
||||
) -> Result<Vec<EditPredictionDefinition>> {
|
||||
definitions
|
||||
.into_iter()
|
||||
.map(|definition| {
|
||||
let start = definition.start.context("missing definition start")?;
|
||||
let end = definition.end.context("missing definition end")?;
|
||||
Ok(EditPredictionDefinition {
|
||||
path: ProjectPath {
|
||||
worktree_id: worktree::WorktreeId::from_proto(definition.worktree_id),
|
||||
path: RelPath::from_unix_str(&definition.path)
|
||||
.context("invalid path")?
|
||||
.into(),
|
||||
},
|
||||
range: Unclipped(PointUtf16::new(start.row, start.column))
|
||||
..Unclipped(PointUtf16::new(end.row, end.column)),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetReferences {
|
||||
type Response = Vec<Location>;
|
||||
|
|
@ -1749,7 +2033,7 @@ impl LspCommand for GetDocumentSymbols {
|
|||
.into_iter()
|
||||
.map(|lsp_symbol| DocumentSymbol {
|
||||
name: lsp_symbol.name,
|
||||
kind: lsp_symbol.kind,
|
||||
kind: lsp_to_symbol_kind(lsp_symbol.kind),
|
||||
range: range_from_lsp(lsp_symbol.location.range),
|
||||
selection_range: range_from_lsp(lsp_symbol.location.range),
|
||||
children: Vec::new(),
|
||||
|
|
@ -1759,7 +2043,7 @@ impl LspCommand for GetDocumentSymbols {
|
|||
fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
|
||||
DocumentSymbol {
|
||||
name: lsp_symbol.name,
|
||||
kind: lsp_symbol.kind,
|
||||
kind: lsp_to_symbol_kind(lsp_symbol.kind),
|
||||
range: range_from_lsp(lsp_symbol.range),
|
||||
selection_range: range_from_lsp(lsp_symbol.selection_range),
|
||||
children: lsp_symbol
|
||||
|
|
@ -1811,7 +2095,7 @@ impl LspCommand for GetDocumentSymbols {
|
|||
fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
|
||||
proto::DocumentSymbol {
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
||||
kind: symbol.kind as i32,
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
|
|
@ -1854,8 +2138,7 @@ impl LspCommand for GetDocumentSymbols {
|
|||
fn deserialize_symbol_with_children(
|
||||
serialized_symbol: proto::DocumentSymbol,
|
||||
) -> Result<DocumentSymbol> {
|
||||
let kind =
|
||||
unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
||||
let kind = language::SymbolKind::from_proto(serialized_symbol.kind);
|
||||
|
||||
let start = serialized_symbol.start.context("invalid start")?;
|
||||
let end = serialized_symbol.end.context("invalid end")?;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ use language::{
|
|||
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, LineEndingSetting,
|
||||
all_language_settings,
|
||||
},
|
||||
modeline, point_to_lsp,
|
||||
lsp_to_symbol_kind, modeline, point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
|
||||
serialize_anchor_range, serialize_version,
|
||||
|
|
@ -98,7 +98,7 @@ use lsp::{
|
|||
FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer,
|
||||
LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName,
|
||||
LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf,
|
||||
RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles,
|
||||
RenameFilesParams, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles,
|
||||
WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles,
|
||||
};
|
||||
use node_runtime::read_package_installed_version;
|
||||
|
|
@ -4273,7 +4273,7 @@ struct CoreSymbol {
|
|||
pub source_language_server_id: LanguageServerId,
|
||||
pub path: SymbolLocation,
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub kind: language::SymbolKind,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
pub container_name: Option<String>,
|
||||
}
|
||||
|
|
@ -4823,8 +4823,21 @@ impl LspStore {
|
|||
|
||||
let diagnostic_updates = local
|
||||
.language_servers
|
||||
.keys()
|
||||
.cloned()
|
||||
.iter()
|
||||
.filter_map(|(server_id, state)| {
|
||||
let supports_workspace_diagnostics = match state {
|
||||
LanguageServerState::Running {
|
||||
workspace_diagnostics_refresh_tasks,
|
||||
..
|
||||
} => !workspace_diagnostics_refresh_tasks.is_empty(),
|
||||
_ => false,
|
||||
};
|
||||
if supports_workspace_diagnostics {
|
||||
None
|
||||
} else {
|
||||
Some(*server_id)
|
||||
}
|
||||
})
|
||||
.map(|server_id| DocumentDiagnosticsUpdate {
|
||||
diagnostics: DocumentDiagnostics {
|
||||
document_abs_path: buffer_abs_path.clone(),
|
||||
|
|
@ -6171,31 +6184,9 @@ impl LspStore {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
self.definitions_with_filter(buffer, position, false, cx)
|
||||
}
|
||||
|
||||
pub fn workspace_definitions(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
self.definitions_with_filter(buffer, position, true, cx)
|
||||
}
|
||||
|
||||
fn definitions_with_filter(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
workspace_only: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||
let request = GetDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
};
|
||||
let request = GetDefinitions { position };
|
||||
if !self.is_capable_for_proto_request(buffer, &request, cx) {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
|
@ -6220,11 +6211,7 @@ impl LspStore {
|
|||
return Ok(None);
|
||||
};
|
||||
let actions = join_all(responses.payload.into_iter().map(|response| {
|
||||
GetDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
}
|
||||
.response_from_proto(
|
||||
GetDefinitions { position }.response_from_proto(
|
||||
response.response,
|
||||
lsp_store.clone(),
|
||||
buffer.clone(),
|
||||
|
|
@ -6247,10 +6234,7 @@ impl LspStore {
|
|||
let definitions_task = self.request_multiple_lsp_locally(
|
||||
buffer,
|
||||
Some(position),
|
||||
GetDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
},
|
||||
GetDefinitions { position },
|
||||
cx,
|
||||
);
|
||||
cx.background_spawn(async move {
|
||||
|
|
@ -6266,6 +6250,107 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn edit_prediction_definitions_for_command<C>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
request: C,
|
||||
position: PointUtf16,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<EditPredictionDefinition>>>
|
||||
where
|
||||
C: LspCommand<Response = Vec<EditPredictionDefinition>> + Clone,
|
||||
C::ProtoRequest: proto::LspRequestMessage,
|
||||
<C::ProtoRequest as proto::LspRequestMessage>::Response:
|
||||
Into<<C::ProtoRequest as proto::RequestMessage>::Response>,
|
||||
<C::LspRequest as lsp::request::Request>::Result: Send,
|
||||
<C::LspRequest as lsp::request::Request>::Params: Send,
|
||||
{
|
||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||
if !self.is_capable_for_proto_request(buffer, &request, cx) {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
}
|
||||
|
||||
let request_timeout = ProjectSettings::get_global(cx)
|
||||
.global_lsp_settings
|
||||
.get_request_timeout();
|
||||
|
||||
let request_task = upstream_client.request_lsp(
|
||||
project_id,
|
||||
None,
|
||||
request_timeout,
|
||||
cx.background_executor().clone(),
|
||||
request.to_proto(project_id, buffer.read(cx)),
|
||||
);
|
||||
let buffer = buffer.clone();
|
||||
cx.spawn(async move |weak_lsp_store, cx| {
|
||||
let Some(lsp_store) = weak_lsp_store.upgrade() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let Some(responses) = request_task.await? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let actions = join_all(responses.payload.into_iter().map(|response| {
|
||||
request.clone().response_from_proto(
|
||||
response.response.into(),
|
||||
lsp_store.clone(),
|
||||
buffer.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
}))
|
||||
.await;
|
||||
|
||||
Ok(actions
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<Vec<_>>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect())
|
||||
})
|
||||
} else {
|
||||
let definitions_task =
|
||||
self.request_multiple_lsp_locally(buffer, Some(position), request, cx);
|
||||
cx.background_spawn(async move {
|
||||
Ok(definitions_task
|
||||
.await
|
||||
.into_iter()
|
||||
.flat_map(|(_, definitions)| definitions)
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_prediction_definitions(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
include_type_definitions: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<EditPredictionDefinition>>> {
|
||||
let definitions = self.edit_prediction_definitions_for_command(
|
||||
buffer,
|
||||
GetEditPredictionDefinitions { position },
|
||||
position,
|
||||
cx,
|
||||
);
|
||||
let type_definitions = include_type_definitions.then(|| {
|
||||
self.edit_prediction_definitions_for_command(
|
||||
buffer,
|
||||
GetEditPredictionTypeDefinitions { position },
|
||||
position,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let mut merged = definitions.await?;
|
||||
if let Some(type_definitions) = type_definitions {
|
||||
merged.extend(type_definitions.await?);
|
||||
}
|
||||
let mut seen = HashSet::default();
|
||||
merged.retain(|definition| seen.insert(definition.clone()));
|
||||
Ok(merged)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn declarations(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
|
@ -6340,31 +6425,9 @@ impl LspStore {
|
|||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
self.type_definitions_with_filter(buffer, position, false, cx)
|
||||
}
|
||||
|
||||
pub fn workspace_type_definitions(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
self.type_definitions_with_filter(buffer, position, true, cx)
|
||||
}
|
||||
|
||||
fn type_definitions_with_filter(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: PointUtf16,
|
||||
workspace_only: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||
let request = GetTypeDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
};
|
||||
let request = GetTypeDefinitions { position };
|
||||
if !self.is_capable_for_proto_request(buffer, &request, cx) {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
|
@ -6387,11 +6450,7 @@ impl LspStore {
|
|||
return Ok(None);
|
||||
};
|
||||
let actions = join_all(responses.payload.into_iter().map(|response| {
|
||||
GetTypeDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
}
|
||||
.response_from_proto(
|
||||
GetTypeDefinitions { position }.response_from_proto(
|
||||
response.response,
|
||||
lsp_store.clone(),
|
||||
buffer.clone(),
|
||||
|
|
@ -6414,10 +6473,7 @@ impl LspStore {
|
|||
let type_definitions_task = self.request_multiple_lsp_locally(
|
||||
buffer,
|
||||
Some(position),
|
||||
GetTypeDefinitions {
|
||||
position,
|
||||
workspace_only,
|
||||
},
|
||||
GetTypeDefinitions { position },
|
||||
cx,
|
||||
);
|
||||
cx.background_spawn(async move {
|
||||
|
|
@ -8117,7 +8173,7 @@ impl LspStore {
|
|||
server_id: LanguageServerId,
|
||||
lsp_adapter: Arc<CachedLspAdapter>,
|
||||
worktree: WeakEntity<Worktree>,
|
||||
lsp_symbols: Vec<(String, SymbolKind, lsp::Location, Option<String>)>,
|
||||
lsp_symbols: Vec<(String, language::SymbolKind, lsp::Location, Option<String>)>,
|
||||
}
|
||||
|
||||
let mut requests = Vec::new();
|
||||
|
|
@ -8187,7 +8243,7 @@ impl LspStore {
|
|||
.map(|lsp_symbol| {
|
||||
(
|
||||
lsp_symbol.name,
|
||||
lsp_symbol.kind,
|
||||
lsp_to_symbol_kind(lsp_symbol.kind),
|
||||
lsp_symbol.location,
|
||||
lsp_symbol.container_name,
|
||||
)
|
||||
|
|
@ -8211,7 +8267,7 @@ impl LspStore {
|
|||
};
|
||||
Some((
|
||||
lsp_symbol.name,
|
||||
lsp_symbol.kind,
|
||||
lsp_to_symbol_kind(lsp_symbol.kind),
|
||||
location,
|
||||
lsp_symbol.container_name,
|
||||
))
|
||||
|
|
@ -8712,7 +8768,7 @@ impl LspStore {
|
|||
project_id: *project_id,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
summary: Some(proto::DiagnosticSummary {
|
||||
path: path.as_ref().to_proto(),
|
||||
path: path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
|
|
@ -9003,7 +9059,7 @@ impl LspStore {
|
|||
diagnostics_summary
|
||||
.more_summaries
|
||||
.push(proto::DiagnosticSummary {
|
||||
path: project_path.path.as_ref().to_proto(),
|
||||
path: project_path.path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: new_summary.error_count,
|
||||
warning_count: new_summary.warning_count,
|
||||
|
|
@ -9014,7 +9070,7 @@ impl LspStore {
|
|||
project_id,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
summary: Some(proto::DiagnosticSummary {
|
||||
path: project_path.path.as_ref().to_proto(),
|
||||
path: project_path.path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: new_summary.error_count,
|
||||
warning_count: new_summary.warning_count,
|
||||
|
|
@ -9098,7 +9154,7 @@ impl LspStore {
|
|||
Ok(ControlFlow::Continue(Some((
|
||||
*project_id,
|
||||
proto::DiagnosticSummary {
|
||||
path: path_in_worktree.to_proto(),
|
||||
path: path_in_worktree.as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: new_summary.error_count as u32,
|
||||
warning_count: new_summary.warning_count as u32,
|
||||
|
|
@ -9615,6 +9671,22 @@ impl LspStore {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
Request::GetEditPredictionDefinition(get_edit_prediction_definition) => {
|
||||
let position = get_edit_prediction_definition
|
||||
.position
|
||||
.clone()
|
||||
.and_then(deserialize_anchor);
|
||||
Self::query_lsp_locally::<GetEditPredictionDefinitions>(
|
||||
lsp_store,
|
||||
server_id,
|
||||
sender_id,
|
||||
lsp_request_id,
|
||||
get_edit_prediction_definition,
|
||||
position,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Request::GetDeclaration(get_declaration) => {
|
||||
let position = get_declaration
|
||||
.position
|
||||
|
|
@ -9647,6 +9719,22 @@ impl LspStore {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
Request::GetEditPredictionTypeDefinition(get_edit_prediction_type_definition) => {
|
||||
let position = get_edit_prediction_type_definition
|
||||
.position
|
||||
.clone()
|
||||
.and_then(deserialize_anchor);
|
||||
Self::query_lsp_locally::<GetEditPredictionTypeDefinitions>(
|
||||
lsp_store,
|
||||
server_id,
|
||||
sender_id,
|
||||
lsp_request_id,
|
||||
get_edit_prediction_type_definition,
|
||||
position,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Request::GetImplementation(get_implementation) => {
|
||||
let position = get_implementation
|
||||
.position
|
||||
|
|
@ -9871,7 +9959,7 @@ impl LspStore {
|
|||
let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
|
||||
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
|
||||
let new_path =
|
||||
RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?;
|
||||
RelPath::from_unix_str(&envelope.payload.new_path).context("invalid relative path")?;
|
||||
|
||||
let (worktree_store, old_worktree, new_worktree, old_entry) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
|
|
@ -9940,7 +10028,9 @@ impl LspStore {
|
|||
{
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: RelPath::from_proto(&message_summary.path).context("invalid path")?,
|
||||
path: RelPath::from_unix_str(&message_summary.path)
|
||||
.context("invalid path")?
|
||||
.into(),
|
||||
};
|
||||
let path = project_path.path.clone();
|
||||
let server_id = LanguageServerId(message_summary.language_server_id as usize);
|
||||
|
|
@ -9975,7 +10065,7 @@ impl LspStore {
|
|||
diagnostics_summary
|
||||
.more_summaries
|
||||
.push(proto::DiagnosticSummary {
|
||||
path: project_path.path.as_ref().to_proto(),
|
||||
path: project_path.path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: summary.error_count as u32,
|
||||
warning_count: summary.warning_count as u32,
|
||||
|
|
@ -9986,7 +10076,7 @@ impl LspStore {
|
|||
project_id: *project_id,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
summary: Some(proto::DiagnosticSummary {
|
||||
path: project_path.path.as_ref().to_proto(),
|
||||
path: project_path.path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: summary.error_count as u32,
|
||||
warning_count: summary.warning_count as u32,
|
||||
|
|
@ -11464,7 +11554,7 @@ impl LspStore {
|
|||
project_id,
|
||||
worktree_id: worktree_id.to_proto(),
|
||||
summary: Some(proto::DiagnosticSummary {
|
||||
path: path.as_ref().to_proto(),
|
||||
path: path.as_ref().as_unix_str().to_owned(),
|
||||
language_server_id: server_id.0 as u64,
|
||||
error_count: 0,
|
||||
warning_count: 0,
|
||||
|
|
@ -12479,7 +12569,7 @@ impl LspStore {
|
|||
source_worktree_id: symbol.source_worktree_id.to_proto(),
|
||||
language_server_id: symbol.source_language_server_id.to_proto(),
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
||||
kind: symbol.kind as i32,
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
|
|
@ -12496,7 +12586,7 @@ impl LspStore {
|
|||
match &symbol.path {
|
||||
SymbolLocation::InProject(path) => {
|
||||
result.worktree_id = path.worktree_id.to_proto();
|
||||
result.path = path.path.to_proto();
|
||||
result.path = path.path.as_unix_str().to_owned();
|
||||
}
|
||||
SymbolLocation::OutsideProject {
|
||||
abs_path,
|
||||
|
|
@ -12512,13 +12602,14 @@ impl LspStore {
|
|||
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
|
||||
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
|
||||
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
|
||||
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
||||
let kind = language::SymbolKind::from_proto(serialized_symbol.kind);
|
||||
|
||||
let path = if serialized_symbol.signature.is_empty() {
|
||||
SymbolLocation::InProject(ProjectPath {
|
||||
worktree_id,
|
||||
path: RelPath::from_proto(&serialized_symbol.path)
|
||||
.context("invalid symbol path")?,
|
||||
path: RelPath::from_unix_str(&serialized_symbol.path)
|
||||
.context("invalid symbol path")?
|
||||
.into(),
|
||||
})
|
||||
} else {
|
||||
SymbolLocation::OutsideProject {
|
||||
|
|
@ -14765,7 +14856,7 @@ impl DiagnosticSummary {
|
|||
path: &RelPath,
|
||||
) -> proto::DiagnosticSummary {
|
||||
proto::DiagnosticSummary {
|
||||
path: path.to_proto(),
|
||||
path: path.as_unix_str().to_owned(),
|
||||
language_server_id: language_server_id.0 as u64,
|
||||
error_count: self.error_count as u32,
|
||||
warning_count: self.warning_count as u32,
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ fn enriched_symbol_text(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use language::lsp_to_symbol_kind;
|
||||
use text::{OffsetRangeExt, Point, Unclipped};
|
||||
|
||||
fn make_symbol(
|
||||
|
|
@ -340,7 +341,7 @@ mod tests {
|
|||
use text::PointUtf16;
|
||||
DocumentSymbol {
|
||||
name: name.to_string(),
|
||||
kind,
|
||||
kind: lsp_to_symbol_kind(kind),
|
||||
range: Unclipped(PointUtf16::new(range.start.0, range.start.1))
|
||||
..Unclipped(PointUtf16::new(range.end.0, range.end.1)),
|
||||
selection_range: Unclipped(PointUtf16::new(
|
||||
|
|
|
|||
|
|
@ -443,7 +443,6 @@ impl LspCommand for GoToParentModule {
|
|||
lsp_store,
|
||||
buffer,
|
||||
server_id,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use path::rel_path::RelPathBuf;
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path.
|
||||
|
|
@ -59,12 +60,12 @@ impl<Label: Ord + Clone> RootPathTrie<Label> {
|
|||
) -> &mut Self {
|
||||
let mut current = self;
|
||||
|
||||
let mut path_so_far = <Arc<RelPath>>::from(RelPath::empty());
|
||||
let mut path_so_far = RelPathBuf::new();
|
||||
for key in path.0.iter() {
|
||||
path_so_far = path_so_far.join(RelPath::unix(key.as_ref()).unwrap());
|
||||
path_so_far = path_so_far.join(RelPath::from_unix_str(key.as_ref()).unwrap());
|
||||
current = match current.children.entry(key.clone()) {
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone()))
|
||||
vacant_entry.insert(RootPathTrie::new_with_key(path_so_far.clone().into()))
|
||||
}
|
||||
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -460,7 +460,7 @@ impl PrettierStore {
|
|||
) {
|
||||
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
||||
.iter()
|
||||
.map(|name| RelPath::unix(name).unwrap())
|
||||
.map(|name| RelPath::from_unix_str(name).unwrap())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let prettier_config_file_changed = changes
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ use lsp::{
|
|||
LanguageServerBinary, LanguageServerId, LanguageServerName, LanguageServerSelector,
|
||||
MessageActionItem,
|
||||
};
|
||||
pub use lsp_command::EditPredictionDefinition;
|
||||
use lsp_command::*;
|
||||
use lsp_store::{CompletionDocumentation, LspFormatTarget, OpenLspBufferHandle};
|
||||
pub use manifest_tree::ManifestProvidersStore;
|
||||
|
|
@ -439,14 +440,14 @@ impl ProjectPath {
|
|||
pub fn from_proto(p: proto::ProjectPath) -> Option<Self> {
|
||||
Some(Self {
|
||||
worktree_id: WorktreeId::from_proto(p.worktree_id),
|
||||
path: RelPath::from_proto(&p.path).log_err()?,
|
||||
path: RelPath::from_unix_str(&p.path).log_err()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ProjectPath {
|
||||
proto::ProjectPath {
|
||||
worktree_id: self.worktree_id.to_proto(),
|
||||
path: self.path.as_ref().to_proto(),
|
||||
path: self.path.as_ref().as_unix_str().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -864,7 +865,7 @@ pub struct Symbol {
|
|||
pub path: SymbolLocation,
|
||||
pub label: CodeLabel,
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub kind: language::SymbolKind,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
pub container_name: Option<String>,
|
||||
}
|
||||
|
|
@ -872,7 +873,7 @@ pub struct Symbol {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentSymbol {
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub kind: language::SymbolKind,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
pub selection_range: Range<Unclipped<PointUtf16>>,
|
||||
pub children: Vec<DocumentSymbol>,
|
||||
|
|
@ -1724,7 +1725,7 @@ impl Project {
|
|||
let path_style = if response.payload.windows_paths {
|
||||
PathStyle::Windows
|
||||
} else {
|
||||
PathStyle::Posix
|
||||
PathStyle::Unix
|
||||
};
|
||||
|
||||
let worktree_store = cx.new(|cx| {
|
||||
|
|
@ -2151,7 +2152,7 @@ impl Project {
|
|||
root_repo_is_linked_worktree: false,
|
||||
},
|
||||
client,
|
||||
PathStyle::Posix,
|
||||
PathStyle::Unix,
|
||||
cx,
|
||||
);
|
||||
self.worktree_store
|
||||
|
|
@ -3151,7 +3152,7 @@ impl Project {
|
|||
// because SSH projects have client_state: Local but still need to communicate with remote server
|
||||
let project_id = self.remote_id().unwrap_or(REMOTE_SERVER_PROJECT_ID);
|
||||
let downloading_files = self.downloading_files.clone();
|
||||
let path_str = path.to_proto();
|
||||
let path_str = path.as_unix_str().to_owned();
|
||||
|
||||
static NEXT_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
let file_id = NEXT_FILE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
|
|
@ -4287,21 +4288,16 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn workspace_definitions<T: ToPointUtf16>(
|
||||
pub fn edit_prediction_definitions<T: ToPointUtf16>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
include_type_definitions: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
) -> Task<Result<Vec<EditPredictionDefinition>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.workspace_definitions(buffer, position, cx)
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let result = task.await;
|
||||
drop(guard);
|
||||
result
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.edit_prediction_definitions(buffer, position, include_type_definitions, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -4341,24 +4337,6 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn workspace_type_definitions<T: ToPointUtf16>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Vec<LocationLink>>>> {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
let guard = self.retain_remotely_created_models(cx);
|
||||
let task = self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.workspace_type_definitions(buffer, position, cx)
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let result = task.await;
|
||||
drop(guard);
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn implementations<T: ToPointUtf16>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
|
@ -5788,7 +5766,7 @@ impl Project {
|
|||
) -> Result<proto::OpenBufferResponse> {
|
||||
let peer_id = envelope.original_sender_id()?;
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let path = RelPath::from_proto(&envelope.payload.path)?;
|
||||
let path = RelPath::from_unix_str(&envelope.payload.path)?.into();
|
||||
let open_buffer = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_buffer(ProjectPath { worktree_id, path }, cx)
|
||||
|
|
|
|||
|
|
@ -1023,7 +1023,7 @@ impl SettingsObserver {
|
|||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path.to_proto(),
|
||||
path: path.as_unix_str().to_owned(),
|
||||
content: Some(content),
|
||||
kind: Some(
|
||||
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
|
||||
|
|
@ -1206,7 +1206,7 @@ impl SettingsObserver {
|
|||
.unwrap()
|
||||
.into();
|
||||
(settings_dir, LocalSettingsKind::Debug)
|
||||
} else if path.ends_with(RelPath::unix(EDITORCONFIG_NAME).unwrap()) {
|
||||
} else if path.ends_with(RelPath::from_unix_str(EDITORCONFIG_NAME).unwrap()) {
|
||||
let Some(settings_dir) = path.parent().map(Arc::from) else {
|
||||
continue;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ impl ToolchainStore {
|
|||
};
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let path = if let Some(path) = envelope.payload.path {
|
||||
RelPath::from_proto(&path)?
|
||||
RelPath::from_unix_str(&path)?.into()
|
||||
} else {
|
||||
RelPath::empty_arc()
|
||||
};
|
||||
|
|
@ -277,7 +277,7 @@ impl ToolchainStore {
|
|||
envelope: TypedEnvelope<proto::ActiveToolchain>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::ActiveToolchainResponse> {
|
||||
let path = RelPath::unix(envelope.payload.path.as_deref().unwrap_or(""))?;
|
||||
let path = RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?;
|
||||
let toolchain = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let language_name = LanguageName::from_proto(envelope.payload.language_name);
|
||||
|
|
@ -314,7 +314,8 @@ impl ToolchainStore {
|
|||
.update(&mut cx, |this, cx| {
|
||||
let language_name = LanguageName::from_proto(envelope.payload.language_name);
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?;
|
||||
let path =
|
||||
RelPath::from_unix_str(envelope.payload.path.as_deref().unwrap_or(""))?.into();
|
||||
anyhow::Ok(this.list_toolchains(
|
||||
ProjectPath { worktree_id, path },
|
||||
language_name,
|
||||
|
|
@ -364,7 +365,7 @@ impl ToolchainStore {
|
|||
has_values,
|
||||
toolchains,
|
||||
groups,
|
||||
relative_worktree_path: Some(relative_path.to_proto()),
|
||||
relative_worktree_path: Some(relative_path.as_unix_str().to_owned()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -637,7 +638,7 @@ impl RemoteToolchainStore {
|
|||
path: path.to_string_lossy().into_owned(),
|
||||
raw_json: toolchain.as_json.to_string(),
|
||||
}),
|
||||
path: Some(project_path.path.to_proto()),
|
||||
path: Some(project_path.path.as_unix_str().to_owned()),
|
||||
})
|
||||
.await
|
||||
.log_err()?;
|
||||
|
|
@ -667,7 +668,7 @@ impl RemoteToolchainStore {
|
|||
project_id,
|
||||
worktree_id: path.worktree_id.to_proto(),
|
||||
language_name: language_name.clone().into(),
|
||||
path: Some(path.path.to_proto()),
|
||||
path: Some(path.path.as_unix_str().to_owned()),
|
||||
})
|
||||
.await
|
||||
.log_err()?;
|
||||
|
|
@ -693,7 +694,7 @@ impl RemoteToolchainStore {
|
|||
Some((usize::try_from(group.start_index).ok()?, group.name.into()))
|
||||
})
|
||||
.collect();
|
||||
let relative_path = RelPath::from_proto(
|
||||
let relative_path = RelPath::from_unix_str(
|
||||
response
|
||||
.relative_worktree_path
|
||||
.as_deref()
|
||||
|
|
@ -706,7 +707,7 @@ impl RemoteToolchainStore {
|
|||
default: None,
|
||||
groups,
|
||||
},
|
||||
relative_path,
|
||||
relative_path.into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
@ -724,7 +725,7 @@ impl RemoteToolchainStore {
|
|||
project_id,
|
||||
worktree_id: path.worktree_id.to_proto(),
|
||||
language_name: language_name.clone().into(),
|
||||
path: Some(path.path.to_proto()),
|
||||
path: Some(path.path.as_unix_str().to_owned()),
|
||||
})
|
||||
.await
|
||||
.log_err()?;
|
||||
|
|
|
|||
|
|
@ -513,7 +513,7 @@ impl WorktreeStore {
|
|||
let response = upstream_client.request(proto::CopyProjectEntry {
|
||||
project_id: *upstream_project_id,
|
||||
entry_id: entry_id.to_proto(),
|
||||
new_path: new_project_path.path.to_proto(),
|
||||
new_path: new_project_path.path.as_unix_str().to_owned(),
|
||||
new_worktree_id: new_project_path.worktree_id.to_proto(),
|
||||
});
|
||||
cx.spawn(async move |_, cx| {
|
||||
|
|
@ -668,7 +668,7 @@ impl WorktreeStore {
|
|||
let response = upstream_client.request(proto::RenameProjectEntry {
|
||||
project_id: *upstream_project_id,
|
||||
entry_id: entry_id.to_proto(),
|
||||
new_path: new_project_path.path.to_proto(),
|
||||
new_path: new_project_path.path.as_unix_str().to_owned(),
|
||||
new_worktree_id: new_project_path.worktree_id.to_proto(),
|
||||
});
|
||||
cx.spawn(async move |_, cx| {
|
||||
|
|
@ -1231,7 +1231,7 @@ impl WorktreeStore {
|
|||
let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id);
|
||||
let new_project_path = (
|
||||
new_worktree_id,
|
||||
RelPath::from_proto(&envelope.payload.new_path)?,
|
||||
RelPath::from_unix_str(&envelope.payload.new_path)?,
|
||||
);
|
||||
let (scan_id, entry) = this.update(&mut cx, |this, cx| {
|
||||
let Some((_, project_id)) = this.downstream_client else {
|
||||
|
|
@ -1289,7 +1289,7 @@ impl WorktreeStore {
|
|||
) -> Result<proto::ProjectEntryResponse> {
|
||||
let entry_id = ProjectEntryId::from_proto(request.entry_id);
|
||||
let new_worktree_id = WorktreeId::from_proto(request.new_worktree_id);
|
||||
let rel_path = RelPath::from_proto(&request.new_path)
|
||||
let rel_path = RelPath::from_unix_str(&request.new_path)
|
||||
.with_context(|| format!("received invalid relative path {:?}", &request.new_path))?;
|
||||
|
||||
let (scan_id, task) = this.update(&mut cx, |this, cx| {
|
||||
|
|
|
|||
|
|
@ -721,7 +721,12 @@ mod git_traversal {
|
|||
|
||||
let traversal = GitTraversal::new(
|
||||
&repo_snapshots,
|
||||
worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()),
|
||||
worktree_snapshot.traverse_from_path(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
RelPath::from_unix_str("x").unwrap(),
|
||||
),
|
||||
);
|
||||
let entries = traversal
|
||||
.map(|entry| (entry.path.clone(), entry.git_summary))
|
||||
|
|
@ -1204,45 +1209,44 @@ mod git_worktrees {
|
|||
let work_dir = Path::new("/code/my-project");
|
||||
|
||||
// Valid: sibling
|
||||
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).is_ok());
|
||||
assert!(worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).is_ok());
|
||||
|
||||
// Valid: subdirectory
|
||||
assert!(
|
||||
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Posix).is_ok()
|
||||
worktrees_directory_for_repo(work_dir, ".git/zed-worktrees", PathStyle::Unix).is_ok()
|
||||
);
|
||||
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Posix).is_ok());
|
||||
assert!(worktrees_directory_for_repo(work_dir, "my-worktrees", PathStyle::Unix).is_ok());
|
||||
|
||||
// Invalid: just ".." would resolve back to the working directory itself
|
||||
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "..", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("must not be \"..\""));
|
||||
|
||||
// Invalid: ".." with trailing separators
|
||||
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "..\\", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("must not be \"..\""));
|
||||
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "../", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("must not be \"..\""));
|
||||
|
||||
// Invalid: empty string would resolve to the working directory itself
|
||||
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("must not be empty"));
|
||||
|
||||
// Invalid: absolute path
|
||||
let err =
|
||||
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Posix).unwrap_err();
|
||||
worktrees_directory_for_repo(work_dir, "/tmp/worktrees", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("relative path"));
|
||||
|
||||
// Invalid: "/" is absolute on Unix
|
||||
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "/", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("relative path"));
|
||||
|
||||
// Invalid: "///" is absolute
|
||||
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Posix).unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "///", PathStyle::Unix).unwrap_err();
|
||||
assert!(err.to_string().contains("relative path"));
|
||||
|
||||
// Invalid: escapes too far up
|
||||
let err =
|
||||
worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Posix)
|
||||
.unwrap_err();
|
||||
let err = worktrees_directory_for_repo(work_dir, "../../other-project/wt", PathStyle::Unix)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("outside"));
|
||||
}
|
||||
|
||||
|
|
@ -1251,7 +1255,7 @@ mod git_worktrees {
|
|||
let work_dir = Path::new("/home/user/dev/lsp-tests");
|
||||
|
||||
let directory =
|
||||
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Posix).unwrap();
|
||||
worktrees_directory_for_repo(work_dir, "../worktrees", PathStyle::Unix).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
directory,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
|
|||
let entry = Entry {
|
||||
id: ProjectEntryId::from_proto(1),
|
||||
kind: EntryKind::UnloadedDir,
|
||||
path: Arc::from(RelPath::unix(Path::new("src/data")).unwrap()),
|
||||
path: Arc::from(RelPath::from_unix_str(Path::new("src/data")).unwrap()),
|
||||
inode: 0,
|
||||
mtime: None,
|
||||
canonical_path: None,
|
||||
|
|
@ -90,7 +90,7 @@ async fn test_path_inclusion_matcher(cx: &mut gpui::TestAppContext) {
|
|||
// 2. Test searching for `field`, including ignored files but updating
|
||||
// `files_to_include` to only include files under `src/lib`.
|
||||
let include_ignored = true;
|
||||
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Posix).unwrap();
|
||||
let files_to_include = PathMatcher::new(vec!["src/lib"], PathStyle::Unix).unwrap();
|
||||
let files_to_exclude = PathMatcher::default();
|
||||
let match_full_paths = false;
|
||||
let search_query = SearchQuery::text(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue