Compare commits
2 Commits
fix-http-t
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3772a467e0 | ||
|
|
1be27e7270 |
3
.github/workflows/deploy_collab.yml
vendored
3
.github/workflows/deploy_collab.yml
vendored
@@ -3,7 +3,8 @@ name: Publish Collab Server Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- collab-production
|
||||
# Pause production deploys while we investigate an issue.
|
||||
# - collab-production
|
||||
- collab-staging
|
||||
|
||||
env:
|
||||
|
||||
105
Cargo.lock
generated
105
Cargo.lock
generated
@@ -3009,7 +3009,7 @@ dependencies = [
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
@@ -5410,19 +5410,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hoot"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936723a03900105673cf6efb8821399ebdc287a494949c375f2ce50db83bf6c9"
|
||||
dependencies = [
|
||||
"http 1.1.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"tinyvec",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
@@ -6116,20 +6103,6 @@ version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -7539,7 +7512,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni 0.21.1",
|
||||
"jni",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
@@ -9731,7 +9704,6 @@ version = "0.23.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -9752,19 +9724,6 @@ dependencies = [
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.0"
|
||||
@@ -9803,33 +9762,6 @@ version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"jni 0.19.0",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.13",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.102.8",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-roots 0.26.6",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -10122,7 +10054,6 @@ dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"num-bigint",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
@@ -12764,27 +12695,18 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.0.0-rc1"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "443349052924188e919be78d5870f6e827435579085947d1ae9ba32bb997e16f"
|
||||
checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"cc",
|
||||
"base64 0.21.7",
|
||||
"flate2",
|
||||
"hoot",
|
||||
"http 1.1.0",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pemfile 2.1.3",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"utf-8",
|
||||
"webpki-roots 0.26.6",
|
||||
"rustls 0.21.12",
|
||||
"rustls-webpki 0.101.7",
|
||||
"url",
|
||||
"webpki-roots 0.25.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13688,15 +13610,6 @@ version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.8"
|
||||
|
||||
@@ -31,12 +31,10 @@ ENV GITHUB_SHA=$GITHUB_SHA
|
||||
# - Staging: `4f408ec65a3867278322a189b4eb20f1ab51f508`
|
||||
# - Production: `fc4c533d0a8c489e5636a4249d2b52a80039fbd7`
|
||||
#
|
||||
# Also add `cmake`, since we need it to build `wasmtime`.
|
||||
#
|
||||
# Installing these as a temporary workaround, but I think ideally we'd want to figure
|
||||
# out what caused them to be included in the first place.
|
||||
RUN apt-get update; \
|
||||
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev cmake
|
||||
apt-get install -y --no-install-recommends libxkbcommon-dev libxkbcommon-x11-dev
|
||||
|
||||
RUN --mount=type=cache,target=./script/node_modules \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
|
||||
@@ -521,10 +521,6 @@ pub struct Usage {
|
||||
pub input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub output_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_tokens: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -485,7 +485,7 @@ impl Telemetry {
|
||||
worktree_id: WorktreeId,
|
||||
updated_entries_set: &UpdatedEntriesSet,
|
||||
) {
|
||||
let project_type_names: Vec<String> = {
|
||||
let project_names: Vec<String> = {
|
||||
let mut state = self.state.lock();
|
||||
state
|
||||
.worktree_id_map
|
||||
@@ -521,8 +521,8 @@ impl Telemetry {
|
||||
};
|
||||
|
||||
// Done on purpose to avoid calling `self.state.lock()` multiple times
|
||||
for project_type_name in project_type_names {
|
||||
self.report_app_event(format!("open {} project", project_type_name));
|
||||
for project_name in project_names {
|
||||
self.report_app_event(format!("open {} project", project_name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
alter table models
|
||||
add column price_per_million_cache_creation_input_tokens integer not null default 0,
|
||||
add column price_per_million_cache_read_input_tokens integer not null default 0;
|
||||
|
||||
alter table usages
|
||||
add column cache_creation_input_tokens_this_month bigint not null default 0,
|
||||
add column cache_read_input_tokens_this_month bigint not null default 0;
|
||||
|
||||
alter table lifetime_usages
|
||||
add column cache_creation_input_tokens bigint not null default 0,
|
||||
add column cache_read_input_tokens bigint not null default 0;
|
||||
@@ -1,3 +0,0 @@
|
||||
alter table usages
|
||||
drop column cache_creation_input_tokens_this_month,
|
||||
drop column cache_read_input_tokens_this_month;
|
||||
@@ -318,31 +318,22 @@ async fn perform_completion(
|
||||
chunks
|
||||
.map(move |event| {
|
||||
let chunk = event?;
|
||||
let (
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_input_tokens,
|
||||
cache_read_input_tokens,
|
||||
) = match &chunk {
|
||||
let (input_tokens, output_tokens) = match &chunk {
|
||||
anthropic::Event::MessageStart {
|
||||
message: anthropic::Response { usage, .. },
|
||||
}
|
||||
| anthropic::Event::MessageDelta { usage, .. } => (
|
||||
usage.input_tokens.unwrap_or(0) as usize,
|
||||
usage.output_tokens.unwrap_or(0) as usize,
|
||||
usage.cache_creation_input_tokens.unwrap_or(0) as usize,
|
||||
usage.cache_read_input_tokens.unwrap_or(0) as usize,
|
||||
),
|
||||
_ => (0, 0, 0, 0),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
anyhow::Ok(CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
anyhow::Ok((
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_input_tokens,
|
||||
cache_read_input_tokens,
|
||||
})
|
||||
))
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
@@ -368,13 +359,11 @@ async fn perform_completion(
|
||||
chunk.usage.as_ref().map_or(0, |u| u.prompt_tokens) as usize;
|
||||
let output_tokens =
|
||||
chunk.usage.as_ref().map_or(0, |u| u.completion_tokens) as usize;
|
||||
CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
(
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
@@ -398,13 +387,13 @@ async fn perform_completion(
|
||||
.map(|event| {
|
||||
event.map(|chunk| {
|
||||
// TODO - implement token counting for Google AI
|
||||
CompletionChunk {
|
||||
bytes: serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
}
|
||||
let input_tokens = 0;
|
||||
let output_tokens = 0;
|
||||
(
|
||||
serde_json::to_vec(&chunk).unwrap(),
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
)
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
@@ -418,8 +407,6 @@ async fn perform_completion(
|
||||
model,
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
inner_stream: stream,
|
||||
})))
|
||||
}
|
||||
@@ -564,14 +551,6 @@ async fn check_usage_limit(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CompletionChunk {
|
||||
bytes: Vec<u8>,
|
||||
input_tokens: usize,
|
||||
output_tokens: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
}
|
||||
|
||||
struct TokenCountingStream<S> {
|
||||
state: Arc<LlmState>,
|
||||
claims: LlmTokenClaims,
|
||||
@@ -579,26 +558,22 @@ struct TokenCountingStream<S> {
|
||||
model: String,
|
||||
input_tokens: usize,
|
||||
output_tokens: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
inner_stream: S,
|
||||
}
|
||||
|
||||
impl<S> Stream for TokenCountingStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<CompletionChunk, anyhow::Error>> + Unpin,
|
||||
S: Stream<Item = Result<(Vec<u8>, usize, usize), anyhow::Error>> + Unpin,
|
||||
{
|
||||
type Item = Result<Vec<u8>, anyhow::Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
match Pin::new(&mut self.inner_stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(mut chunk))) => {
|
||||
chunk.bytes.push(b'\n');
|
||||
self.input_tokens += chunk.input_tokens;
|
||||
self.output_tokens += chunk.output_tokens;
|
||||
self.cache_creation_input_tokens += chunk.cache_creation_input_tokens;
|
||||
self.cache_read_input_tokens += chunk.cache_read_input_tokens;
|
||||
Poll::Ready(Some(Ok(chunk.bytes)))
|
||||
Poll::Ready(Some(Ok((mut bytes, input_tokens, output_tokens)))) => {
|
||||
bytes.push(b'\n');
|
||||
self.input_tokens += input_tokens;
|
||||
self.output_tokens += output_tokens;
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
@@ -615,8 +590,6 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
let model = std::mem::take(&mut self.model);
|
||||
let input_token_count = self.input_tokens;
|
||||
let output_token_count = self.output_tokens;
|
||||
let cache_creation_input_token_count = self.cache_creation_input_tokens;
|
||||
let cache_read_input_token_count = self.cache_read_input_tokens;
|
||||
self.state.executor.spawn_detached(async move {
|
||||
let usage = state
|
||||
.db
|
||||
@@ -626,8 +599,6 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
provider,
|
||||
&model,
|
||||
input_token_count,
|
||||
cache_creation_input_token_count,
|
||||
cache_read_input_token_count,
|
||||
output_token_count,
|
||||
Utc::now(),
|
||||
)
|
||||
@@ -659,20 +630,11 @@ impl<S> Drop for TokenCountingStream<S> {
|
||||
model,
|
||||
provider: provider.to_string(),
|
||||
input_token_count: input_token_count as u64,
|
||||
cache_creation_input_token_count: cache_creation_input_token_count
|
||||
as u64,
|
||||
cache_read_input_token_count: cache_read_input_token_count as u64,
|
||||
output_token_count: output_token_count as u64,
|
||||
requests_this_minute: usage.requests_this_minute as u64,
|
||||
tokens_this_minute: usage.tokens_this_minute as u64,
|
||||
tokens_this_day: usage.tokens_this_day as u64,
|
||||
input_tokens_this_month: usage.input_tokens_this_month as u64,
|
||||
cache_creation_input_tokens_this_month: usage
|
||||
.cache_creation_input_tokens_this_month
|
||||
as u64,
|
||||
cache_read_input_tokens_this_month: usage
|
||||
.cache_read_input_tokens_this_month
|
||||
as u64,
|
||||
output_tokens_this_month: usage.output_tokens_this_month as u64,
|
||||
spending_this_month: usage.spending_this_month as u64,
|
||||
lifetime_spending: usage.lifetime_spending as u64,
|
||||
|
||||
@@ -14,8 +14,6 @@ pub struct Usage {
|
||||
pub tokens_this_minute: usize,
|
||||
pub tokens_this_day: usize,
|
||||
pub input_tokens_this_month: usize,
|
||||
pub cache_creation_input_tokens_this_month: usize,
|
||||
pub cache_read_input_tokens_this_month: usize,
|
||||
pub output_tokens_this_month: usize,
|
||||
pub spending_this_month: usize,
|
||||
pub lifetime_spending: usize,
|
||||
@@ -162,14 +160,17 @@ impl LlmDatabase {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let lifetime_usage = lifetime_usage::Entity::find()
|
||||
let (lifetime_input_tokens, lifetime_output_tokens) = lifetime_usage::Entity::find()
|
||||
.filter(
|
||||
lifetime_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(lifetime_usage::Column::ModelId.eq(model.id)),
|
||||
)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
.await?
|
||||
.map_or((0, 0), |usage| {
|
||||
(usage.input_tokens as usize, usage.output_tokens as usize)
|
||||
});
|
||||
|
||||
let requests_this_minute =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::RequestsPerMinute)?;
|
||||
@@ -179,44 +180,18 @@ impl LlmDatabase {
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::TokensPerDay)?;
|
||||
let input_tokens_this_month =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::InputTokensPerMonth)?;
|
||||
let cache_creation_input_tokens_this_month = self.get_usage_for_measure(
|
||||
&usages,
|
||||
now,
|
||||
UsageMeasure::CacheCreationInputTokensPerMonth,
|
||||
)?;
|
||||
let cache_read_input_tokens_this_month = self.get_usage_for_measure(
|
||||
&usages,
|
||||
now,
|
||||
UsageMeasure::CacheReadInputTokensPerMonth,
|
||||
)?;
|
||||
let output_tokens_this_month =
|
||||
self.get_usage_for_measure(&usages, now, UsageMeasure::OutputTokensPerMonth)?;
|
||||
let spending_this_month = calculate_spending(
|
||||
model,
|
||||
input_tokens_this_month,
|
||||
cache_creation_input_tokens_this_month,
|
||||
cache_read_input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
);
|
||||
let lifetime_spending = if let Some(lifetime_usage) = lifetime_usage {
|
||||
calculate_spending(
|
||||
model,
|
||||
lifetime_usage.input_tokens as usize,
|
||||
lifetime_usage.cache_creation_input_tokens as usize,
|
||||
lifetime_usage.cache_read_input_tokens as usize,
|
||||
lifetime_usage.output_tokens as usize,
|
||||
)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let spending_this_month =
|
||||
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
|
||||
let lifetime_spending =
|
||||
calculate_spending(model, lifetime_input_tokens, lifetime_output_tokens);
|
||||
|
||||
Ok(Usage {
|
||||
requests_this_minute,
|
||||
tokens_this_minute,
|
||||
tokens_this_day,
|
||||
input_tokens_this_month,
|
||||
cache_creation_input_tokens_this_month,
|
||||
cache_read_input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
spending_this_month,
|
||||
lifetime_spending,
|
||||
@@ -233,8 +208,6 @@ impl LlmDatabase {
|
||||
provider: LanguageModelProvider,
|
||||
model_name: &str,
|
||||
input_token_count: usize,
|
||||
cache_creation_input_tokens: usize,
|
||||
cache_read_input_tokens: usize,
|
||||
output_token_count: usize,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Usage> {
|
||||
@@ -262,10 +235,6 @@ impl LlmDatabase {
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let total_token_count = input_token_count
|
||||
+ cache_read_input_tokens
|
||||
+ cache_creation_input_tokens
|
||||
+ output_token_count;
|
||||
let tokens_this_minute = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
@@ -274,7 +243,7 @@ impl LlmDatabase {
|
||||
&usages,
|
||||
UsageMeasure::TokensPerMinute,
|
||||
now,
|
||||
total_token_count,
|
||||
input_token_count + output_token_count,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
@@ -286,7 +255,7 @@ impl LlmDatabase {
|
||||
&usages,
|
||||
UsageMeasure::TokensPerDay,
|
||||
now,
|
||||
total_token_count,
|
||||
input_token_count + output_token_count,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
@@ -302,30 +271,6 @@ impl LlmDatabase {
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let cache_creation_input_tokens_this_month = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
is_staff,
|
||||
model.id,
|
||||
&usages,
|
||||
UsageMeasure::CacheCreationInputTokensPerMonth,
|
||||
now,
|
||||
cache_creation_input_tokens,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let cache_read_input_tokens_this_month = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
is_staff,
|
||||
model.id,
|
||||
&usages,
|
||||
UsageMeasure::CacheReadInputTokensPerMonth,
|
||||
now,
|
||||
cache_read_input_tokens,
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let output_tokens_this_month = self
|
||||
.update_usage_for_measure(
|
||||
user_id,
|
||||
@@ -338,13 +283,8 @@ impl LlmDatabase {
|
||||
&tx,
|
||||
)
|
||||
.await?;
|
||||
let spending_this_month = calculate_spending(
|
||||
model,
|
||||
input_tokens_this_month,
|
||||
cache_creation_input_tokens_this_month,
|
||||
cache_read_input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
);
|
||||
let spending_this_month =
|
||||
calculate_spending(model, input_tokens_this_month, output_tokens_this_month);
|
||||
|
||||
// Update lifetime usage
|
||||
let lifetime_usage = lifetime_usage::Entity::find()
|
||||
@@ -363,12 +303,6 @@ impl LlmDatabase {
|
||||
input_tokens: ActiveValue::set(
|
||||
usage.input_tokens + input_token_count as i64,
|
||||
),
|
||||
cache_creation_input_tokens: ActiveValue::set(
|
||||
usage.cache_creation_input_tokens + cache_creation_input_tokens as i64,
|
||||
),
|
||||
cache_read_input_tokens: ActiveValue::set(
|
||||
usage.cache_read_input_tokens + cache_read_input_tokens as i64,
|
||||
),
|
||||
output_tokens: ActiveValue::set(
|
||||
usage.output_tokens + output_token_count as i64,
|
||||
),
|
||||
@@ -393,8 +327,6 @@ impl LlmDatabase {
|
||||
let lifetime_spending = calculate_spending(
|
||||
model,
|
||||
lifetime_usage.input_tokens as usize,
|
||||
lifetime_usage.cache_creation_input_tokens as usize,
|
||||
lifetime_usage.cache_read_input_tokens as usize,
|
||||
lifetime_usage.output_tokens as usize,
|
||||
);
|
||||
|
||||
@@ -403,8 +335,6 @@ impl LlmDatabase {
|
||||
tokens_this_minute,
|
||||
tokens_this_day,
|
||||
input_tokens_this_month,
|
||||
cache_creation_input_tokens_this_month,
|
||||
cache_read_input_tokens_this_month,
|
||||
output_tokens_this_month,
|
||||
spending_this_month,
|
||||
lifetime_spending,
|
||||
@@ -571,24 +501,13 @@ impl LlmDatabase {
|
||||
fn calculate_spending(
|
||||
model: &model::Model,
|
||||
input_tokens_this_month: usize,
|
||||
cache_creation_input_tokens_this_month: usize,
|
||||
cache_read_input_tokens_this_month: usize,
|
||||
output_tokens_this_month: usize,
|
||||
) -> usize {
|
||||
let input_token_cost =
|
||||
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
|
||||
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
|
||||
* model.price_per_million_cache_creation_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let cache_read_input_token_cost = cache_read_input_tokens_this_month
|
||||
* model.price_per_million_cache_read_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let output_token_cost =
|
||||
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
|
||||
input_token_cost
|
||||
+ cache_creation_input_token_cost
|
||||
+ cache_read_input_token_cost
|
||||
+ output_token_cost
|
||||
input_token_cost + output_token_cost
|
||||
}
|
||||
|
||||
const MINUTE_BUCKET_COUNT: usize = 12;
|
||||
@@ -602,8 +521,6 @@ impl UsageMeasure {
|
||||
UsageMeasure::TokensPerMinute => MINUTE_BUCKET_COUNT,
|
||||
UsageMeasure::TokensPerDay => DAY_BUCKET_COUNT,
|
||||
UsageMeasure::InputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
UsageMeasure::CacheCreationInputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
UsageMeasure::CacheReadInputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
UsageMeasure::OutputTokensPerMonth => MONTH_BUCKET_COUNT,
|
||||
}
|
||||
}
|
||||
@@ -614,8 +531,6 @@ impl UsageMeasure {
|
||||
UsageMeasure::TokensPerMinute => Duration::minutes(1),
|
||||
UsageMeasure::TokensPerDay => Duration::hours(24),
|
||||
UsageMeasure::InputTokensPerMonth => Duration::days(30),
|
||||
UsageMeasure::CacheCreationInputTokensPerMonth => Duration::days(30),
|
||||
UsageMeasure::CacheReadInputTokensPerMonth => Duration::days(30),
|
||||
UsageMeasure::OutputTokensPerMonth => Duration::days(30),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ pub struct Model {
|
||||
pub user_id: UserId,
|
||||
pub model_id: ModelId,
|
||||
pub input_tokens: i64,
|
||||
pub cache_creation_input_tokens: i64,
|
||||
pub cache_read_input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ pub struct Model {
|
||||
pub max_tokens_per_minute: i64,
|
||||
pub max_tokens_per_day: i64,
|
||||
pub price_per_million_input_tokens: i32,
|
||||
pub price_per_million_cache_creation_input_tokens: i32,
|
||||
pub price_per_million_cache_read_input_tokens: i32,
|
||||
pub price_per_million_output_tokens: i32,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ pub enum UsageMeasure {
|
||||
TokensPerMinute,
|
||||
TokensPerDay,
|
||||
InputTokensPerMonth,
|
||||
CacheCreationInputTokensPerMonth,
|
||||
CacheReadInputTokensPerMonth,
|
||||
OutputTokensPerMonth,
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
let user_id = UserId::from_proto(123);
|
||||
|
||||
let now = t0;
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, 0, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = t0 + Duration::seconds(10);
|
||||
db.record_usage(user_id, false, provider, model, 2000, 0, 0, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 2000, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -50,8 +50,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 3000,
|
||||
tokens_this_day: 3000,
|
||||
input_tokens_this_month: 3000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -67,8 +65,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 2000,
|
||||
tokens_this_day: 3000,
|
||||
input_tokens_this_month: 3000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -76,7 +72,7 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
);
|
||||
|
||||
let now = t0 + Duration::seconds(60);
|
||||
db.record_usage(user_id, false, provider, model, 3000, 0, 0, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 3000, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -88,8 +84,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 5000,
|
||||
tokens_this_day: 6000,
|
||||
input_tokens_this_month: 6000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -106,15 +100,13 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 0,
|
||||
tokens_this_day: 5000,
|
||||
input_tokens_this_month: 6000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
db.record_usage(user_id, false, provider, model, 4000, 0, 0, 0, now)
|
||||
db.record_usage(user_id, false, provider, model, 4000, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -126,8 +118,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 4000,
|
||||
tokens_this_day: 9000,
|
||||
input_tokens_this_month: 10000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
@@ -144,50 +134,6 @@ async fn test_tracking_usage(db: &mut LlmDatabase) {
|
||||
tokens_this_minute: 0,
|
||||
tokens_this_day: 0,
|
||||
input_tokens_this_month: 9000,
|
||||
cache_creation_input_tokens_this_month: 0,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Test cache creation input tokens
|
||||
db.record_usage(user_id, false, provider, model, 1000, 500, 0, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(
|
||||
usage,
|
||||
Usage {
|
||||
requests_this_minute: 1,
|
||||
tokens_this_minute: 1500,
|
||||
tokens_this_day: 1500,
|
||||
input_tokens_this_month: 10000,
|
||||
cache_creation_input_tokens_this_month: 500,
|
||||
cache_read_input_tokens_this_month: 0,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// Test cache read input tokens
|
||||
db.record_usage(user_id, false, provider, model, 1000, 0, 300, 0, now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let usage = db.get_usage(user_id, provider, model, now).await.unwrap();
|
||||
assert_eq!(
|
||||
usage,
|
||||
Usage {
|
||||
requests_this_minute: 2,
|
||||
tokens_this_minute: 2800,
|
||||
tokens_this_day: 2800,
|
||||
input_tokens_this_month: 11000,
|
||||
cache_creation_input_tokens_this_month: 500,
|
||||
cache_read_input_tokens_this_month: 300,
|
||||
output_tokens_this_month: 0,
|
||||
spending_this_month: 0,
|
||||
lifetime_spending: 0,
|
||||
|
||||
@@ -12,15 +12,11 @@ pub struct LlmUsageEventRow {
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
pub input_token_count: u64,
|
||||
pub cache_creation_input_token_count: u64,
|
||||
pub cache_read_input_token_count: u64,
|
||||
pub output_token_count: u64,
|
||||
pub requests_this_minute: u64,
|
||||
pub tokens_this_minute: u64,
|
||||
pub tokens_this_day: u64,
|
||||
pub input_tokens_this_month: u64,
|
||||
pub cache_creation_input_tokens_this_month: u64,
|
||||
pub cache_read_input_tokens_this_month: u64,
|
||||
pub output_tokens_this_month: u64,
|
||||
pub spending_this_month: u64,
|
||||
pub lifetime_spending: u64,
|
||||
|
||||
@@ -1533,8 +1533,4 @@ impl HttpClient for NullHttpClient {
|
||||
fn proxy(&self) -> Option<&http_client::Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{any::type_name, borrow::Cow, fmt::Debug, io::Read, pin::Pin, task::Poll};
|
||||
use std::{borrow::Cow, io::Read, pin::Pin, task::Poll};
|
||||
|
||||
use futures::{AsyncRead, AsyncReadExt};
|
||||
|
||||
@@ -34,21 +34,6 @@ impl AsyncBody {
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AsyncBody {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct(type_name::<Self>())
|
||||
.field(
|
||||
"Kind",
|
||||
&match self.0 {
|
||||
Inner::Empty => "Empty",
|
||||
Inner::SyncReader(_) => "SyncReader",
|
||||
Inner::AsyncReader(_) => "AsyncReader",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AsyncBody {
|
||||
fn default() -> Self {
|
||||
Self(Inner::Empty)
|
||||
|
||||
@@ -11,7 +11,6 @@ use http::request::Builder;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{
|
||||
any::type_name,
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -73,8 +72,6 @@ impl HttpRequestExt for http::request::Builder {
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
@@ -94,6 +91,7 @@ pub trait HttpClient: 'static + Send + Sync {
|
||||
RedirectPolicy::NoFollow
|
||||
})
|
||||
.body(body);
|
||||
|
||||
match request {
|
||||
Ok(request) => Box::pin(async move { self.send(request).await.map_err(Into::into) }),
|
||||
Err(e) => Box::pin(async move { Err(e.into()) }),
|
||||
@@ -156,10 +154,6 @@ impl HttpClient for HttpClientWithProxy {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for Arc<HttpClientWithProxy> {
|
||||
@@ -173,10 +167,6 @@ impl HttpClient for Arc<HttpClientWithProxy> {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
@@ -288,10 +278,6 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for HttpClientWithUrl {
|
||||
@@ -305,10 +291,6 @@ impl HttpClient for HttpClientWithUrl {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_proxy_from_env() -> Option<Uri> {
|
||||
@@ -349,10 +331,6 @@ impl HttpClient for BlockedHttpClient {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
@@ -425,8 +403,4 @@ impl HttpClient for FakeHttpClient {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,13 +279,6 @@ impl DevServerProjects {
|
||||
match connection.await {
|
||||
Some(_) => this
|
||||
.update(&mut cx, |this, cx| {
|
||||
let _ = this.workspace.update(cx, |workspace, _| {
|
||||
workspace
|
||||
.client()
|
||||
.telemetry()
|
||||
.report_app_event("create ssh server".to_string())
|
||||
});
|
||||
|
||||
this.add_ssh_server(connection_options, cx);
|
||||
this.mode = Mode::Default(None);
|
||||
cx.notify()
|
||||
@@ -429,15 +422,7 @@ impl DevServerProjects {
|
||||
);
|
||||
|
||||
cx.new_view(|cx| {
|
||||
let workspace =
|
||||
Workspace::new(None, project.clone(), app_state.clone(), cx);
|
||||
|
||||
workspace
|
||||
.client()
|
||||
.telemetry()
|
||||
.report_app_event("create ssh project".to_string());
|
||||
|
||||
workspace
|
||||
Workspace::new(None, project.clone(), app_state.clone(), cx)
|
||||
})
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -16,8 +16,8 @@ path = "src/reqwest_client.rs"
|
||||
doctest = true
|
||||
|
||||
[[example]]
|
||||
name = "reqwest_example"
|
||||
path = "examples/reqwest_example.rs"
|
||||
name = "client"
|
||||
path = "examples/client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{any::type_name, borrow::Cow, io::Read, pin::Pin, task::Poll};
|
||||
use std::{borrow::Cow, io::Read, pin::Pin, task::Poll};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
@@ -163,12 +163,8 @@ impl futures::stream::Stream for WrappedBody {
|
||||
WrappedBodyInner::SyncReader(cursor) => {
|
||||
let mut buf = Vec::new();
|
||||
match cursor.read_to_end(&mut buf) {
|
||||
Ok(bytes) => {
|
||||
if bytes == 0 {
|
||||
return Poll::Ready(None);
|
||||
} else {
|
||||
return Poll::Ready(Some(Ok(Bytes::from(buf))));
|
||||
}
|
||||
Ok(_) => {
|
||||
return Poll::Ready(Some(Ok(Bytes::from(buf))));
|
||||
}
|
||||
Err(e) => return Poll::Ready(Some(Err(e))),
|
||||
}
|
||||
@@ -187,10 +183,6 @@ impl http_client::HttpClient for ReqwestClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<http_client::AsyncBody>,
|
||||
@@ -238,22 +230,3 @@ impl http_client::HttpClient for ReqwestClient {
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use core::str;
|
||||
|
||||
use http_client::AsyncBody;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::WrappedBody;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sync_streaming_upload() {
|
||||
let mut body = WrappedBody::new(AsyncBody::from("hello there".to_string())).fuse();
|
||||
let result = body.next().await.unwrap().unwrap();
|
||||
assert!(body.next().await.is_none());
|
||||
assert_eq!(str::from_utf8(&result).unwrap(), "hello there");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,7 +584,17 @@ impl TitleBar {
|
||||
.separator()
|
||||
})
|
||||
.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.advanced_action(
|
||||
"Default Settings",
|
||||
zed_actions::OpenSettings.boxed_clone(),
|
||||
Some("Settings".into()),
|
||||
)
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.advanced_action(
|
||||
"Some Advanced",
|
||||
Box::new(zed_actions::DecreaseUiFontSize),
|
||||
None,
|
||||
)
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
.separator()
|
||||
@@ -613,7 +623,17 @@ impl TitleBar {
|
||||
.menu(|cx| {
|
||||
ContextMenu::build(cx, |menu, _| {
|
||||
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
|
||||
.advanced_action(
|
||||
"Default Settings",
|
||||
zed_actions::OpenSettings.boxed_clone(),
|
||||
Some("Settings".into()),
|
||||
)
|
||||
.action("Key Bindings", Box::new(zed_actions::OpenKeymap))
|
||||
.advanced_action(
|
||||
"Some Advanced",
|
||||
Box::new(zed_actions::DecreaseUiFontSize),
|
||||
None,
|
||||
)
|
||||
.action("Themes…", theme_selector::Toggle::default().boxed_clone())
|
||||
.action("Extensions", extensions_ui::Extensions.boxed_clone())
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use gpui::{
|
||||
px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
|
||||
IntoElement, Render, Subscription, View, VisualContext,
|
||||
IntoElement, Modifiers, ModifiersChangedEvent, Render, Subscription, View, VisualContext,
|
||||
};
|
||||
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
|
||||
use settings::Settings;
|
||||
@@ -24,6 +24,16 @@ enum ContextMenuItem {
|
||||
action: Option<Box<dyn Action>>,
|
||||
disabled: bool,
|
||||
},
|
||||
AdvancedEntry {
|
||||
toggle: Option<(IconPosition, bool)>,
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
disabled: bool,
|
||||
/// Allows the advanced entry to specify a entry to hide when it is visible, enabling a "swapping" effect between the two.
|
||||
hide_other_entry_when_visible: Option<SharedString>,
|
||||
},
|
||||
CustomEntry {
|
||||
entry_render: Box<dyn Fn(&mut WindowContext) -> AnyElement>,
|
||||
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>,
|
||||
@@ -33,11 +43,14 @@ enum ContextMenuItem {
|
||||
|
||||
pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
item_visibility: Vec<bool>,
|
||||
focus_handle: FocusHandle,
|
||||
action_context: Option<FocusHandle>,
|
||||
selected_index: Option<usize>,
|
||||
show_advanced: bool,
|
||||
delayed: bool,
|
||||
clicked: bool,
|
||||
init_modifiers: Option<Modifiers>,
|
||||
_on_blur_subscription: Subscription,
|
||||
}
|
||||
|
||||
@@ -62,18 +75,22 @@ impl ContextMenu {
|
||||
this.cancel(&menu::Cancel, cx)
|
||||
});
|
||||
cx.refresh();
|
||||
f(
|
||||
Self {
|
||||
items: Default::default(),
|
||||
focus_handle,
|
||||
action_context: None,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
_on_blur_subscription,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
let mut menu = Self {
|
||||
items: Default::default(),
|
||||
item_visibility: Default::default(),
|
||||
focus_handle,
|
||||
action_context: None,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
show_advanced: false,
|
||||
init_modifiers: None,
|
||||
_on_blur_subscription,
|
||||
};
|
||||
menu = f(menu, cx);
|
||||
menu.item_visibility = vec![true; menu.items.len()];
|
||||
menu.handle_update_items(cx);
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,6 +126,26 @@ impl ContextMenu {
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an entry that is only visible when `show_advanced` is enabled.
|
||||
pub fn advanced_entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Option<Box<dyn Action>>,
|
||||
handler: impl Fn(&mut WindowContext) + 'static,
|
||||
hidden_entry_when_visible: impl Into<Option<SharedString>>,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::AdvancedEntry {
|
||||
toggle: None,
|
||||
label: label.into(),
|
||||
handler: Rc::new(move |_, cx| handler(cx)),
|
||||
icon: None,
|
||||
action,
|
||||
disabled: false,
|
||||
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggleable_entry(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -176,6 +213,30 @@ impl ContextMenu {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn advanced_action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
action: Box<dyn Action>,
|
||||
hidden_entry_when_visible: impl Into<Option<SharedString>>,
|
||||
) -> Self {
|
||||
self.items.push(ContextMenuItem::AdvancedEntry {
|
||||
toggle: None,
|
||||
label: label.into(),
|
||||
action: Some(action.boxed_clone()),
|
||||
|
||||
handler: Rc::new(move |context, cx| {
|
||||
if let Some(context) = &context {
|
||||
cx.focus(context);
|
||||
}
|
||||
cx.dispatch_action(action.boxed_clone());
|
||||
}),
|
||||
icon: None,
|
||||
disabled: false,
|
||||
hide_other_entry_when_visible: hidden_entry_when_visible.into(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled_action(
|
||||
mut self,
|
||||
label: impl Into<SharedString>,
|
||||
@@ -323,6 +384,64 @@ impl ContextMenu {
|
||||
self._on_blur_subscription = new_subscription;
|
||||
self
|
||||
}
|
||||
|
||||
fn handle_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(init_modifiers) = self.init_modifiers else {
|
||||
self.init_modifiers = Some(event.modifiers);
|
||||
return;
|
||||
};
|
||||
|
||||
if event.modifiers.alt != init_modifiers.alt {
|
||||
self.show_advanced(event.modifiers.alt, cx);
|
||||
}
|
||||
|
||||
self.init_modifiers = Some(event.modifiers);
|
||||
}
|
||||
|
||||
fn show_advanced(&mut self, show_advanced: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_advanced = show_advanced;
|
||||
self.handle_update_items(cx);
|
||||
}
|
||||
|
||||
fn handle_update_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.item_visibility.clear();
|
||||
self.item_visibility.reserve(self.items.len());
|
||||
|
||||
for item in &self.items {
|
||||
match item {
|
||||
ContextMenuItem::AdvancedEntry {
|
||||
hide_other_entry_when_visible,
|
||||
..
|
||||
} => {
|
||||
let visible = self.show_advanced;
|
||||
self.item_visibility.push(visible);
|
||||
if visible {
|
||||
if let Some(hidden_label) = hide_other_entry_when_visible {
|
||||
// Hide the corresponding entry
|
||||
if let Some(pos) = self.items.iter().position(|i| {
|
||||
matches!(i, ContextMenuItem::Entry { label, .. } if label == hidden_label)
|
||||
}) {
|
||||
self.item_visibility[pos] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ContextMenuItem::Entry { label, .. } => {
|
||||
let visible = !self.show_advanced || !self.items.iter().any(|i| {
|
||||
matches!(i, ContextMenuItem::AdvancedEntry { hide_other_entry_when_visible: Some(hidden), .. } if hidden == label)
|
||||
});
|
||||
self.item_visibility.push(visible);
|
||||
}
|
||||
_ => self.item_visibility.push(true),
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
@@ -332,6 +451,7 @@ impl ContextMenuItem {
|
||||
| ContextMenuItem::Separator
|
||||
| ContextMenuItem::Label { .. } => false,
|
||||
ContextMenuItem::Entry { disabled, .. } => !disabled,
|
||||
ContextMenuItem::AdvancedEntry { disabled, .. } => !disabled,
|
||||
ContextMenuItem::CustomEntry { selectable, .. } => *selectable,
|
||||
}
|
||||
}
|
||||
@@ -357,6 +477,7 @@ impl Render for ContextMenu {
|
||||
.on_action(cx.listener(ContextMenu::select_prev))
|
||||
.on_action(cx.listener(ContextMenu::confirm))
|
||||
.on_action(cx.listener(ContextMenu::cancel))
|
||||
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
|
||||
.when(!self.delayed, |mut el| {
|
||||
for item in self.items.iter() {
|
||||
if let ContextMenuItem::Entry {
|
||||
@@ -374,119 +495,106 @@ impl Render for ContextMenu {
|
||||
el
|
||||
})
|
||||
.flex_none()
|
||||
.child(List::new().children(self.items.iter_mut().enumerate().map(
|
||||
|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone())
|
||||
.inset(true)
|
||||
.into_any_element()
|
||||
.child(
|
||||
List::new().children(self.items.iter_mut().enumerate().filter_map(
|
||||
|(ix, item)| {
|
||||
if !self.item_visibility[ix] {
|
||||
return None;
|
||||
}
|
||||
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(true)
|
||||
.child(Label::new(label.clone()))
|
||||
.into_any_element(),
|
||||
ContextMenuItem::Entry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon).size(IconSize::Small).color(color),
|
||||
)
|
||||
Some(match item {
|
||||
ContextMenuItem::Separator => ListSeparator.into_any_element(),
|
||||
ContextMenuItem::Header(header) => {
|
||||
ListSubHeader::new(header.clone())
|
||||
.inset(true)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone()).color(color).into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
}
|
||||
ContextMenuItem::Label(label) => ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check).color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => list_item.end_slot(contents),
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| format!("MENU_ITEM-{}", label))
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(&**action, cx)
|
||||
})
|
||||
.map(|binding| div().ml_4().child(binding))
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::CustomEntry {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
.disabled(true)
|
||||
.child(Label::new(label.clone()))
|
||||
.into_any_element(),
|
||||
ContextMenuItem::Entry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon)
|
||||
.size(IconSize::Small)
|
||||
.color(color),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone())
|
||||
.color(color)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => {
|
||||
list_item.end_slot(contents)
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| {
|
||||
format!("MENU_ITEM-{}", label)
|
||||
})
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(
|
||||
&**action, cx,
|
||||
)
|
||||
})
|
||||
.map(|binding| {
|
||||
div().ml_4().child(binding)
|
||||
})
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
@@ -497,13 +605,139 @@ impl Render for ContextMenu {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
},
|
||||
))),
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::AdvancedEntry {
|
||||
toggle,
|
||||
label,
|
||||
handler,
|
||||
icon,
|
||||
action,
|
||||
disabled,
|
||||
..
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let color = if *disabled {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
let label_element = if let Some(icon) = icon {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(label.clone()).color(color))
|
||||
.child(
|
||||
Icon::new(*icon)
|
||||
.size(IconSize::Small)
|
||||
.color(color),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(label.clone())
|
||||
.color(color)
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.disabled(*disabled)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.when_some(*toggle, |list_item, (position, toggled)| {
|
||||
let contents = if toggled {
|
||||
v_flex().flex_none().child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.flex_none()
|
||||
.size(IconSize::default().rems())
|
||||
};
|
||||
match position {
|
||||
IconPosition::Start => {
|
||||
list_item.start_slot(contents)
|
||||
}
|
||||
IconPosition::End => {
|
||||
list_item.end_slot(contents)
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(label_element)
|
||||
.debug_selector(|| {
|
||||
format!("MENU_ITEM-{}", label)
|
||||
})
|
||||
.children(action.as_ref().and_then(|action| {
|
||||
self.action_context
|
||||
.as_ref()
|
||||
.map(|focus| {
|
||||
KeyBinding::for_action_in(
|
||||
&**action, focus, cx,
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
KeyBinding::for_action(
|
||||
&**action, cx,
|
||||
)
|
||||
})
|
||||
.map(|binding| {
|
||||
div().ml_4().child(binding)
|
||||
})
|
||||
})),
|
||||
)
|
||||
.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
ContextMenuItem::CustomEntry {
|
||||
entry_render,
|
||||
handler,
|
||||
selectable,
|
||||
} => {
|
||||
let handler = handler.clone();
|
||||
let menu = cx.view().downgrade();
|
||||
let selectable = *selectable;
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(if selectable {
|
||||
Some(ix) == self.selected_index
|
||||
} else {
|
||||
false
|
||||
})
|
||||
.selectable(selectable)
|
||||
.when(selectable, |item| {
|
||||
item.on_click({
|
||||
let context = self.action_context.clone();
|
||||
move |_, cx| {
|
||||
handler(context.as_ref(), cx);
|
||||
menu.update(cx, |menu, cx| {
|
||||
menu.clicked = true;
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(entry_render(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ path = "src/ureq_client.rs"
|
||||
doctest = true
|
||||
|
||||
[[example]]
|
||||
name = "ureq_example"
|
||||
path = "examples/ureq_example.rs"
|
||||
name = "client"
|
||||
path = "examples/client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -27,9 +27,5 @@ http_client.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
smol.workspace = true
|
||||
ureq = "3.0.0-rc1"
|
||||
ureq = "=2.9.1"
|
||||
util.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = {workspace = true, features = ["test-support"]}
|
||||
|
||||
24
crates/ureq_client/examples/client.rs
Normal file
24
crates/ureq_client/examples/client.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use futures::AsyncReadExt;
|
||||
use http_client::{AsyncBody, HttpClient};
|
||||
use ureq_client::UreqClient;
|
||||
|
||||
fn main() {
|
||||
gpui::App::headless().run(|cx| {
|
||||
println!("{:?}", std::thread::current().id());
|
||||
cx.spawn(|cx| async move {
|
||||
let resp = UreqClient::new(
|
||||
None,
|
||||
"Conrad's bot".to_string(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.get("http://zed.dev", AsyncBody::empty(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut body = String::new();
|
||||
resp.into_body().read_to_string(&mut body).await.unwrap();
|
||||
println!("{}", body);
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use http_client::{AsyncBody, HttpClient};
|
||||
use ureq_client::UreqClient;
|
||||
|
||||
fn main() {
|
||||
gpui::App::headless().run(|cx| {
|
||||
println!("{:?}", std::thread::current().id());
|
||||
cx.spawn(|cx| async move {
|
||||
let client = UreqClient::new(
|
||||
None,
|
||||
"Conrad's bot".to_string(),
|
||||
cx.background_executor().clone(),
|
||||
);
|
||||
|
||||
let resp = client
|
||||
.get("http://zed.dev", AsyncBody::empty(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut body = String::new();
|
||||
futures::AsyncReadExt::read_to_string(&mut resp.into_body(), &mut body)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("{}", body);
|
||||
|
||||
// Test sync read
|
||||
let resp = client
|
||||
.get("http://zed.dev", AsyncBody::empty(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut body = String::new();
|
||||
std::io::Read::read_to_string(&mut resp.into_body(), &mut body).unwrap();
|
||||
println!("{}", body);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.quit();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use std::any::type_name;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{pin::Pin, task::Poll};
|
||||
|
||||
@@ -6,14 +8,20 @@ use anyhow::Error;
|
||||
use futures::channel::mpsc;
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{AsyncRead, SinkExt, StreamExt};
|
||||
use http_client::http::Response;
|
||||
use http_client::{http, AsyncBody, HttpClient, RedirectPolicy, Request, Uri};
|
||||
use http_client::{http, AsyncBody, HttpClient, RedirectPolicy, Uri};
|
||||
use smol::future::FutureExt;
|
||||
use ureq::{Agent, Proxy, SendBody, Timeouts};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct UreqClient {
|
||||
agent: ureq::Agent,
|
||||
// Note in ureq 2.x the options are stored on the Agent.
|
||||
// In ureq 3.x we'll be able to set these on the request.
|
||||
// In practice it's probably "fine" to have many clients, the number of distinct options
|
||||
// is low; and most requests to the same connection will have the same options so the
|
||||
// connection pool will work.
|
||||
clients: Arc<parking_lot::Mutex<HashMap<(Duration, RedirectPolicy), ureq::Agent>>>,
|
||||
proxy_url: Option<Uri>,
|
||||
proxy: Option<ureq::Proxy>,
|
||||
user_agent: String,
|
||||
background_executor: gpui::BackgroundExecutor,
|
||||
}
|
||||
|
||||
@@ -23,75 +31,90 @@ impl UreqClient {
|
||||
user_agent: String,
|
||||
background_executor: gpui::BackgroundExecutor,
|
||||
) -> Self {
|
||||
let agent: ureq::Agent = ureq::Config {
|
||||
http_status_as_error: false,
|
||||
proxy: proxy_url.and_then(|url| Proxy::new(&url.to_string()).log_err()),
|
||||
timeouts: Timeouts {
|
||||
connect: Some(Duration::from_secs(5)),
|
||||
..Default::default()
|
||||
},
|
||||
user_agent: Some(user_agent),
|
||||
..Default::default()
|
||||
}
|
||||
.into();
|
||||
|
||||
Self {
|
||||
agent,
|
||||
clients: Arc::default(),
|
||||
proxy_url: proxy_url.clone(),
|
||||
proxy: proxy_url.and_then(|url| ureq::Proxy::new(url.to_string()).log_err()),
|
||||
user_agent,
|
||||
background_executor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_for(&self, redirect_policy: RedirectPolicy, timeout: Duration) -> ureq::Agent {
|
||||
let mut clients = self.clients.lock();
|
||||
// in case our assumption of distinct options is wrong, we'll sporadically clean it out.
|
||||
if clients.len() > 50 {
|
||||
clients.clear()
|
||||
}
|
||||
|
||||
clients
|
||||
.entry((timeout, redirect_policy.clone()))
|
||||
.or_insert_with(|| {
|
||||
let mut builder = ureq::AgentBuilder::new()
|
||||
.timeout_connect(Duration::from_secs(5))
|
||||
.timeout_read(timeout)
|
||||
.timeout_write(timeout)
|
||||
.user_agent(&self.user_agent)
|
||||
.tls_config(http_client::TLS_CONFIG.clone())
|
||||
.redirects(match redirect_policy {
|
||||
RedirectPolicy::NoFollow => 0,
|
||||
RedirectPolicy::FollowLimit(limit) => limit,
|
||||
RedirectPolicy::FollowAll => 100,
|
||||
});
|
||||
if let Some(proxy) = &self.proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
builder.build()
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
impl HttpClient for UreqClient {
|
||||
fn proxy(&self) -> Option<&Uri> {
|
||||
self.agent.config().proxy.as_ref().map(|proxy| proxy.uri())
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
self.proxy_url.as_ref()
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
request: http::Request<AsyncBody>,
|
||||
) -> BoxFuture<'static, Result<http::Response<AsyncBody>, Error>> {
|
||||
let redirect_policy = request
|
||||
.extensions()
|
||||
.get::<RedirectPolicy>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let timeout = request
|
||||
.extensions()
|
||||
.get::<http_client::ReadTimeout>()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.0;
|
||||
let agent = self.agent.clone();
|
||||
let agent = self.agent_for(
|
||||
request
|
||||
.extensions()
|
||||
.get::<RedirectPolicy>()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
request
|
||||
.extensions()
|
||||
.get::<http_client::ReadTimeout>()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
);
|
||||
let mut req = agent.request(&request.method().as_ref(), &request.uri().to_string());
|
||||
for (name, value) in request.headers().into_iter() {
|
||||
req = req.set(name.as_str(), value.to_str().unwrap());
|
||||
}
|
||||
let body = request.into_body();
|
||||
let executor = self.background_executor.clone();
|
||||
|
||||
self.background_executor
|
||||
.spawn(async move {
|
||||
let (parts, body) = request.into_parts();
|
||||
let body = match body.0 {
|
||||
http_client::Inner::Empty => SendBody::none(),
|
||||
_ => SendBody::from_owned_reader(body),
|
||||
};
|
||||
let mut request = Request::from_parts(parts, body);
|
||||
let response = req.send(body)?;
|
||||
|
||||
let config = agent.configure_request(&mut request);
|
||||
config.max_redirects = match redirect_policy {
|
||||
RedirectPolicy::NoFollow => 0,
|
||||
RedirectPolicy::FollowLimit(limit) => limit,
|
||||
RedirectPolicy::FollowAll => 100,
|
||||
};
|
||||
config.timeouts.recv_body = Some(timeout);
|
||||
let mut builder = http::Response::builder()
|
||||
.status(response.status())
|
||||
.version(http::Version::HTTP_11);
|
||||
for name in response.headers_names() {
|
||||
if let Some(value) = response.header(&name) {
|
||||
builder = builder.header(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = agent.run(request)?;
|
||||
let body = AsyncBody::from_reader(UreqResponseReader::new(executor, response));
|
||||
let http_response = builder.body(body)?;
|
||||
|
||||
let (parts, response_body) = response.into_parts();
|
||||
let response_body =
|
||||
AsyncBody::from_reader(UreqResponseReader::new(executor, response_body));
|
||||
Ok(Response::from_parts(parts, response_body))
|
||||
Ok(http_response)
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
@@ -105,14 +128,13 @@ struct UreqResponseReader {
|
||||
}
|
||||
|
||||
impl UreqResponseReader {
|
||||
fn new(background_executor: gpui::BackgroundExecutor, response: ureq::Body) -> Self {
|
||||
fn new(background_executor: gpui::BackgroundExecutor, response: ureq::Response) -> Self {
|
||||
let (mut sender, receiver) = mpsc::channel(1);
|
||||
let mut reader = response.into_reader();
|
||||
|
||||
let task = background_executor.spawn(async move {
|
||||
let mut buffer = vec![0; 8192];
|
||||
loop {
|
||||
let n = match std::io::Read::read(&mut reader, &mut buffer) {
|
||||
let n = match reader.read(&mut buffer) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
@@ -160,7 +182,6 @@ impl AsyncRead for UreqResponseReader {
|
||||
self.buffer.clear();
|
||||
self.idx = 0;
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5575,12 +5575,6 @@ pub fn open_ssh_project(
|
||||
cx.replace_root_view(|cx| {
|
||||
let mut workspace =
|
||||
Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
|
||||
|
||||
workspace
|
||||
.client()
|
||||
.telemetry()
|
||||
.report_app_event("open ssh project".to_string());
|
||||
|
||||
workspace.set_serialized_ssh_project(serialized_ssh_project);
|
||||
workspace
|
||||
});
|
||||
|
||||
@@ -528,8 +528,6 @@ fn main() {
|
||||
session_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
// We should rename these in the future to `first app open`, `first app open for release channel`, and `app open`
|
||||
if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) {
|
||||
match (&system_id, &installation_id) {
|
||||
(IdType::New(_), IdType::New(_)) => {
|
||||
|
||||
Reference in New Issue
Block a user