Compare commits
14 Commits
v0.138.4-p
...
linux-fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b55cb40c1 | ||
|
|
5a149b970c | ||
|
|
bdf627ce07 | ||
|
|
a5011996fb | ||
|
|
b8d9713b4f | ||
|
|
abec028e58 | ||
|
|
08881828ce | ||
|
|
dd328efaa7 | ||
|
|
6294a3b80b | ||
|
|
0f927fa6fb | ||
|
|
5bcb9ed017 | ||
|
|
a22cd95f9d | ||
|
|
44c50da94f | ||
|
|
c34d36161d |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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
169
Cargo.lock
generated
@@ -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",
|
||||
|
||||
19
Cargo.toml
19
Cargo.toml
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
137
crates/assistant/src/slash_command/rustdoc_command.rs
Normal file
137
crates/assistant/src/slash_command/rustdoc_command.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
|
||||
@@ -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)?))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
99
crates/git/src/status.rs
Normal 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([]),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(¶ms.path)?;
|
||||
let Some(bytes) = self.asset_source.load(¶ms.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> {
|
||||
|
||||
@@ -2347,13 +2347,14 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
let raster_bounds = self.text_system().raster_bounds(¶ms)?;
|
||||
if !raster_bounds.is_zero() {
|
||||
let tile =
|
||||
self.window
|
||||
.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||
Ok((size, Cow::Owned(bytes)))
|
||||
})?;
|
||||
let tile = self
|
||||
.window
|
||||
.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||
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(¶ms)?;
|
||||
if !raster_bounds.is_zero() {
|
||||
let tile =
|
||||
self.window
|
||||
.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||
Ok((size, Cow::Owned(bytes)))
|
||||
})?;
|
||||
let tile = self
|
||||
.window
|
||||
.sprite_atlas
|
||||
.get_or_insert_with(¶ms.clone().into(), &mut || {
|
||||
let (size, bytes) = self.text_system().rasterize_glyph(¶ms)?;
|
||||
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(¶ms.clone().into(), &mut || {
|
||||
let bytes = self.svg_renderer.render(¶ms)?;
|
||||
Ok((params.size, Cow::Owned(bytes)))
|
||||
})?;
|
||||
let Some(bytes) = self.svg_renderer.render(¶ms)? 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(¶ms.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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
22
crates/rustdoc_to_markdown/Cargo.toml
Normal file
22
crates/rustdoc_to_markdown/Cargo.toml
Normal 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
|
||||
1
crates/rustdoc_to_markdown/LICENSE-GPL
Symbolic link
1
crates/rustdoc_to_markdown/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
29
crates/rustdoc_to_markdown/examples/test.rs
Normal file
29
crates/rustdoc_to_markdown/examples/test.rs
Normal 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}");
|
||||
}
|
||||
223
crates/rustdoc_to_markdown/src/markdown_writer.rs
Normal file
223
crates/rustdoc_to_markdown/src/markdown_writer.rs
Normal 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(¤t_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(¤t_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(())
|
||||
}
|
||||
}
|
||||
88
crates/rustdoc_to_markdown/src/rustdoc_to_markdown.rs
Normal file
88
crates/rustdoc_to_markdown/src/rustdoc_to_markdown.rs
Normal 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<u32>) {}
|
||||
|
||||
<span class="comment">// `Query` gives you the query parameters and deserializes them.
|
||||
</span><span class="kw">async fn </span>query(Query(params): Query<HashMap<String, String>>) {}
|
||||
|
||||
<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<serde_json::Value>) {}</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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user