Compare commits

...

14 Commits

Author SHA1 Message Date
Mikayla Maki
8b55cb40c1 Restore test running 2024-05-29 19:38:00 -07:00
Mikayla Maki
5a149b970c Make tests less noisy (#12463)
When running the tests for linux, I found a lot of benign errors getting
logged. This PR cuts down some of the noise from unnecessary workspace
serialization and SVG renders

Release Notes:

- N/A
2024-05-29 18:06:45 -07:00
Marshall Bowers
bdf627ce07 rustdoc_to_markdown: Fix code blocks (#12460)
This PR fixes an issue in `rustdoc_to_markdown` with code blocks being
trimmed incorrectly.

We were erroneously popping from the current element stack even if we
didn't push an element onto the stack.

Added test coverage for this case as well, so we don't regress.

Release Notes:

- N/A
2024-05-29 19:23:06 -04:00
Marshall Bowers
a5011996fb rustdoc_to_markdown: Recognize Rust code blocks (#12458)
This PR makes it so Rust code blocks are recognized and
syntax-highlighted when converting from rustdoc to Markdown.

Release Notes:

- N/A
2024-05-29 18:57:20 -04:00
Nathan Sobo
b8d9713b4f Make prompt library icon in context panel staff-only for now (#12457)
This is still pretty raw, so I'd like to hold off on shipping it to all
users.

Release Notes:

- Hide the prompt library for non-staff until it is in a more complete
state.
2024-05-29 16:53:45 -06:00
Marshall Bowers
abec028e58 rustdoc_to_markdown: Clean up heading spacing (#12456)
This PR cleans up the spacing around the Markdown headings in the output
so that they are consistent.

Release Notes:

- N/A
2024-05-29 18:39:51 -04:00
Marshall Bowers
08881828ce assistant: Add /rustdoc slash command (#12453)
This PR adds a `/rustdoc` slash command for retrieving and inserting
rustdoc docs into the Assistant.

Right now the command accepts the crate name as an argument and will
return the top-level docs from `docs.rs`.

Release Notes:

- N/A
2024-05-29 18:14:29 -04:00
Max Brunsfeld
dd328efaa7 Compute git statuses using the bundled git executable, not libgit2 (#12444)
I realized that somehow, the `git` executable is able to compute `git
status` much more quickly than libgit2, so I've switched our git status
logic to use `git`. Follow-up to
https://github.com/zed-industries/zed/pull/12266.

Release Notes:

- Improved the performance of git status updated when working in large
git repositories.
2024-05-29 14:31:24 -07:00
Joshua Ferguson
6294a3b80b Add xdg trash support (#12391)
- Added support for xdg trash when deleting files on linux
- moved ashpd depency to toplevel to use it in both fs and gpui

If I need to add test, or change anything, please let me know. I tested
locally by creating and deleting a file and confirming it showed up in
my trashcan, but that probably a less than ideal method of confirming
correct behavior

Also, I could remove the delete directory function for linux, and change
the one configured for macos to compile for both macos and linux (they
are the same, the version of the function they are calling is
different).

Release Notes:

- N/A
2024-05-29 14:15:29 -07:00
Kirill Bulatov
0f927fa6fb One less unwrap (#12448)
Fixes
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1717011343884699

Release Notes:

- N/A
2024-05-29 23:44:56 +03:00
Marshall Bowers
5bcb9ed017 Add rustdoc_to_markdown crate (#12445)
This PR adds a new crate for converting rustdoc output to Markdown.

We're leveraging Servo's `html5ever` to parse the Markdown content, and
then walking the DOM nodes to convert it to a Markdown string.

The Markdown output will be continued to be refined, but it's in a place
where it should be reasonable.

Release Notes:

- N/A
2024-05-29 16:05:16 -04:00
Bennet Bo Fenner
a22cd95f9d Fix deleted hunk offset when zooming (#12442)
Release Notes:

- Fixed an issue where expanded hunks could be rendered at the wrong
position when zooming
- Fixed an issue where expanded hunks could be rendered at the wrong
position when toggling git blame
([#11941](https://github.com/zed-industries/zed/issues/11941))
2024-05-29 20:06:10 +02:00
Dzmitry Malyshau
44c50da94f blade: Use BufferBelt from blade-utils (#12411)
Release Notes:

- N/A

Follow-up to #12340
Carries https://github.com/kvark/blade/pull/122 and
https://github.com/kvark/blade/pull/119
2024-05-29 09:50:45 -07:00
Joseph T. Lyons
c34d36161d v0.139.x dev 2024-05-29 12:15:12 -04:00
65 changed files with 1222 additions and 621 deletions

View File

@@ -99,7 +99,6 @@ jobs:
- name: Build other binaries and features
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
# todo(linux): Actually run the tests
linux_tests:
name: (Linux) Run Clippy and tests
runs-on:
@@ -117,6 +116,9 @@ jobs:
- name: cargo clippy
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed

169
Cargo.lock generated
View File

@@ -346,6 +346,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.28",
@@ -366,6 +367,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"rope",
"rustdoc_to_markdown",
"schemars",
"search",
"semantic_index",
@@ -1509,7 +1511,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a#9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"ash",
"ash-window",
@@ -1539,13 +1541,24 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a#9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
dependencies = [
"blade-graphics",
"bytemuck",
"log",
"profiling",
]
[[package]]
name = "block"
version = "0.1.6"
@@ -4191,6 +4204,7 @@ name = "fs"
version = "0.1.0"
dependencies = [
"anyhow",
"ashpd",
"async-tar",
"async-trait",
"cocoa",
@@ -4689,6 +4703,7 @@ dependencies = [
"bindgen 0.65.1",
"blade-graphics",
"blade-macros",
"blade-util",
"block",
"bytemuck",
"calloop",
@@ -5048,6 +5063,20 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
[[package]]
name = "html5ever"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "http"
version = "0.1.0"
@@ -5707,7 +5736,7 @@ dependencies = [
"tree-sitter-embedded-template",
"tree-sitter-heex",
"tree-sitter-html",
"tree-sitter-json 0.20.2",
"tree-sitter-json",
"tree-sitter-markdown",
"tree-sitter-ruby",
"tree-sitter-rust",
@@ -5797,7 +5826,7 @@ dependencies = [
"tree-sitter-gomod",
"tree-sitter-gowork",
"tree-sitter-jsdoc",
"tree-sitter-json 0.20.2",
"tree-sitter-json",
"tree-sitter-markdown",
"tree-sitter-proto",
"tree-sitter-python",
@@ -6169,6 +6198,32 @@ dependencies = [
"workspace",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [
"log",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -7274,7 +7329,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared",
"phf_shared 0.11.2",
]
[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
]
[[package]]
@@ -7283,7 +7358,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"phf_shared 0.11.2",
"rand 0.8.5",
]
@@ -7293,13 +7368,22 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher 0.3.11",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
@@ -7543,6 +7627,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettier"
version = "0.1.0"
@@ -8542,6 +8632,18 @@ dependencies = [
"semver",
]
[[package]]
name = "rustdoc_to_markdown"
version = "0.1.0"
dependencies = [
"anyhow",
"html5ever",
"indoc",
"markup5ever_rcdom",
"pretty_assertions",
"regex",
]
[[package]]
name = "rustix"
version = "0.37.23"
@@ -9106,7 +9208,7 @@ dependencies = [
"serde_json_lenient",
"smallvec",
"tree-sitter",
"tree-sitter-json 0.19.0",
"tree-sitter-json",
"unindent",
"util",
]
@@ -9790,6 +9892,32 @@ dependencies = [
"float-cmp",
]
[[package]]
name = "string_cache"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro2",
"quote",
]
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -10979,16 +11107,6 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-json"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90b04c4e1a92139535eb9fca4ec8fa9666cc96b618005d3ae35f3c957fa92f92"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-json"
version = "0.20.2"
@@ -12925,6 +13043,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054a8e68b76250b253f671d1268cb7f1ae089ec35e195b2efb2a4e9a836d0621"
[[package]]
name = "xml5ever"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c376f76ed09df711203e20c3ef5ce556f0166fa03d39590016c0fd625437fad"
dependencies = [
"log",
"mac",
"markup5ever",
]
[[package]]
name = "xmlparser"
version = "0.13.5"
@@ -13046,7 +13175,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.138.0"
version = "0.139.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -76,6 +76,7 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc_to_markdown",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -220,6 +221,7 @@ dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -255,6 +257,7 @@ zed_actions = { path = "crates/zed_actions" }
anyhow = "1.0.57"
any_vec = "0.13"
ashpd = "0.8.0"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-fs = "1.6"
async-recursion = "1.0.0"
@@ -262,8 +265,9 @@ async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "9c9cabf69e869fc7d9aef2fc76f7d5c354d5710a" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -283,10 +287,9 @@ futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { version = "0.20.1", features = [
"read-txn-no-tls",
] }
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
indoc = "1"
# We explicitly disable http2 support in isahc.
@@ -299,6 +302,7 @@ lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nix = "0.28"
once_cell = "1.19.0"
@@ -490,10 +494,5 @@ non_canonical_partial_ord_impl = "allow"
reversed_empty_ranges = "allow"
type_complexity = "allow"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(gles)' # used in gpui
] }
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

@@ -16,9 +16,9 @@ use rust_embed::RustEmbed;
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| f.data)
.map(|f| Some(f.data))
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
@@ -42,7 +42,10 @@ impl Assets {
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx.asset_source().load(&font_path)?;
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Assets should never return None");
embedded_fonts.push(font_bytes);
}
}

View File

@@ -22,6 +22,7 @@ client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
@@ -39,6 +40,7 @@ parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc_to_markdown.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true

View File

@@ -1,5 +1,5 @@
use crate::prompts::{generate_content_prompt, PromptLibrary, PromptManager};
use crate::slash_command::{search_command, tabs_command};
use crate::slash_command::{rustdoc_command, search_command, tabs_command};
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind},
@@ -28,6 +28,7 @@ use editor::{
ToOffset as _, ToPoint,
};
use editor::{display_map::FlapId, FoldPlaceholder};
use feature_flags::{FeatureFlag, FeatureFlagAppExt, FeatureFlagViewExt};
use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
@@ -127,6 +128,12 @@ struct ActiveConversationEditor {
_subscriptions: Vec<Subscription>,
}
struct PromptLibraryFeatureFlag;
impl FeatureFlag for PromptLibraryFeatureFlag {
const NAME: &'static str = "prompt-library";
}
impl AssistantPanel {
const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
@@ -152,6 +159,9 @@ impl AssistantPanel {
let workspace_handle = workspace.clone();
workspace.update(&mut cx, |workspace, cx| {
cx.new_view::<Self>(|cx| {
cx.observe_flag::<PromptLibraryFeatureFlag, _>(|_, _, cx| cx.notify())
.detach();
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
let mut events = fs
@@ -210,6 +220,7 @@ impl AssistantPanel {
slash_command_registry.register_command(tabs_command::TabsSlashCommand);
slash_command_registry.register_command(project_command::ProjectSlashCommand);
slash_command_registry.register_command(search_command::SearchSlashCommand);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand);
Self {
workspace: workspace_handle,
@@ -1178,38 +1189,38 @@ impl AssistantPanel {
}
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let header =
TabBar::new("assistant_header")
.start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
.children(self.active_conversation_editor().map(|editor| {
h_flex()
.h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
.flex_1()
.px_2()
.child(Label::new(editor.read(cx).title(cx)).into_element())
}))
.end_child(
h_flex()
.gap_2()
.when_some(self.active_conversation_editor(), |this, editor| {
let conversation = editor.read(cx).conversation.clone();
this.child(
h_flex()
.gap_1()
.child(self.render_model(&conversation, cx))
.children(self.render_remaining_tokens(&conversation, cx)),
)
.child(
ui::Divider::vertical()
.inset()
.color(ui::DividerColor::Border),
)
})
.child(
let header = TabBar::new("assistant_header")
.start_child(h_flex().gap_1().child(self.render_popover_button(cx)))
.children(self.active_conversation_editor().map(|editor| {
h_flex()
.h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
.flex_1()
.px_2()
.child(Label::new(editor.read(cx).title(cx)).into_element())
}))
.end_child(
h_flex()
.gap_2()
.when_some(self.active_conversation_editor(), |this, editor| {
let conversation = editor.read(cx).conversation.clone();
this.child(
h_flex()
.gap_1()
.child(self.render_inject_context_menu(cx))
.child(
.child(self.render_model(&conversation, cx))
.children(self.render_remaining_tokens(&conversation, cx)),
)
.child(
ui::Divider::vertical()
.inset()
.color(ui::DividerColor::Border),
)
})
.child(
h_flex()
.gap_1()
.child(self.render_inject_context_menu(cx))
.children(
cx.has_flag::<PromptLibraryFeatureFlag>().then_some(
IconButton::new("show_prompt_manager", IconName::Library)
.icon_size(IconSize::Small)
.on_click(cx.listener(|this, _event, cx| {
@@ -1217,8 +1228,9 @@ impl AssistantPanel {
}))
.tooltip(|cx| Tooltip::text("Prompt Library…", cx)),
),
),
);
),
),
);
let contents = if self.active_conversation_editor().is_some() {
let mut registrar = DivRegistrar::new(

View File

@@ -20,6 +20,7 @@ pub mod active_command;
pub mod file_command;
pub mod project_command;
pub mod prompt_command;
pub mod rustdoc_command;
pub mod search_command;
pub mod tabs_command;

View File

@@ -0,0 +1,137 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand {
async fn build_message(
http_client: Arc<HttpClientWithUrl>,
crate_name: String,
) -> Result<String> {
let mut response = http_client
.get(
&format!("https://docs.rs/{crate_name}"),
AsyncBody::default(),
true,
)
.await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading docs.rs response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
convert_rustdoc_to_markdown(&body[..])
}
}
impl SlashCommand for RustdocSlashCommand {
fn name(&self) -> String {
"rustdoc".into()
}
fn description(&self) -> String {
"insert the docs for a Rust crate".into()
}
fn tooltip_text(&self) -> String {
"insert rustdoc".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing crate name")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let http_client = workspace.read(cx).client().http_client();
let crate_name = argument.to_string();
let text = cx.background_executor().spawn({
let crate_name = crate_name.clone();
async move { Self::build_message(http_client, crate_name).await }
});
let crate_name = SharedString::from(crate_name);
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
RustdocPlaceholder {
id,
unfold,
crate_name: crate_name.clone(),
}
.into_any_element()
}),
}],
})
})
}
}
#[derive(IntoElement)]
struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub crate_name: SharedString,
}
impl RenderOnce for RustdocPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust))
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -41,7 +41,12 @@ impl SoundRegistry {
}
let path = format!("sounds/{}.wav", name);
let bytes = self.assets.load(&path)?.into_owned();
let bytes = self
.assets
.load(&path)?
.map(|asset| Ok(asset))
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();

View File

@@ -192,10 +192,13 @@ impl UserStore {
cx.update(|cx| {
if let Some(info) = info {
cx.update_flags(info.staff, info.flags);
let disable_staff = std::env::var("ZED_DISABLE_STAFF")
.map_or(false, |v| v != "" && v != "0");
let staff = info.staff && !disable_staff;
cx.update_flags(staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
staff,
)
}
})?;

View File

@@ -572,7 +572,8 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
let title = remote_workspace
.update(&mut cx, |ws, cx| {
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
let active_item = ws.active_item(cx).unwrap();
active_item.tab_description(0, &cx).unwrap()
})
.unwrap();

View File

@@ -42,7 +42,7 @@ use std::{
},
};
use text::Point;
use workspace::{Workspace, WorkspaceId};
use workspace::Workspace;
#[gpui::test(iterations = 10)]
async fn test_host_disconnect(
@@ -85,14 +85,8 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let workspace_b = cx_b.add_window(|cx| {
Workspace::new(
WorkspaceId::default(),
project_b.clone(),
client_b.app_state.clone(),
cx,
)
});
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();

View File

@@ -42,7 +42,7 @@ use std::{
Arc,
},
};
use workspace::{Workspace, WorkspaceId, WorkspaceStore};
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
pub app_state: Arc<AppState>,
@@ -277,11 +277,7 @@ impl TestServer {
node_runtime: FakeNodeRuntime::new(),
});
let os_keymap = if cfg!(target_os = "linux") {
"keymaps/default-linux.json"
} else {
"keymaps/default-macos.json"
};
let os_keymap = "keymaps/default-macos.json";
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
@@ -906,12 +902,7 @@ impl TestClient {
) -> (View<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(
WorkspaceId::default(),
project.clone(),
self.app_state.clone(),
cx,
)
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
})
}
@@ -922,12 +913,7 @@ impl TestClient {
let project = self.build_test_project(cx).await;
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(
WorkspaceId::default(),
project.clone(),
self.app_state.clone(),
cx,
)
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
})
}

View File

@@ -400,7 +400,11 @@ impl Item for ChannelView {
None
}
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
fn clone_on_split(
&self,
_: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
Some(cx.new_view(|cx| {
Self::new(
self.project.clone(),

View File

@@ -704,7 +704,7 @@ impl Item for ProjectDiagnosticsEditor {
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
_workspace_id: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -484,7 +484,7 @@ pub struct Editor {
current_line_highlight: CurrentLineHighlight,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, WorkspaceId)>,
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
use_modal_editing: bool,

View File

@@ -309,17 +309,18 @@ impl Editor {
let deleted_hunk_color = deleted_hunk_color(cx);
let (editor_height, editor_with_deleted_text) =
editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx);
let parent_gutter_offset = self.gutter_dimensions.width + self.gutter_dimensions.margin;
let editor_model = cx.model().clone();
let mut new_block_ids = self.insert_blocks(
Some(BlockProperties {
position: hunk.multi_buffer_range.start,
height: editor_height.max(deleted_text_height),
style: BlockStyle::Flex,
render: Box::new(move |_| {
render: Box::new(move |cx| {
let gutter_dimensions = editor_model.read(cx).gutter_dimensions;
div()
.bg(deleted_hunk_color)
.size_full()
.pl(parent_gutter_offset)
.pl(gutter_dimensions.width + gutter_dimensions.margin)
.child(editor_with_deleted_text.clone())
.into_any_element()
}),

View File

@@ -657,7 +657,7 @@ impl Item for Editor {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Editor>>
where
@@ -846,9 +846,12 @@ impl Item for Editor {
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
let workspace_id = workspace.database_id();
let item_id = cx.view().item_id().as_u64() as ItemId;
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
let Some(workspace_id) = workspace.database_id() else {
return;
};
let item_id = cx.view().item_id().as_u64() as ItemId;
fn serialize(
buffer: Model<Buffer>,
@@ -873,7 +876,7 @@ impl Item for Editor {
serialize(buffer.clone(), workspace_id, item_id, cx);
cx.subscribe(&buffer, |this, buffer, event, cx| {
if let Some((_, workspace_id)) = this.workspace.as_ref() {
if let Some((_, Some(workspace_id))) = this.workspace.as_ref() {
if let language::Event::FileHandleChanged = event {
serialize(
buffer,

View File

@@ -389,7 +389,8 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
self.scroll_manager.set_scroll_position(
scroll_position,
&display_map,
@@ -409,7 +410,7 @@ impl Editor {
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
let top_row = scroll_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
@@ -424,7 +425,7 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
let snapshot = &self.buffer().read(cx).snapshot(cx);
if !scroll_anchor.anchor.is_valid(snapshot) {
log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);

View File

@@ -992,7 +992,7 @@ impl Item for ExtensionsPage {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
_: &mut ViewContext<Self>,
) -> Option<View<Self>> {
None

View File

@@ -14,6 +14,12 @@ impl FeatureFlags {
impl Global for FeatureFlags {}
/// To create a feature flag, implement this trait on a trivial type and use it as
/// a generic parameter when called [`FeatureFlagAppExt::has_flag`].
///
/// Feature flags are always enabled for members of Zed staff. To disable this behavior
/// so you can test flags being disabled, set ZED_DISABLE_STAFF=1 in your environment,
/// which will force Zed to treat the current user as non-staff.
pub trait FeatureFlag {
const NAME: &'static str;
}

View File

@@ -38,11 +38,10 @@ impl FileIcons {
pub fn new(assets: impl AssetSource) -> Self {
assets
.load("icons/file_icons/file_types.json")
.and_then(|file| {
serde_json::from_str::<FileIcons>(str::from_utf8(&file).unwrap())
.map_err(Into::into)
})
.unwrap_or_else(|_| FileIcons {
.ok()
.flatten()
.and_then(|file| serde_json::from_str::<FileIcons>(str::from_utf8(&file).unwrap()).ok())
.unwrap_or_else(|| FileIcons {
stems: HashMap::default(),
suffixes: HashMap::default(),
types: HashMap::default(),

View File

@@ -46,6 +46,9 @@ notify = "6.1.1"
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
ashpd.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -1,6 +1,11 @@
use anyhow::{anyhow, Result};
use git::GitHostingProviderRegistry;
#[cfg(target_os = "linux")]
use ashpd::desktop::trash;
#[cfg(target_os = "linux")]
use std::{fs::File, os::fd::AsFd};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
@@ -10,6 +15,7 @@ use git::repository::{GitRepository, RealGitRepository};
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
use rope::Rope;
#[cfg(any(test, feature = "test-support"))]
use smol::io::AsyncReadExt;
use smol::io::AsyncWriteExt;
@@ -273,11 +279,25 @@ impl Fs for RealFs {
Ok(())
}
#[cfg(target_os = "linux")]
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
let file = File::open(path)?;
match trash::trash_file(&file.as_fd()).await {
Ok(_) => Ok(()),
Err(err) => Err(anyhow::Error::new(err)),
}
}
#[cfg(target_os = "macos")]
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.trash_file(path, options).await
}
#[cfg(target_os = "linux")]
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.trash_file(path, options).await
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
Ok(Box::new(std::fs::File::open(path)?))
}

View File

@@ -8,15 +8,11 @@ use std::process::{Command, Stdio};
use std::sync::Arc;
use std::{ops::Range, path::Path};
use text::Rope;
use time;
use time::macros::format_description;
use time::OffsetDateTime;
use time::UtcOffset;
use url::Url;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub use git2 as libgit;
#[derive(Debug, Clone, Default)]
@@ -98,7 +94,10 @@ fn run_git_blame(
.stderr(Stdio::piped());
#[cfg(windows)]
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
{
use std::os::windows::process::CommandExt;
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
}
let child = child
.spawn()

View File

@@ -15,6 +15,7 @@ pub mod blame;
pub mod commit;
pub mod diff;
pub mod repository;
pub mod status;
lazy_static! {
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");

View File

@@ -1,8 +1,8 @@
use crate::blame::Blame;
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use anyhow::{Context, Result};
use collections::HashMap;
use git2::{BranchType, StatusShow};
use git2::BranchType;
use parking_lot::Mutex;
use rope::Rope;
use serde::{Deserialize, Serialize};
@@ -10,10 +10,9 @@ use std::{
cmp::Ordering,
path::{Component, Path, PathBuf},
sync::Arc,
time::SystemTime,
};
use sum_tree::{MapSeekTarget, TreeMap};
use util::{paths::PathExt, ResultExt};
use sum_tree::MapSeekTarget;
use util::ResultExt;
pub use git2::Repository as LibGitRepository;
@@ -39,23 +38,11 @@ pub trait GitRepository: Send {
/// Returns the SHA of the current HEAD.
fn head_sha(&self) -> Option<String>;
/// Get the statuses of all of the files in the index that start with the given
/// path and have changes with respect to the HEAD commit. This is fast because
/// the index stores hashes of trees, so that unchanged directories can be skipped.
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;
fn statuses(&self, path_prefix: &Path) -> Result<GitStatus>;
/// Get the status of a given file in the working directory with respect to
/// the index. In the common case, when there are no changes, this only requires
/// an index lookup. The index stores the mtime of each file when it was added,
/// so there's no work to do if the mtime matches.
fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
/// Get the status of a given file in the working directory with respect to
/// the HEAD commit. In the common case, when there are no changes, this only
/// requires an index lookup and blob comparison between the index and the HEAD
/// commit. The index stores the mtime of each file when it was added, so there's
/// no need to consider the working directory file if the mtime matches.
fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
fn status(&self, path: &Path) -> Option<GitFileStatus> {
Some(self.statuses(path).ok()?.entries.first()?.1)
}
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
@@ -137,65 +124,12 @@ impl GitRepository for RealGitRepository {
head.target().map(|oid| oid.to_string())
}
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
let mut map = TreeMap::default();
let mut options = git2::StatusOptions::new();
options.pathspec(path_prefix);
options.show(StatusShow::Index);
if let Some(statuses) = self.repository.statuses(Some(&mut options)).log_err() {
for status in statuses.iter() {
let path = RepoPath(PathBuf::try_from_bytes(status.path_bytes()).unwrap());
let status = status.status();
if !status.contains(git2::Status::IGNORED) {
if let Some(status) = read_status(status) {
map.insert(path, status)
}
}
}
}
map
}
fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
// If the file has not changed since it was added to the index, then
// there can't be any changes.
if matches_index(&self.repository, path, mtime) {
return None;
}
let mut options = git2::StatusOptions::new();
options.pathspec(&path.0);
options.disable_pathspec_match(true);
options.include_untracked(true);
options.recurse_untracked_dirs(true);
options.include_unmodified(true);
options.show(StatusShow::Workdir);
let statuses = self.repository.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
}
fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
let mut options = git2::StatusOptions::new();
options.pathspec(&path.0);
options.disable_pathspec_match(true);
options.include_untracked(true);
options.recurse_untracked_dirs(true);
options.include_unmodified(true);
// If the file has not changed since it was added to the index, then
// there's no need to examine the working directory file: just compare
// the blob in the index to the one in the HEAD commit.
if matches_index(&self.repository, path, mtime) {
options.show(StatusShow::Index);
}
let statuses = self.repository.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
let working_directory = self
.repository
.workdir()
.context("failed to read git work directory")?;
GitStatus::new(&self.git_binary_path, working_directory, path_prefix)
}
fn branches(&self) -> Result<Vec<Branch>> {
@@ -222,6 +156,7 @@ impl GitRepository for RealGitRepository {
.collect();
Ok(valid_branches)
}
fn change_branch(&self, name: &str) -> Result<()> {
let revision = self.repository.find_branch(name, BranchType::Local)?;
let revision = revision.get();
@@ -261,38 +196,6 @@ impl GitRepository for RealGitRepository {
}
}
fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
if let Some(index) = repo.index().log_err() {
if let Some(entry) = index.get_path(path, 0) {
if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
if entry.mtime.seconds() == mtime.as_secs() as i32
&& entry.mtime.nanoseconds() == mtime.subsec_nanos()
{
return true;
}
}
}
}
false
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
if status.contains(git2::Status::CONFLICTED) {
Some(GitFileStatus::Conflict)
} else if status.intersects(
git2::Status::WT_MODIFIED
| git2::Status::WT_RENAMED
| git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_RENAMED,
) {
Some(GitFileStatus::Modified)
} else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
Some(GitFileStatus::Added)
} else {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct FakeGitRepository {
state: Arc<Mutex<FakeGitRepositoryState>>,
@@ -333,24 +236,23 @@ impl GitRepository for FakeGitRepository {
None
}
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
let mut map = TreeMap::default();
fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
let state = self.state.lock();
for (repo_path, status) in state.worktree_statuses.iter() {
if repo_path.0.starts_with(path_prefix) {
map.insert(repo_path.to_owned(), status.to_owned());
}
}
map
}
fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
None
}
fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
let state = self.state.lock();
state.worktree_statuses.get(path).cloned()
let mut entries = state
.worktree_statuses
.iter()
.filter_map(|(repo_path, status)| {
if repo_path.0.starts_with(path_prefix) {
Some((repo_path.to_owned(), *status))
} else {
None
}
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Ok(GitStatus {
entries: entries.into(),
})
}
fn branches(&self) -> Result<Vec<Branch>> {

99
crates/git/src/status.rs Normal file
View File

@@ -0,0 +1,99 @@
use crate::repository::{GitFileStatus, RepoPath};
use anyhow::{anyhow, Result};
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
sync::Arc,
};
#[derive(Clone)]
pub struct GitStatus {
pub entries: Arc<[(RepoPath, GitFileStatus)]>,
}
impl GitStatus {
pub(crate) fn new(
git_binary: &Path,
working_directory: &Path,
mut path_prefix: &Path,
) -> Result<Self> {
let mut child = Command::new(git_binary);
if path_prefix == Path::new("") {
path_prefix = Path::new(".");
}
child
.current_dir(working_directory)
.args([
"--no-optional-locks",
"status",
"--porcelain=v1",
"--untracked-files=all",
"-z",
])
.arg(path_prefix)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
}
let child = child
.spawn()
.map_err(|e| anyhow!("Failed to start git status process: {}", e))?;
let output = child
.wait_with_output()
.map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("git status process failed: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut entries = stdout
.split('\0')
.filter_map(|entry| {
if entry.is_char_boundary(3) {
let (status, path) = entry.split_at(3);
let status = status.trim();
Some((
RepoPath(PathBuf::from(path)),
match status {
"A" | "??" => GitFileStatus::Added,
"M" => GitFileStatus::Modified,
_ => return None,
},
))
} else {
None
}
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
Ok(Self {
entries: entries.into(),
})
}
pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
self.entries
.binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
.ok()
.map(|index| self.entries[index].1)
}
}
impl Default for GitStatus {
fn default() -> Self {
Self {
entries: Arc::new([]),
}
}
}

View File

@@ -12,9 +12,14 @@ workspace = true
[features]
default = []
test-support = ["backtrace", "collections/test-support", "util/test-support", "http/test-support"]
test-support = [
"backtrace",
"collections/test-support",
"util/test-support",
"http/test-support",
]
runtime_shaders = []
macos-blade = ["blade-graphics", "blade-macros", "bytemuck"]
macos-blade = ["blade-graphics", "blade-macros", "blade-util", "bytemuck"]
[lib]
path = "src/gpui.rs"
@@ -26,6 +31,7 @@ async-task = "4.7"
backtrace = { version = "0.3", optional = true }
blade-graphics = { workspace = true, optional = true }
blade-macros = { workspace = true, optional = true }
blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
collections.workspace = true
ctor.workspace = true
@@ -96,13 +102,14 @@ flume = "0.11"
#TODO: use these on all platforms
blade-graphics.workspace = true
blade-macros.workspace = true
blade-util.workspace = true
bytemuck = "1"
cosmic-text = "0.11.2"
copypasta = "0.10.1"
[target.'cfg(target_os = "linux")'.dependencies]
as-raw-xcb-connection = "1"
ashpd = "0.8.0"
ashpd.workspace = true
calloop = "0.12.4"
calloop-wayland-source = "0.2.0"
wayland-backend = { version = "0.3.3", features = ["client_system"] }
@@ -126,7 +133,10 @@ x11rb = { version = "0.13.0", features = [
"resource_manager",
] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = ["x11rb-xcb", "x11rb-client"] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
"x11rb-xcb",
"x11rb-client",
] }
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -5,8 +5,11 @@ use gpui::*;
struct Assets {}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
std::fs::read(path).map(Into::into).map_err(Into::into)
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
std::fs::read(path)
.map(Into::into)
.map_err(Into::into)
.map(|result| Some(result))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {

View File

@@ -1,5 +1,5 @@
use crate::{size, DevicePixels, Result, SharedString, Size};
use anyhow::anyhow;
use image::{Bgra, ImageBuffer};
use std::{
borrow::Cow,
@@ -11,18 +11,15 @@ use std::{
/// A source of assets for this app to use.
pub trait AssetSource: 'static + Send + Sync {
/// Load the given asset from the source path.
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>>;
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>>;
/// List the assets at the given path.
fn list(&self, path: &str) -> Result<Vec<SharedString>>;
}
impl AssetSource for () {
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
Err(anyhow!(
"load called on empty asset provider with \"{}\"",
path
))
fn load(&self, _path: &str) -> Result<Option<Cow<'static, [u8]>>> {
Ok(None)
}
fn list(&self, _path: &str) -> Result<Vec<SharedString>> {

View File

@@ -344,8 +344,8 @@ pub(crate) trait PlatformAtlas: Send + Sync {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile>;
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>>;
}
#[derive(Clone, Debug, PartialEq, Eq)]

View File

@@ -1,8 +1,5 @@
mod blade_atlas;
mod blade_belt;
mod blade_renderer;
pub(crate) use blade_atlas::*;
pub(crate) use blade_renderer::*;
use blade_belt::*;

View File

@@ -1,10 +1,10 @@
use super::{BladeBelt, BladeBeltDescriptor};
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
};
use anyhow::Result;
use blade_graphics as gpu;
use blade_util::{BufferBelt, BufferBeltDescriptor};
use collections::FxHashMap;
use etagere::BucketedAtlasAllocator;
use parking_lot::Mutex;
@@ -22,7 +22,7 @@ struct PendingUpload {
struct BladeAtlasState {
gpu: Arc<gpu::Context>,
upload_belt: BladeBelt,
upload_belt: BufferBelt,
storage: BladeAtlasStorage,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
initializations: Vec<AtlasTextureId>,
@@ -48,7 +48,7 @@ impl BladeAtlas {
pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self {
BladeAtlas(Mutex::new(BladeAtlasState {
gpu: Arc::clone(gpu),
upload_belt: BladeBelt::new(BladeBeltDescriptor {
upload_belt: BufferBelt::new(BufferBeltDescriptor {
memory: gpu::Memory::Upload,
min_chunk_size: 0x10000,
alignment: 64, // Vulkan `optimalBufferCopyOffsetAlignment` on Intel XE
@@ -114,18 +114,20 @@ impl PlatformAtlas for BladeAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile> {
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(tile.clone())
Ok(Some(tile.clone()))
} else {
profiling::scope!("new tile");
let (size, bytes) = build()?;
let Some((size, bytes)) = build()? else {
return Ok(None);
};
let tile = lock.allocate(size, key.texture_kind());
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
Ok(tile)
Ok(Some(tile))
}
}
}
@@ -212,7 +214,7 @@ impl BladeAtlasState {
}
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
let data = unsafe { self.upload_belt.alloc_data(bytes, &self.gpu) };
let data = self.upload_belt.alloc_bytes(bytes, &self.gpu);
self.uploads.push(PendingUpload { id, bounds, data });
}

View File

@@ -1,101 +0,0 @@
use blade_graphics as gpu;
use std::mem;
struct ReusableBuffer {
raw: gpu::Buffer,
size: u64,
}
pub struct BladeBeltDescriptor {
pub memory: gpu::Memory,
pub min_chunk_size: u64,
pub alignment: u64,
}
/// A belt of buffers, used by the BladeAtlas to cheaply
/// find staging space for uploads.
pub struct BladeBelt {
desc: BladeBeltDescriptor,
buffers: Vec<(ReusableBuffer, gpu::SyncPoint)>,
active: Vec<(ReusableBuffer, u64)>,
}
impl BladeBelt {
pub fn new(desc: BladeBeltDescriptor) -> Self {
assert_ne!(desc.alignment, 0);
Self {
desc,
buffers: Vec::new(),
active: Vec::new(),
}
}
pub fn destroy(&mut self, gpu: &gpu::Context) {
for (buffer, _) in self.buffers.drain(..) {
gpu.destroy_buffer(buffer.raw);
}
for (buffer, _) in self.active.drain(..) {
gpu.destroy_buffer(buffer.raw);
}
}
#[profiling::function]
pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece {
for &mut (ref rb, ref mut offset) in self.active.iter_mut() {
let aligned = offset.next_multiple_of(self.desc.alignment);
if aligned + size <= rb.size {
let piece = rb.raw.at(aligned);
*offset = aligned + size;
return piece;
}
}
let index_maybe = self
.buffers
.iter()
.position(|(rb, sp)| size <= rb.size && gpu.wait_for(sp, 0));
if let Some(index) = index_maybe {
let (rb, _) = self.buffers.remove(index);
let piece = rb.raw.into();
self.active.push((rb, size));
return piece;
}
let chunk_index = self.buffers.len() + self.active.len();
let chunk_size = size.max(self.desc.min_chunk_size);
let chunk = gpu.create_buffer(gpu::BufferDesc {
name: &format!("chunk-{}", chunk_index),
size: chunk_size,
memory: self.desc.memory,
});
let rb = ReusableBuffer {
raw: chunk,
size: chunk_size,
};
self.active.push((rb, size));
chunk.into()
}
// SAFETY: T should be zeroable and ordinary data, no references, pointers, cells or other complicated data type.
pub unsafe fn alloc_data<T>(&mut self, data: &[T], gpu: &gpu::Context) -> gpu::BufferPiece {
assert!(!data.is_empty());
let type_alignment = mem::align_of::<T>() as u64;
debug_assert_eq!(
self.desc.alignment % type_alignment,
0,
"Type alignment {} is too big",
type_alignment
);
let total_bytes = std::mem::size_of_val(data);
let bp = self.alloc(total_bytes as u64, gpu);
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr() as *const u8, bp.data(), total_bytes);
}
bp
}
pub fn flush(&mut self, sp: &gpu::SyncPoint) {
self.buffers
.extend(self.active.drain(..).map(|(rb, _)| (rb, sp.clone())));
}
}

View File

@@ -1,7 +1,7 @@
// Doing `if let` gives you nice scoping with passes/encoders
#![allow(irrefutable_let_patterns)]
use super::{BladeAtlas, BladeBelt, BladeBeltDescriptor, PATH_TEXTURE_FORMAT};
use super::{BladeAtlas, PATH_TEXTURE_FORMAT};
use crate::{
AtlasTextureKind, AtlasTile, Bounds, ContentMask, Hsla, MonochromeSprite, Path, PathId,
PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size,
@@ -15,6 +15,7 @@ use media::core_video::CVMetalTextureCache;
use std::{ffi::c_void, ptr::NonNull};
use blade_graphics as gpu;
use blade_util::{BufferBelt, BufferBeltDescriptor};
use std::{mem, sync::Arc};
const MAX_FRAME_TIME_MS: u32 = 1000;
@@ -346,7 +347,7 @@ pub struct BladeRenderer {
command_encoder: gpu::CommandEncoder,
last_sync_point: Option<gpu::SyncPoint>,
pipelines: BladePipelines,
instance_belt: BladeBelt,
instance_belt: BufferBelt,
path_tiles: HashMap<PathId, AtlasTile>,
atlas: Arc<BladeAtlas>,
atlas_sampler: gpu::Sampler,
@@ -371,7 +372,7 @@ impl BladeRenderer {
buffer_count: 2,
});
let pipelines = BladePipelines::new(&gpu, surface_info);
let instance_belt = BladeBelt::new(BladeBeltDescriptor {
let instance_belt = BufferBelt::new(BufferBeltDescriptor {
memory: gpu::Memory::Shared,
min_chunk_size: 0x1000,
alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe
@@ -492,7 +493,7 @@ impl BladeRenderer {
pad: 0,
};
let vertex_buf = unsafe { self.instance_belt.alloc_data(&vertices, &self.gpu) };
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
let mut pass = self.command_encoder.render(gpu::RenderTargetSet {
colors: &[gpu::RenderTarget {
view: tex_info.raw_view,
@@ -557,7 +558,7 @@ impl BladeRenderer {
match batch {
PrimitiveBatch::Quads(quads) => {
let instance_buf =
unsafe { self.instance_belt.alloc_data(quads, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(quads, &self.gpu) };
let mut encoder = pass.with(&self.pipelines.quads);
encoder.bind(
0,
@@ -570,7 +571,7 @@ impl BladeRenderer {
}
PrimitiveBatch::Shadows(shadows) => {
let instance_buf =
unsafe { self.instance_belt.alloc_data(shadows, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(shadows, &self.gpu) };
let mut encoder = pass.with(&self.pipelines.shadows);
encoder.bind(
0,
@@ -598,7 +599,7 @@ impl BladeRenderer {
}];
let instance_buf =
unsafe { self.instance_belt.alloc_data(&sprites, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) };
encoder.bind(
0,
&ShaderPathsData {
@@ -613,7 +614,7 @@ impl BladeRenderer {
}
PrimitiveBatch::Underlines(underlines) => {
let instance_buf =
unsafe { self.instance_belt.alloc_data(underlines, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(underlines, &self.gpu) };
let mut encoder = pass.with(&self.pipelines.underlines);
encoder.bind(
0,
@@ -630,7 +631,7 @@ impl BladeRenderer {
} => {
let tex_info = self.atlas.get_texture_info(texture_id);
let instance_buf =
unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) };
let mut encoder = pass.with(&self.pipelines.mono_sprites);
encoder.bind(
0,
@@ -649,7 +650,7 @@ impl BladeRenderer {
} => {
let tex_info = self.atlas.get_texture_info(texture_id);
let instance_buf =
unsafe { self.instance_belt.alloc_data(sprites, &self.gpu) };
unsafe { self.instance_belt.alloc_typed(sprites, &self.gpu) };
let mut encoder = pass.with(&self.pipelines.poly_sprites);
encoder.bind(
0,

View File

@@ -60,20 +60,22 @@ impl PlatformAtlas for MetalAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<(Size<DevicePixels>, Cow<'a, [u8]>)>,
) -> Result<AtlasTile> {
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(tile.clone())
Ok(Some(tile.clone()))
} else {
let (size, bytes) = build()?;
let Some((size, bytes)) = build()? else {
return Ok(None);
};
let tile = lock
.allocate(size, key.texture_kind())
.ok_or_else(|| anyhow!("failed to allocate"))?;
let texture = lock.texture(tile.texture_id);
texture.upload(tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
Ok(tile)
Ok(Some(tile))
}
}
}

View File

@@ -291,25 +291,26 @@ impl PlatformAtlas for TestAtlas {
fn get_or_insert_with<'a>(
&self,
key: &crate::AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<(
Size<crate::DevicePixels>,
std::borrow::Cow<'a, [u8]>,
)>,
) -> anyhow::Result<crate::AtlasTile> {
build: &mut dyn FnMut() -> anyhow::Result<
Option<(Size<crate::DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<crate::AtlasTile>> {
let mut state = self.0.lock();
if let Some(tile) = state.tiles.get(key) {
return Ok(tile.clone());
return Ok(Some(tile.clone()));
}
drop(state);
let Some((size, _)) = build()? else {
return Ok(None);
};
let mut state = self.0.lock();
state.next_id += 1;
let texture_id = state.next_id;
state.next_id += 1;
let tile_id = state.next_id;
drop(state);
let (size, _) = build()?;
let mut state = self.0.lock();
state.tiles.insert(
key.clone(),
crate::AtlasTile {
@@ -326,6 +327,6 @@ impl PlatformAtlas for TestAtlas {
},
);
Ok(state.tiles[key].clone())
Ok(Some(state.tiles[key].clone()))
}
}

View File

@@ -24,13 +24,15 @@ impl SvgRenderer {
Self { asset_source }
}
pub fn render(&self, params: &RenderSvgParams) -> Result<Vec<u8>> {
pub fn render(&self, params: &RenderSvgParams) -> Result<Option<Vec<u8>>> {
if params.size.is_zero() {
return Err(anyhow!("can't render at a zero size"));
}
// Load the tree.
let bytes = self.asset_source.load(&params.path)?;
let Some(bytes) = self.asset_source.load(&params.path)? else {
return Ok(None);
};
let pixmap = self.render_pixmap(&bytes, SvgSize::Size(params.size))?;
@@ -40,7 +42,7 @@ impl SvgRenderer {
.iter()
.map(|p| p.alpha())
.collect::<Vec<_>>();
Ok(alpha_mask)
Ok(Some(alpha_mask))
}
pub fn render_pixmap(&self, bytes: &[u8], size: SvgSize) -> Result<Pixmap, usvg::Error> {

View File

@@ -2347,13 +2347,14 @@ impl<'a> WindowContext<'a> {
let raster_bounds = self.text_system().raster_bounds(&params)?;
if !raster_bounds.is_zero() {
let tile =
self.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
Ok((size, Cow::Owned(bytes)))
})?;
let tile = self
.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
Ok(Some((size, Cow::Owned(bytes))))
})?
.expect("Callback above only errors or returns Some");
let bounds = Bounds {
origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
size: tile.bounds.size.map(Into::into),
@@ -2410,13 +2411,15 @@ impl<'a> WindowContext<'a> {
let raster_bounds = self.text_system().raster_bounds(&params)?;
if !raster_bounds.is_zero() {
let tile =
self.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
Ok((size, Cow::Owned(bytes)))
})?;
let tile = self
.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let (size, bytes) = self.text_system().rasterize_glyph(&params)?;
Ok(Some((size, Cow::Owned(bytes))))
})?
.expect("Callback above only errors or returns Some");
let bounds = Bounds {
origin: glyph_origin.map(|px| px.floor()) + raster_bounds.origin.map(Into::into),
size: tile.bounds.size.map(Into::into),
@@ -2464,13 +2467,18 @@ impl<'a> WindowContext<'a> {
.map(|pixels| DevicePixels::from((pixels.0 * 2.).ceil() as i32)),
};
let tile =
let Some(tile) =
self.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
let bytes = self.svg_renderer.render(&params)?;
Ok((params.size, Cow::Owned(bytes)))
})?;
let Some(bytes) = self.svg_renderer.render(&params)? else {
return Ok(None);
};
Ok(Some((params.size, Cow::Owned(bytes))))
})?
else {
return Ok(());
};
let content_mask = self.content_mask().scale(scale_factor);
self.window
@@ -2513,8 +2521,9 @@ impl<'a> WindowContext<'a> {
.window
.sprite_atlas
.get_or_insert_with(&params.clone().into(), &mut || {
Ok((data.size(), Cow::Borrowed(data.as_bytes())))
})?;
Ok(Some((data.size(), Cow::Borrowed(data.as_bytes()))))
})?
.expect("Callback above only returns Some");
let content_mask = self.content_mask().scale(scale_factor);
let corner_radii = corner_radii.scale(scale_factor);

View File

@@ -95,17 +95,19 @@ impl Item for ImageView {
let workspace_id = workspace.database_id();
let image_path = self.path.clone();
cx.background_executor()
.spawn({
let image_path = image_path.clone();
async move {
IMAGE_VIEWER
.save_image_path(item_id, workspace_id, image_path)
.await
.log_err();
}
})
.detach();
if let Some(workspace_id) = workspace_id {
cx.background_executor()
.spawn({
let image_path = image_path.clone();
async move {
IMAGE_VIEWER
.save_image_path(item_id, workspace_id, image_path)
.await
.log_err();
}
})
.detach();
}
}
fn serialized_item_kind() -> Option<&'static str> {
@@ -133,7 +135,7 @@ impl Item for ImageView {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -407,7 +407,7 @@ impl Item for SyntaxTreeView {
fn clone_on_split(
&self,
_: workspace::WorkspaceId,
_: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -89,8 +89,10 @@ impl Project {
path,
});
let is_terminal = spawn_task.is_none() && (working_directory.as_ref().is_none())
|| (working_directory.as_ref().unwrap().is_local());
let is_terminal = spawn_task.is_none()
&& working_directory
.as_ref()
.map_or(true, |work_dir| work_dir.is_local());
let settings = TerminalSettings::get(settings_location, cx);
let python_settings = settings.detect_venv.clone();
let (completion_tx, completion_rx) = bounded(1);

View File

@@ -287,7 +287,7 @@ impl PickerDelegate for RecentProjectsDelegate {
};
workspace
.update(cx, |workspace, cx| {
if workspace.database_id() == *candidate_workspace_id {
if workspace.database_id() == Some(*candidate_workspace_id) {
Task::ready(Ok(()))
} else {
match candidate_workspace_location {
@@ -675,7 +675,7 @@ impl RecentProjectsDelegate {
) -> bool {
if let Some(workspace) = self.workspace.upgrade() {
let workspace = workspace.read(cx);
if workspace_id == workspace.database_id() {
if Some(workspace_id) == workspace.database_id() {
return true;
}
}

View File

@@ -0,0 +1,22 @@
[package]
name = "rustdoc_to_markdown"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/rustdoc_to_markdown.rs"
[dependencies]
anyhow.workspace = true
html5ever.workspace = true
markup5ever_rcdom.workspace = true
regex.workspace = true
[dev-dependencies]
indoc.workspace = true
pretty_assertions.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,29 @@
use indoc::indoc;
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
pub fn main() {
let html = indoc! {"
<html>
<body>
<h1>Hello World</h1>
<p>
Here is some content.
</p>
<h2>Some items</h2>
<ul>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
</body>
</html>
"};
// To test this out with some real input, try this:
//
// ```
// let html = include_str!("/path/to/zed/target/doc/gpui/index.html");
// ```
let markdown = convert_rustdoc_to_markdown(html.as_bytes()).unwrap();
println!("{markdown}");
}

View File

@@ -0,0 +1,223 @@
use std::cell::RefCell;
use std::collections::VecDeque;
use std::sync::OnceLock;
use anyhow::Result;
use html5ever::Attribute;
use markup5ever_rcdom::{Handle, NodeData};
use regex::Regex;
fn empty_line_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"^\s*$").unwrap())
}
fn more_than_three_newlines_regex() -> &'static Regex {
static REGEX: OnceLock<Regex> = OnceLock::new();
REGEX.get_or_init(|| Regex::new(r"\n{3,}").unwrap())
}
#[derive(Debug, Clone)]
struct HtmlElement {
tag: String,
attrs: RefCell<Vec<Attribute>>,
}
enum StartTagOutcome {
Continue,
Skip,
}
pub struct MarkdownWriter {
current_element_stack: VecDeque<HtmlElement>,
/// The Markdown output.
markdown: String,
}
impl MarkdownWriter {
pub fn new() -> Self {
Self {
current_element_stack: VecDeque::new(),
markdown: String::new(),
}
}
fn is_inside(&self, tag: &str) -> bool {
self.current_element_stack
.iter()
.any(|parent_element| parent_element.tag == tag)
}
/// Appends the given string slice onto the end of the Markdown output.
fn push_str(&mut self, str: &str) {
self.markdown.push_str(str);
}
/// Appends a newline to the end of the Markdown output.
fn push_newline(&mut self) {
self.push_str("\n");
}
pub fn run(mut self, root_node: &Handle) -> Result<String> {
self.visit_node(&root_node)?;
Ok(Self::prettify_markdown(self.markdown))
}
fn prettify_markdown(markdown: String) -> String {
let markdown = empty_line_regex().replace_all(&markdown, "");
let markdown = more_than_three_newlines_regex().replace_all(&markdown, "\n\n");
markdown.trim().to_string()
}
fn visit_node(&mut self, node: &Handle) -> Result<()> {
let mut current_element = None;
match node.data {
NodeData::Document
| NodeData::Doctype { .. }
| NodeData::ProcessingInstruction { .. }
| NodeData::Comment { .. } => {
// Currently left unimplemented, as we're not interested in this data
// at this time.
}
NodeData::Element {
ref name,
ref attrs,
..
} => {
let tag_name = name.local.to_string();
if !tag_name.is_empty() {
current_element = Some(HtmlElement {
tag: tag_name,
attrs: attrs.clone(),
});
}
}
NodeData::Text { ref contents } => {
let text = contents.borrow().to_string();
self.visit_text(text)?;
}
}
if let Some(current_element) = current_element.as_ref() {
match self.start_tag(&current_element) {
StartTagOutcome::Continue => {}
StartTagOutcome::Skip => return Ok(()),
}
self.current_element_stack
.push_back(current_element.clone());
}
for child in node.children.borrow().iter() {
self.visit_node(child)?;
}
if let Some(current_element) = current_element {
self.current_element_stack.pop_back();
self.end_tag(&current_element);
}
Ok(())
}
fn start_tag(&mut self, tag: &HtmlElement) -> StartTagOutcome {
match tag.tag.as_str() {
"head" | "script" | "nav" => return StartTagOutcome::Skip,
"h1" => self.push_str("\n\n# "),
"h2" => self.push_str("\n\n## "),
"h3" => self.push_str("\n\n### "),
"h4" => self.push_str("\n\n#### "),
"h5" => self.push_str("\n\n##### "),
"h6" => self.push_str("\n\n###### "),
"code" => {
if !self.is_inside("pre") {
self.push_str("`")
}
}
"pre" => {
let attrs = tag.attrs.borrow();
let classes = attrs
.iter()
.find(|attr| attr.name.local.to_string() == "class")
.map(|attr| {
attr.value
.split(' ')
.map(|class| class.trim())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let is_rust = classes.into_iter().any(|class| class == "rust");
let language = if is_rust { "rs" } else { "" };
self.push_str(&format!("\n```{language}\n"))
}
"ul" | "ol" => self.push_newline(),
"li" => self.push_str("- "),
"summary" => {
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class" && attr.value.to_string() == "hideme"
}) {
return StartTagOutcome::Skip;
}
}
"div" | "span" => {
let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class"
&& attr
.value
.split(' ')
.any(|class| classes_to_skip.contains(&class.trim()))
}) {
return StartTagOutcome::Skip;
}
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class" && attr.value.to_string() == "item-name"
}) {
self.push_str("`");
}
}
_ => {}
}
StartTagOutcome::Continue
}
fn end_tag(&mut self, tag: &HtmlElement) {
match tag.tag.as_str() {
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => self.push_str("\n\n"),
"code" => {
if !self.is_inside("pre") {
self.push_str("`")
}
}
"pre" => self.push_str("\n```\n"),
"ul" | "ol" => self.push_newline(),
"li" => self.push_newline(),
"div" => {
if tag.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class" && attr.value.to_string() == "item-name"
}) {
self.push_str("`: ");
}
}
_ => {}
}
}
fn visit_text(&mut self, text: String) -> Result<()> {
if self.is_inside("pre") {
self.push_str(&text);
return Ok(());
}
let trimmed_text = text.trim_matches(|char| char == '\n' || char == '\r' || char == '§');
self.push_str(trimmed_text);
Ok(())
}
}

View File

@@ -0,0 +1,88 @@
//! Provides conversion from rustdoc's HTML output to Markdown.
#![deny(missing_docs)]
mod markdown_writer;
use std::io::Read;
use anyhow::{Context, Result};
use html5ever::driver::ParseOpts;
use html5ever::parse_document;
use html5ever::tendril::TendrilSink;
use html5ever::tree_builder::TreeBuilderOpts;
use markup5ever_rcdom::RcDom;
use crate::markdown_writer::MarkdownWriter;
/// Converts the provided rustdoc HTML to Markdown.
pub fn convert_rustdoc_to_markdown(mut html: impl Read) -> Result<String> {
let parse_options = ParseOpts {
tree_builder: TreeBuilderOpts {
drop_doctype: true,
..Default::default()
},
..Default::default()
};
let dom = parse_document(RcDom::default(), parse_options)
.from_utf8()
.read_from(&mut html)
.context("failed to parse rustdoc HTML")?;
let markdown_writer = MarkdownWriter::new();
let markdown = markdown_writer
.run(&dom.document)
.context("failed to convert rustdoc to HTML")?;
Ok(markdown)
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_code_blocks() {
let html = indoc! {r#"
<pre class="rust rust-example-rendered"><code><span class="kw">use </span>axum::extract::{Path, Query, Json};
<span class="kw">use </span>std::collections::HashMap;
<span class="comment">// `Path` gives you the path parameters and deserializes them.
</span><span class="kw">async fn </span>path(Path(user_id): Path&lt;u32&gt;) {}
<span class="comment">// `Query` gives you the query parameters and deserializes them.
</span><span class="kw">async fn </span>query(Query(params): Query&lt;HashMap&lt;String, String&gt;&gt;) {}
<span class="comment">// Buffer the request body and deserialize it as JSON into a
// `serde_json::Value`. `Json` supports any type that implements
// `serde::Deserialize`.
</span><span class="kw">async fn </span>json(Json(payload): Json&lt;serde_json::Value&gt;) {}</code></pre>
"#};
let expected = indoc! {"
```rs
use axum::extract::{Path, Query, Json};
use std::collections::HashMap;
// `Path` gives you the path parameters and deserializes them.
async fn path(Path(user_id): Path<u32>) {}
// `Query` gives you the query parameters and deserializes them.
async fn query(Query(params): Query<HashMap<String, String>>) {}
// Buffer the request body and deserialize it as JSON into a
// `serde_json::Value`. `Json` supports any type that implements
// `serde::Deserialize`.
async fn json(Json(payload): Json<serde_json::Value>) {}
```
"}
.trim();
assert_eq!(
convert_rustdoc_to_markdown(html.as_bytes()).unwrap(),
expected
)
}
}

View File

@@ -456,7 +456,7 @@ impl Item for ProjectSearchView {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -283,7 +283,7 @@ impl Item for ProjectIndexDebugView {
fn clone_on_split(
&self,
_: workspace::WorkspaceId,
_: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -15,10 +15,11 @@ use rust_embed::RustEmbed;
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
.map(|data| Some(data))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {

View File

@@ -128,7 +128,10 @@ fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> {
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx.asset_source().load(&font_path)?;
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Should never be None in the storybook");
embedded_fonts.push(font_bytes);
}
}

View File

@@ -221,7 +221,9 @@ impl TerminalPanel {
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
let items = if let Some((serialized_panel, database_id)) =
serialized_panel.as_ref().zip(workspace.database_id())
{
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height.map(|h| h.round());
@@ -234,7 +236,7 @@ impl TerminalPanel {
TerminalView::deserialize(
workspace.project().clone(),
workspace.weak_handle(),
workspace.database_id(),
database_id,
*item_id,
cx,
)

View File

@@ -91,7 +91,7 @@ pub struct TerminalView {
blinking_paused: bool,
blink_epoch: usize,
can_navigate_to_selected_word: bool,
workspace_id: WorkspaceId,
workspace_id: Option<WorkspaceId>,
show_title: bool,
_subscriptions: Vec<Subscription>,
_terminal_subscriptions: Vec<Subscription>,
@@ -142,7 +142,7 @@ impl TerminalView {
pub fn new(
terminal: Model<Terminal>,
workspace: WeakView<Workspace>,
workspace_id: WorkspaceId,
workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Self {
let workspace_handle = workspace.clone();
@@ -458,15 +458,16 @@ fn subscribe_for_terminal_events(
if terminal.task().is_none() {
if let Some(cwd) = terminal.get_cwd() {
let item_id = cx.entity_id();
let workspace_id = this.workspace_id;
cx.background_executor()
.spawn(async move {
TERMINAL_DB
.save_working_directory(item_id.as_u64(), workspace_id, cwd)
.await
.log_err();
})
.detach();
if let Some(workspace_id) = this.workspace_id {
cx.background_executor()
.spawn(async move {
TERMINAL_DB
.save_working_directory(item_id.as_u64(), workspace_id, cwd)
.await
.log_err();
})
.detach();
}
}
}
}
@@ -853,7 +854,7 @@ impl Item for TerminalView {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
_cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
//From what I can tell, there's no way to tell the current working
@@ -941,20 +942,18 @@ impl Item for TerminalView {
project.create_terminal(cwd, None, window, cx)
})??;
pane.update(&mut cx, |_, cx| {
cx.new_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
})
})
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
if self.terminal().read(cx).task().is_none() {
cx.background_executor()
.spawn(TERMINAL_DB.update_workspace_id(
workspace.database_id(),
self.workspace_id,
cx.entity_id().as_u64(),
))
.detach();
if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
cx.background_executor()
.spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64()))
.detach();
}
self.workspace_id = workspace.database_id();
}
}

View File

@@ -1,5 +1,5 @@
use std::path::Path;
use std::sync::Arc;
use std::{fmt::Debug, path::Path};
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
@@ -226,7 +226,7 @@ impl ThemeRegistry {
.filter(|path| path.ends_with(".json"));
for path in theme_paths {
let Some(theme) = self.assets.load(&path).log_err() else {
let Some(theme) = self.assets.load(&path).log_err().flatten() else {
continue;
};

View File

@@ -11,10 +11,11 @@ use rust_embed::RustEmbed;
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
.map(|result| Some(result))
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {

View File

@@ -313,7 +313,7 @@ impl Item for WelcomePage {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
Some(cx.new_view(|cx| WelcomePage {

View File

@@ -172,7 +172,7 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
_: &mut ViewContext<Self>,
) -> Option<View<Self>>
where
@@ -287,7 +287,7 @@ pub trait ItemHandle: 'static + Send {
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
fn clone_on_split(
&self,
workspace_id: WorkspaceId,
workspace_id: Option<WorkspaceId>,
cx: &mut WindowContext,
) -> Option<Box<dyn ItemHandle>>;
fn added_to_pane(
@@ -437,7 +437,7 @@ impl<T: Item> ItemHandle for View<T> {
fn clone_on_split(
&self,
workspace_id: WorkspaceId,
workspace_id: Option<WorkspaceId>,
cx: &mut WindowContext,
) -> Option<Box<dyn ItemHandle>> {
self.update(cx, |item, cx| item.clone_on_split(workspace_id, cx))
@@ -528,7 +528,6 @@ impl<T: Item> ItemHandle for View<T> {
{
pane
} else {
log::error!("unexpected item event after pane was dropped");
return;
};
@@ -883,7 +882,7 @@ pub mod test {
}
pub struct TestItem {
pub workspace_id: WorkspaceId,
pub workspace_id: Option<WorkspaceId>,
pub state: String,
pub label: String,
pub save_count: usize,
@@ -964,7 +963,7 @@ pub mod test {
pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
let mut this = Self::new(cx);
this.workspace_id = id;
this.workspace_id = Some(id);
this
}
@@ -1081,7 +1080,7 @@ pub mod test {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where

View File

@@ -119,7 +119,7 @@ impl Item for SharedScreen {
fn clone_on_split(
&self,
_workspace_id: WorkspaceId,
_workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
let track = self.track.upgrade()?;

View File

@@ -588,7 +588,7 @@ pub struct Workspace {
window_edited: bool,
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
database_id: Option<WorkspaceId>,
app_state: Arc<AppState>,
dispatching_keystrokes: Rc<RefCell<Vec<Keystroke>>>,
_subscriptions: Vec<Subscription>,
@@ -622,7 +622,7 @@ impl Workspace {
const MAX_PADDING: f32 = 0.4;
pub fn new(
workspace_id: WorkspaceId,
workspace_id: Option<WorkspaceId>,
project: Model<Project>,
app_state: Arc<AppState>,
cx: &mut ViewContext<Self>,
@@ -795,13 +795,15 @@ impl Workspace {
if let Some(display) = cx.display() {
if let Some(display_uuid) = display.uuid().log_err() {
let window_bounds = cx.window_bounds();
cx.background_executor()
.spawn(DB.set_window_open_status(
workspace_id,
SerializedWindowBounds(window_bounds),
display_uuid,
))
.detach_and_log_err(cx);
if let Some(database_id) = workspace_id {
cx.background_executor()
.spawn(DB.set_window_open_status(
database_id,
SerializedWindowBounds(window_bounds),
display_uuid,
))
.detach_and_log_err(cx);
}
}
}
this.bounds_save_task_queued.take();
@@ -956,7 +958,12 @@ impl Workspace {
let window = if let Some(window) = requesting_window {
cx.update_window(window.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
Workspace::new(
Some(workspace_id),
project_handle.clone(),
app_state.clone(),
cx,
)
});
})?;
window
@@ -994,7 +1001,7 @@ impl Workspace {
move |cx| {
cx.new_view(|cx| {
let mut workspace =
Workspace::new(workspace_id, project_handle, app_state, cx);
Workspace::new(Some(workspace_id), project_handle, app_state, cx);
workspace.centered_layout = centered_layout;
workspace
})
@@ -3464,9 +3471,12 @@ impl Workspace {
pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
if cx.is_window_active() {
self.update_active_view_for_followers(cx);
cx.background_executor()
.spawn(persistence::DB.update_timestamp(self.database_id()))
.detach();
if let Some(database_id) = self.database_id {
cx.background_executor()
.spawn(persistence::DB.update_timestamp(database_id))
.detach();
}
} else {
for pane in &self.panes {
pane.update(cx, |pane, cx| {
@@ -3506,7 +3516,7 @@ impl Workspace {
}
}
pub fn database_id(&self) -> WorkspaceId {
pub fn database_id(&self) -> Option<WorkspaceId> {
self.database_id
}
@@ -3566,6 +3576,10 @@ impl Workspace {
}
fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
let Some(database_id) = self.database_id() else {
return Task::ready(());
};
fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
let (items, active) = {
let pane = pane_handle.read(cx);
@@ -3701,7 +3715,7 @@ impl Workspace {
let docks = build_serialized_docks(self, cx);
let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
id: database_id,
location,
center_group,
window_bounds,
@@ -3944,9 +3958,11 @@ impl Workspace {
pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
self.centered_layout = !self.centered_layout;
cx.background_executor()
.spawn(DB.set_centered_layout(self.database_id, self.centered_layout))
.detach_and_log_err(cx);
if let Some(database_id) = self.database_id() {
cx.background_executor()
.spawn(DB.set_centered_layout(database_id, self.centered_layout))
.detach_and_log_err(cx);
}
cx.notify();
}

View File

@@ -21,6 +21,7 @@ use futures::{
FutureExt as _, Stream, StreamExt,
};
use fuzzy::CharBag;
use git::status::GitStatus;
use git::{
repository::{GitFileStatus, GitRepository, RepoPath},
DOT_GIT, GITIGNORE,
@@ -77,7 +78,7 @@ pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
#[cfg(not(feature = "test-support"))]
pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
const GIT_STATUS_UPDATE_BATCH_SIZE: usize = 100;
const GIT_STATUS_UPDATE_BATCH_SIZE: usize = 1024;
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -2019,18 +2020,13 @@ impl Snapshot {
include_ignored: bool,
path: &Path,
) -> Traversal {
let mut cursor = self.entries_by_path.cursor();
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
let mut traversal = Traversal {
cursor,
Traversal::new(
&self.entries_by_path,
include_files,
include_dirs,
include_ignored,
};
if traversal.end_offset() == traversal.start_offset() {
traversal.next();
}
traversal
path,
)
}
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
@@ -2558,8 +2554,12 @@ impl BackgroundScannerState {
if let Ok(repo_path) = repo_entry.relativize(&self.snapshot, &path) {
containing_repository = Some(ScanJobContainingRepository {
work_directory: workdir_path,
repository: repo.repo_ptr.clone(),
staged_statuses: repo.repo_ptr.lock().staged_statuses(&repo_path),
statuses: repo
.repo_ptr
.lock()
.statuses(&repo_path)
.log_err()
.unwrap_or_default(),
});
}
}
@@ -3832,11 +3832,14 @@ impl BackgroundScanner {
.lock()
.build_git_repository(child_path.clone(), self.fs.as_ref())
{
let staged_statuses = repository.lock().staged_statuses(Path::new(""));
let statuses = repository
.lock()
.statuses(Path::new(""))
.log_err()
.unwrap_or_default();
containing_repository = Some(ScanJobContainingRepository {
work_directory,
repository,
staged_statuses,
statuses,
});
}
} else if child_name == *GITIGNORE {
@@ -3946,13 +3949,8 @@ impl BackgroundScanner {
if !child_entry.is_ignored {
if let Some(repo) = &containing_repository {
if let Ok(repo_path) = child_entry.path.strip_prefix(&repo.work_directory) {
if let Some(mtime) = child_entry.mtime {
let repo_path = RepoPath(repo_path.into());
child_entry.git_status = combine_git_statuses(
repo.staged_statuses.get(&repo_path).copied(),
repo.repository.lock().unstaged_status(&repo_path, mtime),
);
}
let repo_path = RepoPath(repo_path.into());
child_entry.git_status = repo.statuses.get(&repo_path);
}
}
}
@@ -4079,10 +4077,7 @@ impl BackgroundScanner {
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
if let Some((repo_entry, repo)) = state.snapshot.repo_for_path(path) {
if let Ok(repo_path) = repo_entry.relativize(&state.snapshot, path) {
if let Some(mtime) = fs_entry.mtime {
let repo = repo.repo_ptr.lock();
fs_entry.git_status = repo.status(&repo_path, mtime);
}
fs_entry.git_status = repo.repo_ptr.lock().status(&repo_path);
}
}
}
@@ -4267,11 +4262,8 @@ impl BackgroundScanner {
path_entry.is_ignored = entry.is_ignored;
if !entry.is_dir() && !entry.is_ignored && !entry.is_external {
if let Some((ref repo_entry, local_repo)) = repo {
if let Some(mtime) = &entry.mtime {
if let Ok(repo_path) = repo_entry.relativize(&snapshot, &entry.path) {
let repo = local_repo.repo_ptr.lock();
entry.git_status = repo.status(&repo_path, *mtime);
}
if let Ok(repo_path) = repo_entry.relativize(&snapshot, &entry.path) {
entry.git_status = local_repo.repo_ptr.lock().status(&repo_path);
}
}
}
@@ -4351,7 +4343,17 @@ impl BackgroundScanner {
}
};
let staged_statuses = repository.lock().staged_statuses(Path::new(""));
let statuses = repository
.lock()
.statuses(Path::new(""))
.log_err()
.unwrap_or_default();
let entries = state.snapshot.entries_by_path.clone();
let location_in_repo = state
.snapshot
.repository_entries
.get(&work_dir)
.and_then(|repo| repo.location_in_repo.clone());
let mut files =
state
.snapshot
@@ -4363,10 +4365,11 @@ impl BackgroundScanner {
smol::block_on(update_job_tx.send(UpdateGitStatusesJob {
start_path: start_path.clone(),
end_path: end_path.clone(),
entries: entries.clone(),
location_in_repo: location_in_repo.clone(),
containing_repository: ScanJobContainingRepository {
work_directory: work_dir.clone(),
repository: repository.clone(),
staged_statuses: staged_statuses.clone(),
statuses: statuses.clone(),
},
}))
.unwrap();
@@ -4414,16 +4417,12 @@ impl BackgroundScanner {
self.executor
.scoped(|scope| {
// Git status updates are currently not very parallelizable,
// because they need to lock the git repository. Limit the number
// of workers so that
for _ in 0..self.executor.num_cpus().min(3) {
for _ in 0..self.executor.num_cpus() {
scope.spawn(async {
let mut entries = Vec::with_capacity(GIT_STATUS_UPDATE_BATCH_SIZE);
loop {
select_biased! {
// Process any path refresh requests before moving on to process
// the queue of ignore statuses.
// the queue of git statuses.
request = self.scan_requests_rx.recv().fuse() => {
let Ok(request) = request else { break };
if !self.process_scan_request(request, true).await {
@@ -4434,7 +4433,7 @@ impl BackgroundScanner {
// Process git status updates in batches.
job = update_job_rx.recv().fuse() => {
let Ok(job) = job else { break };
self.update_git_statuses(job, &mut entries);
self.update_git_statuses(job);
}
}
}
@@ -4445,61 +4444,30 @@ impl BackgroundScanner {
}
/// Update the git statuses for a given batch of entries.
fn update_git_statuses(&self, job: UpdateGitStatusesJob, entries: &mut Vec<Entry>) {
fn update_git_statuses(&self, job: UpdateGitStatusesJob) {
// Determine which entries in this batch have changed their git status.
let t0 = Instant::now();
let repo_work_dir = &job.containing_repository.work_directory;
let state = self.state.lock();
let Some(repo_entry) = state
.snapshot
.repository_entries
.get(&repo_work_dir)
.cloned()
else {
return;
};
// Retrieve a batch of entries for this job, and then release the state lock.
entries.clear();
for entry in state
.snapshot
.traverse_from_path(true, false, false, &job.start_path)
{
let mut edits = Vec::new();
for entry in Traversal::new(&job.entries, true, false, false, &job.start_path) {
if job
.end_path
.as_ref()
.map_or(false, |end| &entry.path >= end)
|| !entry.path.starts_with(&repo_work_dir)
{
break;
}
entries.push(entry.clone());
}
drop(state);
// Determine which entries in this batch have changed their git status.
let mut edits = vec![];
for entry in entries.iter() {
let Ok(repo_path) = entry.path.strip_prefix(&repo_work_dir) else {
let Ok(repo_path) = entry
.path
.strip_prefix(&job.containing_repository.work_directory)
else {
continue;
};
let Some(mtime) = entry.mtime else {
continue;
};
let repo_path = RepoPath(if let Some(location) = &repo_entry.location_in_repo {
let repo_path = RepoPath(if let Some(location) = &job.location_in_repo {
location.join(repo_path)
} else {
repo_path.to_path_buf()
});
let git_status = combine_git_statuses(
job.containing_repository
.staged_statuses
.get(&repo_path)
.copied(),
job.containing_repository
.repository
.lock()
.unstaged_status(&repo_path, mtime),
);
let git_status = job.containing_repository.statuses.get(&repo_path);
if entry.git_status != git_status {
let mut entry = entry.clone();
entry.git_status = git_status;
@@ -4508,20 +4476,22 @@ impl BackgroundScanner {
}
// Apply the git status changes.
let mut state = self.state.lock();
let path_changes = edits.iter().map(|edit| {
if let Edit::Insert(entry) = edit {
entry.path.clone()
} else {
unreachable!()
}
});
util::extend_sorted(&mut state.changed_paths, path_changes, usize::MAX, Ord::cmp);
state.snapshot.entries_by_path.edit(edits, &());
if edits.len() > 0 {
let mut state = self.state.lock();
let path_changes = edits.iter().map(|edit| {
if let Edit::Insert(entry) = edit {
entry.path.clone()
} else {
unreachable!()
}
});
util::extend_sorted(&mut state.changed_paths, path_changes, usize::MAX, Ord::cmp);
state.snapshot.entries_by_path.edit(edits, &());
}
log::trace!(
"refreshed git status of {} entries starting with {} in {:?}",
entries.len(),
"refreshed git status of entries starting with {} in {:?}",
// entries.len(),
job.start_path.display(),
t0.elapsed()
);
@@ -4682,8 +4652,7 @@ struct ScanJob {
#[derive(Clone)]
struct ScanJobContainingRepository {
work_directory: RepositoryWorkDirectory,
repository: Arc<Mutex<dyn GitRepository>>,
staged_statuses: TreeMap<RepoPath, GitFileStatus>,
statuses: GitStatus,
}
struct UpdateIgnoreStatusJob {
@@ -4694,9 +4663,11 @@ struct UpdateIgnoreStatusJob {
}
struct UpdateGitStatusesJob {
entries: SumTree<Entry>,
start_path: Arc<Path>,
end_path: Option<Arc<Path>>,
containing_repository: ScanJobContainingRepository,
location_in_repo: Option<Arc<Path>>,
}
pub trait WorktreeModelHandle {
@@ -4906,6 +4877,26 @@ pub struct Traversal<'a> {
}
impl<'a> Traversal<'a> {
fn new(
entries: &'a SumTree<Entry>,
include_files: bool,
include_dirs: bool,
include_ignored: bool,
start_path: &Path,
) -> Self {
let mut cursor = entries.cursor();
cursor.seek(&TraversalTarget::Path(start_path), Bias::Left, &());
let mut traversal = Self {
cursor,
include_files,
include_dirs,
include_ignored,
};
if traversal.end_offset() == traversal.start_offset() {
traversal.next();
}
traversal
}
pub fn advance(&mut self) -> bool {
self.advance_by(1)
}
@@ -5079,25 +5070,6 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
}
}
fn combine_git_statuses(
staged: Option<GitFileStatus>,
unstaged: Option<GitFileStatus>,
) -> Option<GitFileStatus> {
if let Some(staged) = staged {
if let Some(unstaged) = unstaged {
if unstaged == staged {
Some(staged)
} else {
Some(GitFileStatus::Modified)
}
} else {
Some(staged)
}
} else {
unstaged
}
}
fn git_status_from_proto(git_status: Option<i32>) -> Option<GitFileStatus> {
git_status.and_then(|status| {
proto::GitStatus::from_i32(status).map(|status| match status {

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.138.0"
version = "0.139.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -832,7 +832,7 @@ fn load_embedded_fonts(cx: &AppContext) {
}
scope.spawn(async {
let font_bytes = asset_source.load(font_path).unwrap();
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
embedded_fonts.lock().push(font_bytes);
});
}

View File

@@ -3043,8 +3043,14 @@ mod tests {
fn test_bundled_settings_and_themes(cx: &mut AppContext) {
cx.text_system()
.add_fonts(vec![
Assets.load("fonts/zed-sans/zed-sans-extended.ttf").unwrap(),
Assets.load("fonts/zed-mono/zed-mono-extended.ttf").unwrap(),
Assets
.load("fonts/zed-sans/zed-sans-extended.ttf")
.unwrap()
.unwrap(),
Assets
.load("fonts/zed-mono/zed-mono-extended.ttf")
.unwrap()
.unwrap(),
])
.unwrap();
let themes = ThemeRegistry::default();