update
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# Local copy keeps the registry manifest shape and narrows log features so
|
||||
# GPUI and Servo can share the same workspace dependency graph.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "gpui_http_client"
|
||||
version = "0.2.2"
|
||||
build = false
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A HTTP client library for Zed and GPUI"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lib]
|
||||
name = "gpui_http_client"
|
||||
path = "src/http_client.rs"
|
||||
doctest = true
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0.86"
|
||||
|
||||
[dependencies.async-compression]
|
||||
version = "0.4"
|
||||
features = [
|
||||
"gzip",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[dependencies.async-fs]
|
||||
version = "2.1"
|
||||
|
||||
[dependencies.async-tar]
|
||||
version = "0.5.0-zed"
|
||||
package = "zed-async-tar"
|
||||
|
||||
[dependencies.bytes]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.derive_more]
|
||||
version = "0.99.17"
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.http]
|
||||
version = "1.1"
|
||||
|
||||
[dependencies.http-body]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
|
||||
[dependencies.parking_lot]
|
||||
version = "0.12.1"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12.15-zed"
|
||||
features = [
|
||||
"charset",
|
||||
"http2",
|
||||
"macos-system-configuration",
|
||||
"multipart",
|
||||
"rustls-tls-native-roots",
|
||||
"socks",
|
||||
"stream",
|
||||
]
|
||||
default-features = false
|
||||
package = "zed-reqwest"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.221"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0.144"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.10"
|
||||
|
||||
[dependencies.tempfile]
|
||||
version = "3.20.0"
|
||||
|
||||
[dependencies.url]
|
||||
version = "2.2"
|
||||
|
||||
[dependencies.util]
|
||||
version = "0.2.2"
|
||||
package = "gpui_util"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::{
|
||||
io::{Cursor, Read},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures::AsyncRead;
|
||||
use http_body::{Body, Frame};
|
||||
|
||||
/// Based on the implementation of AsyncBody in
|
||||
/// <https://github.com/sagebind/isahc/blob/5c533f1ef4d6bdf1fd291b5103c22110f41d0bf0/src/body/mod.rs>.
|
||||
pub struct AsyncBody(pub Inner);
|
||||
|
||||
pub enum Inner {
|
||||
/// An empty body.
|
||||
Empty,
|
||||
|
||||
/// A body stored in memory.
|
||||
Bytes(std::io::Cursor<Bytes>),
|
||||
|
||||
/// An asynchronous reader.
|
||||
AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
|
||||
}
|
||||
|
||||
impl AsyncBody {
|
||||
/// Create a new empty body.
|
||||
///
|
||||
/// An empty body represents the *absence* of a body, which is semantically
|
||||
/// different than the presence of a body of zero length.
|
||||
pub fn empty() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
/// Create a streaming body that reads from the given reader.
|
||||
pub fn from_reader<R>(read: R) -> Self
|
||||
where
|
||||
R: AsyncRead + Send + Sync + 'static,
|
||||
{
|
||||
Self(Inner::AsyncReader(Box::pin(read)))
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Bytes) -> Self {
|
||||
Self(Inner::Bytes(Cursor::new(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AsyncBody {
|
||||
fn default() -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<()> for AsyncBody {
|
||||
fn from(_: ()) -> Self {
|
||||
Self(Inner::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for AsyncBody {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
Self::from_bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for AsyncBody {
|
||||
fn from(body: Vec<u8>) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AsyncBody {
|
||||
fn from(body: String) -> Self {
|
||||
Self::from_bytes(body.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static [u8]) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for AsyncBody {
|
||||
#[inline]
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::from_bytes(Bytes::from_static(s.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<reqwest::Body> for AsyncBody {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: reqwest::Body) -> Result<Self, Self::Error> {
|
||||
value
|
||||
.as_bytes()
|
||||
.ok_or_else(|| anyhow::anyhow!("Underlying data is a stream"))
|
||||
.map(|bytes| Self::from_bytes(Bytes::copy_from_slice(bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Self>> From<Option<T>> for AsyncBody {
|
||||
fn from(body: Option<T>) -> Self {
|
||||
match body {
|
||||
Some(body) => body.into(),
|
||||
None => Self::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl futures::AsyncRead for AsyncBody {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<std::io::Result<usize>> {
|
||||
// SAFETY: Standard Enum pin projection
|
||||
let inner = unsafe { &mut self.get_unchecked_mut().0 };
|
||||
match inner {
|
||||
Inner::Empty => Poll::Ready(Ok(0)),
|
||||
// Blocking call is over an in-memory buffer
|
||||
Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
|
||||
Inner::AsyncReader(async_reader) => {
|
||||
AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body for AsyncBody {
|
||||
type Data = Bytes;
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn poll_frame(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
let mut buffer = vec![0; 8192];
|
||||
match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
|
||||
Poll::Ready(Ok(0)) => Poll::Ready(None),
|
||||
Poll::Ready(Ok(n)) => {
|
||||
let data = Bytes::copy_from_slice(&buffer[..n]);
|
||||
Poll::Ready(Some(Ok(Frame::data(data))))
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use crate::HttpClient;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use futures::AsyncReadExt;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
pub struct GitHubLspBinaryVersion {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubRelease {
|
||||
pub tag_name: String,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
pub zipball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubReleaseAsset {
|
||||
pub name: String,
|
||||
pub browser_download_url: String,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
require_assets: bool,
|
||||
pre_release: bool,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let mut response = http
|
||||
.get(
|
||||
format!("https://api.github.com/repos/{repo_name_with_owner}/releases").as_str(),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
||||
Ok(releases) => releases,
|
||||
|
||||
Err(err) => {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow::bail!("error deserializing latest release: {err:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let mut release = releases
|
||||
.into_iter()
|
||||
.filter(|release| !require_assets || !release.assets.is_empty())
|
||||
.find(|release| release.pre_release == pre_release)
|
||||
.context("finding a prerelease")?;
|
||||
release.assets.iter_mut().for_each(|asset| {
|
||||
if let Some(digest) = &mut asset.digest
|
||||
&& let Some(stripped) = digest.strip_prefix("sha256:")
|
||||
{
|
||||
*digest = stripped.to_owned();
|
||||
}
|
||||
});
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
pub async fn get_release_by_tag_name(
|
||||
repo_name_with_owner: &str,
|
||||
tag: &str,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> anyhow::Result<GithubRelease> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/tags/{tag}"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
let status = response.status();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
if status.is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
|
||||
log::error!("Error deserializing: {err:?}");
|
||||
log::error!(
|
||||
"GitHub API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
anyhow!("error deserializing GitHub release: {err:?}")
|
||||
})?;
|
||||
|
||||
Ok(release)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum AssetKind {
|
||||
TarGz,
|
||||
Gz,
|
||||
Zip,
|
||||
}
|
||||
|
||||
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
|
||||
let mut url = Url::parse(&format!(
|
||||
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
|
||||
))?;
|
||||
// We're pushing this here, because tags may contain `/` and other characters
|
||||
// that need to be escaped.
|
||||
let asset_filename = format!(
|
||||
"{tag}.{extension}",
|
||||
extension = match kind {
|
||||
AssetKind::TarGz => "tar.gz",
|
||||
AssetKind::Gz => "gz",
|
||||
AssetKind::Zip => "zip",
|
||||
}
|
||||
);
|
||||
url.path_segments_mut()
|
||||
.map_err(|()| anyhow!("cannot modify url path segments"))?
|
||||
.push(&asset_filename);
|
||||
Ok(url.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::github::{AssetKind, build_asset_url};
|
||||
|
||||
#[test]
|
||||
fn test_build_asset_url() {
|
||||
let tag = "release/2.3.5";
|
||||
let repo_name_with_owner = "microsoft/vscode-eslint";
|
||||
|
||||
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
|
||||
assert_eq!(
|
||||
tarball,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
|
||||
);
|
||||
|
||||
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
|
||||
assert_eq!(
|
||||
zip,
|
||||
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
use std::{path::Path, pin::Pin, task::Poll};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::{HttpClient, github::AssetKind};
|
||||
|
||||
#[derive(serde::Deserialize, serde::Serialize, Debug)]
|
||||
pub struct GithubBinaryMetadata {
|
||||
pub metadata_version: u64,
|
||||
pub digest: Option<String>,
|
||||
}
|
||||
|
||||
impl GithubBinaryMetadata {
|
||||
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
|
||||
let metadata_content = async_fs::read_to_string(metadata_path)
|
||||
.await
|
||||
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
|
||||
serde_json::from_str(&metadata_content)
|
||||
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
|
||||
}
|
||||
|
||||
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
|
||||
let metadata_content = serde_json::to_string(self)
|
||||
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
|
||||
async_fs::write(metadata_path, metadata_content.as_bytes())
|
||||
.await
|
||||
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_server_binary(
|
||||
http_client: &dyn HttpClient,
|
||||
url: &str,
|
||||
digest: Option<&str>,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
log::info!("downloading github artifact from {url}");
|
||||
let mut response = http_client
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.with_context(|| format!("downloading release from {url}"))?;
|
||||
let body = response.body_mut();
|
||||
match digest {
|
||||
Some(expected_sha_256) => {
|
||||
let temp_asset_file = tempfile::NamedTempFile::new()
|
||||
.with_context(|| format!("creating a temporary file for {url}"))?;
|
||||
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
|
||||
let mut writer = HashingWriter {
|
||||
writer: async_fs::File::from(temp_asset_file),
|
||||
hasher: Sha256::new(),
|
||||
};
|
||||
futures::io::copy(&mut BufReader::new(body), &mut writer)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("saving archive contents into the temporary file for {url}",)
|
||||
})?;
|
||||
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
|
||||
|
||||
anyhow::ensure!(
|
||||
asset_sha_256 == expected_sha_256,
|
||||
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
|
||||
);
|
||||
writer
|
||||
.writer
|
||||
.seek(std::io::SeekFrom::Start(0))
|
||||
.await
|
||||
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
|
||||
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting downloaded asset for {url} into {destination_path:?}",)
|
||||
})?;
|
||||
}
|
||||
None => stream_response_archive(body, url, destination_path, asset_kind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("extracting response for asset {url} into {destination_path:?}",)
|
||||
})?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_response_archive(
|
||||
response: impl AsyncRead + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, response).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stream_file_archive(
|
||||
file_archive: impl AsyncRead + AsyncSeek + Unpin,
|
||||
url: &str,
|
||||
destination_path: &Path,
|
||||
asset_kind: AssetKind,
|
||||
) -> Result<()> {
|
||||
match asset_kind {
|
||||
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
|
||||
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
|
||||
#[cfg(not(windows))]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
AssetKind::Zip => {
|
||||
util::archive::extract_zip(destination_path, file_archive).await?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_tar_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
let archive = async_tar::Archive::new(decompressed_bytes);
|
||||
archive
|
||||
.unpack(&destination_path)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn extract_gz(
|
||||
destination_path: &Path,
|
||||
url: &str,
|
||||
from: impl AsyncRead + Unpin,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
|
||||
let mut file = async_fs::File::create(&destination_path)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("creating a file {destination_path:?} for a download from {url}")
|
||||
})?;
|
||||
futures::io::copy(&mut decompressed_bytes, &mut file)
|
||||
.await
|
||||
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct HashingWriter<W: AsyncWrite + Unpin> {
|
||||
writer: W,
|
||||
hasher: Sha256,
|
||||
}
|
||||
|
||||
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::result::Result<usize, std::io::Error>> {
|
||||
match Pin::new(&mut self.writer).poll_write(cx, buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
self.hasher.update(&buf[..n]);
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<std::result::Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.writer).poll_close(cx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
mod async_body;
|
||||
pub mod github;
|
||||
pub mod github_download;
|
||||
|
||||
pub use anyhow::{Result, anyhow};
|
||||
pub use async_body::{AsyncBody, Inner};
|
||||
use derive_more::Deref;
|
||||
use http::HeaderValue;
|
||||
pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
|
||||
|
||||
use futures::{
|
||||
FutureExt as _,
|
||||
future::{self, BoxFuture},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{any::type_name, sync::Arc};
|
||||
pub use url::Url;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RedirectPolicy {
|
||||
#[default]
|
||||
NoFollow,
|
||||
FollowLimit(u32),
|
||||
FollowAll,
|
||||
}
|
||||
pub struct FollowRedirects(pub bool);
|
||||
|
||||
pub trait HttpRequestExt {
|
||||
/// Conditionally modify self with the given closure.
|
||||
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if condition { then(self) } else { self }
|
||||
}
|
||||
|
||||
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
|
||||
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match option {
|
||||
Some(value) => then(self, value),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not to follow redirects
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self;
|
||||
}
|
||||
|
||||
impl HttpRequestExt for http::request::Builder {
|
||||
fn follow_redirects(self, follow: RedirectPolicy) -> Self {
|
||||
self.extension(follow)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
|
||||
|
||||
fn get(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.follow_redirects(if follow_redirects {
|
||||
RedirectPolicy::FollowAll
|
||||
} else {
|
||||
RedirectPolicy::NoFollow
|
||||
})
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn post_json(
|
||||
&self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
let request = Builder::new()
|
||||
.uri(uri)
|
||||
.method(Method::POST)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => self.send(request),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
_url: &str,
|
||||
_request: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
future::ready(Err(anyhow!("not implemented"))).boxed()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that may have a proxy.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithProxy {
|
||||
#[deref]
|
||||
client: Arc<dyn HttpClient>,
|
||||
proxy: Option<Url>,
|
||||
}
|
||||
|
||||
impl HttpClientWithProxy {
|
||||
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
|
||||
pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
|
||||
let proxy_url = proxy_url
|
||||
.and_then(|proxy| proxy.parse().ok())
|
||||
.or_else(read_proxy_from_env);
|
||||
|
||||
Self::new_url(client, proxy_url)
|
||||
}
|
||||
pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
proxy: proxy_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithProxy {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
url: &str,
|
||||
form: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send_multipart_form(url, form)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HttpClientWithUrl {
|
||||
type Target = HttpClientWithProxy;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<String>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_url(
|
||||
client: Arc<dyn HttpClient>,
|
||||
base_url: impl Into<String>,
|
||||
proxy_url: Option<Url>,
|
||||
) -> Self {
|
||||
let client = HttpClientWithProxy::new_url(client, proxy_url);
|
||||
|
||||
Self {
|
||||
base_url: Mutex::new(base_url.into()),
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL.
|
||||
pub fn base_url(&self) -> String {
|
||||
self.base_url.lock().clone()
|
||||
}
|
||||
|
||||
/// Sets the base URL.
|
||||
pub fn set_base_url(&self, base_url: impl Into<String>) {
|
||||
let base_url = base_url.into();
|
||||
*self.base_url.lock() = base_url;
|
||||
}
|
||||
|
||||
/// Builds a URL using the given path.
|
||||
pub fn build_url(&self, path: &str) -> String {
|
||||
format!("{}{}", self.base_url(), path)
|
||||
}
|
||||
|
||||
/// Builds a Zed API URL using the given path.
|
||||
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://api.zed.dev",
|
||||
"https://staging.zed.dev" => "https://api-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8080",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed Cloud URL using the given path.
|
||||
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://llm-staging.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
}
|
||||
|
||||
fn send_multipart_form<'a>(
|
||||
&'a self,
|
||||
url: &str,
|
||||
request: reqwest::multipart::Form,
|
||||
) -> BoxFuture<'a, anyhow::Result<Response<AsyncBody>>> {
|
||||
self.client.send_multipart_form(url, request)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_proxy_from_env() -> Option<Url> {
|
||||
const ENV_VARS: &[&str] = &[
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
];
|
||||
|
||||
ENV_VARS
|
||||
.iter()
|
||||
.find_map(|var| std::env::var(var).ok())
|
||||
.and_then(|env| env.parse().ok())
|
||||
}
|
||||
|
||||
pub fn read_no_proxy_from_env() -> Option<String> {
|
||||
const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
|
||||
|
||||
ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
|
||||
}
|
||||
|
||||
pub struct BlockedHttpClient;
|
||||
|
||||
impl BlockedHttpClient {
|
||||
pub fn new() -> Self {
|
||||
BlockedHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for BlockedHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
_req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
Box::pin(async {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::PermissionDenied,
|
||||
"BlockedHttpClient disallowed request",
|
||||
)
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
type FakeHttpHandler = Arc<
|
||||
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
|
||||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: Mutex<Option<FakeHttpHandler>>,
|
||||
user_agent: HeaderValue,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl FakeHttpClient {
|
||||
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
Arc::new(HttpClientWithUrl {
|
||||
base_url: Mutex::new("http://test.example".into()),
|
||||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
|
||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(404)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
|
||||
Self::create(|_| async move {
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.body(Default::default())
|
||||
.unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_handler<Fut, F>(&self, new_handler: F)
|
||||
where
|
||||
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
|
||||
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
|
||||
{
|
||||
let mut handler = self.handler.lock();
|
||||
let old_handler = handler.take().unwrap();
|
||||
*handler = Some(Arc::new(move |req| {
|
||||
Box::pin(new_handler(old_handler.clone(), req))
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send(
|
||||
&self,
|
||||
req: Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
|
||||
((self.handler.lock().as_ref().unwrap())(req)) as _
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
Some(&self.user_agent)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# Local copy keeps the registry manifest shape and narrows log features so
|
||||
# GPUI and Servo can share the same workspace dependency graph.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "gpui_sum_tree"
|
||||
version = "0.2.2"
|
||||
build = false
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A sum tree data structure, a concurrency-friendly B-tree"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "gpui_sum_tree"
|
||||
path = "src/sum_tree.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies.arrayvec]
|
||||
version = "0.7.1"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
|
||||
[dependencies.rayon]
|
||||
version = "1.8"
|
||||
|
||||
[dev-dependencies.ctor]
|
||||
version = "0.4.0"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,819 @@
|
||||
use super::*;
|
||||
use arrayvec::ArrayVec;
|
||||
use std::{cmp::Ordering, mem, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StackEntry<'a, T: Item, D> {
|
||||
tree: &'a SumTree<T>,
|
||||
index: u32,
|
||||
position: D,
|
||||
}
|
||||
|
||||
impl<'a, T: Item, D> StackEntry<'a, T, D> {
|
||||
#[inline]
|
||||
fn index(&self) -> usize {
|
||||
self.index as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for StackEntry<'_, T, D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StackEntry")
|
||||
.field("index", &self.index)
|
||||
.field("position", &self.position)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cursor<'a, 'b, T: Item, D> {
|
||||
tree: &'a SumTree<T>,
|
||||
stack: ArrayVec<StackEntry<'a, T, D>, 16>,
|
||||
position: D,
|
||||
did_seek: bool,
|
||||
at_end: bool,
|
||||
cx: <T::Summary as Summary>::Context<'b>,
|
||||
}
|
||||
|
||||
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for Cursor<'_, '_, T, D>
|
||||
where
|
||||
T::Summary: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Cursor")
|
||||
.field("tree", &self.tree)
|
||||
.field("stack", &self.stack)
|
||||
.field("position", &self.position)
|
||||
.field("did_seek", &self.did_seek)
|
||||
.field("at_end", &self.at_end)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a, T: Item> {
|
||||
tree: &'a SumTree<T>,
|
||||
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
|
||||
where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
pub fn new(tree: &'a SumTree<T>, cx: <T::Summary as Summary>::Context<'b>) -> Self {
|
||||
Self {
|
||||
tree,
|
||||
stack: ArrayVec::new(),
|
||||
position: D::zero(cx),
|
||||
did_seek: false,
|
||||
at_end: tree.is_empty(),
|
||||
cx,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.did_seek = false;
|
||||
self.at_end = self.tree.is_empty();
|
||||
self.stack.truncate(0);
|
||||
self.position = D::zero(self.cx);
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &D {
|
||||
&self.position
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn end(&self) -> D {
|
||||
if let Some(item_summary) = self.item_summary() {
|
||||
let mut end = self.start().clone();
|
||||
end.add_summary(item_summary, self.cx);
|
||||
end
|
||||
} else {
|
||||
self.start().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Item is None, when the list is empty, or this cursor is at the end of the list.
|
||||
#[track_caller]
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => {
|
||||
if entry.index() == items.len() {
|
||||
None
|
||||
} else {
|
||||
Some(&items[entry.index()])
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn item_summary(&self) -> Option<&'a T::Summary> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf {
|
||||
ref item_summaries, ..
|
||||
} => {
|
||||
if entry.index() == item_summaries.len() {
|
||||
None
|
||||
} else {
|
||||
Some(&item_summaries[entry.index()])
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
if entry.index() == entry.tree.0.items().len() - 1 {
|
||||
if let Some(next_leaf) = self.next_leaf() {
|
||||
Some(next_leaf.0.items().first().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => Some(&items[entry.index() + 1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else if self.at_end {
|
||||
None
|
||||
} else {
|
||||
self.tree.first()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn next_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index() < entry.tree.0.child_trees().len() - 1 {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_trees, ..
|
||||
} => return Some(child_trees[entry.index() + 1].leftmost_leaf()),
|
||||
Node::Leaf { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
if entry.index() == 0 {
|
||||
if let Some(prev_leaf) = self.prev_leaf() {
|
||||
Some(prev_leaf.0.items().last().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else if self.at_end {
|
||||
self.tree.last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn prev_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index() != 0 {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_trees, ..
|
||||
} => return Some(child_trees[entry.index() - 1].rightmost_leaf()),
|
||||
Node::Leaf { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev(&mut self) {
|
||||
self.search_backward(|_| true)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn search_backward<F>(&mut self, mut filter_node: F)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
{
|
||||
if !self.did_seek {
|
||||
self.did_seek = true;
|
||||
self.at_end = true;
|
||||
}
|
||||
|
||||
if self.at_end {
|
||||
self.position = D::zero(self.cx);
|
||||
self.at_end = self.tree.is_empty();
|
||||
if !self.tree.is_empty() {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: self.tree.0.child_summaries().len() as u32,
|
||||
position: D::from_summary(self.tree.summary(), self.cx),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut descending = false;
|
||||
while !self.stack.is_empty() {
|
||||
if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) {
|
||||
self.position = position.clone();
|
||||
} else {
|
||||
self.position = D::zero(self.cx);
|
||||
}
|
||||
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
if !descending {
|
||||
if entry.index() == 0 {
|
||||
self.stack.pop();
|
||||
continue;
|
||||
} else {
|
||||
entry.index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
for summary in &entry.tree.0.child_summaries()[..entry.index()] {
|
||||
self.position.add_summary(summary, self.cx);
|
||||
}
|
||||
entry.position = self.position.clone();
|
||||
|
||||
descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]);
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal { child_trees, .. } => {
|
||||
if descending {
|
||||
let tree = &child_trees[entry.index()];
|
||||
self.stack.push(StackEntry {
|
||||
position: D::zero(self.cx),
|
||||
tree,
|
||||
index: tree.0.child_summaries().len() as u32 - 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
Node::Leaf { .. } => {
|
||||
if descending {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next(&mut self) {
|
||||
self.search_forward(|_| true)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn search_forward<F>(&mut self, mut filter_node: F)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
{
|
||||
let mut descend = false;
|
||||
|
||||
if self.stack.is_empty() {
|
||||
if !self.at_end {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: D::zero(self.cx),
|
||||
});
|
||||
descend = true;
|
||||
}
|
||||
self.did_seek = true;
|
||||
}
|
||||
|
||||
while !self.stack.is_empty() {
|
||||
let new_subtree = {
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal {
|
||||
child_trees,
|
||||
child_summaries,
|
||||
..
|
||||
} => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
}
|
||||
|
||||
while entry.index() < child_summaries.len() {
|
||||
let next_summary = &child_summaries[entry.index()];
|
||||
if filter_node(next_summary) {
|
||||
break;
|
||||
} else {
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(next_summary, self.cx);
|
||||
self.position.add_summary(next_summary, self.cx);
|
||||
}
|
||||
}
|
||||
|
||||
child_trees.get(entry.index())
|
||||
}
|
||||
Node::Leaf { item_summaries, .. } => {
|
||||
if !descend {
|
||||
let item_summary = &item_summaries[entry.index()];
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(item_summary, self.cx);
|
||||
self.position.add_summary(item_summary, self.cx);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(next_item_summary) = item_summaries.get(entry.index()) {
|
||||
if filter_node(next_item_summary) {
|
||||
return;
|
||||
} else {
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(next_item_summary, self.cx);
|
||||
self.position.add_summary(next_item_summary, self.cx);
|
||||
}
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(subtree) = new_subtree {
|
||||
descend = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: subtree,
|
||||
index: 0,
|
||||
position: self.position.clone(),
|
||||
});
|
||||
} else {
|
||||
descend = false;
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
self.at_end = self.stack.is_empty();
|
||||
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_did_seek(&self) {
|
||||
assert!(
|
||||
self.did_seek,
|
||||
"Must call `seek`, `next` or `prev` before calling this method"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
|
||||
where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
/// Returns whether we found the item you were seeking for.
|
||||
#[track_caller]
|
||||
pub fn seek<Target>(&mut self, pos: &Target, bias: Bias) -> bool
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
self.reset();
|
||||
self.seek_internal(pos, bias, &mut ())
|
||||
}
|
||||
|
||||
/// Returns whether we found the item you were seeking for.
|
||||
#[track_caller]
|
||||
pub fn seek_forward<Target>(&mut self, pos: &Target, bias: Bias) -> bool
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
self.seek_internal(pos, bias, &mut ())
|
||||
}
|
||||
|
||||
/// Advances the cursor and returns traversed items as a tree.
|
||||
#[track_caller]
|
||||
pub fn slice<Target>(&mut self, end: &Target, bias: Bias) -> SumTree<T>
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
let mut slice = SliceSeekAggregate {
|
||||
tree: SumTree::new(self.cx),
|
||||
leaf_items: ArrayVec::new(),
|
||||
leaf_item_summaries: ArrayVec::new(),
|
||||
leaf_summary: <T::Summary as Summary>::zero(self.cx),
|
||||
};
|
||||
self.seek_internal(end, bias, &mut slice);
|
||||
slice.tree
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn suffix(&mut self) -> SumTree<T> {
|
||||
self.slice(&End::new(), Bias::Right)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn summary<Target, Output>(&mut self, end: &Target, bias: Bias) -> Output
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
Output: Dimension<'a, T::Summary>,
|
||||
{
|
||||
let mut summary = SummarySeekAggregate(Output::zero(self.cx));
|
||||
self.seek_internal(end, bias, &mut summary);
|
||||
summary.0
|
||||
}
|
||||
|
||||
/// Returns whether we found the item you were seeking for.
|
||||
#[track_caller]
|
||||
fn seek_internal(
|
||||
&mut self,
|
||||
target: &dyn SeekTarget<'a, T::Summary, D>,
|
||||
bias: Bias,
|
||||
aggregate: &mut dyn SeekAggregate<'a, T>,
|
||||
) -> bool {
|
||||
assert!(
|
||||
target.cmp(&self.position, self.cx) >= Ordering::Equal,
|
||||
"cannot seek backward",
|
||||
);
|
||||
|
||||
if !self.did_seek {
|
||||
self.did_seek = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: D::zero(self.cx),
|
||||
});
|
||||
}
|
||||
|
||||
let mut ascending = false;
|
||||
'outer: while let Some(entry) = self.stack.last_mut() {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_summaries,
|
||||
ref child_trees,
|
||||
..
|
||||
} => {
|
||||
if ascending {
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
}
|
||||
|
||||
for (child_tree, child_summary) in child_trees[entry.index()..]
|
||||
.iter()
|
||||
.zip(&child_summaries[entry.index()..])
|
||||
{
|
||||
let mut child_end = self.position.clone();
|
||||
child_end.add_summary(child_summary, self.cx);
|
||||
|
||||
let comparison = target.cmp(&child_end, self.cx);
|
||||
if comparison == Ordering::Greater
|
||||
|| (comparison == Ordering::Equal && bias == Bias::Right)
|
||||
{
|
||||
self.position = child_end;
|
||||
aggregate.push_tree(child_tree, child_summary, self.cx);
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
} else {
|
||||
self.stack.push(StackEntry {
|
||||
tree: child_tree,
|
||||
index: 0,
|
||||
position: self.position.clone(),
|
||||
});
|
||||
ascending = false;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Leaf {
|
||||
ref items,
|
||||
ref item_summaries,
|
||||
..
|
||||
} => {
|
||||
aggregate.begin_leaf();
|
||||
|
||||
for (item, item_summary) in items[entry.index()..]
|
||||
.iter()
|
||||
.zip(&item_summaries[entry.index()..])
|
||||
{
|
||||
let mut child_end = self.position.clone();
|
||||
child_end.add_summary(item_summary, self.cx);
|
||||
|
||||
let comparison = target.cmp(&child_end, self.cx);
|
||||
if comparison == Ordering::Greater
|
||||
|| (comparison == Ordering::Equal && bias == Bias::Right)
|
||||
{
|
||||
self.position = child_end;
|
||||
aggregate.push_item(item, item_summary, self.cx);
|
||||
entry.index += 1;
|
||||
} else {
|
||||
aggregate.end_leaf(self.cx);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
aggregate.end_leaf(self.cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.stack.pop();
|
||||
ascending = true;
|
||||
}
|
||||
|
||||
self.at_end = self.stack.is_empty();
|
||||
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
|
||||
|
||||
let mut end = self.position.clone();
|
||||
if bias == Bias::Left
|
||||
&& let Some(summary) = self.item_summary()
|
||||
{
|
||||
end.add_summary(summary, self.cx);
|
||||
}
|
||||
|
||||
target.cmp(&end, self.cx) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item> Iter<'a, T> {
|
||||
pub(crate) fn new(tree: &'a SumTree<T>) -> Self {
|
||||
Self {
|
||||
tree,
|
||||
stack: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item> Iterator for Iter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut descend = false;
|
||||
|
||||
if self.stack.is_empty() {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: (),
|
||||
});
|
||||
descend = true;
|
||||
}
|
||||
|
||||
while !self.stack.is_empty() {
|
||||
let new_subtree = {
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal { child_trees, .. } => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
}
|
||||
child_trees.get(entry.index())
|
||||
}
|
||||
Node::Leaf { items, .. } => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
}
|
||||
|
||||
if let Some(next_item) = items.get(entry.index()) {
|
||||
return Some(next_item);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(subtree) = new_subtree {
|
||||
descend = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: subtree,
|
||||
index: 0,
|
||||
position: (),
|
||||
});
|
||||
} else {
|
||||
descend = false;
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T: Item, D> Iterator for Cursor<'a, 'b, T, D>
|
||||
where
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.did_seek {
|
||||
self.next();
|
||||
}
|
||||
|
||||
if let Some(item) = self.item() {
|
||||
self.next();
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilterCursor<'a, 'b, F, T: Item, D> {
|
||||
cursor: Cursor<'a, 'b, T, D>,
|
||||
filter_node: F,
|
||||
}
|
||||
|
||||
impl<'a, 'b, F, T: Item, D> FilterCursor<'a, 'b, F, T, D>
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
pub fn new(
|
||||
tree: &'a SumTree<T>,
|
||||
cx: <T::Summary as Summary>::Context<'b>,
|
||||
filter_node: F,
|
||||
) -> Self {
|
||||
let cursor = tree.cursor::<D>(cx);
|
||||
Self {
|
||||
cursor,
|
||||
filter_node,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &D {
|
||||
self.cursor.start()
|
||||
}
|
||||
|
||||
pub fn end(&self) -> D {
|
||||
self.cursor.end()
|
||||
}
|
||||
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.cursor.item()
|
||||
}
|
||||
|
||||
pub fn item_summary(&self) -> Option<&'a T::Summary> {
|
||||
self.cursor.item_summary()
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.cursor.search_forward(&mut self.filter_node);
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) {
|
||||
self.cursor.search_backward(&mut self.filter_node);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, F, T: Item, U> Iterator for FilterCursor<'a, 'b, F, T, U>
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
U: Dimension<'a, T::Summary>,
|
||||
{
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.cursor.did_seek {
|
||||
self.next();
|
||||
}
|
||||
|
||||
if let Some(item) = self.item() {
|
||||
self.cursor.search_forward(&mut self.filter_node);
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait SeekAggregate<'a, T: Item> {
|
||||
fn begin_leaf(&mut self);
|
||||
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>);
|
||||
fn push_item(
|
||||
&mut self,
|
||||
item: &'a T,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
);
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
tree: &'a SumTree<T>,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
);
|
||||
}
|
||||
|
||||
struct SliceSeekAggregate<T: Item> {
|
||||
tree: SumTree<T>,
|
||||
leaf_items: ArrayVec<T, { 2 * TREE_BASE }>,
|
||||
leaf_item_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>,
|
||||
leaf_summary: T::Summary,
|
||||
}
|
||||
|
||||
struct SummarySeekAggregate<D>(D);
|
||||
|
||||
impl<T: Item> SeekAggregate<'_, T> for () {
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_item(&mut self, _: &T, _: &T::Summary, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
_: &SumTree<T>,
|
||||
_: &T::Summary,
|
||||
_: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> SeekAggregate<'_, T> for SliceSeekAggregate<T> {
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>) {
|
||||
self.tree.append(
|
||||
SumTree(Arc::new(Node::Leaf {
|
||||
summary: mem::replace(&mut self.leaf_summary, <T::Summary as Summary>::zero(cx)),
|
||||
items: mem::take(&mut self.leaf_items),
|
||||
item_summaries: mem::take(&mut self.leaf_item_summaries),
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
fn push_item(
|
||||
&mut self,
|
||||
item: &T,
|
||||
summary: &T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.leaf_items.push(item.clone());
|
||||
self.leaf_item_summaries.push(summary.clone());
|
||||
Summary::add_summary(&mut self.leaf_summary, summary, cx);
|
||||
}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
tree: &SumTree<T>,
|
||||
_: &T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.tree.append(tree.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate<D>
|
||||
where
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_item(
|
||||
&mut self,
|
||||
_: &T,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.0.add_summary(summary, cx);
|
||||
}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
_: &SumTree<T>,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.0.add_summary(summary, cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct End<D>(PhantomData<D>);
|
||||
|
||||
impl<D> End<D> {
|
||||
fn new() -> Self {
|
||||
Self(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End<D> {
|
||||
fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering {
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> fmt::Debug for End<D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("End").finish()
|
||||
}
|
||||
}
|
||||
+1558
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,521 @@
|
||||
use std::{cmp::Ordering, fmt::Debug};
|
||||
|
||||
use crate::{Bias, ContextLessSummary, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree};
|
||||
|
||||
/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree).
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MapEntry<K, V> {
|
||||
key: K,
|
||||
value: V,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MapKey<K>(Option<K>);
|
||||
|
||||
impl<K> Default for MapKey<K> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapKeyRef<'a, K>(Option<&'a K>);
|
||||
|
||||
impl<K> Default for MapKeyRef<'_, K> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeSet<K>(TreeMap<K, ()>)
|
||||
where
|
||||
K: Clone + Ord;
|
||||
|
||||
impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = (K, V)>) -> Self {
|
||||
let tree = SumTree::from_iter(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(key, value)| MapEntry { key, value }),
|
||||
(),
|
||||
);
|
||||
Self(tree)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &K) -> Option<&V> {
|
||||
let (.., item) = self
|
||||
.0
|
||||
.find::<MapKeyRef<'_, K>, _>((), &MapKeyRef(Some(key)), Bias::Left);
|
||||
if let Some(item) = item {
|
||||
if Some(key) == item.key().0.as_ref() {
|
||||
Some(&item.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K, value: V) {
|
||||
self.0.insert_or_replace(MapEntry { key, value }, ());
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = (K, V)>) {
|
||||
let edits: Vec<_> = iter
|
||||
.into_iter()
|
||||
.map(|(key, value)| Edit::Insert(MapEntry { key, value }))
|
||||
.collect();
|
||||
self.0.edit(edits, ());
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0 = SumTree::default();
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||
let mut removed = None;
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
let mut new_tree = cursor.slice(&key, Bias::Left);
|
||||
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
|
||||
removed = Some(cursor.item().unwrap().value.clone());
|
||||
cursor.next();
|
||||
}
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn remove_range(&mut self, start: &impl MapSeekTarget<K>, end: &impl MapSeekTarget<K>) {
|
||||
let start = MapSeekTargetAdaptor(start);
|
||||
let end = MapSeekTargetAdaptor(end);
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let mut new_tree = cursor.slice(&start, Bias::Left);
|
||||
cursor.seek(&end, Bias::Left);
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
}
|
||||
|
||||
/// Returns the key-value pair with the greatest key less than or equal to the given key.
|
||||
pub fn closest(&self, key: &K) -> Option<(&K, &V)> {
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
cursor.seek(&key, Bias::Right);
|
||||
cursor.prev();
|
||||
cursor.item().map(|item| (&item.key, &item.value))
|
||||
}
|
||||
|
||||
pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator<Item = (&'a K, &'a V)> + 'a {
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let from_key = MapKeyRef(Some(from));
|
||||
cursor.seek(&from_key, Bias::Left);
|
||||
|
||||
cursor.map(|map_entry| (&map_entry.key, &map_entry.value))
|
||||
}
|
||||
|
||||
pub fn update<F, T>(&mut self, key: &K, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(&mut V) -> T,
|
||||
{
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
let mut new_tree = cursor.slice(&key, Bias::Left);
|
||||
let mut result = None;
|
||||
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
|
||||
let mut updated = cursor.item().unwrap().clone();
|
||||
result = Some(f(&mut updated.value));
|
||||
new_tree.push(updated, ());
|
||||
cursor.next();
|
||||
}
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut predicate: F) {
|
||||
let mut new_map = SumTree::<MapEntry<K, V>>::default();
|
||||
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
cursor.next();
|
||||
while let Some(item) = cursor.item() {
|
||||
if predicate(&item.key, &item.value) {
|
||||
new_map.push(item.clone(), ());
|
||||
}
|
||||
cursor.next();
|
||||
}
|
||||
drop(cursor);
|
||||
|
||||
self.0 = new_map;
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
|
||||
self.0.iter().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &V> + '_ {
|
||||
self.0.iter().map(|entry| &entry.value)
|
||||
}
|
||||
|
||||
pub fn first(&self) -> Option<(&K, &V)> {
|
||||
self.0.first().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn last(&self) -> Option<(&K, &V)> {
|
||||
self.0.last().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
|
||||
let edits = other
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
Edit::Insert(MapEntry {
|
||||
key: key.to_owned(),
|
||||
value: value.to_owned(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.0.edit(edits, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Debug for TreeMap<K, V>
|
||||
where
|
||||
K: Clone + Debug + Ord,
|
||||
V: Clone + Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_map().entries(self.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MapSeekTargetAdaptor<'a, T>(&'a T);
|
||||
|
||||
impl<'a, K: Clone + Ord, T: MapSeekTarget<K>> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>>
|
||||
for MapSeekTargetAdaptor<'_, T>
|
||||
{
|
||||
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
|
||||
if let Some(key) = &cursor_location.0 {
|
||||
MapSeekTarget::cmp_cursor(self.0, key)
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MapSeekTarget<K> {
|
||||
fn cmp_cursor(&self, cursor_location: &K) -> Ordering;
|
||||
}
|
||||
|
||||
impl<K: Ord> MapSeekTarget<K> for K {
|
||||
fn cmp_cursor(&self, cursor_location: &K) -> Ordering {
|
||||
self.cmp(cursor_location)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Default for TreeMap<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Item for MapEntry<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
type Summary = MapKey<K>;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
self.key()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> KeyedItem for MapEntry<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
type Key = MapKey<K>;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
MapKey(Some(self.key.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> ContextLessSummary for MapKey<K>
|
||||
where
|
||||
K: Clone,
|
||||
{
|
||||
fn zero() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self) {
|
||||
*self = summary.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K> Dimension<'a, MapKey<K>> for MapKeyRef<'a, K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn zero(_cx: ()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &'a MapKey<K>, _: ()) {
|
||||
self.0 = summary.0.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>> for MapKeyRef<'_, K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
|
||||
Ord::cmp(&self.0, &cursor_location.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for TreeSet<K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> TreeSet<K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
|
||||
Self(TreeMap::from_ordered_entries(
|
||||
entries.into_iter().map(|key| (key, ())),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.0.insert(key, ());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> bool {
|
||||
self.0.remove(key).is_some()
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = K>) {
|
||||
self.0.extend(iter.into_iter().map(|key| (key, ())));
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &K) -> bool {
|
||||
self.0.get(key).is_some()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &K> + '_ {
|
||||
self.0.iter().map(|(k, _)| k)
|
||||
}
|
||||
|
||||
pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator<Item = &'a K> + 'a {
|
||||
self.0.iter_from(key).map(move |(k, _)| k)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let mut map = TreeMap::default();
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
|
||||
|
||||
map.insert(3, "c");
|
||||
assert_eq!(map.get(&3), Some(&"c"));
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&3, &"c")]);
|
||||
|
||||
map.insert(1, "a");
|
||||
assert_eq!(map.get(&1), Some(&"a"));
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
|
||||
|
||||
map.insert(2, "b");
|
||||
assert_eq!(map.get(&2), Some(&"b"));
|
||||
assert_eq!(map.get(&1), Some(&"a"));
|
||||
assert_eq!(map.get(&3), Some(&"c"));
|
||||
assert_eq!(
|
||||
map.iter().collect::<Vec<_>>(),
|
||||
vec![(&1, &"a"), (&2, &"b"), (&3, &"c")]
|
||||
);
|
||||
|
||||
assert_eq!(map.closest(&0), None);
|
||||
assert_eq!(map.closest(&1), Some((&1, &"a")));
|
||||
assert_eq!(map.closest(&10), Some((&3, &"c")));
|
||||
|
||||
map.remove(&2);
|
||||
assert_eq!(map.get(&2), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
|
||||
|
||||
assert_eq!(map.closest(&2), Some((&1, &"a")));
|
||||
|
||||
map.remove(&3);
|
||||
assert_eq!(map.get(&3), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a")]);
|
||||
|
||||
map.remove(&1);
|
||||
assert_eq!(map.get(&1), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
|
||||
|
||||
map.insert(4, "d");
|
||||
map.insert(5, "e");
|
||||
map.insert(6, "f");
|
||||
map.retain(|key, _| *key % 2 == 0);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&4, &"d"), (&6, &"f")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_from() {
|
||||
let mut map = TreeMap::default();
|
||||
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("baa", 3);
|
||||
map.insert("baaab", 4);
|
||||
map.insert("c", 5);
|
||||
|
||||
let result = map
|
||||
.iter_from(&"ba")
|
||||
.take_while(|(key, _)| key.starts_with("ba"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().any(|(k, _)| k == &&"baa"));
|
||||
assert!(result.iter().any(|(k, _)| k == &&"baaab"));
|
||||
|
||||
let result = map
|
||||
.iter_from(&"c")
|
||||
.take_while(|(key, _)| key.starts_with("c"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.iter().any(|(k, _)| k == &&"c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_tree() {
|
||||
let mut map = TreeMap::default();
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("c", 3);
|
||||
|
||||
let mut other = TreeMap::default();
|
||||
other.insert("a", 2);
|
||||
other.insert("b", 2);
|
||||
other.insert("d", 4);
|
||||
|
||||
map.insert_tree(other);
|
||||
|
||||
assert_eq!(map.iter().count(), 4);
|
||||
assert_eq!(map.get(&"a"), Some(&2));
|
||||
assert_eq!(map.get(&"b"), Some(&2));
|
||||
assert_eq!(map.get(&"c"), Some(&3));
|
||||
assert_eq!(map.get(&"d"), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
let mut map = TreeMap::default();
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("c", 3);
|
||||
map.extend([("a", 2), ("b", 2), ("d", 4)]);
|
||||
assert_eq!(map.iter().count(), 4);
|
||||
assert_eq!(map.get(&"a"), Some(&2));
|
||||
assert_eq!(map.get(&"b"), Some(&2));
|
||||
assert_eq!(map.get(&"c"), Some(&3));
|
||||
assert_eq!(map.get(&"d"), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_between_and_path_successor() {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PathDescendants<'a>(&'a Path);
|
||||
|
||||
impl MapSeekTarget<PathBuf> for PathDescendants<'_> {
|
||||
fn cmp_cursor(&self, key: &PathBuf) -> Ordering {
|
||||
if key.starts_with(self.0) {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
self.0.cmp(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut map = TreeMap::default();
|
||||
|
||||
map.insert(PathBuf::from("a"), 1);
|
||||
map.insert(PathBuf::from("a/a"), 1);
|
||||
map.insert(PathBuf::from("b"), 2);
|
||||
map.insert(PathBuf::from("b/a/a"), 3);
|
||||
map.insert(PathBuf::from("b/a/a/a/b"), 4);
|
||||
map.insert(PathBuf::from("c"), 5);
|
||||
map.insert(PathBuf::from("c/a"), 6);
|
||||
|
||||
map.remove_range(
|
||||
&PathBuf::from("b/a"),
|
||||
&PathDescendants(&PathBuf::from("b/a")),
|
||||
);
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
assert_eq!(map.get(&PathBuf::from("b/a/a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("c")), Some(&5));
|
||||
assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6));
|
||||
|
||||
map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
assert_eq!(map.get(&PathBuf::from("c")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("c/a")), None);
|
||||
|
||||
map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
|
||||
map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("b")), None);
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# Local copy keeps the registry manifest shape and narrows log features so
|
||||
# GPUI and Servo can share the same workspace dependency graph.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "gpui_util"
|
||||
version = "0.2.2"
|
||||
build = false
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A collection of utility structs and functions used by Zed and GPUI"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"git2",
|
||||
"rand",
|
||||
"util_macros",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "gpui_util"
|
||||
path = "src/util.rs"
|
||||
doctest = true
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0.86"
|
||||
|
||||
[dependencies.async-fs]
|
||||
version = "2.1"
|
||||
|
||||
[dependencies.async_zip]
|
||||
version = "0.0.17"
|
||||
features = [
|
||||
"deflate",
|
||||
"deflate64",
|
||||
]
|
||||
|
||||
[dependencies.collections]
|
||||
version = "0.2.2"
|
||||
package = "gpui_collections"
|
||||
|
||||
[dependencies.dirs]
|
||||
version = "4.0"
|
||||
|
||||
[dependencies.dunce]
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.futures]
|
||||
version = "0.3"
|
||||
|
||||
[dependencies.futures-lite]
|
||||
version = "1.13"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.20.1"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.globset]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.14.0"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.5"
|
||||
|
||||
[dependencies.rust-embed]
|
||||
version = "8.4"
|
||||
features = ["include-exclude"]
|
||||
|
||||
[dependencies.schemars]
|
||||
version = "1.0"
|
||||
features = ["indexmap2"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.221"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0.144"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.serde_json_lenient]
|
||||
version = "0.2"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.shlex]
|
||||
version = "1.3.0"
|
||||
|
||||
[dependencies.smol]
|
||||
version = "2.0"
|
||||
|
||||
[dependencies.take-until]
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies.tempfile]
|
||||
version = "3.20.0"
|
||||
|
||||
[dependencies.unicase]
|
||||
version = "2.6"
|
||||
|
||||
[dependencies.util_macros]
|
||||
version = "0.2.2"
|
||||
optional = true
|
||||
package = "gpui_util_macros"
|
||||
|
||||
[dependencies.walkdir]
|
||||
version = "2.5"
|
||||
|
||||
[dependencies.which]
|
||||
version = "6.0.0"
|
||||
|
||||
[dev-dependencies.git2]
|
||||
version = "0.20.1"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.indoc]
|
||||
version = "2"
|
||||
|
||||
[dev-dependencies.pretty_assertions]
|
||||
version = "1.3.0"
|
||||
features = ["unstable"]
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[dev-dependencies.util_macros]
|
||||
version = "0.2.2"
|
||||
package = "gpui_util_macros"
|
||||
|
||||
[target."cfg(unix)".dependencies.command-fds]
|
||||
version = "0.3.1"
|
||||
|
||||
[target."cfg(unix)".dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target."cfg(unix)".dependencies.nix]
|
||||
version = "0.29"
|
||||
features = ["user"]
|
||||
|
||||
[target."cfg(windows)".dependencies.tendril]
|
||||
version = "0.4.3"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
fmt::{self, Debug},
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub enum ArcCow<'a, T: ?Sized> {
|
||||
Borrowed(&'a T),
|
||||
Owned(Arc<T>),
|
||||
}
|
||||
|
||||
impl<T: ?Sized + PartialEq> PartialEq for ArcCow<'_, T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let a = self.as_ref();
|
||||
let b = other.as_ref();
|
||||
a == b
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + PartialOrd> PartialOrd for ArcCow<'_, T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.as_ref().partial_cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Ord> Ord for ArcCow<'_, T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.as_ref().cmp(other.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Eq> Eq for ArcCow<'_, T> {}
|
||||
|
||||
impl<T: ?Sized + Hash> Hash for ArcCow<'_, T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Borrowed(borrowed) => Hash::hash(borrowed, state),
|
||||
Self::Owned(owned) => Hash::hash(&**owned, state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Clone for ArcCow<'_, T> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
|
||||
Self::Owned(owned) => Self::Owned(owned.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
|
||||
fn from(s: &'a T) -> Self {
|
||||
Self::Borrowed(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: Arc<T>) -> Self {
|
||||
Self::Owned(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
|
||||
fn from(s: &'_ Arc<T>) -> Self {
|
||||
Self::Owned(s.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ArcCow<'_, str> {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Owned(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for ArcCow<'_, str> {
|
||||
fn from(value: &String) -> Self {
|
||||
Self::Owned(value.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
|
||||
fn from(value: Cow<'a, str>) -> Self {
|
||||
match value {
|
||||
Cow::Borrowed(borrowed) => Self::Borrowed(borrowed),
|
||||
Cow::Owned(owned) => Self::Owned(owned.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for ArcCow<'_, [T]> {
|
||||
fn from(vec: Vec<T>) -> Self {
|
||||
ArcCow::Owned(Arc::from(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ArcCow<'a, [u8]> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
ArcCow::Borrowed(s.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'_, T> {
|
||||
fn borrow(&self) -> &T {
|
||||
match self {
|
||||
ArcCow::Borrowed(borrowed) => borrowed,
|
||||
ArcCow::Owned(owned) => owned.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
ArcCow::Borrowed(s) => s,
|
||||
ArcCow::Owned(s) => s.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
match self {
|
||||
ArcCow::Borrowed(borrowed) => borrowed,
|
||||
ArcCow::Owned(owned) => owned.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + Debug> Debug for ArcCow<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f),
|
||||
ArcCow::Owned(owned) => Debug::fmt(&**owned, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_zip::base::read;
|
||||
#[cfg(not(windows))]
|
||||
use futures::AsyncSeek;
|
||||
use futures::{AsyncRead, io::BufReader};
|
||||
|
||||
#[cfg(windows)]
|
||||
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
|
||||
let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));
|
||||
|
||||
let destination = &destination
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| destination.to_path_buf());
|
||||
|
||||
while let Some(mut item) = reader.next_with_entry().await? {
|
||||
let entry_reader = item.reader_mut();
|
||||
let entry = entry_reader.entry();
|
||||
let path = destination.join(
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.context("reading zip entry file name")?,
|
||||
);
|
||||
|
||||
if entry
|
||||
.dir()
|
||||
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
|
||||
{
|
||||
std::fs::create_dir_all(&path)
|
||||
.with_context(|| format!("creating directory {path:?}"))?;
|
||||
} else {
|
||||
let parent_dir = path
|
||||
.parent()
|
||||
.with_context(|| format!("no parent directory for {path:?}"))?;
|
||||
std::fs::create_dir_all(parent_dir)
|
||||
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
|
||||
let mut file = smol::fs::File::create(&path)
|
||||
.await
|
||||
.with_context(|| format!("creating file {path:?}"))?;
|
||||
futures::io::copy(entry_reader, &mut file)
|
||||
.await
|
||||
.with_context(|| format!("extracting into file {path:?}"))?;
|
||||
}
|
||||
|
||||
reader = item.skip().await.context("reading next zip entry")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
|
||||
// Unix needs file permissions copied when extracting.
|
||||
// This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.
|
||||
// `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`.
|
||||
//
|
||||
// A typical `reader` comes from a streaming network response, so cannot be sought right away,
|
||||
// and reading the entire archive into the memory seems wasteful.
|
||||
//
|
||||
// So, save the stream into a temporary file first and then get it read with a seeking reader.
|
||||
let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);
|
||||
futures::io::copy(&mut BufReader::new(reader), &mut file)
|
||||
.await
|
||||
.context("saving archive contents into the temporary file")?;
|
||||
extract_seekable_zip(destination, file).await
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
destination: &Path,
|
||||
reader: R,
|
||||
) -> Result<()> {
|
||||
let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader))
|
||||
.await
|
||||
.context("reading the zip archive")?;
|
||||
let destination = &destination
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| destination.to_path_buf());
|
||||
for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {
|
||||
let path = destination.join(
|
||||
entry
|
||||
.filename()
|
||||
.as_str()
|
||||
.context("reading zip entry file name")?,
|
||||
);
|
||||
|
||||
if entry
|
||||
.dir()
|
||||
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
|
||||
{
|
||||
std::fs::create_dir_all(&path)
|
||||
.with_context(|| format!("creating directory {path:?}"))?;
|
||||
} else {
|
||||
let parent_dir = path
|
||||
.parent()
|
||||
.with_context(|| format!("no parent directory for {path:?}"))?;
|
||||
std::fs::create_dir_all(parent_dir)
|
||||
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
|
||||
let mut file = smol::fs::File::create(&path)
|
||||
.await
|
||||
.with_context(|| format!("creating file {path:?}"))?;
|
||||
let mut entry_reader = reader
|
||||
.reader_with_entry(i)
|
||||
.await
|
||||
.with_context(|| format!("reading entry for path {path:?}"))?;
|
||||
futures::io::copy(&mut entry_reader, &mut file)
|
||||
.await
|
||||
.with_context(|| format!("extracting into file {path:?}"))?;
|
||||
|
||||
if let Some(perms) = entry.unix_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let permissions = std::fs::Permissions::from_mode(u32::from(perms));
|
||||
file.set_permissions(permissions)
|
||||
.await
|
||||
.with_context(|| format!("setting permissions for file {path:?}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use async_zip::ZipEntryBuilder;
|
||||
use async_zip::base::write::ZipFileWriter;
|
||||
use futures::{AsyncSeek, AsyncWriteExt};
|
||||
use smol::io::Cursor;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
|
||||
let mut out = smol::fs::File::create(dst).await?;
|
||||
let mut writer = ZipFileWriter::new(&mut out);
|
||||
|
||||
for entry in walkdir::WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_path = path.strip_prefix(src_dir)?;
|
||||
let data = smol::fs::read(&path).await?;
|
||||
|
||||
let filename = relative_path.display().to_string();
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut builder =
|
||||
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let perms = metadata.permissions().mode() as u16;
|
||||
builder = builder.unix_permissions(perms);
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let builder =
|
||||
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
}
|
||||
|
||||
writer.close().await?;
|
||||
out.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_file_content(path: &Path, content: &str) {
|
||||
assert!(path.exists(), "file not found: {:?}", path);
|
||||
let actual = std::fs::read_to_string(path).unwrap();
|
||||
assert_eq!(actual, content);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn make_test_data() -> TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
|
||||
std::fs::write(dst.join("test"), "Hello world.").unwrap();
|
||||
std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
|
||||
std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
|
||||
std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
|
||||
std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {
|
||||
let data = smol::fs::read(&path).await.unwrap();
|
||||
Cursor::new(data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_zip() {
|
||||
let test_dir = make_test_data();
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
|
||||
smol::block_on(async {
|
||||
compress_zip(test_dir.path(), &zip_file).await.unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dst = dir.path();
|
||||
extract_zip(dst, reader).await.unwrap();
|
||||
|
||||
assert_file_content(&dst.join("test"), "Hello world.");
|
||||
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
|
||||
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
|
||||
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_extract_zip_preserves_executable_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
smol::block_on(async {
|
||||
let test_dir = tempfile::tempdir().unwrap();
|
||||
let executable_path = test_dir.path().join("my_script");
|
||||
|
||||
// Create an executable file
|
||||
std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
|
||||
let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();
|
||||
perms.set_mode(0o755); // rwxr-xr-x
|
||||
std::fs::set_permissions(&executable_path, perms).unwrap();
|
||||
|
||||
// Create zip
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
compress_zip(test_dir.path(), &zip_file).await.unwrap();
|
||||
|
||||
// Extract to new location
|
||||
let extract_dir = tempfile::tempdir().unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
extract_zip(extract_dir.path(), reader).await.unwrap();
|
||||
|
||||
// Check permissions are preserved
|
||||
let extracted_path = extract_dir.path().join("my_script");
|
||||
assert!(extracted_path.exists());
|
||||
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
|
||||
assert_eq!(extracted_perms.mode() & 0o777, 0o755);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
let mut command = std::process::Command::new(program);
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
|
||||
std::process::Command::new(program)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||
use smol::process::windows::CommandExt;
|
||||
|
||||
let mut command = smol::process::Command::new(program);
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
|
||||
smol::process::Command::new(program)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
use crate::ResultExt;
|
||||
use anyhow::{Result, bail};
|
||||
use async_fs as fs;
|
||||
use futures_lite::StreamExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Removes all files and directories matching the given predicate
|
||||
pub async fn remove_matching<F>(dir: &Path, predicate: F)
|
||||
where
|
||||
F: Fn(&Path) -> bool,
|
||||
{
|
||||
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
if predicate(entry_path.as_path())
|
||||
&& let Ok(metadata) = fs::metadata(&entry_path).await
|
||||
{
|
||||
if metadata.is_file() {
|
||||
fs::remove_file(&entry_path).await.log_err();
|
||||
} else {
|
||||
fs::remove_dir_all(&entry_path).await.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn collect_matching<F>(dir: &Path, predicate: F) -> Vec<PathBuf>
|
||||
where
|
||||
F: Fn(&Path) -> bool,
|
||||
{
|
||||
let mut matching = vec![];
|
||||
|
||||
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err()
|
||||
&& predicate(entry.path().as_path())
|
||||
{
|
||||
matching.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
matching
|
||||
}
|
||||
|
||||
pub async fn find_file_name_in_dir<F>(dir: &Path, predicate: F) -> Option<PathBuf>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
|
||||
while let Some(entry) = entries.next().await {
|
||||
if let Some(entry) = entry.log_err() {
|
||||
let entry_path = entry.path();
|
||||
|
||||
if let Some(file_name) = entry_path
|
||||
.file_name()
|
||||
.map(|file_name| file_name.to_string_lossy())
|
||||
&& predicate(&file_name)
|
||||
{
|
||||
return Some(entry_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn move_folder_files_to_folder<P: AsRef<Path>>(
|
||||
source_path: P,
|
||||
target_path: P,
|
||||
) -> Result<()> {
|
||||
if !target_path.as_ref().is_dir() {
|
||||
bail!("Folder not found or is not a directory");
|
||||
}
|
||||
|
||||
let mut entries = fs::read_dir(source_path.as_ref()).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
let old_path = entry.path();
|
||||
let new_path = target_path.as_ref().join(entry.file_name());
|
||||
|
||||
fs::rename(&old_path, &new_path).await?;
|
||||
}
|
||||
|
||||
fs::remove_dir(source_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
/// Set the permissions for the given path so that the file becomes executable.
|
||||
/// This is a noop for non-unix platforms.
|
||||
pub async fn make_file_executable(path: &Path) -> std::io::Result<()> {
|
||||
fs::set_permissions(
|
||||
path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[allow(clippy::unused_async)]
|
||||
/// Set the permissions for the given path so that the file becomes executable.
|
||||
/// This is a noop for non-unix platforms.
|
||||
pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// Indicates that the wrapped `String` is markdown text.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarkdownString(pub String);
|
||||
|
||||
impl Display for MarkdownString {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow
|
||||
/// different rules and `MarkdownInlineCode` or `MarkdownCodeBlock` should be used in that case.
|
||||
///
|
||||
/// Also escapes the following markdown extensions:
|
||||
///
|
||||
/// * `^` for superscripts
|
||||
/// * `$` for inline math
|
||||
/// * `~` for strikethrough
|
||||
///
|
||||
/// Escape of some characters is unnecessary, because while they are involved in markdown syntax,
|
||||
/// the other characters involved are escaped:
|
||||
///
|
||||
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as
|
||||
/// plaintext.
|
||||
///
|
||||
/// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext.
|
||||
///
|
||||
/// TODO: There is one escape this doesn't do currently. Period after numbers at the start of the
|
||||
/// line (`[0-9]*\.`) should also be escaped to avoid it being interpreted as a list item.
|
||||
pub struct MarkdownEscaped<'a>(pub &'a str);
|
||||
|
||||
/// Implements `Display` to format markdown inline code (wrapped in backticks), handling code that
|
||||
/// contains backticks and spaces. All whitespace is treated as a single space character. For text
|
||||
/// that does not contain whitespace other than ' ', this escaping roundtrips through
|
||||
/// pulldown-cmark.
|
||||
///
|
||||
/// When used in tables, `|` should be escaped like `\|` in the text provided to this function.
|
||||
pub struct MarkdownInlineCode<'a>(pub &'a str);
|
||||
|
||||
/// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed.
|
||||
pub struct MarkdownCodeBlock<'a> {
|
||||
pub tag: &'a str,
|
||||
pub text: &'a str,
|
||||
}
|
||||
|
||||
impl Display for MarkdownEscaped<'_> {
|
||||
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut start_of_unescaped = None;
|
||||
for (ix, c) in self.0.char_indices() {
|
||||
match c {
|
||||
// Always escaped.
|
||||
'\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' |
|
||||
// TODO: these only need to be escaped when they are the first non-whitespace
|
||||
// character of the line of a block. There should probably be both an `escape_block`
|
||||
// which does this and an `escape_inline` method which does not escape these.
|
||||
'#' | '+' | '=' | '-' => {
|
||||
match start_of_unescaped {
|
||||
None => {}
|
||||
Some(start_of_unescaped) => {
|
||||
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
|
||||
}
|
||||
}
|
||||
write!(formatter, "\\")?;
|
||||
// Can include this char in the "unescaped" text since a
|
||||
// backslash was just emitted.
|
||||
start_of_unescaped = Some(ix);
|
||||
}
|
||||
// Escaped since `<` is used in opening HTML tags. `<` is used since Markdown
|
||||
// supports HTML entities, and this allows the text to be used directly in HTML.
|
||||
'<' => {
|
||||
match start_of_unescaped {
|
||||
None => {}
|
||||
Some(start_of_unescaped) => {
|
||||
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
|
||||
}
|
||||
}
|
||||
write!(formatter, "<")?;
|
||||
start_of_unescaped = None;
|
||||
}
|
||||
// Escaped since `>` is used for blockquotes. `>` is used since Markdown supports
|
||||
// HTML entities, and this allows the text to be used directly in HTML.
|
||||
'>' => {
|
||||
match start_of_unescaped {
|
||||
None => {}
|
||||
Some(start_of_unescaped) => {
|
||||
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
|
||||
}
|
||||
}
|
||||
write!(formatter, ">")?;
|
||||
start_of_unescaped = None;
|
||||
}
|
||||
_ => {
|
||||
if start_of_unescaped.is_none() {
|
||||
start_of_unescaped = Some(ix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(start_of_unescaped) = start_of_unescaped {
|
||||
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MarkdownInlineCode<'_> {
|
||||
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// Apache License 2.0, same as this crate.
|
||||
//
|
||||
// Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications:
|
||||
//
|
||||
// * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating
|
||||
// `Code` events parsed by pulldown-cmark.
|
||||
//
|
||||
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290
|
||||
|
||||
let mut all_whitespace = true;
|
||||
let text = self
|
||||
.0
|
||||
.chars()
|
||||
.map(|c| {
|
||||
if c.is_whitespace() {
|
||||
' '
|
||||
} else {
|
||||
all_whitespace = false;
|
||||
c
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
// When inline code has leading and trailing ' ' characters, additional space is needed
|
||||
// to escape it, unless all characters are space.
|
||||
if all_whitespace {
|
||||
write!(formatter, "`{text}`")
|
||||
} else {
|
||||
// More backticks are needed to delimit the inline code than the maximum number of
|
||||
// backticks in a consecutive run.
|
||||
let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1);
|
||||
let space = match text.as_bytes() {
|
||||
&[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick.
|
||||
&[b' ', .., b' '] => " ", // Space needed to escape inner space.
|
||||
_ => "", // No space needed.
|
||||
};
|
||||
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MarkdownCodeBlock<'_> {
|
||||
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let tag = self.tag;
|
||||
let text = self.text;
|
||||
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
|
||||
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from `pulldown-cmark-to-cmark-20.0.0` with changed names.
|
||||
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L1063
|
||||
// Apache License 2.0, same as this code.
|
||||
fn count_max_consecutive_chars(text: &str, search: char) -> usize {
|
||||
let mut in_search_chars = false;
|
||||
let mut max_count = 0;
|
||||
let mut cur_count = 0;
|
||||
|
||||
for ch in text.chars() {
|
||||
if ch == search {
|
||||
cur_count += 1;
|
||||
in_search_chars = true;
|
||||
} else if in_search_chars {
|
||||
max_count = max_count.max(cur_count);
|
||||
cur_count = 0;
|
||||
in_search_chars = false;
|
||||
}
|
||||
}
|
||||
max_count.max(cur_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_markdown_escaped() {
|
||||
let input = r#"
|
||||
# Heading
|
||||
|
||||
Another heading
|
||||
===
|
||||
|
||||
Another heading variant
|
||||
---
|
||||
|
||||
Paragraph with [link](https://example.com) and `code`, *emphasis*, and ~strikethrough~.
|
||||
|
||||
```
|
||||
code block
|
||||
```
|
||||
|
||||
List with varying leaders:
|
||||
- Item 1
|
||||
* Item 2
|
||||
+ Item 3
|
||||
|
||||
Some math: $`\sqrt{3x-1}+(1+x)^2`$
|
||||
|
||||
HTML entity:
|
||||
"#;
|
||||
|
||||
let expected = r#"
|
||||
\# Heading
|
||||
|
||||
Another heading
|
||||
\=\=\=
|
||||
|
||||
Another heading variant
|
||||
\-\-\-
|
||||
|
||||
Paragraph with \[link](https://example.com) and \`code\`, \*emphasis\*, and \~strikethrough\~.
|
||||
|
||||
\`\`\`
|
||||
code block
|
||||
\`\`\`
|
||||
|
||||
List with varying leaders:
|
||||
\- Item 1
|
||||
\* Item 2
|
||||
\+ Item 3
|
||||
|
||||
Some math: \$\`\\sqrt{3x\-1}\+(1\+x)\^2\`\$
|
||||
|
||||
HTML entity: \
|
||||
"#;
|
||||
|
||||
assert_eq!(MarkdownEscaped(input).to_string(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_inline_code() {
|
||||
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
|
||||
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
|
||||
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
|
||||
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
|
||||
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
|
||||
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
|
||||
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
|
||||
assert_eq!(
|
||||
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
|
||||
"``some `text` no leading or trailing backticks``"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_max_consecutive_chars() {
|
||||
assert_eq!(
|
||||
count_max_consecutive_chars("``a```b``", '`'),
|
||||
3,
|
||||
"the highest seen consecutive segment of backticks counts"
|
||||
);
|
||||
assert_eq!(
|
||||
count_max_consecutive_chars("```a``b`", '`'),
|
||||
3,
|
||||
"it can't be downgraded later"
|
||||
);
|
||||
}
|
||||
}
|
||||
+1934
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
/// Whether a given environment variable name should have its value redacted
|
||||
pub fn should_redact(env_var_name: &str) -> bool {
|
||||
const REDACTED_SUFFIXES: &[&str] = &[
|
||||
"KEY",
|
||||
"TOKEN",
|
||||
"PASSWORD",
|
||||
"SECRET",
|
||||
"PASS",
|
||||
"CREDENTIALS",
|
||||
"LICENSE",
|
||||
];
|
||||
REDACTED_SUFFIXES
|
||||
.iter()
|
||||
.any(|suffix| env_var_name.ends_with(suffix))
|
||||
}
|
||||
+579
@@ -0,0 +1,579 @@
|
||||
use crate::paths::{PathStyle, is_absolute};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A file system path that is guaranteed to be relative and normalized.
|
||||
///
|
||||
/// This type can be used to represent paths in a uniform way, regardless of
|
||||
/// whether they refer to Windows or POSIX file systems, and regardless of
|
||||
/// the host platform.
|
||||
///
|
||||
/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
|
||||
/// be displayed in either POSIX or Windows format.
|
||||
///
|
||||
/// Relative paths are also guaranteed to be valid unicode.
|
||||
#[repr(transparent)]
|
||||
#[derive(PartialEq, Eq, Hash, Serialize)]
|
||||
pub struct RelPath(str);
|
||||
|
||||
/// An owned representation of a file system path that is guaranteed to be
|
||||
/// relative and normalized.
|
||||
///
|
||||
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct RelPathBuf(String);
|
||||
|
||||
impl RelPath {
|
||||
/// Creates an empty [`RelPath`].
|
||||
pub fn empty() -> &'static Self {
|
||||
Self::new_unchecked("")
|
||||
}
|
||||
|
||||
/// Converts a path with a given style into a [`RelPath`].
|
||||
///
|
||||
/// Returns an error if the path is absolute, or is not valid unicode.
|
||||
///
|
||||
/// This method will normalize the path by removing `.` components,
|
||||
/// processing `..` components, and removing trailing separators. It does
|
||||
/// not allocate unless it's necessary to reformat the path.
|
||||
#[track_caller]
|
||||
pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
|
||||
let mut path = path.to_str().context("non utf-8 path")?;
|
||||
|
||||
let (prefixes, suffixes): (&[_], &[_]) = match path_style {
|
||||
PathStyle::Posix => (&["./"], &['/']),
|
||||
PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
|
||||
};
|
||||
|
||||
while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
|
||||
path = &path[prefixes[0].len()..];
|
||||
}
|
||||
while let Some(prefix) = path.strip_suffix(suffixes)
|
||||
&& !prefix.is_empty()
|
||||
{
|
||||
path = prefix;
|
||||
}
|
||||
|
||||
if is_absolute(&path, path_style) {
|
||||
return Err(anyhow!("absolute path not allowed: {path:?}"));
|
||||
}
|
||||
|
||||
let mut string = Cow::Borrowed(path);
|
||||
if path_style == PathStyle::Windows && path.contains('\\') {
|
||||
string = Cow::Owned(string.as_ref().replace('\\', "/"))
|
||||
}
|
||||
|
||||
let mut result = match string {
|
||||
Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
|
||||
Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
|
||||
};
|
||||
|
||||
if result
|
||||
.components()
|
||||
.any(|component| component == "" || component == "." || component == "..")
|
||||
{
|
||||
let mut normalized = RelPathBuf::new();
|
||||
for component in result.components() {
|
||||
match component {
|
||||
"" => {}
|
||||
"." => {}
|
||||
".." => {
|
||||
if !normalized.pop() {
|
||||
return Err(anyhow!("path is not relative: {result:?}"));
|
||||
}
|
||||
}
|
||||
other => normalized.push(RelPath::new_unchecked(other)),
|
||||
}
|
||||
}
|
||||
result = Cow::Owned(normalized)
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Converts a path that is already normalized and uses '/' separators
|
||||
/// into a [`RelPath`] .
|
||||
///
|
||||
/// Returns an error if the path is not already in the correct format.
|
||||
#[track_caller]
|
||||
pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
|
||||
let path = path.as_ref();
|
||||
match Self::new(path, PathStyle::Posix)? {
|
||||
Cow::Borrowed(path) => Ok(path),
|
||||
Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_unchecked(s: &str) -> &Self {
|
||||
// Safety: `RelPath` is a transparent wrapper around `str`.
|
||||
unsafe { &*(s as *const str as *const Self) }
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn components(&self) -> RelPathComponents<'_> {
|
||||
RelPathComponents(&self.0)
|
||||
}
|
||||
|
||||
pub fn ancestors(&self) -> RelPathAncestors<'_> {
|
||||
RelPathAncestors(Some(&self.0))
|
||||
}
|
||||
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.components().next_back()
|
||||
}
|
||||
|
||||
pub fn file_stem(&self) -> Option<&str> {
|
||||
Some(self.as_std_path().file_stem()?.to_str().unwrap())
|
||||
}
|
||||
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
Some(self.as_std_path().extension()?.to_str().unwrap())
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Option<&Self> {
|
||||
let mut components = self.components();
|
||||
components.next_back()?;
|
||||
Some(components.rest())
|
||||
}
|
||||
|
||||
pub fn starts_with(&self, other: &Self) -> bool {
|
||||
self.strip_prefix(other).is_ok()
|
||||
}
|
||||
|
||||
pub fn ends_with(&self, other: &Self) -> bool {
|
||||
if let Some(suffix) = self.0.strip_suffix(&other.0) {
|
||||
if suffix.ends_with('/') {
|
||||
return true;
|
||||
} else if suffix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self> {
|
||||
if other.is_empty() {
|
||||
return Ok(self);
|
||||
}
|
||||
if let Some(suffix) = self.0.strip_prefix(&other.0) {
|
||||
if let Some(suffix) = suffix.strip_prefix('/') {
|
||||
return Ok(Self::new_unchecked(suffix));
|
||||
} else if suffix.is_empty() {
|
||||
return Ok(Self::empty());
|
||||
}
|
||||
}
|
||||
Err(anyhow!("failed to strip prefix: {other:?} from {self:?}"))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.matches('/').count() + 1
|
||||
}
|
||||
|
||||
pub fn last_n_components(&self, count: usize) -> Option<&Self> {
|
||||
let len = self.len();
|
||||
if len >= count {
|
||||
let mut components = self.components();
|
||||
for _ in 0..(len - count) {
|
||||
components.next()?;
|
||||
}
|
||||
Some(components.rest())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(&self, other: &Self) -> Arc<Self> {
|
||||
let result = if self.0.is_empty() {
|
||||
Cow::Borrowed(&other.0)
|
||||
} else if other.0.is_empty() {
|
||||
Cow::Borrowed(&self.0)
|
||||
} else {
|
||||
Cow::Owned(format!("{}/{}", &self.0, &other.0))
|
||||
};
|
||||
Arc::from(Self::new_unchecked(result.as_ref()))
|
||||
}
|
||||
|
||||
pub fn to_rel_path_buf(&self) -> RelPathBuf {
|
||||
RelPathBuf(self.0.to_string())
|
||||
}
|
||||
|
||||
pub fn into_arc(&self) -> Arc<Self> {
|
||||
Arc::from(self)
|
||||
}
|
||||
|
||||
/// Convert the path into the wire representation.
|
||||
pub fn to_proto(&self) -> String {
|
||||
self.as_unix_str().to_owned()
|
||||
}
|
||||
|
||||
/// Load the path from its wire representation.
|
||||
pub fn from_proto(path: &str) -> Result<Arc<Self>> {
|
||||
Ok(Arc::from(Self::unix(path)?))
|
||||
}
|
||||
|
||||
/// Convert the path into a string with the given path style.
|
||||
///
|
||||
/// Whenever a path is presented to the user, it should be converted to
|
||||
/// a string via this method.
|
||||
pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
|
||||
match style {
|
||||
PathStyle::Posix => Cow::Borrowed(&self.0),
|
||||
PathStyle::Windows => Cow::Owned(self.0.replace('/', "\\")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the internal unix-style representation of the path.
|
||||
///
|
||||
/// This should not be shown to the user.
|
||||
pub fn as_unix_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Interprets the path as a [`std::path::Path`], suitable for file system calls.
|
||||
///
|
||||
/// This is guaranteed to be a valid path regardless of the host platform, because
|
||||
/// the `/` is accepted as a path separator on windows.
|
||||
///
|
||||
/// This should not be shown to the user.
|
||||
pub fn as_std_path(&self) -> &Path {
|
||||
Path::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for RelPath {
|
||||
type Owned = RelPathBuf;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
self.to_rel_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<RelPath> for RelPathBuf {
|
||||
fn borrow(&self) -> &RelPath {
|
||||
self.as_rel_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for RelPath {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for RelPath {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.components().cmp(other.components())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RelPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RelPathBuf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl RelPathBuf {
|
||||
pub fn new() -> Self {
|
||||
Self(String::new())
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> bool {
|
||||
if let Some(ix) = self.0.rfind('/') {
|
||||
self.0.truncate(ix);
|
||||
true
|
||||
} else if !self.is_empty() {
|
||||
self.0.clear();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, path: &RelPath) {
|
||||
if !self.is_empty() {
|
||||
self.0.push('/');
|
||||
}
|
||||
self.0.push_str(&path.0);
|
||||
}
|
||||
|
||||
pub fn as_rel_path(&self) -> &RelPath {
|
||||
RelPath::new_unchecked(self.0.as_str())
|
||||
}
|
||||
|
||||
pub fn set_extension(&mut self, extension: &str) -> bool {
|
||||
if let Some(filename) = self.file_name() {
|
||||
let mut filename = PathBuf::from(filename);
|
||||
filename.set_extension(extension);
|
||||
self.pop();
|
||||
self.0.push_str(filename.to_str().unwrap());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Arc<RelPath>> for RelPathBuf {
|
||||
fn into(self) -> Arc<RelPath> {
|
||||
Arc::from(self.as_rel_path())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RelPath> for RelPathBuf {
|
||||
fn as_ref(&self) -> &RelPath {
|
||||
self.as_rel_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RelPathBuf {
|
||||
type Target = RelPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
|
||||
fn from(value: &'a RelPath) -> Self {
|
||||
Self::Borrowed(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RelPath> for Arc<RelPath> {
|
||||
fn from(rel_path: &RelPath) -> Self {
|
||||
let bytes: Arc<str> = Arc::from(&rel_path.0);
|
||||
unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn rel_path(path: &str) -> &RelPath {
|
||||
RelPath::unix(path).unwrap()
|
||||
}
|
||||
|
||||
impl PartialEq<str> for RelPath {
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.0 == *other
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RelPathComponents<'a>(&'a str);
|
||||
|
||||
pub struct RelPathAncestors<'a>(Option<&'a str>);
|
||||
|
||||
const SEPARATOR: char = '/';
|
||||
|
||||
impl<'a> RelPathComponents<'a> {
|
||||
pub fn rest(&self) -> &'a RelPath {
|
||||
RelPath::new_unchecked(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RelPathComponents<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sep_ix) = self.0.find(SEPARATOR) {
|
||||
let (head, tail) = self.0.split_at(sep_ix);
|
||||
self.0 = &tail[1..];
|
||||
Some(head)
|
||||
} else if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let result = self.0;
|
||||
self.0 = "";
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RelPathAncestors<'a> {
|
||||
type Item = &'a RelPath;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.0?;
|
||||
if let Some(sep_ix) = result.rfind(SEPARATOR) {
|
||||
self.0 = Some(&result[..sep_ix]);
|
||||
} else if !result.is_empty() {
|
||||
self.0 = Some("");
|
||||
} else {
|
||||
self.0 = None;
|
||||
}
|
||||
Some(RelPath::new_unchecked(result))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
|
||||
let (head, tail) = self.0.split_at(sep_ix);
|
||||
self.0 = head;
|
||||
Some(&tail[1..])
|
||||
} else if self.0.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let result = self.0;
|
||||
self.0 = "";
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use pretty_assertions::assert_matches;
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_new() {
|
||||
assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
|
||||
assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
|
||||
assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
|
||||
|
||||
let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
assert_eq!(
|
||||
RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
rel_path("foo/baz/quux")
|
||||
);
|
||||
|
||||
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path, rel_path("foo").into());
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Owned(_));
|
||||
|
||||
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Borrowed(_));
|
||||
|
||||
let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
|
||||
assert_eq!(path.as_ref(), rel_path("foo/bar"));
|
||||
assert_matches!(path, Cow::Owned(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_components() {
|
||||
let path = rel_path("foo/bar/baz");
|
||||
assert_eq!(
|
||||
path.components().collect::<Vec<_>>(),
|
||||
vec!["foo", "bar", "baz"]
|
||||
);
|
||||
assert_eq!(
|
||||
path.components().rev().collect::<Vec<_>>(),
|
||||
vec!["baz", "bar", "foo"]
|
||||
);
|
||||
|
||||
let path = rel_path("");
|
||||
let mut components = path.components();
|
||||
assert_eq!(components.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_ancestors() {
|
||||
let path = rel_path("foo/bar/baz");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo")));
|
||||
assert_eq!(ancestors.next(), Some(rel_path("")));
|
||||
assert_eq!(ancestors.next(), None);
|
||||
|
||||
let path = rel_path("foo");
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next(), Some(rel_path("foo")));
|
||||
assert_eq!(ancestors.next(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next(), None);
|
||||
|
||||
let path = RelPath::empty();
|
||||
let mut ancestors = path.ancestors();
|
||||
assert_eq!(ancestors.next(), Some(RelPath::empty()));
|
||||
assert_eq!(ancestors.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_parent() {
|
||||
assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
|
||||
assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
|
||||
assert_eq!(rel_path("").parent(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_partial_ord_is_compatible_with_std() {
|
||||
let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
|
||||
for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
|
||||
assert_eq!(
|
||||
Path::new(lhs).cmp(Path::new(rhs)),
|
||||
RelPath::unix(lhs)
|
||||
.unwrap()
|
||||
.cmp(&RelPath::unix(rhs).unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_prefix() {
|
||||
let parent = rel_path("");
|
||||
let child = rel_path(".foo");
|
||||
|
||||
assert!(child.starts_with(parent));
|
||||
assert_eq!(child.strip_prefix(parent).unwrap(), child);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rel_path_constructors_absolute_path() {
|
||||
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
|
||||
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
|
||||
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pop() {
|
||||
let mut path = rel_path("a/b").to_rel_path_buf();
|
||||
path.pop();
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "a");
|
||||
path.pop();
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "");
|
||||
path.pop();
|
||||
assert_eq!(path.as_rel_path().as_unix_str(), "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use schemars::{JsonSchema, transform::transform_subschemas};
|
||||
|
||||
const DEFS_PATH: &str = "#/$defs/";
|
||||
|
||||
/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and
|
||||
/// returns a reference to it.
|
||||
///
|
||||
/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that
|
||||
/// there are multiple types that use this name, and unfortunately schemars APIs do not support
|
||||
/// resolving this ambiguity - see <https://github.com/GREsau/schemars/issues/449>
|
||||
///
|
||||
/// This takes a closure for `schema` because some settings types are not available on the remote
|
||||
/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry.
|
||||
pub fn replace_subschema<T: JsonSchema>(
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
schema: impl Fn() -> schemars::Schema,
|
||||
) -> schemars::Schema {
|
||||
let schema_name = T::schema_name();
|
||||
let definitions = generator.definitions_mut();
|
||||
assert!(!definitions.contains_key(&format!("{schema_name}2")));
|
||||
assert!(definitions.contains_key(schema_name.as_ref()));
|
||||
definitions.insert(schema_name.to_string(), schema().to_value());
|
||||
schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
|
||||
}
|
||||
|
||||
/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
|
||||
/// already in use.
|
||||
pub fn add_new_subschema(
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
name: &str,
|
||||
schema: serde_json::Value,
|
||||
) -> schemars::Schema {
|
||||
let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
|
||||
assert_eq!(old_definition, None);
|
||||
schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
|
||||
}
|
||||
|
||||
/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
|
||||
/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
|
||||
/// on a map).
|
||||
#[derive(Clone)]
|
||||
pub struct DefaultDenyUnknownFields;
|
||||
|
||||
impl schemars::transform::Transform for DefaultDenyUnknownFields {
|
||||
fn transform(&mut self, schema: &mut schemars::Schema) {
|
||||
if let Some(object) = schema.as_object_mut()
|
||||
&& object.contains_key("properties")
|
||||
&& !object.contains_key("additionalProperties")
|
||||
&& !object.contains_key("unevaluatedProperties")
|
||||
{
|
||||
object.insert("additionalProperties".to_string(), false.into());
|
||||
}
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
||||
*value == T::default()
|
||||
}
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ShellKind {
|
||||
#[default]
|
||||
Posix,
|
||||
Csh,
|
||||
Tcsh,
|
||||
Rc,
|
||||
Fish,
|
||||
PowerShell,
|
||||
Nushell,
|
||||
Cmd,
|
||||
Xonsh,
|
||||
}
|
||||
|
||||
pub fn get_system_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
get_windows_system_shell()
|
||||
} else {
|
||||
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_default_system_shell() -> String {
|
||||
if cfg!(windows) {
|
||||
get_windows_system_shell()
|
||||
} else {
|
||||
"/bin/sh".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default system shell, preferring git-bash on Windows.
|
||||
pub fn get_default_system_shell_preferring_bash() -> String {
|
||||
if cfg!(windows) {
|
||||
get_windows_git_bash().unwrap_or_else(|| get_windows_system_shell())
|
||||
} else {
|
||||
"/bin/sh".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_windows_git_bash() -> Option<String> {
|
||||
static GIT_BASH: LazyLock<Option<String>> = LazyLock::new(|| {
|
||||
// /path/to/git/cmd/git.exe/../../bin/bash.exe
|
||||
let git = which::which("git").ok()?;
|
||||
let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe");
|
||||
if git_bash.is_file() {
|
||||
log::info!("Found git-bash at {}", git_bash.display());
|
||||
Some(git_bash.to_string_lossy().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
(*GIT_BASH).clone()
|
||||
}
|
||||
|
||||
pub fn get_windows_system_shell() -> String {
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramFiles(x86)"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
let env_var = if find_alternate {
|
||||
"ProgramW6432"
|
||||
} else {
|
||||
"ProgramFiles"
|
||||
};
|
||||
|
||||
let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
|
||||
install_base_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
|
||||
.filter_map(|entry| {
|
||||
let dir_name = entry.file_name();
|
||||
let dir_name = dir_name.to_string_lossy();
|
||||
|
||||
let version = if find_preview {
|
||||
let dash_index = dir_name.find('-')?;
|
||||
if &dir_name[dash_index + 1..] != "preview" {
|
||||
return None;
|
||||
};
|
||||
dir_name[..dash_index].parse::<u32>().ok()?
|
||||
} else {
|
||||
dir_name.parse::<u32>().ok()?
|
||||
};
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
if exe_path.exists() {
|
||||
Some((version, exe_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(version, _)| *version)
|
||||
.map(|(_, path)| path)
|
||||
}
|
||||
|
||||
fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
|
||||
let msix_app_dir =
|
||||
PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
|
||||
if !msix_app_dir.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let prefix = if find_preview {
|
||||
"Microsoft.PowerShellPreview_"
|
||||
} else {
|
||||
"Microsoft.PowerShell_"
|
||||
};
|
||||
msix_app_dir
|
||||
.read_dir()
|
||||
.ok()?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !entry.file_name().to_string_lossy().starts_with(prefix) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let exe_path = entry.path().join("pwsh.exe");
|
||||
exe_path.exists().then_some(exe_path)
|
||||
})
|
||||
.next()
|
||||
}
|
||||
|
||||
fn find_pwsh_in_scoop() -> Option<PathBuf> {
|
||||
let pwsh_exe =
|
||||
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
|
||||
pwsh_exe.exists().then_some(pwsh_exe)
|
||||
}
|
||||
|
||||
static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
|
||||
find_pwsh_in_programfiles(false, false)
|
||||
.or_else(|| find_pwsh_in_programfiles(true, false))
|
||||
.or_else(|| find_pwsh_in_msix(false))
|
||||
.or_else(|| find_pwsh_in_programfiles(false, true))
|
||||
.or_else(|| find_pwsh_in_msix(true))
|
||||
.or_else(|| find_pwsh_in_programfiles(true, true))
|
||||
.or_else(find_pwsh_in_scoop)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or("powershell.exe".to_string())
|
||||
});
|
||||
|
||||
(*SYSTEM_SHELL).clone()
|
||||
}
|
||||
|
||||
impl fmt::Display for ShellKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ShellKind::Posix => write!(f, "sh"),
|
||||
ShellKind::Csh => write!(f, "csh"),
|
||||
ShellKind::Tcsh => write!(f, "tcsh"),
|
||||
ShellKind::Fish => write!(f, "fish"),
|
||||
ShellKind::PowerShell => write!(f, "powershell"),
|
||||
ShellKind::Nushell => write!(f, "nu"),
|
||||
ShellKind::Cmd => write!(f, "cmd"),
|
||||
ShellKind::Rc => write!(f, "rc"),
|
||||
ShellKind::Xonsh => write!(f, "xonsh"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellKind {
|
||||
pub fn system() -> Self {
|
||||
Self::new(&get_system_shell(), cfg!(windows))
|
||||
}
|
||||
|
||||
pub fn new(program: impl AsRef<Path>, is_windows: bool) -> Self {
|
||||
let program = program.as_ref();
|
||||
let program = program
|
||||
.file_stem()
|
||||
.unwrap_or_else(|| program.as_os_str())
|
||||
.to_string_lossy();
|
||||
|
||||
if program == "powershell" || program == "pwsh" {
|
||||
ShellKind::PowerShell
|
||||
} else if program == "cmd" {
|
||||
ShellKind::Cmd
|
||||
} else if program == "nu" {
|
||||
ShellKind::Nushell
|
||||
} else if program == "fish" {
|
||||
ShellKind::Fish
|
||||
} else if program == "csh" {
|
||||
ShellKind::Csh
|
||||
} else if program == "tcsh" {
|
||||
ShellKind::Tcsh
|
||||
} else if program == "rc" {
|
||||
ShellKind::Rc
|
||||
} else if program == "xonsh" {
|
||||
ShellKind::Xonsh
|
||||
} else if program == "sh" || program == "bash" {
|
||||
ShellKind::Posix
|
||||
} else {
|
||||
if is_windows {
|
||||
ShellKind::PowerShell
|
||||
} else {
|
||||
// Some other shell detected, the user might install and use a
|
||||
// unix-like shell.
|
||||
ShellKind::Posix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_shell_variable(self, input: &str) -> String {
|
||||
match self {
|
||||
Self::PowerShell => Self::to_powershell_variable(input),
|
||||
Self::Cmd => Self::to_cmd_variable(input),
|
||||
Self::Posix => input.to_owned(),
|
||||
Self::Fish => input.to_owned(),
|
||||
Self::Csh => input.to_owned(),
|
||||
Self::Tcsh => input.to_owned(),
|
||||
Self::Rc => input.to_owned(),
|
||||
Self::Nushell => Self::to_nushell_variable(input),
|
||||
Self::Xonsh => input.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_cmd_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("%{}%", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("%{}%", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_powershell_variable(input: &str) -> String {
|
||||
if let Some(var_str) = input.strip_prefix("${") {
|
||||
if var_str.find(':').is_none() {
|
||||
// If the input starts with "${", remove the trailing "}"
|
||||
format!("$env:{}", &var_str[..var_str.len() - 1])
|
||||
} else {
|
||||
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
|
||||
// which will result in the task failing to run in such cases.
|
||||
input.into()
|
||||
}
|
||||
} else if let Some(var_str) = input.strip_prefix('$') {
|
||||
// If the input starts with "$", directly append to "$env:"
|
||||
format!("$env:{}", var_str)
|
||||
} else {
|
||||
// If no prefix is found, return the input as is
|
||||
input.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn to_nushell_variable(input: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut source = input;
|
||||
let mut is_start = true;
|
||||
|
||||
loop {
|
||||
match source.chars().next() {
|
||||
None => return result,
|
||||
Some('$') => {
|
||||
source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
|
||||
is_start = false;
|
||||
}
|
||||
Some(_) => {
|
||||
is_start = false;
|
||||
let chunk_end = source.find('$').unwrap_or(source.len());
|
||||
let (chunk, rest) = source.split_at(chunk_end);
|
||||
result.push_str(chunk);
|
||||
source = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
|
||||
if source.starts_with("env.") {
|
||||
text.push('$');
|
||||
return source;
|
||||
}
|
||||
|
||||
match source.chars().next() {
|
||||
Some('{') => {
|
||||
let source = &source[1..];
|
||||
if let Some(end) = source.find('}') {
|
||||
let var_name = &source[..end];
|
||||
if !var_name.is_empty() {
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end + 1..]
|
||||
} else {
|
||||
text.push_str("${}");
|
||||
&source[end + 1..]
|
||||
}
|
||||
} else {
|
||||
text.push_str("${");
|
||||
source
|
||||
}
|
||||
}
|
||||
Some(c) if c.is_alphabetic() || c == '_' => {
|
||||
let end = source
|
||||
.find(|c: char| !c.is_alphanumeric() && c != '_')
|
||||
.unwrap_or(source.len());
|
||||
let var_name = &source[..end];
|
||||
if !is_start {
|
||||
text.push_str("(");
|
||||
}
|
||||
text.push_str("$env.");
|
||||
text.push_str(var_name);
|
||||
if !is_start {
|
||||
text.push_str(")");
|
||||
}
|
||||
&source[end..]
|
||||
}
|
||||
_ => {
|
||||
text.push('$');
|
||||
source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
|
||||
match self {
|
||||
ShellKind::PowerShell => vec!["-C".to_owned(), combined_command],
|
||||
ShellKind::Cmd => vec!["/C".to_owned(), combined_command],
|
||||
ShellKind::Posix
|
||||
| ShellKind::Nushell
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc
|
||||
| ShellKind::Xonsh => interactive
|
||||
.then(|| "-i".to_owned())
|
||||
.into_iter()
|
||||
.chain(["-c".to_owned(), combined_command])
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn command_prefix(&self) -> Option<char> {
|
||||
match self {
|
||||
ShellKind::PowerShell => Some('&'),
|
||||
ShellKind::Nushell => Some('^'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn sequential_commands_separator(&self) -> char {
|
||||
match self {
|
||||
ShellKind::Cmd => '&',
|
||||
_ => ';',
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
|
||||
shlex::try_quote(arg).ok().map(|arg| match self {
|
||||
// If we are running in PowerShell, we want to take extra care when escaping strings.
|
||||
// In particular, we want to escape strings with a backtick (`) rather than a backslash (\).
|
||||
// TODO double escaping backslashes is not necessary in PowerShell and probably CMD
|
||||
ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"")),
|
||||
_ => arg,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn activate_keyword(&self) -> &'static str {
|
||||
match self {
|
||||
ShellKind::Cmd => "",
|
||||
ShellKind::Nushell => "overlay use",
|
||||
ShellKind::PowerShell => ".",
|
||||
ShellKind::Fish => "source",
|
||||
ShellKind::Csh => "source",
|
||||
ShellKind::Tcsh => "source",
|
||||
ShellKind::Posix | ShellKind::Rc => "source",
|
||||
ShellKind::Xonsh => "source",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn clear_screen_command(&self) -> &'static str {
|
||||
match self {
|
||||
ShellKind::Cmd => "cls",
|
||||
_ => "clear",
|
||||
}
|
||||
}
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::shell::ShellKind;
|
||||
|
||||
pub fn print_env() {
|
||||
let env_vars: HashMap<String, String> = std::env::vars().collect();
|
||||
let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
|
||||
eprintln!("Error serializing environment variables: {}", err);
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!("{}", json);
|
||||
}
|
||||
|
||||
/// Capture all environment variables from the login shell in the given directory.
|
||||
pub async fn capture(
|
||||
shell_path: impl AsRef<Path>,
|
||||
args: &[String],
|
||||
directory: impl AsRef<Path>,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
#[cfg(windows)]
|
||||
return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
|
||||
#[cfg(unix)]
|
||||
return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn capture_unix(
|
||||
shell_path: &Path,
|
||||
args: &[String],
|
||||
directory: &Path,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
let zed_path = super::get_shell_safe_zed_path()?;
|
||||
let shell_kind = ShellKind::new(shell_path, false);
|
||||
|
||||
let mut command_string = String::new();
|
||||
let mut command = std::process::Command::new(shell_path);
|
||||
command.args(args);
|
||||
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
|
||||
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
|
||||
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
|
||||
const FD_STDIN: std::os::fd::RawFd = 0;
|
||||
const FD_STDOUT: std::os::fd::RawFd = 1;
|
||||
const FD_STDERR: std::os::fd::RawFd = 2;
|
||||
|
||||
let (fd_num, redir) = match shell_kind {
|
||||
ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
|
||||
ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
|
||||
// xonsh doesn't support redirecting to stdin, and control sequences are printed to
|
||||
// stdout on startup
|
||||
ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
|
||||
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
|
||||
};
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::piped());
|
||||
|
||||
match shell_kind {
|
||||
ShellKind::Csh | ShellKind::Tcsh => {
|
||||
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
|
||||
command.arg0("-");
|
||||
}
|
||||
ShellKind::Fish => {
|
||||
// in fish, asdf, direnv attach to the `fish_prompt` event
|
||||
command_string.push_str("emit fish_prompt;");
|
||||
command.arg("-l");
|
||||
}
|
||||
_ => {
|
||||
command.arg("-l");
|
||||
}
|
||||
}
|
||||
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
|
||||
command_string.push_str(&format!("cd '{}';", directory.display()));
|
||||
if let Some(prefix) = shell_kind.command_prefix() {
|
||||
command_string.push(prefix);
|
||||
}
|
||||
command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
|
||||
command.args(["-i", "-c", &command_string]);
|
||||
|
||||
super::set_pre_exec_to_start_new_session(&mut command);
|
||||
|
||||
let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?;
|
||||
let env_output = String::from_utf8_lossy(&env_output);
|
||||
|
||||
anyhow::ensure!(
|
||||
process_output.status.success(),
|
||||
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
|
||||
process_output.status,
|
||||
String::from_utf8_lossy(&process_output.stdout),
|
||||
String::from_utf8_lossy(&process_output.stderr),
|
||||
);
|
||||
|
||||
// Parse the JSON output from zed --printenv
|
||||
let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
|
||||
.with_context(|| "Failed to deserialize environment variables from json")?;
|
||||
Ok(env_map)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn spawn_and_read_fd(
|
||||
mut command: std::process::Command,
|
||||
child_fd: std::os::fd::RawFd,
|
||||
) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
|
||||
use command_fds::{CommandFdExt, FdMapping};
|
||||
use std::io::Read;
|
||||
|
||||
let (mut reader, writer) = std::io::pipe()?;
|
||||
|
||||
command.fd_mappings(vec![FdMapping {
|
||||
parent_fd: writer.into(),
|
||||
child_fd,
|
||||
}])?;
|
||||
|
||||
let process = smol::process::Command::from(command).spawn()?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
reader.read_to_end(&mut buffer)?;
|
||||
|
||||
Ok((buffer, process.output().await?))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn capture_windows(
|
||||
shell_path: &Path,
|
||||
_args: &[String],
|
||||
directory: &Path,
|
||||
) -> Result<collections::HashMap<String, String>> {
|
||||
use std::process::Stdio;
|
||||
|
||||
let zed_path =
|
||||
std::env::current_exe().context("Failed to determine current zed executable path.")?;
|
||||
|
||||
let shell_kind = ShellKind::new(shell_path, true);
|
||||
let env_output = match shell_kind {
|
||||
ShellKind::Posix
|
||||
| ShellKind::Csh
|
||||
| ShellKind::Tcsh
|
||||
| ShellKind::Rc
|
||||
| ShellKind::Fish
|
||||
| ShellKind::Xonsh => {
|
||||
return Err(anyhow::anyhow!("unsupported shell kind"));
|
||||
}
|
||||
ShellKind::PowerShell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-NonInteractive",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
&format!(
|
||||
"Set-Location '{}'; & '{}' --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Nushell => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"-c",
|
||||
&format!(
|
||||
"cd '{}'; {}{} --printenv",
|
||||
directory.display(),
|
||||
shell_kind
|
||||
.command_prefix()
|
||||
.map(|prefix| prefix.to_string())
|
||||
.unwrap_or_default(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
ShellKind::Cmd => {
|
||||
let output = crate::command::new_smol_command(shell_path)
|
||||
.args([
|
||||
"/c",
|
||||
&format!(
|
||||
"cd '{}'; {} --printenv",
|
||||
directory.display(),
|
||||
zed_path.display()
|
||||
),
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
output
|
||||
}
|
||||
};
|
||||
|
||||
let env_output = String::from_utf8_lossy(&env_output.stdout);
|
||||
|
||||
// Parse the JSON output from zed --printenv
|
||||
serde_json::from_str(&env_output)
|
||||
.with_context(|| "Failed to deserialize environment variables from json")
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
pub fn format_file_size(size: u64, use_decimal: bool) -> String {
|
||||
if use_decimal {
|
||||
if size < 1000 {
|
||||
format!("{size}B")
|
||||
} else if size < 1000 * 1000 {
|
||||
format!("{:.1}KB", size as f64 / 1000.0)
|
||||
} else {
|
||||
format!("{:.1}MB", size as f64 / (1000.0 * 1000.0))
|
||||
}
|
||||
} else if size < 1024 {
|
||||
format!("{size}B")
|
||||
} else if size < 1024 * 1024 {
|
||||
format!("{:.1}KiB", size as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_file_size_decimal() {
|
||||
assert_eq!(format_file_size(0, true), "0B");
|
||||
assert_eq!(format_file_size(999, true), "999B");
|
||||
assert_eq!(format_file_size(1000, true), "1.0KB");
|
||||
assert_eq!(format_file_size(1500, true), "1.5KB");
|
||||
assert_eq!(format_file_size(999999, true), "1000.0KB");
|
||||
assert_eq!(format_file_size(1000000, true), "1.0MB");
|
||||
assert_eq!(format_file_size(1500000, true), "1.5MB");
|
||||
assert_eq!(format_file_size(10000000, true), "10.0MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_file_size_binary() {
|
||||
assert_eq!(format_file_size(0, false), "0B");
|
||||
assert_eq!(format_file_size(1023, false), "1023B");
|
||||
assert_eq!(format_file_size(1024, false), "1.0KiB");
|
||||
assert_eq!(format_file_size(1536, false), "1.5KiB");
|
||||
assert_eq!(format_file_size(1048575, false), "1024.0KiB");
|
||||
assert_eq!(format_file_size(1048576, false), "1.0MiB");
|
||||
assert_eq!(format_file_size(1572864, false), "1.5MiB");
|
||||
assert_eq!(format_file_size(10485760, false), "10.0MiB");
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
mod assertions;
|
||||
mod marked_text;
|
||||
|
||||
use git2;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
pub use assertions::*;
|
||||
pub use marked_text::*;
|
||||
|
||||
pub struct TempTree {
|
||||
_temp_dir: TempDir,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempTree {
|
||||
pub fn new(tree: serde_json::Value) -> Self {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = std::fs::canonicalize(dir.path()).unwrap();
|
||||
write_tree(path.as_path(), tree);
|
||||
|
||||
Self {
|
||||
_temp_dir: dir,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
fn write_tree(path: &Path, tree: serde_json::Value) {
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
|
||||
if let Value::Object(map) = tree {
|
||||
for (name, contents) in map {
|
||||
let mut path = PathBuf::from(path);
|
||||
path.push(name);
|
||||
match contents {
|
||||
Value::Object(_) => {
|
||||
fs::create_dir(&path).unwrap();
|
||||
|
||||
if path.file_name() == Some(OsStr::new(".git")) {
|
||||
git2::Repository::init(path.parent().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
write_tree(&path, contents);
|
||||
}
|
||||
Value::Null => {
|
||||
fs::create_dir(&path).unwrap();
|
||||
}
|
||||
Value::String(contents) => {
|
||||
fs::write(&path, contents).unwrap();
|
||||
}
|
||||
_ => {
|
||||
panic!("JSON object must contain only objects, strings, or null");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("You must pass a JSON object to this helper")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
|
||||
let mut text = String::new();
|
||||
for row in 0..rows {
|
||||
let c: char = (start_char as u32 + row as u32) as u8 as char;
|
||||
let mut line = c.to_string().repeat(cols);
|
||||
if row < rows - 1 {
|
||||
line.push('\n');
|
||||
}
|
||||
text += &line;
|
||||
}
|
||||
text
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
pub enum SetEqError<T> {
|
||||
LeftMissing(T),
|
||||
RightMissing(T),
|
||||
}
|
||||
|
||||
impl<T> SetEqError<T> {
|
||||
pub fn map<R, F: FnOnce(T) -> R>(self, update: F) -> SetEqError<R> {
|
||||
match self {
|
||||
SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)),
|
||||
SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! set_eq {
|
||||
($left:expr,$right:expr) => {{
|
||||
use util::test::*;
|
||||
|
||||
let left = $left;
|
||||
let right = $right;
|
||||
|
||||
let mut result = Ok(());
|
||||
for right_value in right.iter() {
|
||||
if !left.contains(right_value) {
|
||||
result = Err(SetEqError::LeftMissing(right_value.clone()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if result.is_ok() {
|
||||
for left_value in left.iter() {
|
||||
if !right.contains(left_value) {
|
||||
result = Err(SetEqError::RightMissing(left_value.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_set_eq {
|
||||
($left:expr,$right:expr) => {{
|
||||
use util::test::*;
|
||||
use util::set_eq;
|
||||
|
||||
let left = $left;
|
||||
let right = $right;
|
||||
|
||||
match set_eq!(&left, &right) {
|
||||
Err(SetEqError::LeftMissing(missing)) => {
|
||||
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing);
|
||||
},
|
||||
Err(SetEqError::RightMissing(missing)) => {
|
||||
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}};
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use collections::HashMap;
|
||||
use std::{cmp::Ordering, ops::Range};
|
||||
|
||||
/// Construct a string and a list of offsets within that string using a single
|
||||
/// string containing embedded position markers.
|
||||
pub fn marked_text_offsets_by(
|
||||
marked_text: &str,
|
||||
markers: Vec<char>,
|
||||
) -> (String, HashMap<char, Vec<usize>>) {
|
||||
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
|
||||
let mut unmarked_text = String::new();
|
||||
|
||||
for char in marked_text.chars() {
|
||||
if markers.contains(&char) {
|
||||
let char_offsets = extracted_markers.entry(char).or_default();
|
||||
char_offsets.push(unmarked_text.len());
|
||||
} else {
|
||||
unmarked_text.push(char);
|
||||
}
|
||||
}
|
||||
|
||||
(unmarked_text, extracted_markers)
|
||||
}
|
||||
|
||||
/// Construct a string and a list of ranges within that string using a single
|
||||
/// string containing embedded range markers, using arbitrary characters as
|
||||
/// range markers. By using multiple different range markers, you can construct
|
||||
/// ranges that overlap each other.
|
||||
///
|
||||
/// The returned ranges will be grouped by their range marking characters.
|
||||
pub fn marked_text_ranges_by(
|
||||
marked_text: &str,
|
||||
markers: Vec<TextRangeMarker>,
|
||||
) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
|
||||
let all_markers = markers.iter().flat_map(|m| m.markers()).collect();
|
||||
|
||||
let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers);
|
||||
let range_lookup = markers
|
||||
.into_iter()
|
||||
.map(|marker| {
|
||||
(
|
||||
marker.clone(),
|
||||
match marker {
|
||||
TextRangeMarker::Empty(empty_marker_char) => marker_offsets
|
||||
.remove(&empty_marker_char)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|empty_index| empty_index..empty_index)
|
||||
.collect::<Vec<Range<usize>>>(),
|
||||
TextRangeMarker::Range(start_marker, end_marker) => {
|
||||
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
|
||||
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
|
||||
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
|
||||
starts
|
||||
.into_iter()
|
||||
.zip(ends)
|
||||
.map(|(start, end)| {
|
||||
assert!(end >= start, "marked ranges must be disjoint");
|
||||
start..end
|
||||
})
|
||||
.collect::<Vec<Range<usize>>>()
|
||||
}
|
||||
TextRangeMarker::ReverseRange(start_marker, end_marker) => {
|
||||
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
|
||||
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
|
||||
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
|
||||
starts
|
||||
.into_iter()
|
||||
.zip(ends)
|
||||
.map(|(start, end)| {
|
||||
assert!(end >= start, "marked ranges must be disjoint");
|
||||
end..start
|
||||
})
|
||||
.collect::<Vec<Range<usize>>>()
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
(unmarked_text, range_lookup)
|
||||
}
|
||||
|
||||
/// Construct a string and a list of ranges within that string using a single
|
||||
/// string containing embedded range markers. The characters used to mark the
|
||||
/// ranges are as follows:
|
||||
///
|
||||
/// 1. To mark a range of text, surround it with the `«` and `»` angle brackets,
|
||||
/// which can be typed on a US keyboard with the `alt-|` and `alt-shift-|` keys.
|
||||
///
|
||||
/// ```text
|
||||
/// foo «selected text» bar
|
||||
/// ```
|
||||
///
|
||||
/// 2. To mark a single position in the text, use the `ˇ` caron,
|
||||
/// which can be typed on a US keyboard with the `alt-shift-t` key.
|
||||
///
|
||||
/// ```text
|
||||
/// the cursors are hereˇ and hereˇ.
|
||||
/// ```
|
||||
///
|
||||
/// 3. To mark a range whose direction is meaningful (like a selection),
|
||||
/// put a caron character beside one of its bounds, on the inside:
|
||||
///
|
||||
/// ```text
|
||||
/// one «ˇreversed» selection and one «forwardˇ» selection
|
||||
/// ```
|
||||
///
|
||||
/// Any • characters in the input string will be replaced with spaces. This makes
|
||||
/// it easier to test cases with trailing spaces, which tend to get trimmed from the
|
||||
/// source code.
|
||||
#[track_caller]
|
||||
pub fn marked_text_ranges(
|
||||
marked_text: &str,
|
||||
ranges_are_directed: bool,
|
||||
) -> (String, Vec<Range<usize>>) {
|
||||
let mut unmarked_text = String::with_capacity(marked_text.len());
|
||||
let mut ranges = Vec::new();
|
||||
let mut prev_marked_ix = 0;
|
||||
let mut current_range_start = None;
|
||||
let mut current_range_cursor = None;
|
||||
|
||||
let marked_text = marked_text.replace('•', " ");
|
||||
for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) {
|
||||
unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]);
|
||||
let unmarked_len = unmarked_text.len();
|
||||
let len = marker.len();
|
||||
prev_marked_ix = marked_ix + len;
|
||||
|
||||
match marker {
|
||||
"ˇ" => {
|
||||
if current_range_start.is_some() {
|
||||
if current_range_cursor.is_some() {
|
||||
panic!("duplicate point marker 'ˇ' at index {marked_ix}");
|
||||
}
|
||||
|
||||
current_range_cursor = Some(unmarked_len);
|
||||
} else {
|
||||
ranges.push(unmarked_len..unmarked_len);
|
||||
}
|
||||
}
|
||||
"«" => {
|
||||
if current_range_start.is_some() {
|
||||
panic!("unexpected range start marker '«' at index {marked_ix}");
|
||||
}
|
||||
current_range_start = Some(unmarked_len);
|
||||
}
|
||||
"»" => {
|
||||
let current_range_start = if let Some(start) = current_range_start.take() {
|
||||
start
|
||||
} else {
|
||||
panic!("unexpected range end marker '»' at index {marked_ix}");
|
||||
};
|
||||
|
||||
let mut reversed = false;
|
||||
if let Some(current_range_cursor) = current_range_cursor.take() {
|
||||
if current_range_cursor == current_range_start {
|
||||
reversed = true;
|
||||
} else if current_range_cursor != unmarked_len {
|
||||
panic!("unexpected 'ˇ' marker in the middle of a range");
|
||||
}
|
||||
} else if ranges_are_directed {
|
||||
panic!("missing 'ˇ' marker to indicate range direction");
|
||||
}
|
||||
|
||||
ranges.push(if reversed {
|
||||
unmarked_len..current_range_start
|
||||
} else {
|
||||
current_range_start..unmarked_len
|
||||
});
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
unmarked_text.push_str(&marked_text[prev_marked_ix..]);
|
||||
(unmarked_text, ranges)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn marked_text_offsets(marked_text: &str) -> (String, Vec<usize>) {
|
||||
let (text, ranges) = marked_text_ranges(marked_text, false);
|
||||
(
|
||||
text,
|
||||
ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
assert_eq!(range.start, range.end);
|
||||
range.start
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn generate_marked_text(
|
||||
unmarked_text: &str,
|
||||
ranges: &[Range<usize>],
|
||||
indicate_cursors: bool,
|
||||
) -> String {
|
||||
let mut marked_text = unmarked_text.to_string();
|
||||
for range in ranges.iter().rev() {
|
||||
if indicate_cursors {
|
||||
match range.start.cmp(&range.end) {
|
||||
Ordering::Less => {
|
||||
marked_text.insert_str(range.end, "ˇ»");
|
||||
marked_text.insert(range.start, '«');
|
||||
}
|
||||
Ordering::Equal => {
|
||||
marked_text.insert(range.start, 'ˇ');
|
||||
}
|
||||
Ordering::Greater => {
|
||||
marked_text.insert(range.start, '»');
|
||||
marked_text.insert_str(range.end, "«ˇ");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match range.start.cmp(&range.end) {
|
||||
Ordering::Equal => {
|
||||
marked_text.insert(range.start, 'ˇ');
|
||||
}
|
||||
_ => {
|
||||
marked_text.insert(range.end, '»');
|
||||
marked_text.insert(range.start, '«');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
marked_text
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum TextRangeMarker {
|
||||
Empty(char),
|
||||
Range(char, char),
|
||||
ReverseRange(char, char),
|
||||
}
|
||||
|
||||
impl TextRangeMarker {
|
||||
fn markers(&self) -> Vec<char> {
|
||||
match self {
|
||||
Self::Empty(m) => vec![*m],
|
||||
Self::Range(l, r) => vec![*l, *r],
|
||||
Self::ReverseRange(l, r) => vec![*l, *r],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for TextRangeMarker {
|
||||
fn from(marker: char) -> Self {
|
||||
Self::Empty(marker)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(char, char)> for TextRangeMarker {
|
||||
fn from((left_marker, right_marker): (char, char)) -> Self {
|
||||
Self::Range(left_marker, right_marker)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{generate_marked_text, marked_text_ranges};
|
||||
|
||||
#[allow(clippy::reversed_empty_ranges)]
|
||||
#[test]
|
||||
fn test_marked_text() {
|
||||
let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true);
|
||||
|
||||
assert_eq!(text, "one two three four five six");
|
||||
assert_eq!(ranges.len(), 4);
|
||||
assert_eq!(ranges[0], 7..4);
|
||||
assert_eq!(ranges[1], 8..13);
|
||||
assert_eq!(ranges[2], 18..14);
|
||||
assert_eq!(ranges[3], 23..23);
|
||||
|
||||
assert_eq!(
|
||||
generate_marked_text(&text, &ranges, true),
|
||||
"one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six"
|
||||
);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn duration_alt_display(duration: Duration) -> String {
|
||||
if duration < Duration::from_secs(60) {
|
||||
format!("{}s", duration.as_secs())
|
||||
} else {
|
||||
duration_clock_format(duration)
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_clock_format(duration: Duration) -> String {
|
||||
let hours = duration.as_secs() / 3600;
|
||||
let minutes = (duration.as_secs() % 3600) / 60;
|
||||
let seconds = duration.as_secs() % 60;
|
||||
|
||||
if hours > 0 {
|
||||
format!("{hours}:{minutes:02}:{seconds:02}")
|
||||
} else if minutes > 0 {
|
||||
format!("{minutes}:{seconds:02}")
|
||||
} else {
|
||||
format!("{seconds}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_duration_to_clock_format() {
|
||||
use duration_clock_format as f;
|
||||
assert_eq!("0", f(Duration::from_secs(0)));
|
||||
assert_eq!("59", f(Duration::from_secs(59)));
|
||||
assert_eq!("1:00", f(Duration::from_secs(60)));
|
||||
assert_eq!("10:00", f(Duration::from_secs(600)));
|
||||
assert_eq!("1:00:00", f(Duration::from_secs(3600)));
|
||||
assert_eq!("3:02:01", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1)));
|
||||
assert_eq!("23:59:59", f(Duration::from_secs(3600 * 24 - 1)));
|
||||
assert_eq!("100:00:00", f(Duration::from_secs(3600 * 100)));
|
||||
}
|
||||
}
|
||||
+1310
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# Local copy keeps the registry manifest shape and narrows log features so
|
||||
# GPUI and Servo can share the same workspace dependency graph.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "zed-sum-tree"
|
||||
version = "0.2.0"
|
||||
build = false
|
||||
publish = true
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A sum tree data structure, a concurrency-friendly B-tree"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "zed_sum_tree"
|
||||
path = "src/sum_tree.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies.arrayvec]
|
||||
version = "0.7.1"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
|
||||
[dependencies.rayon]
|
||||
version = "1.8"
|
||||
|
||||
[dependencies.workspace-hack]
|
||||
version = "0.1.0"
|
||||
|
||||
[dev-dependencies.ctor]
|
||||
version = "0.4.0"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[lints.clippy]
|
||||
dbg_macro = "deny"
|
||||
declare_interior_mutable_const = "deny"
|
||||
disallowed_methods = "deny"
|
||||
large_enum_variant = "allow"
|
||||
let_underscore_future = "allow"
|
||||
nonminimal_bool = "allow"
|
||||
redundant_clone = "deny"
|
||||
single_range_in_vec_init = "allow"
|
||||
todo = "deny"
|
||||
too_many_arguments = "allow"
|
||||
type_complexity = "allow"
|
||||
|
||||
[lints.clippy.style]
|
||||
level = "allow"
|
||||
priority = -1
|
||||
|
||||
[lints.rust.unexpected_cfgs]
|
||||
level = "allow"
|
||||
priority = 0
|
||||
@@ -0,0 +1,222 @@
|
||||
Copyright 2022 - 2025 Zed Industries, Inc.
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
+817
@@ -0,0 +1,817 @@
|
||||
use super::*;
|
||||
use arrayvec::ArrayVec;
|
||||
use std::{cmp::Ordering, mem, sync::Arc};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct StackEntry<'a, T: Item, D> {
|
||||
tree: &'a SumTree<T>,
|
||||
index: u32,
|
||||
position: D,
|
||||
}
|
||||
|
||||
impl<'a, T: Item, D> StackEntry<'a, T, D> {
|
||||
#[inline]
|
||||
fn index(&self) -> usize {
|
||||
self.index as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for StackEntry<'_, T, D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("StackEntry")
|
||||
.field("index", &self.index)
|
||||
.field("position", &self.position)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Cursor<'a, 'b, T: Item, D> {
|
||||
tree: &'a SumTree<T>,
|
||||
stack: ArrayVec<StackEntry<'a, T, D>, 16>,
|
||||
position: D,
|
||||
did_seek: bool,
|
||||
at_end: bool,
|
||||
cx: <T::Summary as Summary>::Context<'b>,
|
||||
}
|
||||
|
||||
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for Cursor<'_, '_, T, D>
|
||||
where
|
||||
T::Summary: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Cursor")
|
||||
.field("tree", &self.tree)
|
||||
.field("stack", &self.stack)
|
||||
.field("position", &self.position)
|
||||
.field("did_seek", &self.did_seek)
|
||||
.field("at_end", &self.at_end)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Iter<'a, T: Item> {
|
||||
tree: &'a SumTree<T>,
|
||||
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
|
||||
where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
pub fn new(tree: &'a SumTree<T>, cx: <T::Summary as Summary>::Context<'b>) -> Self {
|
||||
Self {
|
||||
tree,
|
||||
stack: ArrayVec::new(),
|
||||
position: D::zero(cx),
|
||||
did_seek: false,
|
||||
at_end: tree.is_empty(),
|
||||
cx,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.did_seek = false;
|
||||
self.at_end = self.tree.is_empty();
|
||||
self.stack.truncate(0);
|
||||
self.position = D::zero(self.cx);
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &D {
|
||||
&self.position
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn end(&self) -> D {
|
||||
if let Some(item_summary) = self.item_summary() {
|
||||
let mut end = self.start().clone();
|
||||
end.add_summary(item_summary, self.cx);
|
||||
end
|
||||
} else {
|
||||
self.start().clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Item is None, when the list is empty, or this cursor is at the end of the list.
|
||||
#[track_caller]
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => {
|
||||
if entry.index() == items.len() {
|
||||
None
|
||||
} else {
|
||||
Some(&items[entry.index()])
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn item_summary(&self) -> Option<&'a T::Summary> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf {
|
||||
ref item_summaries, ..
|
||||
} => {
|
||||
if entry.index() == item_summaries.len() {
|
||||
None
|
||||
} else {
|
||||
Some(&item_summaries[entry.index()])
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
if entry.index() == entry.tree.0.items().len() - 1 {
|
||||
if let Some(next_leaf) = self.next_leaf() {
|
||||
Some(next_leaf.0.items().first().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => Some(&items[entry.index() + 1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else if self.at_end {
|
||||
None
|
||||
} else {
|
||||
self.tree.first()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn next_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index() < entry.tree.0.child_trees().len() - 1 {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_trees, ..
|
||||
} => return Some(child_trees[entry.index() + 1].leftmost_leaf()),
|
||||
Node::Leaf { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
if entry.index() == 0 {
|
||||
if let Some(prev_leaf) = self.prev_leaf() {
|
||||
Some(prev_leaf.0.items().last().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else if self.at_end {
|
||||
self.tree.last()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn prev_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index() != 0 {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_trees, ..
|
||||
} => return Some(child_trees[entry.index() - 1].rightmost_leaf()),
|
||||
Node::Leaf { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn prev(&mut self) {
|
||||
self.search_backward(|_| true)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn search_backward<F>(&mut self, mut filter_node: F)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
{
|
||||
if !self.did_seek {
|
||||
self.did_seek = true;
|
||||
self.at_end = true;
|
||||
}
|
||||
|
||||
if self.at_end {
|
||||
self.position = D::zero(self.cx);
|
||||
self.at_end = self.tree.is_empty();
|
||||
if !self.tree.is_empty() {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: self.tree.0.child_summaries().len() as u32,
|
||||
position: D::from_summary(self.tree.summary(), self.cx),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut descending = false;
|
||||
while !self.stack.is_empty() {
|
||||
if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) {
|
||||
self.position = position.clone();
|
||||
} else {
|
||||
self.position = D::zero(self.cx);
|
||||
}
|
||||
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
if !descending {
|
||||
if entry.index() == 0 {
|
||||
self.stack.pop();
|
||||
continue;
|
||||
} else {
|
||||
entry.index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
for summary in &entry.tree.0.child_summaries()[..entry.index()] {
|
||||
self.position.add_summary(summary, self.cx);
|
||||
}
|
||||
entry.position = self.position.clone();
|
||||
|
||||
descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]);
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal { child_trees, .. } => {
|
||||
if descending {
|
||||
let tree = &child_trees[entry.index()];
|
||||
self.stack.push(StackEntry {
|
||||
position: D::zero(self.cx),
|
||||
tree,
|
||||
index: tree.0.child_summaries().len() as u32 - 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
Node::Leaf { .. } => {
|
||||
if descending {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn next(&mut self) {
|
||||
self.search_forward(|_| true)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn search_forward<F>(&mut self, mut filter_node: F)
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
{
|
||||
let mut descend = false;
|
||||
|
||||
if self.stack.is_empty() {
|
||||
if !self.at_end {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: D::zero(self.cx),
|
||||
});
|
||||
descend = true;
|
||||
}
|
||||
self.did_seek = true;
|
||||
}
|
||||
|
||||
while !self.stack.is_empty() {
|
||||
let new_subtree = {
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal {
|
||||
child_trees,
|
||||
child_summaries,
|
||||
..
|
||||
} => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
}
|
||||
|
||||
while entry.index() < child_summaries.len() {
|
||||
let next_summary = &child_summaries[entry.index()];
|
||||
if filter_node(next_summary) {
|
||||
break;
|
||||
} else {
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(next_summary, self.cx);
|
||||
self.position.add_summary(next_summary, self.cx);
|
||||
}
|
||||
}
|
||||
|
||||
child_trees.get(entry.index())
|
||||
}
|
||||
Node::Leaf { item_summaries, .. } => {
|
||||
if !descend {
|
||||
let item_summary = &item_summaries[entry.index()];
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(item_summary, self.cx);
|
||||
self.position.add_summary(item_summary, self.cx);
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Some(next_item_summary) = item_summaries.get(entry.index()) {
|
||||
if filter_node(next_item_summary) {
|
||||
return;
|
||||
} else {
|
||||
entry.index += 1;
|
||||
entry.position.add_summary(next_item_summary, self.cx);
|
||||
self.position.add_summary(next_item_summary, self.cx);
|
||||
}
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(subtree) = new_subtree {
|
||||
descend = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: subtree,
|
||||
index: 0,
|
||||
position: self.position.clone(),
|
||||
});
|
||||
} else {
|
||||
descend = false;
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
self.at_end = self.stack.is_empty();
|
||||
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_did_seek(&self) {
|
||||
assert!(
|
||||
self.did_seek,
|
||||
"Must call `seek`, `next` or `prev` before calling this method"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
|
||||
where
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
#[track_caller]
|
||||
pub fn seek<Target>(&mut self, pos: &Target, bias: Bias) -> bool
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
self.reset();
|
||||
self.seek_internal(pos, bias, &mut ())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn seek_forward<Target>(&mut self, pos: &Target, bias: Bias) -> bool
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
self.seek_internal(pos, bias, &mut ())
|
||||
}
|
||||
|
||||
/// Advances the cursor and returns traversed items as a tree.
|
||||
#[track_caller]
|
||||
pub fn slice<Target>(&mut self, end: &Target, bias: Bias) -> SumTree<T>
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
{
|
||||
let mut slice = SliceSeekAggregate {
|
||||
tree: SumTree::new(self.cx),
|
||||
leaf_items: ArrayVec::new(),
|
||||
leaf_item_summaries: ArrayVec::new(),
|
||||
leaf_summary: <T::Summary as Summary>::zero(self.cx),
|
||||
};
|
||||
self.seek_internal(end, bias, &mut slice);
|
||||
slice.tree
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn suffix(&mut self) -> SumTree<T> {
|
||||
self.slice(&End::new(), Bias::Right)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn summary<Target, Output>(&mut self, end: &Target, bias: Bias) -> Output
|
||||
where
|
||||
Target: SeekTarget<'a, T::Summary, D>,
|
||||
Output: Dimension<'a, T::Summary>,
|
||||
{
|
||||
let mut summary = SummarySeekAggregate(Output::zero(self.cx));
|
||||
self.seek_internal(end, bias, &mut summary);
|
||||
summary.0
|
||||
}
|
||||
|
||||
/// Returns whether we found the item you were seeking for
|
||||
#[track_caller]
|
||||
fn seek_internal(
|
||||
&mut self,
|
||||
target: &dyn SeekTarget<'a, T::Summary, D>,
|
||||
bias: Bias,
|
||||
aggregate: &mut dyn SeekAggregate<'a, T>,
|
||||
) -> bool {
|
||||
assert!(
|
||||
target.cmp(&self.position, self.cx) >= Ordering::Equal,
|
||||
"cannot seek backward",
|
||||
);
|
||||
|
||||
if !self.did_seek {
|
||||
self.did_seek = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: D::zero(self.cx),
|
||||
});
|
||||
}
|
||||
|
||||
let mut ascending = false;
|
||||
'outer: while let Some(entry) = self.stack.last_mut() {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_summaries,
|
||||
ref child_trees,
|
||||
..
|
||||
} => {
|
||||
if ascending {
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
}
|
||||
|
||||
for (child_tree, child_summary) in child_trees[entry.index()..]
|
||||
.iter()
|
||||
.zip(&child_summaries[entry.index()..])
|
||||
{
|
||||
let mut child_end = self.position.clone();
|
||||
child_end.add_summary(child_summary, self.cx);
|
||||
|
||||
let comparison = target.cmp(&child_end, self.cx);
|
||||
if comparison == Ordering::Greater
|
||||
|| (comparison == Ordering::Equal && bias == Bias::Right)
|
||||
{
|
||||
self.position = child_end;
|
||||
aggregate.push_tree(child_tree, child_summary, self.cx);
|
||||
entry.index += 1;
|
||||
entry.position = self.position.clone();
|
||||
} else {
|
||||
self.stack.push(StackEntry {
|
||||
tree: child_tree,
|
||||
index: 0,
|
||||
position: self.position.clone(),
|
||||
});
|
||||
ascending = false;
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::Leaf {
|
||||
ref items,
|
||||
ref item_summaries,
|
||||
..
|
||||
} => {
|
||||
aggregate.begin_leaf();
|
||||
|
||||
for (item, item_summary) in items[entry.index()..]
|
||||
.iter()
|
||||
.zip(&item_summaries[entry.index()..])
|
||||
{
|
||||
let mut child_end = self.position.clone();
|
||||
child_end.add_summary(item_summary, self.cx);
|
||||
|
||||
let comparison = target.cmp(&child_end, self.cx);
|
||||
if comparison == Ordering::Greater
|
||||
|| (comparison == Ordering::Equal && bias == Bias::Right)
|
||||
{
|
||||
self.position = child_end;
|
||||
aggregate.push_item(item, item_summary, self.cx);
|
||||
entry.index += 1;
|
||||
} else {
|
||||
aggregate.end_leaf(self.cx);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
aggregate.end_leaf(self.cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.stack.pop();
|
||||
ascending = true;
|
||||
}
|
||||
|
||||
self.at_end = self.stack.is_empty();
|
||||
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
|
||||
|
||||
let mut end = self.position.clone();
|
||||
if bias == Bias::Left
|
||||
&& let Some(summary) = self.item_summary()
|
||||
{
|
||||
end.add_summary(summary, self.cx);
|
||||
}
|
||||
|
||||
target.cmp(&end, self.cx) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item> Iter<'a, T> {
|
||||
pub(crate) fn new(tree: &'a SumTree<T>) -> Self {
|
||||
Self {
|
||||
tree,
|
||||
stack: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item> Iterator for Iter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut descend = false;
|
||||
|
||||
if self.stack.is_empty() {
|
||||
self.stack.push(StackEntry {
|
||||
tree: self.tree,
|
||||
index: 0,
|
||||
position: (),
|
||||
});
|
||||
descend = true;
|
||||
}
|
||||
|
||||
while !self.stack.is_empty() {
|
||||
let new_subtree = {
|
||||
let entry = self.stack.last_mut().unwrap();
|
||||
match entry.tree.0.as_ref() {
|
||||
Node::Internal { child_trees, .. } => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
}
|
||||
child_trees.get(entry.index())
|
||||
}
|
||||
Node::Leaf { items, .. } => {
|
||||
if !descend {
|
||||
entry.index += 1;
|
||||
}
|
||||
|
||||
if let Some(next_item) = items.get(entry.index()) {
|
||||
return Some(next_item);
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(subtree) = new_subtree {
|
||||
descend = true;
|
||||
self.stack.push(StackEntry {
|
||||
tree: subtree,
|
||||
index: 0,
|
||||
position: (),
|
||||
});
|
||||
} else {
|
||||
descend = false;
|
||||
self.stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, T: Item, D> Iterator for Cursor<'a, 'b, T, D>
|
||||
where
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.did_seek {
|
||||
self.next();
|
||||
}
|
||||
|
||||
if let Some(item) = self.item() {
|
||||
self.next();
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilterCursor<'a, 'b, F, T: Item, D> {
|
||||
cursor: Cursor<'a, 'b, T, D>,
|
||||
filter_node: F,
|
||||
}
|
||||
|
||||
impl<'a, 'b, F, T: Item, D> FilterCursor<'a, 'b, F, T, D>
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
T: Item,
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
pub fn new(
|
||||
tree: &'a SumTree<T>,
|
||||
cx: <T::Summary as Summary>::Context<'b>,
|
||||
filter_node: F,
|
||||
) -> Self {
|
||||
let cursor = tree.cursor::<D>(cx);
|
||||
Self {
|
||||
cursor,
|
||||
filter_node,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &D {
|
||||
self.cursor.start()
|
||||
}
|
||||
|
||||
pub fn end(&self) -> D {
|
||||
self.cursor.end()
|
||||
}
|
||||
|
||||
pub fn item(&self) -> Option<&'a T> {
|
||||
self.cursor.item()
|
||||
}
|
||||
|
||||
pub fn item_summary(&self) -> Option<&'a T::Summary> {
|
||||
self.cursor.item_summary()
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.cursor.search_forward(&mut self.filter_node);
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) {
|
||||
self.cursor.search_backward(&mut self.filter_node);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, F, T: Item, U> Iterator for FilterCursor<'a, 'b, F, T, U>
|
||||
where
|
||||
F: FnMut(&T::Summary) -> bool,
|
||||
U: Dimension<'a, T::Summary>,
|
||||
{
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.cursor.did_seek {
|
||||
self.next();
|
||||
}
|
||||
|
||||
if let Some(item) = self.item() {
|
||||
self.cursor.search_forward(&mut self.filter_node);
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait SeekAggregate<'a, T: Item> {
|
||||
fn begin_leaf(&mut self);
|
||||
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>);
|
||||
fn push_item(
|
||||
&mut self,
|
||||
item: &'a T,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
);
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
tree: &'a SumTree<T>,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
);
|
||||
}
|
||||
|
||||
struct SliceSeekAggregate<T: Item> {
|
||||
tree: SumTree<T>,
|
||||
leaf_items: ArrayVec<T, { 2 * TREE_BASE }>,
|
||||
leaf_item_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>,
|
||||
leaf_summary: T::Summary,
|
||||
}
|
||||
|
||||
struct SummarySeekAggregate<D>(D);
|
||||
|
||||
impl<T: Item> SeekAggregate<'_, T> for () {
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_item(&mut self, _: &T, _: &T::Summary, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
_: &SumTree<T>,
|
||||
_: &T::Summary,
|
||||
_: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> SeekAggregate<'_, T> for SliceSeekAggregate<T> {
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>) {
|
||||
self.tree.append(
|
||||
SumTree(Arc::new(Node::Leaf {
|
||||
summary: mem::replace(&mut self.leaf_summary, <T::Summary as Summary>::zero(cx)),
|
||||
items: mem::take(&mut self.leaf_items),
|
||||
item_summaries: mem::take(&mut self.leaf_item_summaries),
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
fn push_item(
|
||||
&mut self,
|
||||
item: &T,
|
||||
summary: &T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.leaf_items.push(item.clone());
|
||||
self.leaf_item_summaries.push(summary.clone());
|
||||
Summary::add_summary(&mut self.leaf_summary, summary, cx);
|
||||
}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
tree: &SumTree<T>,
|
||||
_: &T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.tree.append(tree.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate<D>
|
||||
where
|
||||
D: Dimension<'a, T::Summary>,
|
||||
{
|
||||
fn begin_leaf(&mut self) {}
|
||||
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
|
||||
fn push_item(
|
||||
&mut self,
|
||||
_: &T,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.0.add_summary(summary, cx);
|
||||
}
|
||||
fn push_tree(
|
||||
&mut self,
|
||||
_: &SumTree<T>,
|
||||
summary: &'a T::Summary,
|
||||
cx: <T::Summary as Summary>::Context<'_>,
|
||||
) {
|
||||
self.0.add_summary(summary, cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct End<D>(PhantomData<D>);
|
||||
|
||||
impl<D> End<D> {
|
||||
fn new() -> Self {
|
||||
Self(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End<D> {
|
||||
fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering {
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> fmt::Debug for End<D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("End").finish()
|
||||
}
|
||||
}
|
||||
+1444
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,520 @@
|
||||
use std::{cmp::Ordering, fmt::Debug};
|
||||
|
||||
use crate::{Bias, ContextLessSummary, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree};
|
||||
|
||||
/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree).
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MapEntry<K, V> {
|
||||
key: K,
|
||||
value: V,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct MapKey<K>(Option<K>);
|
||||
|
||||
impl<K> Default for MapKey<K> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapKeyRef<'a, K>(Option<&'a K>);
|
||||
|
||||
impl<K> Default for MapKeyRef<'_, K> {
|
||||
fn default() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TreeSet<K>(TreeMap<K, ()>)
|
||||
where
|
||||
K: Clone + Ord;
|
||||
|
||||
impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = (K, V)>) -> Self {
|
||||
let tree = SumTree::from_iter(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|(key, value)| MapEntry { key, value }),
|
||||
(),
|
||||
);
|
||||
Self(tree)
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &K) -> Option<&V> {
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
cursor.seek(&MapKeyRef(Some(key)), Bias::Left);
|
||||
if let Some(item) = cursor.item() {
|
||||
if Some(key) == item.key().0.as_ref() {
|
||||
Some(&item.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K, value: V) {
|
||||
self.0.insert_or_replace(MapEntry { key, value }, ());
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = (K, V)>) {
|
||||
let edits: Vec<_> = iter
|
||||
.into_iter()
|
||||
.map(|(key, value)| Edit::Insert(MapEntry { key, value }))
|
||||
.collect();
|
||||
self.0.edit(edits, ());
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.0 = SumTree::default();
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||
let mut removed = None;
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
let mut new_tree = cursor.slice(&key, Bias::Left);
|
||||
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
|
||||
removed = Some(cursor.item().unwrap().value.clone());
|
||||
cursor.next();
|
||||
}
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
removed
|
||||
}
|
||||
|
||||
pub fn remove_range(&mut self, start: &impl MapSeekTarget<K>, end: &impl MapSeekTarget<K>) {
|
||||
let start = MapSeekTargetAdaptor(start);
|
||||
let end = MapSeekTargetAdaptor(end);
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let mut new_tree = cursor.slice(&start, Bias::Left);
|
||||
cursor.seek(&end, Bias::Left);
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
}
|
||||
|
||||
/// Returns the key-value pair with the greatest key less than or equal to the given key.
|
||||
pub fn closest(&self, key: &K) -> Option<(&K, &V)> {
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
cursor.seek(&key, Bias::Right);
|
||||
cursor.prev();
|
||||
cursor.item().map(|item| (&item.key, &item.value))
|
||||
}
|
||||
|
||||
pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator<Item = (&'a K, &'a V)> + 'a {
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let from_key = MapKeyRef(Some(from));
|
||||
cursor.seek(&from_key, Bias::Left);
|
||||
|
||||
cursor.map(|map_entry| (&map_entry.key, &map_entry.value))
|
||||
}
|
||||
|
||||
pub fn update<F, T>(&mut self, key: &K, f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(&mut V) -> T,
|
||||
{
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
let key = MapKeyRef(Some(key));
|
||||
let mut new_tree = cursor.slice(&key, Bias::Left);
|
||||
let mut result = None;
|
||||
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
|
||||
let mut updated = cursor.item().unwrap().clone();
|
||||
result = Some(f(&mut updated.value));
|
||||
new_tree.push(updated, ());
|
||||
cursor.next();
|
||||
}
|
||||
new_tree.append(cursor.suffix(), ());
|
||||
drop(cursor);
|
||||
self.0 = new_tree;
|
||||
result
|
||||
}
|
||||
|
||||
pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut predicate: F) {
|
||||
let mut new_map = SumTree::<MapEntry<K, V>>::default();
|
||||
|
||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
|
||||
cursor.next();
|
||||
while let Some(item) = cursor.item() {
|
||||
if predicate(&item.key, &item.value) {
|
||||
new_map.push(item.clone(), ());
|
||||
}
|
||||
cursor.next();
|
||||
}
|
||||
drop(cursor);
|
||||
|
||||
self.0 = new_map;
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
|
||||
self.0.iter().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &V> + '_ {
|
||||
self.0.iter().map(|entry| &entry.value)
|
||||
}
|
||||
|
||||
pub fn first(&self) -> Option<(&K, &V)> {
|
||||
self.0.first().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn last(&self) -> Option<(&K, &V)> {
|
||||
self.0.last().map(|entry| (&entry.key, &entry.value))
|
||||
}
|
||||
|
||||
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
|
||||
let edits = other
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
Edit::Insert(MapEntry {
|
||||
key: key.to_owned(),
|
||||
value: value.to_owned(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.0.edit(edits, ());
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Debug for TreeMap<K, V>
|
||||
where
|
||||
K: Clone + Debug + Ord,
|
||||
V: Clone + Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_map().entries(self.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MapSeekTargetAdaptor<'a, T>(&'a T);
|
||||
|
||||
impl<'a, K: Clone + Ord, T: MapSeekTarget<K>> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>>
|
||||
for MapSeekTargetAdaptor<'_, T>
|
||||
{
|
||||
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
|
||||
if let Some(key) = &cursor_location.0 {
|
||||
MapSeekTarget::cmp_cursor(self.0, key)
|
||||
} else {
|
||||
Ordering::Greater
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MapSeekTarget<K> {
|
||||
fn cmp_cursor(&self, cursor_location: &K) -> Ordering;
|
||||
}
|
||||
|
||||
impl<K: Ord> MapSeekTarget<K> for K {
|
||||
fn cmp_cursor(&self, cursor_location: &K) -> Ordering {
|
||||
self.cmp(cursor_location)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Default for TreeMap<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Item for MapEntry<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
type Summary = MapKey<K>;
|
||||
|
||||
fn summary(&self, _cx: ()) -> Self::Summary {
|
||||
self.key()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> KeyedItem for MapEntry<K, V>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
V: Clone,
|
||||
{
|
||||
type Key = MapKey<K>;
|
||||
|
||||
fn key(&self) -> Self::Key {
|
||||
MapKey(Some(self.key.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> ContextLessSummary for MapKey<K>
|
||||
where
|
||||
K: Clone,
|
||||
{
|
||||
fn zero() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &Self) {
|
||||
*self = summary.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K> Dimension<'a, MapKey<K>> for MapKeyRef<'a, K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn zero(_cx: ()) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, summary: &'a MapKey<K>, _: ()) {
|
||||
self.0 = summary.0.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>> for MapKeyRef<'_, K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
|
||||
Ord::cmp(&self.0, &cursor_location.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for TreeSet<K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> TreeSet<K>
|
||||
where
|
||||
K: Clone + Ord,
|
||||
{
|
||||
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
|
||||
Self(TreeMap::from_ordered_entries(
|
||||
entries.into_iter().map(|key| (key, ())),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.0.insert(key, ());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> bool {
|
||||
self.0.remove(key).is_some()
|
||||
}
|
||||
|
||||
pub fn extend(&mut self, iter: impl IntoIterator<Item = K>) {
|
||||
self.0.extend(iter.into_iter().map(|key| (key, ())));
|
||||
}
|
||||
|
||||
pub fn contains(&self, key: &K) -> bool {
|
||||
self.0.get(key).is_some()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &K> + '_ {
|
||||
self.0.iter().map(|(k, _)| k)
|
||||
}
|
||||
|
||||
pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator<Item = &'a K> + 'a {
|
||||
self.0.iter_from(key).map(move |(k, _)| k)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_basic() {
|
||||
let mut map = TreeMap::default();
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
|
||||
|
||||
map.insert(3, "c");
|
||||
assert_eq!(map.get(&3), Some(&"c"));
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&3, &"c")]);
|
||||
|
||||
map.insert(1, "a");
|
||||
assert_eq!(map.get(&1), Some(&"a"));
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
|
||||
|
||||
map.insert(2, "b");
|
||||
assert_eq!(map.get(&2), Some(&"b"));
|
||||
assert_eq!(map.get(&1), Some(&"a"));
|
||||
assert_eq!(map.get(&3), Some(&"c"));
|
||||
assert_eq!(
|
||||
map.iter().collect::<Vec<_>>(),
|
||||
vec![(&1, &"a"), (&2, &"b"), (&3, &"c")]
|
||||
);
|
||||
|
||||
assert_eq!(map.closest(&0), None);
|
||||
assert_eq!(map.closest(&1), Some((&1, &"a")));
|
||||
assert_eq!(map.closest(&10), Some((&3, &"c")));
|
||||
|
||||
map.remove(&2);
|
||||
assert_eq!(map.get(&2), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
|
||||
|
||||
assert_eq!(map.closest(&2), Some((&1, &"a")));
|
||||
|
||||
map.remove(&3);
|
||||
assert_eq!(map.get(&3), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a")]);
|
||||
|
||||
map.remove(&1);
|
||||
assert_eq!(map.get(&1), None);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
|
||||
|
||||
map.insert(4, "d");
|
||||
map.insert(5, "e");
|
||||
map.insert(6, "f");
|
||||
map.retain(|key, _| *key % 2 == 0);
|
||||
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&4, &"d"), (&6, &"f")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_from() {
|
||||
let mut map = TreeMap::default();
|
||||
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("baa", 3);
|
||||
map.insert("baaab", 4);
|
||||
map.insert("c", 5);
|
||||
|
||||
let result = map
|
||||
.iter_from(&"ba")
|
||||
.take_while(|(key, _)| key.starts_with("ba"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.iter().any(|(k, _)| k == &&"baa"));
|
||||
assert!(result.iter().any(|(k, _)| k == &&"baaab"));
|
||||
|
||||
let result = map
|
||||
.iter_from(&"c")
|
||||
.take_while(|(key, _)| key.starts_with("c"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.iter().any(|(k, _)| k == &&"c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_tree() {
|
||||
let mut map = TreeMap::default();
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("c", 3);
|
||||
|
||||
let mut other = TreeMap::default();
|
||||
other.insert("a", 2);
|
||||
other.insert("b", 2);
|
||||
other.insert("d", 4);
|
||||
|
||||
map.insert_tree(other);
|
||||
|
||||
assert_eq!(map.iter().count(), 4);
|
||||
assert_eq!(map.get(&"a"), Some(&2));
|
||||
assert_eq!(map.get(&"b"), Some(&2));
|
||||
assert_eq!(map.get(&"c"), Some(&3));
|
||||
assert_eq!(map.get(&"d"), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extend() {
|
||||
let mut map = TreeMap::default();
|
||||
map.insert("a", 1);
|
||||
map.insert("b", 2);
|
||||
map.insert("c", 3);
|
||||
map.extend([("a", 2), ("b", 2), ("d", 4)]);
|
||||
assert_eq!(map.iter().count(), 4);
|
||||
assert_eq!(map.get(&"a"), Some(&2));
|
||||
assert_eq!(map.get(&"b"), Some(&2));
|
||||
assert_eq!(map.get(&"c"), Some(&3));
|
||||
assert_eq!(map.get(&"d"), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_between_and_path_successor() {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PathDescendants<'a>(&'a Path);
|
||||
|
||||
impl MapSeekTarget<PathBuf> for PathDescendants<'_> {
|
||||
fn cmp_cursor(&self, key: &PathBuf) -> Ordering {
|
||||
if key.starts_with(self.0) {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
self.0.cmp(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut map = TreeMap::default();
|
||||
|
||||
map.insert(PathBuf::from("a"), 1);
|
||||
map.insert(PathBuf::from("a/a"), 1);
|
||||
map.insert(PathBuf::from("b"), 2);
|
||||
map.insert(PathBuf::from("b/a/a"), 3);
|
||||
map.insert(PathBuf::from("b/a/a/a/b"), 4);
|
||||
map.insert(PathBuf::from("c"), 5);
|
||||
map.insert(PathBuf::from("c/a"), 6);
|
||||
|
||||
map.remove_range(
|
||||
&PathBuf::from("b/a"),
|
||||
&PathDescendants(&PathBuf::from("b/a")),
|
||||
);
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
assert_eq!(map.get(&PathBuf::from("b/a/a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("c")), Some(&5));
|
||||
assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6));
|
||||
|
||||
map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
assert_eq!(map.get(&PathBuf::from("c")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("c/a")), None);
|
||||
|
||||
map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("a/a")), None);
|
||||
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
|
||||
|
||||
map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b")));
|
||||
|
||||
assert_eq!(map.get(&PathBuf::from("b")), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user