Compare commits

...

39 Commits

Author SHA1 Message Date
Conrad Irwin
0ec803cea9 zed 0.124.8 2024-02-29 10:34:13 -07:00
gcp-cherry-pick-bot[bot]
c6ad241322 Ensure panel and pane sizes are integral (cherry-pick #8619) (#8621)
Cherry-picked Ensure panel and pane sizes are integral (#8619)

Fixes: #8050

For some reason that we didn't investigate, if you have view caching
enabled,
and you have non-integer sized bounds, and you are right aligning
things, the
co-ordinates can differ by +/- 1px when using the cached view.

The easiest fix for now is to just not do that.

Co-Authored-By: Antonio <as-cii@zed.dev>

Release Notes:

- Fixed the pane icons flickering
([#8050](https://github.com/zed-industries/zed/issues/8050)).

Co-authored-by: Antonio <as-cii@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio <as-cii@zed.dev>
2024-02-29 10:03:07 -07:00
gcp-cherry-pick-bot[bot]
fc025b248d Revert "Introduce a new ToggleGraphicsProfiler command (#7607)" (cherry-pick #8567) (#8570)
Cherry-picked Revert "Introduce a new `ToggleGraphicsProfiler` command
(#7607)" (#8567)

This reverts commit 0cebf68306.

Although this thing is very cool, it is a top source of crashes.

Example crash:
```
Segmentation fault: 11 on thread 26
  objc_retain +16
  invocation function for block in Overlay::onCommandBufferCommit(id<MTLCommandBuffer>) +60
  MTLDispatchListApply +52
```

Release Notes:

- Removed "Toggle Graphics Profiler" as it crashes too much.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-28 19:06:18 -07:00
gcp-cherry-pick-bot[bot]
d6c7c9ad7e Avoid an unwrap when loading languages (#8562) (#8563)
Cherry-picked Avoid an unwrap when loading languages (#8562)

We couldn't reproduce the panic, but I believe it was possible when
uninstalling an extension while one if its grammars was still loading.

Release Notes:
- Fixed a crash that could happen when uninstalling a language extension
while its grammar was loading.

---------

Co-authored-by: Conrad <conrad@zed.dev>

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
2024-02-28 15:35:30 -07:00
Joseph T. Lyons
fed2c96d6b v0.124.x stable 2024-02-28 13:13:28 -05:00
Antonio Scandurra
d0aec52010 Fix flickering cursor style when a pane was zoomed (#8546)
Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <max@zed.dev>
2024-02-28 13:11:10 -05:00
Antonio Scandurra
0619017b37 Introduce a short-term solution for flickering (#8542)
This uses bounds checking alone to determine hover state to avoid
flicker. It's a short-term solution because the rendering is incorrect.
We think this is better than flickering though and buys us some time as
we work on a more robust solution overall.

Release Notes:

- Fixed flickering when hovering.

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-02-28 13:07:23 -05:00
Conrad Irwin
7eb07a41d1 zed 0.124.7 2024-02-27 19:49:26 -07:00
Kirill Bulatov
e41b0b1bf2 Use proper buffer versions when [de]serializing hint proto requests (#8502)
During inlay hint<->proto conversions, uses proper buffer versions and
never carries them across the .await points, to fix the
```
thread 'main' panicked at 'invalid anchor Anchor { timestamp: Lamport {0: 8558}, offset: 54, bias: Right, buffer_id: Some(BufferId(8)) }. buffer id: 8, version: Global {0: 8546, 1: 8378}'
/Users/administrator/actions-runner-2/_work/zed/zed/crates/text/src/text.rs:1919
<backtrace::capture::Backtrace>::create
<backtrace::capture::Backtrace>::new
Zed::init_panic_hook::{closure#0}
std::panicking::rust_panic_with_hook
std::panicking::begin_panic_handler::{{closure}}
std::sys_common::backtrace::__rust_end_short_backtrace
_rust_begin_unwind
core::panicking::panic_fmt
<text::BufferSnapshot>::summary_for_anchor::<usize>
<multi_buffer::anchor::Anchor as multi_buffer::ToOffset>::to_offset
<editor::display_map::DisplayMap>::splice_inlays
<editor::Editor>::splice_inlay_hints
editor::inlay_hint_cache::fetch_and_update_hints::{closure#0}::{closure#0}
<async_task::raw::RawTask<<async_task::runnable::Builder<_>>::spawn_local::Checked<core::pin::Pin<alloc::boxed::Box<dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>, core::result::Result<(), anyhow::Error>, <gpui::executor::ForegroundExecutor>::spawn::inner<core::result::Result<(), anyhow::Error>>::{closure#0}, ()>>::run
<gpui::platform::mac::platform::MacPlatform as gpui::platform::Platform>::run
Zed::main
std::sys_common::backtrace::__rust_begin_short_backtrace::<fn(), ()>
std::rt::lang_start::<()>::{closure#0}
std::rt::lang_start_internal
_main
```
class of panics.


Release Notes:

- Fixed occasional panics during collaborative editing with inlay hints
on both sides

Co-authored-by: Conrad <conrad@zed.dev>
2024-02-27 19:48:34 -07:00
Conrad Irwin
9cf30a0395 Add missing wait_for_anchors (#8509)
Release Notes:

- Fixed a panic when hovering in a collaboration session
2024-02-27 19:48:26 -07:00
Marshall Bowers
54e29d8942 zed 0.124.6 2024-02-26 11:29:21 -05:00
Marshall Bowers
39405936fa Use SystemClock in EventCoalescer (#8317)
This PR updates the `EventCoalescer` to use the `SystemClock` trait to
abstract over the clock.

This allows us to test the advancement of time without relying on the
caller passing in the current time.

Release Notes:

- N/A
2024-02-26 11:26:50 -05:00
Conrad Irwin
df42d5b333 Expose extensions API from api.zed.dev (#8307)
This avoids the need to pay for bandwidth

Co-Authored-By: Marshall <marshall@zed.dev>



Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-02-26 11:26:06 -05:00
Conrad Irwin
5844fcc89e Log HTTP path in http logs (#8305)
Co-Authored-By: Marshall <marshall@zed.dev>


Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-02-26 11:26:00 -05:00
Conrad Irwin
cd01276c0e collab: Log HTTP requests (#8297)
Co-Authored-By: Marshall <marshall@zed.dev>



Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-02-26 11:25:53 -05:00
Conrad Irwin
e62717e6f4 oops 2024-02-26 11:25:49 -05:00
Marshall Bowers
e2c459a497 Only spawn the extensions reconciliation task in the collab service (#8301)
This PR makes it so the background task that reconciles the extensions
database with the blob store only runs on the `collab` service.

This avoids us having multiple of these jobs running at once.

Release Notes:

- N/A
2024-02-26 11:25:45 -05:00
Conrad Irwin
b9cc649211 Fix collab (#8298)
Co-Authored-By: Marshall <marshall@zed.dev>

We broke it by deploying two servers simultaneously.

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-02-26 11:25:41 -05:00
Conrad Irwin
cbc7645bdd Fix error logging (#8295)
and some more clickhouse type mismatches,

Co-Authored-By: Marshall <marshall@zed.dev>

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-02-26 11:25:36 -05:00
Conrad Irwin
50ff0d75f9 Deploy the ZED_CLIENT_CHECKSUM_SEED too (#8289)
Release Notes:

- N/A
2024-02-26 11:25:27 -05:00
Marshall Bowers
012aa33eca Adjust Kubernetes manifests for deploying API service (#8281)
This PR adjusts our Kubernetes manifests for deploying the new API
service.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-02-26 11:25:16 -05:00
Conrad Irwin
5e635264ef Add telemetry events backend for collab (#8220)
Send telemetry to collab not zed.dev

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-02-26 11:22:29 -05:00
Marshall Bowers
197a81285a Add SystemClock (#8239)
This PR adds a `SystemClock` trait for abstracting away the system
clock.

This allows us to swap out the real system clock with a
`FakeSystemClock` in the tests, thus allowing the fake passage of time.

We're using this in `Telemetry` to better mock the clock for testing
purposes.

Release Notes:

- N/A
2024-02-26 11:22:23 -05:00
Kirill Bulatov
a1e6258ce4 zed 0.124.5 2024-02-26 09:48:25 +02:00
Mikayla Maki
2b4e6e7828 Switch LSP prompts to use a non-blocking toast (#8312)
This fixes a major degradation in usability that some users ran into.

Fixes https://github.com/zed-industries/zed/issues/8255 
Fixes https://github.com/zed-industries/zed/issues/8229

Release Notes:

- Switch from using platform prompts to toasts for LSP prompts.
([8255](https://github.com/zed-industries/zed/issues/8255),
[8229](https://github.com/zed-industries/zed/issues/8229))

<img width="583" alt="Screenshot 2024-02-23 at 2 40 05 PM"
src="https://github.com/zed-industries/zed/assets/2280405/1bfc027b-b7a8-4563-88b6-020e47869668">

Co-authored-by: Marshall <marshall@zed.dev>
2024-02-24 21:47:19 +02:00
Conrad Irwin
b4be47ba1e zed 0.124.4 2024-02-23 11:06:10 -07:00
Conrad Irwin
dae95e03e6 Disable swift for now (#8291)
It causes segfaults on load

Release Notes:

- Fixed a segfault opening a Swift file with the Swift extension
installed.
2024-02-23 11:05:15 -07:00
Conrad Irwin
4faf3acf67 zed 0.124.3 2024-02-23 10:41:41 -07:00
Conrad Irwin
9e9ea6fcb4 Allow typing space in workspace::SendKeystrokes (#8288)
Fixes #8222

Release Notes:

- N/A
2024-02-23 10:40:56 -07:00
Uladzislau Kaminski
e3d5a0f649 Fix for toggles on the Welcome page (#8159)
Release Notes:

The issue is that when welcome page appears settings.json file is not
created yet. So the idea of this fix is to create the file in case it is
not there yet.

- Fixed the toggles on the welcome screen not working if no settings
file exists yet.
([#8153](https://github.com/zed-industries/zed/issues/8153)).

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
2024-02-23 18:24:46 +01:00
Conrad Irwin
bd317eea49 zed 0.124.2 2024-02-23 08:34:08 -07:00
Conrad Irwin
77cdc280c2 Fix a panic in the assistant panel (#8244)
Release Notes:

- Fixed a panic in the assistant panel when the app is shutting down.
2024-02-23 08:33:04 -07:00
Conrad Irwin
1ba33763a4 fix vim panics (#8245)
Release Notes:

- vim: Fixed a panic when using H/M/L when scrolled beyond the end of
the buffer
2024-02-23 08:33:04 -07:00
Thorsten Ball
292d32eb70 Pick up more home dir shell env when spawning (#8273)
Release Notes:

- Improved how Zed picks up shell environment when spawned.
2024-02-23 15:20:54 +01:00
Thorsten Ball
10df9dfca1 Detect and possibly use user-installed gopls / zls language servers (#8188)
After a lot of back-and-forth, this is a small attempt to implement
solutions (1) and (3) in
https://github.com/zed-industries/zed/issues/7902. The goal is to have a
minimal change that helps users get started with Zed, until we have
extensions ready.

Release Notes:

- Added detection of user-installed `gopls` to Go language server
adapter. If a user has `gopls` in `$PATH` when opening a worktree, it
will be used.
- Added detection of user-installed `zls` to Zig language server
adapter. If a user has `zls` in `$PATH` when opening a worktree, it will
be used.

Example:

I don't have `go` installed globally, but I do have `gopls`:

```
~ $ which go
go not found
~ $ which gopls
/Users/thorstenball/code/go/bin/gopls
```

But I do have `go` in a project's directory:

```
~/tmp/go-testing φ which go
/Users/thorstenball/.local/share/mise/installs/go/1.21.5/go/bin/go
~/tmp/go-testing φ which gopls
/Users/thorstenball/code/go/bin/gopls
```

With current Zed when I run `zed ~/tmp/go-testing`, I'd get the dreaded
error:

![screenshot-2024-02-23-11 14
08@2x](https://github.com/zed-industries/zed/assets/1185253/822ea59b-c63e-4102-a50e-75501cc4e0e3)

But with the changes in this PR, it works:

```
[2024-02-23T11:14:42+01:00 INFO  language::language_registry] starting language server "gopls", path: "/Users/thorstenball/tmp/go-testing", id: 1
[2024-02-23T11:14:42+01:00 INFO  language::language_registry] found user-installed language server for Go. path: "/Users/thorstenball/code/go/bin/gopls", arguments: ["-mode=stdio"]
[2024-02-23T11:14:42+01:00 INFO  lsp] starting language server. binary path: "/Users/thorstenball/code/go/bin/gopls", working directory: "/Users/thorstenball/tmp/go-testing", args: ["-mode=stdio"]
```

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-02-23 14:01:22 +01:00
Kirill Bulatov
57426b925e Ensure default prettier installs correctly when certain FS entries are missing (#8261)
Fixes https://github.com/zed-industries/zed/issues/7865

* bind default prettier (re)installation decision to
`prettier_server.js` existence
* ensure the `prettier_server.js` file is created last, after all
default prettier packages installed
* ensure that default prettier directory exists before installing the
packages
* reinstall default prettier if the `prettier_server.js` file is
different from what Zed expects

Release Notes:

- Fixed incorrect default prettier installation process
2024-02-23 12:30:08 +02:00
Kirill Bulatov
e12d617264 zed 0.124.1 2024-02-22 17:10:37 +02:00
Kirill Bulatov
ca1a95e6e2 Require prerelease eslint version (#8197)
Fixes https://github.com/zed-industries/zed/issues/7650

Release Notes:

- Fixed eslint diagnostics not showing up due to old eslint version used
2024-02-22 16:38:31 +02:00
Joseph T. Lyons
bc8e7e0cc7 v0.124.x preview 2024-02-21 12:22:24 -05:00
109 changed files with 2417 additions and 578 deletions

View File

@@ -120,6 +120,12 @@ jobs:
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
export ZED_SERVICE_NAME=collab
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/collab --watch
echo "deployed collab.template.yml to ${ZED_KUBE_NAMESPACE}"
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
export ZED_SERVICE_NAME=api
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"

144
Cargo.lock generated
View File

@@ -360,6 +360,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"telemetry_events",
"theme",
"tiktoken-rs",
"ui",
@@ -1187,7 +1188,7 @@ dependencies = [
"tokio",
"tokio-tungstenite",
"tower",
"tower-http",
"tower-http 0.3.5",
"tower-layer",
"tower-service",
]
@@ -1224,7 +1225,7 @@ dependencies = [
"serde_json",
"tokio",
"tower",
"tower-http",
"tower-http 0.3.5",
"tower-layer",
"tower-service",
]
@@ -1349,7 +1350,7 @@ dependencies = [
"rustc-hash",
"shlex",
"syn 2.0.48",
"which",
"which 4.4.2",
]
[[package]]
@@ -1901,6 +1902,49 @@ dependencies = [
"util",
]
[[package]]
name = "clickhouse"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f"
dependencies = [
"bstr",
"bytes 1.5.0",
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.28",
"hyper",
"hyper-tls",
"lz4",
"sealed",
"serde",
"static_assertions",
"thiserror",
"tokio",
"url",
]
[[package]]
name = "clickhouse-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18af5425854858c507eec70f7deb4d5d8cec4216fcb086283a78872387281ea5"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 1.0.109",
]
[[package]]
name = "clickhouse-rs-cityhash-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4baf9d4700a28d6cb600e17ed6ae2b43298a5245f1f76b4eab63027ebfd592b9"
dependencies = [
"cc",
]
[[package]]
name = "client"
version = "0.1.0"
@@ -1909,6 +1953,7 @@ dependencies = [
"async-recursion 0.3.2",
"async-tungstenite",
"chrono",
"clock",
"collections",
"db",
"feature_flags",
@@ -1932,6 +1977,7 @@ dependencies = [
"smol",
"sum_tree",
"sysinfo",
"telemetry_events",
"tempfile",
"text",
"thiserror",
@@ -1946,6 +1992,8 @@ dependencies = [
name = "clock"
version = "0.1.0"
dependencies = [
"chrono",
"parking_lot 0.11.2",
"smallvec",
]
@@ -2015,6 +2063,7 @@ dependencies = [
"channel",
"chrono",
"clap 3.2.25",
"clickhouse",
"client",
"clock",
"collab_ui",
@@ -2029,6 +2078,7 @@ dependencies = [
"futures 0.3.28",
"git",
"gpui",
"hex",
"hyper",
"indoc",
"language",
@@ -2059,8 +2109,10 @@ dependencies = [
"serde_json",
"settings",
"sha-1 0.9.8",
"sha2 0.10.7",
"smallvec",
"sqlx",
"telemetry_events",
"text",
"theme",
"time",
@@ -2069,6 +2121,7 @@ dependencies = [
"toml 0.8.10",
"tonic",
"tower",
"tower-http 0.4.4",
"tracing",
"tracing-log",
"tracing-subscriber",
@@ -4340,11 +4393,11 @@ dependencies = [
[[package]]
name = "home"
version = "0.5.5"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -5252,6 +5305,26 @@ dependencies = [
"url",
]
[[package]]
name = "lz4"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1"
dependencies = [
"libc",
"lz4-sys",
]
[[package]]
name = "lz4-sys"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -6915,6 +6988,7 @@ dependencies = [
"toml 0.8.10",
"unindent",
"util",
"which 6.0.0",
]
[[package]]
@@ -7024,7 +7098,7 @@ dependencies = [
"prost-types 0.9.0",
"regex",
"tempfile",
"which",
"which 4.4.2",
]
[[package]]
@@ -8172,6 +8246,18 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sealed"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c"
dependencies = [
"heck 0.3.3",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "search"
version = "0.1.0"
@@ -9371,6 +9457,14 @@ dependencies = [
"workspace",
]
[[package]]
name = "telemetry_events"
version = "0.1.0"
dependencies = [
"serde",
"util",
]
[[package]]
name = "tempfile"
version = "3.9.0"
@@ -9959,6 +10053,25 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.4.1",
"bytes 1.5.0",
"futures-core",
"futures-util",
"http 0.2.9",
"http-body",
"http-range-header",
"pin-project-lite 0.2.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
@@ -11396,6 +11509,19 @@ dependencies = [
"rustix 0.38.30",
]
[[package]]
name = "which"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.30",
"windows-sys 0.52.0",
]
[[package]]
name = "whoami"
version = "1.4.1"
@@ -11707,6 +11833,7 @@ dependencies = [
"bincode",
"call",
"client",
"clock",
"collections",
"db",
"derive_more",
@@ -11911,7 +12038,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.124.0"
version = "0.124.8"
dependencies = [
"activity_indicator",
"ai",
@@ -11931,6 +12058,7 @@ dependencies = [
"chrono",
"cli",
"client",
"clock",
"collab_ui",
"collections",
"command_palette",

View File

@@ -80,6 +80,7 @@ members = [
"crates/theme",
"crates/theme_importer",
"crates/theme_selector",
"crates/telemetry_events",
"crates/ui",
"crates/util",
"crates/vcs_menu",
@@ -172,6 +173,7 @@ text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
telemetry_events = { path ="crates/telemetry_events" }
ui = { path = "crates/ui" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
@@ -189,12 +191,14 @@ blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f394
blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
blade-rwh = { package = "raw-window-handle", version = "0.5" }
chrono = { version = "0.4", features = ["serde"] }
clickhouse = { version = "0.11.6" }
ctor = "0.2.6"
derive_more = "0.99.17"
env_logger = "0.9"
futures = "0.3"
git2 = { version = "0.15", default-features = false }
globset = "0.4"
hex = "0.4.3"
indoc = "1"
# We explicitly disable a http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
@@ -219,6 +223,7 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
serde_repr = "0.1"
sha2 = "0.10"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -228,6 +233,7 @@ thiserror = "1.0.29"
tiktoken-rs = "0.5.7"
time = { version = "0.3", features = ["serde", "serde-well-known", "formatting"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.20", features = ["wasm"] }
tree-sitter-astro = { git = "https://github.com/virchau13/tree-sitter-astro.git", rev = "e924787e12e8a03194f36a113290ac11d6dc10f3" }
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
@@ -278,6 +284,7 @@ unindent = "0.1.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
wasmtime = "16"
which = "6.0.0"
sys-locale = "0.3.1"
[patch.crates-io]

View File

@@ -1,3 +1,3 @@
collab: RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run --package=collab serve
collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve
livekit: livekit-server --dev
blob_store: MINIO_ROOT_USER=the-blob-store-access-key MINIO_ROOT_PASSWORD=the-blob-store-secret-key minio server .blob_store

View File

@@ -36,6 +36,7 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
ui.workspace = true

View File

@@ -15,7 +15,6 @@ use ai::{
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
use client::telemetry::AssistantKind;
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
actions::{MoveDown, MoveUp},
@@ -52,6 +51,7 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::AssistantKind;
use theme::ThemeSettings;
use ui::{
prelude::*,
@@ -122,16 +122,13 @@ impl AssistantPanel {
.await
.log_err()
.unwrap_or_default();
let (api_url, model_name) = cx
.update(|cx| {
let settings = AssistantSettings::get_global(cx);
(
settings.openai_api_url.clone(),
settings.default_open_ai_model.full_name().to_string(),
)
})
.log_err()
.unwrap();
let (api_url, model_name) = cx.update(|cx| {
let settings = AssistantSettings::get_global(cx);
(
settings.openai_api_url.clone(),
settings.default_open_ai_model.full_name().to_string(),
)
})?;
let completion_provider = OpenAiCompletionProvider::new(
api_url,
model_name,

View File

@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, TestAppContext};
use rpc::proto::{self};
use settings::SettingsStore;
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
release_channel::init("0.0.0", cx);
client::init_settings(cx);
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
client::init(&client, cx);

View File

@@ -10,10 +10,11 @@ path = "src/client.rs"
doctest = false
[features]
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
clock.workspace = true
collections.workspace = true
db.workspace = true
gpui.workspace = true
@@ -40,9 +41,10 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha2 = "0.10"
sha2.workspace = true
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
tempfile.workspace = true
thiserror.workspace = true
time.workspace = true
@@ -51,6 +53,7 @@ uuid.workspace = true
url.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }

View File

@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use clock::SystemClock;
use collections::HashMap;
use futures::{
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
@@ -45,7 +46,7 @@ use util::http::{HttpClient, ZedHttpClient};
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
pub use telemetry::Event;
pub use telemetry_events::Event;
pub use user::*;
lazy_static! {
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
}
impl Client {
pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
pub fn new(
clock: Arc<dyn SystemClock>,
http: Arc<ZedHttpClient>,
cx: &mut AppContext,
) -> Arc<Self> {
let client = Arc::new(Self {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(http.clone(), cx),
telemetry: Telemetry::new(clock, http.clone(), cx),
http,
state: Default::default(),
@@ -1455,6 +1460,7 @@ mod tests {
use super::*;
use crate::test::FakeServer;
use clock::FakeSystemClock;
use gpui::{BackgroundExecutor, Context, TestAppContext};
use parking_lot::Mutex;
use settings::SettingsStore;
@@ -1465,7 +1471,13 @@ mod tests {
async fn test_reconnection(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let mut status = client.status();
assert!(matches!(
@@ -1500,7 +1512,13 @@ mod tests {
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let mut status = client.status();
// Time out when client tries to connect.
@@ -1573,7 +1591,13 @@ mod tests {
init_test(cx);
let auth_count = Arc::new(Mutex::new(0));
let dropped_auth_count = Arc::new(Mutex::new(0));
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
client.override_authenticate({
let auth_count = auth_count.clone();
let dropped_auth_count = dropped_auth_count.clone();
@@ -1621,7 +1645,13 @@ mod tests {
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
@@ -1675,7 +1705,13 @@ mod tests {
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
@@ -1704,7 +1740,13 @@ mod tests {
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());

View File

@@ -2,12 +2,12 @@ mod event_coalescer;
use crate::TelemetrySettings;
use chrono::{DateTime, Utc};
use clock::SystemClock;
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use serde::Serialize;
use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::io::Write;
@@ -15,6 +15,10 @@ use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent,
};
use tempfile::NamedTempFile;
use util::http::{self, HttpClient, Method, ZedHttpClient};
#[cfg(not(debug_assertions))]
@@ -24,6 +28,7 @@ use util::TryFutureExt;
use self::event_coalescer::EventCoalescer;
pub struct Telemetry {
clock: Arc<dyn SystemClock>,
http_client: Arc<ZedHttpClient>,
executor: BackgroundExecutor,
state: Arc<Mutex<TelemetryState>>,
@@ -33,7 +38,7 @@ struct TelemetryState {
settings: TelemetrySettings,
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<Arc<str>>, // Per app launch
session_id: Option<String>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
@@ -46,93 +51,6 @@ struct TelemetryState {
max_queue_size: usize,
}
#[derive(Serialize, Debug)]
struct EventRequestBody {
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<String>,
os_name: &'static str,
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<EventWrapper>,
}
#[derive(Serialize, Debug)]
struct EventWrapper {
signed_in: bool,
#[serde(flatten)]
event: Event,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum Event {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
milliseconds_since_first_event: i64,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
milliseconds_since_first_event: i64,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
milliseconds_since_first_event: i64,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
milliseconds_since_first_event: i64,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
milliseconds_since_first_event: i64,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
milliseconds_since_first_event: i64,
},
App {
operation: String,
milliseconds_since_first_event: i64,
},
Setting {
setting: &'static str,
value: String,
milliseconds_since_first_event: i64,
},
Edit {
duration: i64,
environment: &'static str,
milliseconds_since_first_event: i64,
},
Action {
source: &'static str,
action: String,
milliseconds_since_first_event: i64,
},
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 5;
@@ -144,7 +62,6 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
@@ -156,7 +73,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
});
impl Telemetry {
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
pub fn new(
clock: Arc<dyn SystemClock>,
client: Arc<ZedHttpClient>,
cx: &mut AppContext,
) -> Arc<Self> {
let release_channel =
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
@@ -175,7 +96,7 @@ impl Telemetry {
log_file: None,
is_staff: None,
first_event_date_time: None,
event_coalescer: EventCoalescer::new(),
event_coalescer: EventCoalescer::new(clock.clone()),
max_queue_size: MAX_QUEUE_LEN,
}));
@@ -205,6 +126,7 @@ impl Telemetry {
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
clock,
http_client: client,
executor: cx.background_executor().clone(),
state,
@@ -311,14 +233,13 @@ impl Telemetry {
copilot_enabled: bool,
copilot_enabled_for_language: bool,
) {
let event = Event::Editor {
let event = Event::Editor(EditorEvent {
file_extension,
vim_mode,
operation,
operation: operation.into(),
copilot_enabled,
copilot_enabled_for_language,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
@@ -329,12 +250,11 @@ impl Telemetry {
suggestion_accepted: bool,
file_extension: Option<String>,
) {
let event = Event::Copilot {
let event = Event::Copilot(CopilotEvent {
suggestion_id,
suggestion_accepted,
file_extension,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
@@ -345,12 +265,11 @@ impl Telemetry {
kind: AssistantKind,
model: &'static str,
) {
let event = Event::Assistant {
let event = Event::Assistant(AssistantEvent {
conversation_id,
kind,
model,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
model: model.to_string(),
});
self.report_event(event)
}
@@ -361,22 +280,20 @@ impl Telemetry {
room_id: Option<u64>,
channel_id: Option<u64>,
) {
let event = Event::Call {
operation,
let event = Event::Call(CallEvent {
operation: operation.to_string(),
room_id,
channel_id,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
let event = Event::Cpu {
let event = Event::Cpu(CpuEvent {
usage_as_percentage,
core_count,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
@@ -386,28 +303,16 @@ impl Telemetry {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
) {
let event = Event::Memory {
let event = Event::Memory(MemoryEvent {
memory_in_bytes,
virtual_memory_in_bytes,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
pub fn report_app_event(self: &Arc<Self>, operation: String) {
self.report_app_event_with_date_time(operation, Utc::now());
}
fn report_app_event_with_date_time(
self: &Arc<Self>,
operation: String,
date_time: DateTime<Utc>,
) -> Event {
let event = Event::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
};
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
let event = Event::App(AppEvent { operation });
self.report_event(event.clone());
@@ -415,11 +320,10 @@ impl Telemetry {
}
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting {
setting,
let event = Event::Setting(SettingEvent {
setting: setting.to_string(),
value,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
@@ -430,40 +334,24 @@ impl Telemetry {
drop(state);
if let Some((start, end, environment)) = period_data {
let event = Event::Edit {
let event = Event::Edit(EditEvent {
duration: end.timestamp_millis() - start.timestamp_millis(),
environment,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
environment: environment.to_string(),
});
self.report_event(event);
}
}
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
let event = Event::Action {
source,
let event = Event::Action(ActionEvent {
source: source.to_string(),
action,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
};
});
self.report_event(event)
}
fn milliseconds_since_first_event(self: &Arc<Self>, date_time: DateTime<Utc>) -> i64 {
let mut state = self.state.lock();
match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
None => {
state.first_event_date_time = Some(date_time);
0
}
}
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();
@@ -480,8 +368,24 @@ impl Telemetry {
}));
}
let date_time = self.clock.utc_now();
let milliseconds_since_first_event = match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
None => {
state.first_event_date_time = Some(date_time);
0
}
};
let signed_in = state.metrics_id.is_some();
state.events_queue.push(EventWrapper { signed_in, event });
state.events_queue.push(EventWrapper {
signed_in,
milliseconds_since_first_event,
event,
});
if state.installation_id.is_some() {
if state.events_queue.len() >= state.max_queue_size {
@@ -536,21 +440,22 @@ impl Telemetry {
{
let state = this.state.lock();
let request_body = EventRequestBody {
installation_id: state.installation_id.clone(),
installation_id: state.installation_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state
.app_metadata
.app_version
.map(|version| version.to_string()),
os_name: state.app_metadata.os_name,
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture,
architecture: state.architecture.to_string(),
release_channel: state.release_channel,
release_channel: state.release_channel.map(Into::into),
events,
};
json_bytes.clear();
@@ -569,7 +474,7 @@ impl Telemetry {
let request = http::Request::builder()
.method(Method::POST)
.uri(&this.http_client.zed_url("/api/events"))
.uri(this.http_client.zed_api_url("/telemetry/events"))
.header("Content-Type", "text/plain")
.header("x-zed-checksum", checksum)
.body(json_bytes.into());
@@ -590,35 +495,37 @@ impl Telemetry {
mod tests {
use super::*;
use chrono::TimeZone;
use clock::FakeSystemClock;
use gpui::TestAppContext;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let first_date_time = clock.utc_now();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
Event::App(AppEvent {
operation: operation.clone(),
milliseconds_since_first_event: 0
}
})
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -627,15 +534,14 @@ mod tests {
Some(first_date_time)
);
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
Event::App(AppEvent {
operation: operation.clone(),
milliseconds_since_first_event: 100
}
})
);
assert_eq!(telemetry.state.lock().events_queue.len(), 2);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -644,15 +550,14 @@ mod tests {
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
Event::App(AppEvent {
operation: operation.clone(),
milliseconds_since_first_event: 200
}
})
);
assert_eq!(telemetry.state.lock().events_queue.len(), 3);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -661,16 +566,15 @@ mod tests {
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
// Adding a 4th event should cause a flush
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
Event::App(AppEvent {
operation: operation.clone(),
milliseconds_since_first_event: 300
}
})
);
assert!(is_empty_state(&telemetry));
@@ -680,28 +584,29 @@ mod tests {
#[gpui::test]
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let first_date_time = clock.utc_now();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
Event::App(AppEvent {
operation: operation.clone(),
milliseconds_since_first_event: 0
}
})
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());

View File

@@ -1,6 +1,9 @@
use chrono::{DateTime, Duration, Utc};
use std::sync::Arc;
use std::time;
use chrono::{DateTime, Duration, Utc};
use clock::SystemClock;
const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1);
@@ -12,30 +15,20 @@ struct PeriodData {
}
pub struct EventCoalescer {
clock: Arc<dyn SystemClock>,
state: Option<PeriodData>,
}
impl EventCoalescer {
pub fn new() -> Self {
Self { state: None }
pub fn new(clock: Arc<dyn SystemClock>) -> Self {
Self { clock, state: None }
}
pub fn log_event(
&mut self,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
self.log_event_with_time(Utc::now(), environment)
}
// pub fn close_current_period(&mut self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
// self.environment.map(|env| self.log_event(env)).flatten()
// }
fn log_event_with_time(
&mut self,
log_time: DateTime<Utc>,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
let log_time = self.clock.utc_now();
let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
let Some(state) = &mut self.state else {
@@ -78,18 +71,22 @@ impl EventCoalescer {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use clock::FakeSystemClock;
use super::*;
#[test]
fn test_same_context_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new();
let mut event_coalescer = EventCoalescer::new(clock.clone());
assert_eq!(event_coalescer.state, None);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -102,12 +99,12 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let mut period_end = period_start;
// Ensure that many calls within the timeout don't start a new period
for _ in 0..100 {
period_end += within_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
clock.advance(within_timeout_adjustment);
let period_data = event_coalescer.log_event(environment_1);
let period_end = clock.utc_now();
assert_eq!(period_data, None);
assert_eq!(
@@ -120,10 +117,12 @@ mod tests {
);
}
let period_end = clock.utc_now();
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
// Logging an event exceeding the timeout should start a new period
let new_period_start = period_end + exceed_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(new_period_start, environment_1);
clock.advance(exceed_timeout_adjustment);
let new_period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -138,13 +137,16 @@ mod tests {
#[test]
fn test_different_environment_under_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new();
let mut event_coalescer = EventCoalescer::new(clock.clone());
assert_eq!(event_coalescer.state, None);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -157,8 +159,9 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let period_end = period_start + within_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -170,10 +173,12 @@ mod tests {
})
);
clock.advance(within_timeout_adjustment);
// Logging an event within the timeout but with a different environment should start a new period
let period_end = period_end + within_timeout_adjustment;
let period_end = clock.utc_now();
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
let period_data = event_coalescer.log_event(environment_2);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -188,13 +193,16 @@ mod tests {
#[test]
fn test_switching_environment_while_within_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new();
let mut event_coalescer = EventCoalescer::new(clock.clone());
assert_eq!(event_coalescer.state, None);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -207,9 +215,10 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let period_end = period_start + within_timeout_adjustment;
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
let period_data = event_coalescer.log_event(environment_2);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -221,22 +230,26 @@ mod tests {
})
);
}
// // 0 20 40 60
// // |-------------------|-------------------|-------------------|-------------------
// // |--------|----------env change
// // |-------------------
// // |period_start |period_end
// // |new_period_start
// 0 20 40 60
// |-------------------|-------------------|-------------------|-------------------
// |--------|----------env change
// |-------------------
// |period_start |period_end
// |new_period_start
#[test]
fn test_switching_environment_while_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new();
let mut event_coalescer = EventCoalescer::new(clock.clone());
assert_eq!(event_coalescer.state, None);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -249,9 +262,10 @@ mod tests {
);
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
let period_end = period_start + exceed_timeout_adjustment;
clock.advance(exceed_timeout_adjustment);
let period_end = clock.utc_now();
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
let period_data = event_coalescer.log_event(environment_2);
assert_eq!(
period_data,
@@ -270,6 +284,7 @@ mod tests {
})
);
}
// 0 20 40 60
// |-------------------|-------------------|-------------------|-------------------
// |--------|----------------------------------------env change

View File

@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
path = "src/clock.rs"
doctest = false
[features]
test-support = ["dep:parking_lot"]
[dependencies]
chrono.workspace = true
parking_lot = { workspace = true, optional = true }
smallvec.workspace = true

View File

@@ -1,13 +1,17 @@
mod system_clock;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
fmt, iter,
};
/// A unique identifier for each distributed node
pub use system_clock::*;
/// A unique identifier for each distributed node.
pub type ReplicaId = u16;
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp),
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
pub type Seq = u32;
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
@@ -18,7 +22,7 @@ pub struct Lamport {
pub value: Seq,
}
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock)
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
#[derive(Clone, Default, Hash, Eq, PartialEq)]
pub struct Global(SmallVec<[u32; 8]>);

View File

@@ -0,0 +1,59 @@
use chrono::{DateTime, Utc};
pub trait SystemClock: Send + Sync {
/// Returns the current date and time in UTC.
fn utc_now(&self) -> DateTime<Utc>;
}
pub struct RealSystemClock;
impl SystemClock for RealSystemClock {
fn utc_now(&self) -> DateTime<Utc> {
Utc::now()
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeSystemClockState {
now: DateTime<Utc>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeSystemClock {
// Use an unfair lock to ensure tests are deterministic.
state: parking_lot::Mutex<FakeSystemClockState>,
}
#[cfg(any(test, feature = "test-support"))]
impl Default for FakeSystemClock {
fn default() -> Self {
Self::new(Utc::now())
}
}
#[cfg(any(test, feature = "test-support"))]
impl FakeSystemClock {
pub fn new(now: DateTime<Utc>) -> Self {
let state = FakeSystemClockState { now };
Self {
state: parking_lot::Mutex::new(state),
}
}
pub fn set_now(&self, now: DateTime<Utc>) {
self.state.lock().now = now;
}
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
pub fn advance(&self, duration: chrono::Duration) {
self.state.lock().now += duration;
}
}
#[cfg(any(test, feature = "test-support"))]
impl SystemClock for FakeSystemClock {
fn utc_now(&self) -> DateTime<Utc> {
self.state.lock().now
}
}

View File

@@ -12,6 +12,12 @@ BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
BLOB_STORE_BUCKET = "the-extensions-bucket"
BLOB_STORE_URL = "http://127.0.0.1:9000"
BLOB_STORE_REGION = "the-region"
ZED_CLIENT_CHECKSUM_SEED = "development-checksum-seed"
# CLICKHOUSE_URL = ""
# CLICKHOUSE_USER = "default"
# CLICKHOUSE_PASSWORD = ""
# CLICKHOUSE_DATABASE = "default"
# RUST_LOG=info
# LOG_JSON=true

View File

@@ -25,10 +25,12 @@ base64 = "0.13"
chrono.workspace = true
clap = { version = "3.1", features = ["derive"], optional = true }
clock.workspace = true
clickhouse.workspace = true
collections.workspace = true
dashmap = "5.4"
envy = "0.4.2"
futures.workspace = true
hex.workspace = true
hyper = "0.14"
lazy_static.workspace = true
lipsum = { version = "0.8", optional = true }
@@ -48,8 +50,10 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha-1 = "0.9"
sha2.workspace = true
smallvec.workspace = true
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
telemetry_events.workspace = true
text.workspace = true
time.workspace = true
tokio = { version = "1", features = ["full"] }
@@ -57,6 +61,7 @@ tokio-tungstenite = "0.17"
toml.workspace = true
tonic = "0.6"
tower = "0.4"
tower-http = { workspace = true, features = ["trace"] }
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }

View File

@@ -9,7 +9,7 @@ kind: Service
apiVersion: v1
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
name: collab
name: ${ZED_SERVICE_NAME}
annotations:
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
@@ -17,7 +17,7 @@ metadata:
spec:
type: LoadBalancer
selector:
app: collab
app: ${ZED_SERVICE_NAME}
ports:
- name: web
protocol: TCP
@@ -29,17 +29,17 @@ apiVersion: apps/v1
kind: Deployment
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
name: collab
name: ${ZED_SERVICE_NAME}
spec:
replicas: 1
selector:
matchLabels:
app: collab
app: ${ZED_SERVICE_NAME}
template:
metadata:
labels:
app: collab
app: ${ZED_SERVICE_NAME}
annotations:
ad.datadoghq.com/collab.check_names: |
["openmetrics"]
@@ -55,10 +55,11 @@ spec:
]
spec:
containers:
- name: collab
- name: ${ZED_SERVICE_NAME}
image: "${ZED_IMAGE_ID}"
args:
- serve
- ${ZED_SERVICE_NAME}
ports:
- containerPort: 8080
protocol: TCP
@@ -90,6 +91,11 @@ spec:
secretKeyRef:
name: api
key: token
- name: ZED_CLIENT_CHECKSUM_SEED
valueFrom:
secretKeyRef:
name: zed-client
key: checksum-seed
- name: LIVE_KIT_SERVER
valueFrom:
secretKeyRef:
@@ -130,6 +136,26 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: CLICKHOUSE_URL
valueFrom:
secretKeyRef:
name: clickhouse
key: url
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse
key: user
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse
key: password
- name: CLICKHOUSE_DATABASE
valueFrom:
secretKeyRef:
name: clickhouse
key: database
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE

View File

@@ -1,4 +1,5 @@
mod extensions;
pub mod events;
pub mod extensions;
use crate::{
auth,
@@ -24,7 +25,7 @@ use tracing::instrument;
pub use extensions::fetch_extensions_from_blob_store_periodically;
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Router<Body> {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users/:id/access_tokens", post(create_access_token))
@@ -32,7 +33,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.route("/contributors", get(get_contributors).post(add_contributor))
.route("/contributor", get(check_is_contributor))
.merge(extensions::router())
.layer(
ServiceBuilder::new()
.layer(Extension(state))
@@ -135,8 +135,12 @@ async fn trace_panic(panic: Json<Panic>) -> Result<()> {
}
async fn get_rpc_server_snapshot(
Extension(rpc_server): Extension<Arc<rpc::Server>>,
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
) -> Result<ErasedJson> {
let Some(rpc_server) = rpc_server else {
return Err(Error::Internal(anyhow!("rpc server is not available")));
};
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
}

View File

@@ -0,0 +1,805 @@
use std::sync::Arc;
use anyhow::{anyhow, Context};
use axum::{
body::Bytes, headers::Header, http::HeaderName, routing::post, Extension, Router, TypedHeader,
};
use hyper::StatusCode;
use lazy_static::lazy_static;
use serde::{Serialize, Serializer};
use sha2::{Digest, Sha256};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent,
};
use crate::{AppState, Error, Result};
pub fn router() -> Router {
Router::new().route("/telemetry/events", post(post_events))
}
lazy_static! {
static ref ZED_CHECKSUM_HEADER: HeaderName = HeaderName::from_static("x-zed-checksum");
static ref CLOUDFLARE_IP_COUNTRY_HEADER: HeaderName = HeaderName::from_static("cf-ipcountry");
}
pub struct ZedChecksumHeader(Vec<u8>);
impl Header for ZedChecksumHeader {
fn name() -> &'static HeaderName {
&ZED_CHECKSUM_HEADER
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let checksum = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(bytes))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
pub struct CloudflareIpCountryHeader(String);
impl Header for CloudflareIpCountryHeader {
fn name() -> &'static HeaderName {
&CLOUDFLARE_IP_COUNTRY_HEADER
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let country_code = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(country_code.to_string()))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
body: Bytes,
) -> Result<()> {
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
Err(Error::Http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
return Err(Error::Http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&body);
summer.update(checksum_seed);
if &checksum[..] != &summer.finalize()[..] {
return Err(Error::Http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let request_body: telemetry_events::EventRequestBody =
serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse event json: {err}");
Error::Internal(anyhow!(err))
})?;
let mut to_upload = ToUpload::default();
let Some(last_event) = request_body.events.last() else {
return Err(Error::Http(StatusCode::BAD_REQUEST, "no events".into()))?;
};
let country_code = country_code_header.map(|h| h.0 .0);
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
for wrapper in &request_body.events {
match &wrapper.event {
Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
)),
Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
)),
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Assistant(event) => {
to_upload
.assistant_events
.push(AssistantEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
))
}
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
}
}
to_upload
.upload(&clickhouse_client)
.await
.map_err(|err| Error::Internal(anyhow!(err)))?;
Ok(())
}
#[derive(Default)]
struct ToUpload {
editor_events: Vec<EditorEventRow>,
copilot_events: Vec<CopilotEventRow>,
assistant_events: Vec<AssistantEventRow>,
call_events: Vec<CallEventRow>,
cpu_events: Vec<CpuEventRow>,
memory_events: Vec<MemoryEventRow>,
app_events: Vec<AppEventRow>,
setting_events: Vec<SettingEventRow>,
edit_events: Vec<EditEventRow>,
action_events: Vec<ActionEventRow>,
}
impl ToUpload {
pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
Self::upload_to_table("editor_events", &self.editor_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'editor_events'"))?;
Self::upload_to_table("copilot_events", &self.copilot_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'copilot_events'"))?;
Self::upload_to_table(
"assistant_events",
&self.assistant_events,
clickhouse_client,
)
.await
.with_context(|| format!("failed to upload to table 'assistant_events'"))?;
Self::upload_to_table("call_events", &self.call_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'call_events'"))?;
Self::upload_to_table("cpu_events", &self.cpu_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'cpu_events'"))?;
Self::upload_to_table("memory_events", &self.memory_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'memory_events'"))?;
Self::upload_to_table("app_events", &self.app_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'app_events'"))?;
Self::upload_to_table("setting_events", &self.setting_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'setting_events'"))?;
Self::upload_to_table("edit_events", &self.edit_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'edit_events'"))?;
Self::upload_to_table("action_events", &self.action_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'action_events'"))?;
Ok(())
}
async fn upload_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if !rows.is_empty() {
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
}
Ok(())
}
}
pub fn serialize_country_code<S>(country_code: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if country_code.len() != 2 {
use serde::ser::Error;
return Err(S::Error::custom(
"country_code must be exactly 2 characters",
));
}
let country_code = country_code.as_bytes();
serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16)
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditorEventRow {
pub installation_id: String,
pub operation: String,
pub app_version: String,
pub file_extension: String,
pub os_name: String,
pub os_version: String,
pub release_channel: String,
pub signed_in: bool,
pub vim_mode: bool,
#[serde(serialize_with = "serialize_country_code")]
pub country_code: String,
pub region_code: String,
pub city: String,
pub time: i64,
pub copilot_enabled: bool,
pub copilot_enabled_for_language: bool,
pub historical_event: bool,
pub architecture: String,
pub is_staff: Option<bool>,
pub session_id: Option<String>,
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl EditorEventRow {
fn from_event(
event: EditorEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
architecture: body.architecture.clone(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
file_extension: event.file_extension.unwrap_or_default(),
signed_in: wrapper.signed_in,
vim_mode: event.vim_mode,
copilot_enabled: event.copilot_enabled,
copilot_enabled_for_language: event.copilot_enabled_for_language,
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
historical_event: false,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct CopilotEventRow {
pub installation_id: String,
pub suggestion_id: String,
pub suggestion_accepted: bool,
pub app_version: String,
pub file_extension: String,
pub os_name: String,
pub os_version: String,
pub release_channel: String,
pub signed_in: bool,
#[serde(serialize_with = "serialize_country_code")]
pub country_code: String,
pub region_code: String,
pub city: String,
pub time: i64,
pub is_staff: Option<bool>,
pub session_id: Option<String>,
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl CopilotEventRow {
fn from_event(
event: CopilotEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
file_extension: event.file_extension.unwrap_or_default(),
signed_in: wrapper.signed_in,
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
suggestion_id: event.suggestion_id.unwrap_or_default(),
suggestion_accepted: event.suggestion_accepted,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct CallEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: String,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// CallEventRow
operation: String,
room_id: Option<u64>,
channel_id: Option<u64>,
}
impl CallEventRow {
fn from_event(
event: CallEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
room_id: event.room_id,
channel_id: event.channel_id,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct AssistantEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// AssistantEventRow
conversation_id: String,
kind: String,
model: String,
}
impl AssistantEventRow {
fn from_event(
event: AssistantEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
conversation_id: event.conversation_id.unwrap_or_default(),
kind: event.kind.to_string(),
model: event.model,
}
}
}
#[derive(Debug, clickhouse::Row, Serialize)]
pub struct CpuEventRow {
pub installation_id: Option<String>,
pub is_staff: Option<bool>,
pub usage_as_percentage: f32,
pub core_count: u32,
pub app_version: String,
pub release_channel: String,
pub time: i64,
pub session_id: Option<String>,
// pub normalized_cpu_usage: f64, MATERIALIZED
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl CpuEventRow {
fn from_event(
event: CpuEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
usage_as_percentage: event.usage_as_percentage,
core_count: event.core_count,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct MemoryEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// MemoryEventRow
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
}
impl MemoryEventRow {
fn from_event(
event: MemoryEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
memory_in_bytes: event.memory_in_bytes,
virtual_memory_in_bytes: event.virtual_memory_in_bytes,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct AppEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// AppEventRow
operation: String,
}
impl AppEventRow {
fn from_event(
event: AppEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct SettingEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// SettingEventRow
setting: String,
value: String,
}
impl SettingEventRow {
fn from_event(
event: SettingEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
setting: event.setting,
value: event.value,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// SystemInfoBase
os_name: String,
os_version: Option<String>,
architecture: String,
// ClientEventBase
installation_id: Option<String>,
// Note: This column name has a typo in the ClickHouse table.
#[serde(rename = "sesssion_id")]
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// EditEventRow
period_start: i64,
period_end: i64,
environment: String,
}
impl EditEventRow {
fn from_event(
event: EditEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
let period_start = time - chrono::Duration::milliseconds(event.duration);
let period_end = time;
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone(),
architecture: body.architecture.clone(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
period_start: period_start.timestamp_millis(),
period_end: period_end.timestamp_millis(),
environment: event.environment,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct ActionEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
// Note: This column name has a typo in the ClickHouse table.
#[serde(rename = "sesssion_id")]
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// ActionEventRow
source: String,
action: String,
}
impl ActionEventRow {
fn from_event(
event: ActionEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
source: event.source,
action: event.action,
}
}
}

View File

@@ -58,11 +58,24 @@ impl From<serde_json::Error> for Error {
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
match self {
Error::Http(code, message) => (code, message).into_response(),
Error::Http(code, message) => {
log::error!("HTTP error {}: {}", code, &message);
(code, message).into_response()
}
Error::Database(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
Error::Internal(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
}
@@ -97,6 +110,10 @@ pub struct Config {
pub database_url: String,
pub database_max_connections: u32,
pub api_token: String,
pub clickhouse_url: Option<String>,
pub clickhouse_user: Option<String>,
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
@@ -109,6 +126,7 @@ pub struct Config {
pub blob_store_secret_key: Option<String>,
pub blob_store_bucket: Option<String>,
pub zed_environment: Arc<str>,
pub zed_client_checksum_seed: Option<String>,
}
impl Config {
@@ -127,6 +145,7 @@ pub struct AppState {
pub db: Arc<Database>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub clickhouse_client: Option<clickhouse::Client>,
pub config: Config,
}
@@ -156,6 +175,7 @@ impl AppState {
db: Arc::new(db),
live_kit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
clickhouse_client: build_clickhouse_client(&config).log_err(),
config,
};
Ok(Arc::new(this))
@@ -196,3 +216,31 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
Ok(aws_sdk_s3::Client::new(&s3_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<clickhouse::Client> {
Ok(clickhouse::Client::default()
.with_url(
config
.clickhouse_url
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_url"))?,
)
.with_user(
config
.clickhouse_user
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_user"))?,
)
.with_password(
config
.clickhouse_password
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_password"))?,
)
.with_database(
config
.clickhouse_database
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_database"))?,
))
}

View File

@@ -1,10 +1,11 @@
use anyhow::anyhow;
use axum::{routing::get, Extension, Router};
use axum::{extract::MatchedPath, routing::get, Extension, Router};
use collab::{
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
Config, MigrateConfig, Result,
};
use db::Database;
use hyper::Request;
use std::{
env::args,
net::{SocketAddr, TcpListener},
@@ -12,6 +13,8 @@ use std::{
sync::Arc,
};
use tokio::signal::unix::SignalKind;
use tower_http::trace::{self, TraceLayer};
use tracing::Level;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
@@ -28,7 +31,8 @@ async fn main() -> Result<()> {
);
}
match args().skip(1).next().as_deref() {
let mut args = args().skip(1);
match args.next().as_deref() {
Some("version") => {
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
}
@@ -36,6 +40,17 @@ async fn main() -> Result<()> {
run_migrations().await?;
}
Some("serve") => {
let (is_api, is_collab) = if let Some(next) = args.next() {
(next == "api", next == "collab")
} else {
(true, true)
};
if !is_api && !is_collab {
Err(anyhow!(
"usage: collab <version | migrate | serve [api|collab]>"
))?;
}
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
@@ -46,22 +61,52 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
let rpc_server = if is_collab {
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server =
collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
Some(rpc_server)
} else {
None
};
let app = collab::api::routes(rpc_server.clone(), state.clone())
.merge(collab::rpc::routes(rpc_server.clone()))
if is_api {
fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
}
let mut app = collab::api::routes(rpc_server.clone(), state.clone());
if let Some(rpc_server) = rpc_server.clone() {
app = app.merge(collab::rpc::routes(rpc_server))
}
app = app
.merge(
Router::new()
.route("/", get(handle_root))
.route("/healthz", get(handle_liveness_probe))
.merge(collab::api::extensions::router())
.merge(collab::api::events::router())
.layer(Extension(state.clone())),
)
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
)
})
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
);
axum::Server::from_tcp(listener)?
@@ -76,12 +121,17 @@ async fn main() -> Result<()> {
futures::pin_mut!(sigterm, sigint);
futures::future::select(sigterm, sigint).await;
tracing::info!("Received interrupt signal");
rpc_server.teardown();
if let Some(rpc_server) = rpc_server {
rpc_server.teardown();
}
})
.await?;
}
_ => {
Err(anyhow!("usage: collab <version | migrate | serve>"))?;
Err(anyhow!(
"usage: collab <version | migrate | serve [api|collab]>"
))?;
}
}
Ok(())

View File

@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
use client::{
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use clock::FakeSystemClock;
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use fs::FakeFs;
@@ -163,6 +164,7 @@ impl TestServer {
client::init_settings(cx);
});
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
{
@@ -185,7 +187,7 @@ impl TestServer {
.user_id
};
let client_name = name.to_string();
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
let server = self.server.clone();
let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone();
@@ -480,6 +482,7 @@ impl TestServer {
db: test_db.db().clone(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
blob_store_client: None,
clickhouse_client: None,
config: Config {
http_port: 0,
database_url: "".into(),
@@ -497,6 +500,11 @@ impl TestServer {
blob_store_access_key: None,
blob_store_secret_key: None,
blob_store_bucket: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
zed_client_checksum_seed: None,
},
})
}

View File

@@ -204,7 +204,7 @@ impl ChatPanel {
let panel = Self::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.width = serialized_panel.width.map(|r| r.round());
cx.notify();
});
}

View File

@@ -385,6 +385,7 @@ impl Render for MessageEditor {
mod tests {
use super::*;
use client::{Client, User, UserStore};
use clock::FakeSystemClock;
use gpui::TestAppContext;
use language::{Language, LanguageConfig};
use rpc::proto;
@@ -455,8 +456,9 @@ mod tests {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);

View File

@@ -323,7 +323,7 @@ impl CollabPanel {
let panel = CollabPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.width = serialized_panel.width.map(|w| w.round());
panel.collapsed_channels = serialized_panel
.collapsed_channels
.unwrap_or_else(|| Vec::new());

View File

@@ -183,7 +183,7 @@ impl NotificationPanel {
let panel = Self::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.width = serialized_panel.width.map(|w| w.round());
cx.notify();
});
}

View File

@@ -428,6 +428,8 @@ impl Copilot {
let binary = LanguageServerBinary {
path: node_path,
arguments,
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
env: None,
};
let server = LanguageServer::new(

View File

@@ -714,6 +714,11 @@ impl EditorElement {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_top = scroll_position.y * line_height;
if bounds.contains(&cx.mouse_position()) {
let stacking_order = cx.stacking_order().clone();
cx.set_cursor_style(CursorStyle::Arrow, stacking_order);
}
let show_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
@@ -904,7 +909,7 @@ impl EditorElement {
bounds: text_bounds,
stacking_order: cx.stacking_order().clone(),
};
if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
if text_bounds.contains(&cx.mouse_position()) {
if self
.editor
.read(cx)
@@ -912,9 +917,15 @@ impl EditorElement {
.as_ref()
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
{
cx.set_cursor_style(CursorStyle::PointingHand);
cx.set_cursor_style(
CursorStyle::PointingHand,
interactive_text_bounds.stacking_order.clone(),
);
} else {
cx.set_cursor_style(CursorStyle::IBeam);
cx.set_cursor_style(
CursorStyle::IBeam,
interactive_text_bounds.stacking_order.clone(),
);
}
}
@@ -1538,8 +1549,11 @@ impl EditorElement {
stacking_order: cx.stacking_order().clone(),
};
let mut mouse_position = cx.mouse_position();
if interactive_track_bounds.visibly_contains(&mouse_position, cx) {
cx.set_cursor_style(CursorStyle::Arrow);
if track_bounds.contains(&mouse_position) {
cx.set_cursor_style(
CursorStyle::Arrow,
interactive_track_bounds.stacking_order.clone(),
);
}
cx.on_mouse_event({

View File

@@ -1,7 +1,6 @@
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::ClientSettings;
use collections::{BTreeMap, HashSet};
use fs::{Fs, RemoveOptions};
use futures::channel::mpsc::unbounded;
@@ -13,7 +12,6 @@ use language::{
};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::cmp::Ordering;
use std::{
ffi::OsStr,
@@ -22,7 +20,7 @@ use std::{
time::Duration,
};
use theme::{ThemeRegistry, ThemeSettings};
use util::http::AsyncBody;
use util::http::{AsyncBody, ZedHttpClient};
use util::TryFutureExt;
use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
@@ -57,7 +55,7 @@ pub enum ExtensionStatus {
pub struct ExtensionStore {
manifest: Arc<RwLock<Manifest>>,
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
http_client: Arc<ZedHttpClient>,
extensions_dir: PathBuf,
extensions_being_installed: HashSet<Arc<str>>,
extensions_being_uninstalled: HashSet<Arc<str>>,
@@ -113,7 +111,7 @@ actions!(zed, [ReloadExtensions]);
pub fn init(
fs: Arc<fs::RealFs>,
http_client: Arc<dyn HttpClient>,
http_client: Arc<ZedHttpClient>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut AppContext,
@@ -145,7 +143,7 @@ impl ExtensionStore {
pub fn new(
extensions_dir: PathBuf,
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
http_client: Arc<ZedHttpClient>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut ModelContext<Self>,
@@ -224,14 +222,12 @@ impl ExtensionStore {
search: Option<&str>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Extension>>> {
let url = format!(
"{}/{}{query}",
ClientSettings::get_global(cx).server_url,
"api/extensions",
let url = self.http_client.zed_api_url(&format!(
"/extensions{query}",
query = search
.map(|search| format!("?filter={search}"))
.unwrap_or_default()
);
));
let http_client = self.http_client.clone();
cx.spawn(move |_, _| async move {
let mut response = http_client.get(&url, AsyncBody::empty(), true).await?;
@@ -264,10 +260,9 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>,
) {
log::info!("installing extension {extension_id} {version}");
let url = format!(
"{}/api/extensions/{extension_id}/{version}/download",
ClientSettings::get_global(cx).server_url
);
let url = self
.http_client
.zed_api_url(&format!("/extensions/{extension_id}/{version}/download"));
let extensions_dir = self.extensions_dir();
let http_client = self.http_client.clone();
@@ -406,6 +401,10 @@ impl ExtensionStore {
}));
for language_name in &languages_to_add {
if language_name.as_ref() == "Swift" {
continue;
}
let language = manifest.languages.get(language_name.as_ref()).unwrap();
let mut language_path = self.extensions_dir.clone();
language_path.extend([language.extension.as_ref(), language.path.as_path()]);

View File

@@ -1221,7 +1221,8 @@ pub struct InteractiveBounds {
}
impl InteractiveBounds {
/// Checks whether this point was inside these bounds, and that these bounds where the topmost layer
/// Checks whether this point was inside these bounds in the rendered frame, and that these bounds where the topmost layer
/// Never call this during paint to perform hover calculations. It will reference the previous frame and could cause flicker.
pub fn visibly_contains(&self, point: &Point<Pixels>, cx: &WindowContext) -> bool {
self.bounds.contains(point) && cx.was_top_layer(point, &self.stacking_order)
}
@@ -1449,11 +1450,12 @@ impl Interactivity {
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
let mouse_position = &cx.mouse_position();
let hovered =
interactive_bounds.visibly_contains(mouse_position, cx);
let hovered = bounds.contains(&cx.mouse_position());
if hovered {
cx.set_cursor_style(mouse_cursor);
cx.set_cursor_style(
mouse_cursor,
interactive_bounds.stacking_order.clone(),
);
}
}
}
@@ -1955,9 +1957,7 @@ impl Interactivity {
if let Some(group_bounds) =
GroupBounds::get(&group_hover.group, cx.deref_mut())
{
if group_bounds.contains(&mouse_position)
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
{
if group_bounds.contains(&mouse_position) {
style.refine(&group_hover.style);
}
}
@@ -1967,7 +1967,6 @@ impl Interactivity {
if bounds
.intersect(&cx.content_mask().bounds)
.contains(&mouse_position)
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
{
style.refine(hover_style);
}

View File

@@ -427,9 +427,9 @@ impl Element for InteractiveText {
.clickable_ranges
.iter()
.any(|range| range.contains(&ix))
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
{
cx.set_cursor_style(crate::CursorStyle::PointingHand)
let stacking_order = cx.stacking_order().clone();
cx.set_cursor_style(crate::CursorStyle::PointingHand, stacking_order);
}
}

View File

@@ -192,8 +192,8 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
fn set_graphics_profiler_enabled(&self, enabled: bool);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {

View File

@@ -108,6 +108,35 @@ impl Keystroke {
ime_key,
})
}
/// Returns a new keystroke with the ime_key filled.
/// This is used for dispatch_keystroke where we want users to
/// be able to simulate typing "space", etc.
pub fn with_simulated_ime(mut self) -> Self {
if self.ime_key.is_none()
&& !self.modifiers.command
&& !self.modifiers.control
&& !self.modifiers.function
&& !self.modifiers.alt
{
self.ime_key = match self.key.as_str() {
"space" => Some(" ".into()),
"tab" => Some("\t".into()),
"enter" => Some("\n".into()),
"up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end"
| "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6"
| "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None,
key => {
if self.modifiers.shift {
Some(key.to_uppercase())
} else {
Some(key.into())
}
}
}
}
self
}
}
impl std::fmt::Display for Keystroke {

View File

@@ -390,10 +390,6 @@ impl PlatformWindow for WaylandWindow {
let inner = self.0.inner.lock();
inner.renderer.sprite_atlas().clone()
}
fn set_graphics_profiler_enabled(&self, enabled: bool) {
//todo!(linux)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]

View File

@@ -513,8 +513,4 @@ impl PlatformWindow for X11Window {
let inner = self.0.inner.lock();
inner.renderer.sprite_atlas().clone()
}
fn set_graphics_profiler_enabled(&self, enabled: bool) {
unimplemented!("linux")
}
}

View File

@@ -1060,27 +1060,6 @@ impl PlatformWindow for MacWindow {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.lock().renderer.sprite_atlas().clone()
}
/// Enables or disables the Metal HUD for debugging purposes. Note that this only works
/// when the app is bundled and it has the `MetalHudEnabled` key set to true in Info.plist.
fn set_graphics_profiler_enabled(&self, enabled: bool) {
let this_lock = self.0.lock();
let layer = this_lock.renderer.layer();
unsafe {
if enabled {
let hud_properties = NSDictionary::dictionaryWithObject_forKey_(
nil,
ns_string("default"),
ns_string("mode"),
);
let _: () = msg_send![layer, setDeveloperHUDProperties: hud_properties];
} else {
let _: () =
msg_send![layer, setDeveloperHUDProperties: NSDictionary::dictionary(nil)];
}
}
}
}
impl HasWindowHandle for MacWindow {

View File

@@ -251,8 +251,6 @@ impl PlatformWindow for TestWindow {
self.0.lock().sprite_atlas.clone()
}
fn set_graphics_profiler_enabled(&self, _enabled: bool) {}
fn as_test(&mut self) -> Option<&mut TestWindow> {
Some(self)
}

View File

@@ -280,7 +280,6 @@ pub struct Window {
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
graphics_profiler_enabled: bool,
}
#[derive(Default, Debug)]
@@ -474,7 +473,6 @@ impl Window {
focus: None,
focus_enabled: true,
pending_input: None,
graphics_profiler_enabled: false,
}
}
fn new_focus_listener(
@@ -1022,13 +1020,10 @@ impl<'a> WindowContext<'a> {
self.window.root_view = Some(root_view);
// Set the cursor only if we're the active window.
let cursor_style = self
.window
.next_frame
.requested_cursor_style
.take()
.unwrap_or(CursorStyle::Arrow);
let cursor_style_request = self.window.next_frame.requested_cursor_style.take();
if self.is_window_active() {
let cursor_style =
cursor_style_request.map_or(CursorStyle::Arrow, |request| request.style);
self.platform.set_cursor_style(cursor_style);
}
@@ -1101,18 +1096,8 @@ impl<'a> WindowContext<'a> {
/// Dispatch a given keystroke as though the user had typed it.
/// You can create a keystroke with Keystroke::parse("").
pub fn dispatch_keystroke(&mut self, mut keystroke: Keystroke) -> bool {
if keystroke.ime_key.is_none()
&& !keystroke.modifiers.command
&& !keystroke.modifiers.control
&& !keystroke.modifiers.function
{
keystroke.ime_key = Some(if keystroke.modifiers.shift {
keystroke.key.to_uppercase().clone()
} else {
keystroke.key.clone()
})
}
pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool {
let keystroke = keystroke.with_simulated_ime();
if self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
@@ -1512,14 +1497,6 @@ impl<'a> WindowContext<'a> {
}
}
/// Toggle the graphics profiler to debug your application's rendering performance.
pub fn toggle_graphics_profiler(&mut self) {
self.window.graphics_profiler_enabled = !self.window.graphics_profiler_enabled;
self.window
.platform_window
.set_graphics_profiler_enabled(self.window.graphics_profiler_enabled);
}
/// Register the given handler to be invoked whenever the global of the given type
/// is updated.
pub fn observe_global<G: Global>(

View File

@@ -51,6 +51,12 @@ pub(crate) struct TooltipRequest {
pub(crate) tooltip: AnyTooltip,
}
#[derive(Clone)]
pub(crate) struct CursorStyleRequest {
pub(crate) style: CursorStyle,
stacking_order: StackingOrder,
}
pub(crate) struct Frame {
pub(crate) focus: Option<FocusId>,
pub(crate) window_active: bool,
@@ -66,8 +72,8 @@ pub(crate) struct Frame {
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) requested_input_handler: Option<RequestedInputHandler>,
pub(crate) tooltip_request: Option<TooltipRequest>,
pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyle>,
pub(crate) requested_cursor_style: Option<CursorStyle>,
pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyleRequest>,
pub(crate) requested_cursor_style: Option<CursorStyleRequest>,
pub(crate) view_stack: Vec<EntityId>,
pub(crate) reused_views: FxHashSet<EntityId>,
@@ -346,9 +352,13 @@ impl<'a> ElementContext<'a> {
}
// Reuse the cursor styles previously requested during painting of the reused view.
if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) {
self.window.next_frame.cursor_styles.insert(view_id, style);
self.window.next_frame.requested_cursor_style = Some(style);
if let Some(cursor_style_request) =
self.window.rendered_frame.cursor_styles.remove(&view_id)
{
self.set_cursor_style(
cursor_style_request.style,
cursor_style_request.stacking_order,
);
}
}
@@ -387,10 +397,27 @@ impl<'a> ElementContext<'a> {
}
/// Updates the cursor style at the platform level.
pub fn set_cursor_style(&mut self, style: CursorStyle) {
pub fn set_cursor_style(&mut self, style: CursorStyle, stacking_order: StackingOrder) {
let view_id = self.parent_view_id();
self.window.next_frame.cursor_styles.insert(view_id, style);
self.window.next_frame.requested_cursor_style = Some(style);
let style_request = CursorStyleRequest {
style,
stacking_order,
};
if self
.window
.next_frame
.requested_cursor_style
.as_ref()
.map_or(true, |prev_style_request| {
style_request.stacking_order >= prev_style_request.stacking_order
})
{
self.window.next_frame.requested_cursor_style = Some(style_request.clone());
}
self.window
.next_frame
.cursor_styles
.insert(view_id, style_request);
}
/// Sets a tooltip to be rendered for the upcoming frame

View File

@@ -399,6 +399,8 @@ fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
("72", quote! { rems(18.) }, "288px (18rem)"),
("80", quote! { rems(20.) }, "320px (20rem)"),
("96", quote! { rems(24.) }, "384px (24rem)"),
("112", quote! { rems(28.) }, "448px (28rem)"),
("128", quote! { rems(32.) }, "512px (32rem)"),
("auto", quote! { auto() }, "Auto"),
("px", quote! { px(1.) }, "1px"),
("full", quote! { relative(1.) }, "100%"),

View File

@@ -97,14 +97,14 @@ fn test_select_language() {
// matching file extension
assert_eq!(
registry
.language_for_file("zed/lib.rs", None)
.language_for_file("zed/lib.rs".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Rust".into())
);
assert_eq!(
registry
.language_for_file("zed/lib.mk", None)
.language_for_file("zed/lib.mk".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@@ -113,7 +113,7 @@ fn test_select_language() {
// matching filename
assert_eq!(
registry
.language_for_file("zed/Makefile", None)
.language_for_file("zed/Makefile".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@@ -122,21 +122,21 @@ fn test_select_language() {
// matching suffix that is not the full file extension or filename
assert_eq!(
registry
.language_for_file("zed/cars", None)
.language_for_file("zed/cars".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
.language_for_file("zed/a.cars", None)
.language_for_file("zed/a.cars".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
.language_for_file("zed/sumk", None)
.language_for_file("zed/sumk".as_ref(), None)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None

View File

@@ -38,6 +38,7 @@ use serde_json::Value;
use std::{
any::Any,
cell::RefCell,
ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
@@ -140,6 +141,14 @@ impl CachedLspAdapter {
})
}
pub fn check_if_user_installed(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
self.adapter.check_if_user_installed(delegate, cx)
}
pub async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -240,6 +249,11 @@ impl CachedLspAdapter {
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
}
#[async_trait]
@@ -248,6 +262,14 @@ pub trait LspAdapter: 'static + Send + Sync {
fn short_name(&self) -> &'static str;
fn check_if_user_installed(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
None
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -1494,16 +1516,16 @@ mod tests {
});
languages
.language_for_file("the/script", None)
.language_for_file("the/script".as_ref(), None)
.await
.unwrap_err();
languages
.language_for_file("the/script", Some(&"nothing".into()))
.language_for_file("the/script".as_ref(), Some(&"nothing".into()))
.await
.unwrap_err();
assert_eq!(
languages
.language_for_file("the/script", Some(&"#!/bin/env node".into()))
.language_for_file("the/script".as_ref(), Some(&"#!/bin/env node".into()))
.await
.unwrap()
.name()

View File

@@ -7,7 +7,7 @@ use collections::{hash_map, HashMap};
use futures::{
channel::{mpsc, oneshot},
future::Shared,
FutureExt as _, TryFutureExt as _,
Future, FutureExt as _, TryFutureExt as _,
};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use lsp::{LanguageServerBinary, LanguageServerId};
@@ -24,7 +24,7 @@ use sum_tree::Bias;
use text::{Point, Rope};
use theme::Theme;
use unicase::UniCase;
use util::{paths::PathExt, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
use util::{paths::PathExt, post_inc, ResultExt};
pub struct LanguageRegistry {
state: RwLock<LanguageRegistryState>,
@@ -291,35 +291,36 @@ impl LanguageRegistry {
pub fn language_for_name(
self: &Arc<Self>,
name: &str,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
) -> impl Future<Output = Result<Arc<Language>>> {
let name = UniCase::new(name);
self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name)
let rx = self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name);
async move { rx.await? }
}
pub fn language_for_name_or_extension(
self: &Arc<Self>,
string: &str,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
) -> impl Future<Output = Result<Arc<Language>>> {
let string = UniCase::new(string);
self.get_or_load_language(|name, config| {
let rx = self.get_or_load_language(|name, config| {
UniCase::new(name) == string
|| config
.path_suffixes
.iter()
.any(|suffix| UniCase::new(suffix) == string)
})
});
async move { rx.await? }
}
pub fn language_for_file(
self: &Arc<Self>,
path: impl AsRef<Path>,
path: &Path,
content: Option<&Rope>,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
let path = path.as_ref();
) -> impl Future<Output = Result<Arc<Language>>> {
let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension_or_hidden_file_name();
let path_suffixes = [extension, filename];
self.get_or_load_language(|_, config| {
let rx = self.get_or_load_language(move |_, config| {
let path_matches = config
.path_suffixes
.iter()
@@ -334,13 +335,14 @@ impl LanguageRegistry {
},
);
path_matches || content_matches
})
});
async move { rx.await? }
}
fn get_or_load_language(
self: &Arc<Self>,
callback: impl Fn(&str, &LanguageMatcher) -> bool,
) -> UnwrapFuture<oneshot::Receiver<Result<Arc<Language>>>> {
) -> oneshot::Receiver<Result<Arc<Language>>> {
let (tx, rx) = oneshot::channel();
let mut state = self.state.write();
@@ -421,13 +423,13 @@ impl LanguageRegistry {
let _ = tx.send(Err(anyhow!("executor does not exist")));
}
rx.unwrap()
rx
}
fn get_or_load_grammar(
self: &Arc<Self>,
name: Arc<str>,
) -> UnwrapFuture<oneshot::Receiver<Result<tree_sitter::Language>>> {
) -> impl Future<Output = Result<tree_sitter::Language>> {
let (tx, rx) = oneshot::channel();
let mut state = self.state.write();
@@ -483,7 +485,7 @@ impl LanguageRegistry {
tx.send(Err(anyhow!("no such grammar {}", name))).ok();
}
rx.unwrap()
async move { rx.await? }
}
pub fn to_vec(&self) -> Vec<Arc<Language>> {
@@ -558,34 +560,41 @@ impl LanguageRegistry {
let task = {
let container_dir = container_dir.clone();
cx.spawn(move |mut cx| async move {
login_shell_env_loaded.await;
// First we check whether the adapter can give us a user-installed binary.
// If so, we do *not* want to cache that, because each worktree might give us a different
// binary:
//
// worktree 1: user-installed at `.bin/gopls`
// worktree 2: user-installed at `~/bin/gopls`
// worktree 3: no gopls found in PATH -> fallback to Zed installation
//
// We only want to cache when we fall back to the global one,
// because we don't want to download and overwrite our global one
// for each worktree we might have open.
let entry = this
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();
let user_binary_task = check_user_installed_binary(
adapter.clone(),
language.clone(),
delegate.clone(),
&mut cx,
);
let binary = if let Some(user_binary) = user_binary_task.await {
user_binary
} else {
// If we want to install a binary globally, we need to wait for
// the login shell to be set on our process.
login_shell_env_loaded.await;
let binary = match entry.await {
Ok(binary) => binary,
Err(err) => anyhow::bail!("{err}"),
get_or_install_binary(
this,
&adapter,
language,
&delegate,
&cx,
container_dir,
lsp_binary_statuses,
)
.await?
};
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
@@ -724,6 +733,62 @@ impl LspBinaryStatusSender {
}
}
async fn check_user_installed_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<LanguageServerBinary> {
let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
return None;
};
task.await.and_then(|binary| {
log::info!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
language.name(),
binary.path,
binary.arguments
);
Some(binary)
})
}
async fn get_or_install_binary(
registry: Arc<LanguageRegistry>,
adapter: &Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &AsyncAppContext,
container_dir: Arc<Path>,
lsp_binary_statuses: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let entry = registry
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();
entry.await.map_err(|err| anyhow!("{:?}", err))
}
async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
@@ -757,15 +822,20 @@ async fn get_binary(
.await
{
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
return Ok(binary);
} else {
statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
log::info!(
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
adapter.name,
binary.path.display()
);
return Ok(binary);
}
statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
);
}
binary
@@ -779,14 +849,23 @@ async fn fetch_latest_binary(
lsp_binary_statuses_tx: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let container_dir: Arc<Path> = container_dir.into();
lsp_binary_statuses_tx.send(
language.clone(),
LanguageServerBinaryStatus::CheckingForUpdate,
);
log::info!(
"querying GitHub for latest version of language server {:?}",
adapter.name.0
);
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
log::info!(
"checking if Zed already installed or fetching version for language server {:?}",
adapter.name.0
);
let binary = adapter
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;

View File

@@ -55,6 +55,7 @@ pub enum IoKind {
pub struct LanguageServerBinary {
pub path: PathBuf,
pub arguments: Vec<OsString>,
pub env: Option<HashMap<String, String>>,
}
/// A running language server process.
@@ -189,6 +190,7 @@ impl LanguageServer {
let mut server = process::Command::new(&binary.path)
.current_dir(working_dir)
.args(binary.arguments)
.envs(binary.env.unwrap_or_default())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())

View File

@@ -192,6 +192,7 @@ impl Prettier {
LanguageServerBinary {
path: node_path,
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
env: None,
},
Path::new("/"),
None,

View File

@@ -65,6 +65,7 @@ text.workspace = true
thiserror.workspace = true
toml.workspace = true
util.workspace = true
which.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -1308,6 +1308,13 @@ impl LspCommand for GetHover {
} else {
None
};
if let Some(range) = range.as_ref() {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_anchors([range.start.clone(), range.end.clone()])
})?
.await?;
}
Ok(Some(Hover {
contents,

View File

@@ -70,9 +70,14 @@ pub(super) async fn format_with_prettier(
match prettier.format(buffer, buffer_path, cx).await {
Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
Err(e) => {
log::error!(
"Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
);
match prettier_path {
Some(prettier_path) => log::error!(
"Prettier instance from path {prettier_path:?} failed to format a buffer: {e:#}"
),
None => log::error!(
"Default prettier instance failed to format a buffer: {e:#}"
),
}
}
}
}
@@ -366,6 +371,7 @@ fn register_new_prettier(
}
async fn install_prettier_packages(
fs: &dyn Fs,
plugins_to_install: HashSet<&'static str>,
node: Arc<dyn NodeRuntime>,
) -> anyhow::Result<()> {
@@ -385,18 +391,32 @@ async fn install_prettier_packages(
.await
.context("fetching latest npm versions")?;
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
match fs.metadata(default_prettier_dir).await.with_context(|| {
format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}")
})? {
Some(prettier_dir_metadata) => anyhow::ensure!(
prettier_dir_metadata.is_dir,
"default prettier dir {default_prettier_dir:?} is not a directory"
),
None => fs
.create_dir(default_prettier_dir)
.await
.with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?,
}
log::info!("Installing default prettier and plugins: {packages_to_versions:?}");
let borrowed_packages = packages_to_versions
.iter()
.map(|(package, version)| (package.as_str(), version.as_str()))
.collect::<Vec<_>>();
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
node.npm_install_packages(default_prettier_dir, &borrowed_packages)
.await
.context("fetching formatter packages")?;
anyhow::Ok(())
}
async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> {
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
fs.save(
&prettier_wrapper_path,
@@ -413,6 +433,17 @@ async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
Ok(())
}
async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool {
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
if !fs.is_file(&prettier_wrapper_path).await {
return true;
}
let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else {
return true;
};
prettier_server_file_contents != prettier::PRETTIER_SERVER_JS
}
impl Project {
pub fn update_prettier_settings(
&self,
@@ -623,6 +654,7 @@ impl Project {
_cx: &mut ModelContext<Self>,
) {
// suppress unused code warnings
let _ = should_write_prettier_server_file;
let _ = install_prettier_packages;
let _ = save_prettier_server_file;
@@ -643,7 +675,6 @@ impl Project {
let Some(node) = self.node.as_ref().cloned() else {
return;
};
log::info!("Initializing default prettier with plugins {new_plugins:?}");
let fs = Arc::clone(&self.fs);
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
self.worktree_for_id(worktree_id, cx)
@@ -689,6 +720,7 @@ impl Project {
}
};
log::info!("Initializing default prettier with plugins {new_plugins:?}");
let plugins_to_install = new_plugins.clone();
let fs = Arc::clone(&self.fs);
let new_installation_task = cx
@@ -703,7 +735,7 @@ impl Project {
if prettier_path.is_some() {
new_plugins.clear();
}
let mut needs_install = false;
let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await;
if let Some(previous_installation_task) = previous_installation_task {
if let Err(e) = previous_installation_task.await {
log::error!("Failed to install default prettier: {e:#}");
@@ -744,8 +776,10 @@ impl Project {
let installed_plugins = new_plugins.clone();
cx.background_executor()
.spawn(async move {
install_prettier_packages(fs.as_ref(), new_plugins, node).await?;
// Save the server file last, so the reinstall need could be determined by the absence of the file.
save_prettier_server_file(fs.as_ref()).await?;
install_prettier_packages(new_plugins, node).await
anyhow::Ok(())
})
.await
.context("prettier & plugins install")

View File

@@ -71,6 +71,8 @@ use smol::lock::Semaphore;
use std::{
cmp::{self, Ordering},
convert::TryInto,
env,
ffi::OsString,
hash::Hash,
mem,
num::NonZeroU32,
@@ -226,6 +228,7 @@ pub struct LanguageServerPromptRequest {
pub level: PromptLevel,
pub message: String,
pub actions: Vec<MessageActionItem>,
pub lsp_name: String,
response_channel: Sender<MessageActionItem>,
}
@@ -504,11 +507,6 @@ pub enum FormatTrigger {
Manual,
}
struct ProjectLspAdapterDelegate {
project: Model<Project>,
http_client: Arc<dyn HttpClient>,
}
// Currently, formatting operations are represented differently depending on
// whether they come from a language server or an external command.
enum FormatOperation {
@@ -853,10 +851,13 @@ impl Project {
root_paths: impl IntoIterator<Item = &Path>,
cx: &mut gpui::TestAppContext,
) -> Model<Project> {
use clock::FakeSystemClock;
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.executor());
let clock = Arc::new(FakeSystemClock::default());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let project = cx.update(|cx| {
Project::local(
@@ -2800,7 +2801,7 @@ impl Project {
fn start_language_server(
&mut self,
worktree: &Model<Worktree>,
worktree_handle: &Model<Worktree>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
@@ -2809,7 +2810,7 @@ impl Project {
return;
}
let worktree = worktree.read(cx);
let worktree = worktree_handle.read(cx);
let worktree_id = worktree.id();
let worktree_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone());
@@ -2823,7 +2824,7 @@ impl Project {
language.clone(),
adapter.clone(),
Arc::clone(&worktree_path),
ProjectLspAdapterDelegate::new(self, cx),
ProjectLspAdapterDelegate::new(self, worktree_handle, cx),
cx,
) {
Some(pending_server) => pending_server,
@@ -3013,6 +3014,7 @@ impl Project {
cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?;
let language_server = pending_server.task.await?;
let name = language_server.name();
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone();
@@ -3151,8 +3153,10 @@ impl Project {
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = this.clone();
let name = name.to_string();
move |params, mut cx| {
let this = this.clone();
let name = name.to_string();
async move {
if let Some(actions) = params.actions {
let (tx, mut rx) = smol::channel::bounded(1);
@@ -3165,6 +3169,7 @@ impl Project {
message: params.message,
actions,
response_channel: tx,
lsp_name: name.clone(),
};
if let Ok(_) = this.update(&mut cx, |_, cx| {
@@ -3202,6 +3207,7 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
@@ -5884,7 +5890,6 @@ impl Project {
let range_start = range.start;
let range_end = range.end;
let buffer_id = buffer.remote_id().into();
let buffer_version = buffer.version().clone();
let lsp_request = InlayHints { range };
if self.is_local() {
@@ -5910,23 +5915,22 @@ impl Project {
buffer_id,
start: Some(serialize_anchor(&range_start)),
end: Some(serialize_anchor(&range_end)),
version: serialize_version(&buffer_version),
version: serialize_version(&buffer_handle.read(cx).version()),
};
cx.spawn(move |project, cx| async move {
let response = client
.request(request)
.await
.context("inlay hints proto request")?;
let hints_request_result = LspCommand::response_from_proto(
LspCommand::response_from_proto(
lsp_request,
response,
project.upgrade().ok_or_else(|| anyhow!("No project"))?,
buffer_handle.clone(),
cx,
cx.clone(),
)
.await;
hints_request_result.context("inlay hints proto response conversion")
.await
.context("inlay hints proto response conversion")
})
} else {
Task::ready(Err(anyhow!("project does not have a remote id")))
@@ -8202,20 +8206,12 @@ impl Project {
.and_then(|buffer| buffer.upgrade())
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
})??;
let buffer_version = deserialize_version(&envelope.payload.version);
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(buffer_version.clone())
buffer.wait_for_version(deserialize_version(&envelope.payload.version))
})?
.await
.with_context(|| {
format!(
"waiting for version {:?} for buffer {}",
buffer_version,
buffer.entity_id()
)
})?;
.with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?;
let start = envelope
.payload
@@ -8229,13 +8225,19 @@ impl Project {
.context("missing range end")?;
let buffer_hints = this
.update(&mut cx, |project, cx| {
project.inlay_hints(buffer, start..end, cx)
project.inlay_hints(buffer.clone(), start..end, cx)
})?
.await
.context("inlay hints fetch")?;
Ok(this.update(&mut cx, |project, cx| {
InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx)
InlayHints::response_to_proto(
buffer_hints,
project,
sender_id,
&buffer.read(cx).version(),
cx,
)
})?)
}
@@ -8311,10 +8313,14 @@ impl Project {
cx.clone(),
)
.await?;
let buffer_version = buffer_handle.update(&mut cx, |buffer, _| buffer.version())?;
let response = this
.update(&mut cx, |this, cx| {
this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx)
this.request_lsp(
buffer_handle.clone(),
LanguageServerToQuery::Primary,
request,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| {
@@ -8322,7 +8328,7 @@ impl Project {
response,
this,
sender_id,
&buffer_version,
&buffer_handle.read(cx).version(),
cx,
))
})?
@@ -9271,10 +9277,17 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
}
}
struct ProjectLspAdapterDelegate {
project: Model<Project>,
worktree: Model<Worktree>,
http_client: Arc<dyn HttpClient>,
}
impl ProjectLspAdapterDelegate {
fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self {
project: cx.handle(),
worktree: worktree.clone(),
http_client: project.client.http_client(),
})
}
@@ -9289,6 +9302,43 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn http_client(&self) -> Arc<dyn HttpClient> {
self.http_client.clone()
}
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
let worktree_abs_path = self.worktree.read(cx).abs_path();
let command = command.to_owned();
cx.background_executor().spawn(async move {
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!(
"failed to determine load login shell environment in {worktree_abs_path:?}"
)
})
.log_err();
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
);
None
}
}
} else {
None
}
})
}
}
fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
@@ -9396,3 +9446,55 @@ fn include_text(server: &lsp::LanguageServer) -> bool {
})
.unwrap_or(false)
}
async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
let marker = "ZED_SHELL_START";
let shell = env::var("SHELL").context(
"SHELL environment variable is not assigned so we can't source login environment variables",
)?;
let output = smol::process::Command::new(&shell)
.args([
"-i",
"-c",
// What we're doing here is to spawn a shell and then `cd` into
// the project directory to get the env in there as if the user
// `cd`'d into it. We do that because tools like direnv, asdf, ...
// hook into `cd` and only set up the env after that.
//
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
&format!("cd {dir:?}; echo {marker}; /usr/bin/env -0; exit 0;"),
])
.output()
.await
.context("failed to spawn login shell to source login environment variables")?;
anyhow::ensure!(
output.status.success(),
"login shell exited with error {:?}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
let env_output_start = stdout.find(marker).ok_or_else(|| {
anyhow!(
"failed to parse output of `env` command in login shell: {}",
stdout
)
})?;
let mut parsed_env = HashMap::default();
let env_output = &stdout[env_output_start + marker.len()..];
for line in env_output.split_terminator('\0') {
if let Some(separator_index) = line.find('=') {
let key = line[..separator_index].to_string();
let value = line[separator_index + 1..].to_string();
parsed_env.insert(key, value);
}
}
Ok(parsed_env)
}

View File

@@ -5,6 +5,7 @@ use crate::{
};
use anyhow::Result;
use client::Client;
use clock::FakeSystemClock;
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
use git::GITIGNORE;
use gpui::{ModelContext, Task, TestAppContext};
@@ -1263,7 +1264,13 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_fake = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let fs_fake = FakeFs::new(cx.background_executor.clone());
fs_fake
@@ -1304,7 +1311,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_real = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({
@@ -2396,8 +2409,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
}
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
let clock = Arc::new(FakeSystemClock::default());
let http_client = FakeHttpClient::with_404_response();
cx.update(|cx| Client::new(http_client, cx))
cx.update(|cx| Client::new(clock, http_client, cx))
}
#[track_caller]

View File

@@ -338,7 +338,7 @@ impl ProjectPanel {
let panel = ProjectPanel::new(workspace, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width;
panel.width = serialized_panel.width.map(|px| px.round());
cx.notify();
});
}

View File

@@ -315,10 +315,13 @@ impl Peer {
"incoming response: requester resumed"
);
} else {
let message_type = proto::build_typed_envelope(connection_id, incoming)
.map(|p| p.payload_type_name());
tracing::warn!(
%connection_id,
message_id,
responding_to,
message_type,
"incoming response: unknown request"
);
}

View File

@@ -116,13 +116,20 @@ pub fn update_settings_file<T: Settings>(
store.new_text_for_update::<T>(old_text, update)
})?;
let initial_path = paths::SETTINGS.as_path();
let resolved_path = fs
.canonicalize(initial_path)
.await
.with_context(|| format!("Failed to canonicalize settings path {:?}", initial_path))?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?;
if !fs.is_file(initial_path).await {
fs.atomic_write(initial_path.to_path_buf(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", initial_path))?;
} else {
let resolved_path = fs.canonicalize(initial_path).await.with_context(|| {
format!("Failed to canonicalize settings path {:?}", initial_path)
})?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);

View File

@@ -0,0 +1,13 @@
[package]
name = "telemetry_events"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/telemetry_events.rs"
[dependencies]
serde.workspace = true
util.workspace = true

View File

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

View File

@@ -0,0 +1,131 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use util::SemanticVersion;
#[derive(Serialize, Deserialize, Debug)]
pub struct EventRequestBody {
pub installation_id: Option<String>,
pub session_id: Option<String>,
pub is_staff: Option<bool>,
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
pub release_channel: Option<String>,
pub events: Vec<EventWrapper>,
}
impl EventRequestBody {
pub fn semver(&self) -> Option<SemanticVersion> {
self.app_version.parse().ok()
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct EventWrapper {
pub signed_in: bool,
pub milliseconds_since_first_event: i64,
#[serde(flatten)]
pub event: Event,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
impl Display for AssistantKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Panel => "panel",
Self::Inline => "inline",
}
)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Event {
Editor(EditorEvent),
Copilot(CopilotEvent),
Call(CallEvent),
Assistant(AssistantEvent),
Cpu(CpuEvent),
Memory(MemoryEvent),
App(AppEvent),
Setting(SettingEvent),
Edit(EditEvent),
Action(ActionEvent),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditorEvent {
pub operation: String,
pub file_extension: Option<String>,
pub vim_mode: bool,
pub copilot_enabled: bool,
pub copilot_enabled_for_language: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CopilotEvent {
pub suggestion_id: Option<String>,
pub suggestion_accepted: bool,
pub file_extension: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CallEvent {
pub operation: String,
pub room_id: Option<u64>,
pub channel_id: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AssistantEvent {
pub conversation_id: Option<String>,
pub kind: AssistantKind,
pub model: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CpuEvent {
pub usage_as_percentage: f32,
pub core_count: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MemoryEvent {
pub memory_in_bytes: u64,
pub virtual_memory_in_bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActionEvent {
pub source: String,
pub action: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditEvent {
pub duration: i64,
pub environment: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SettingEvent {
pub setting: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AppEvent {
pub operation: String,
}

View File

@@ -489,15 +489,12 @@ impl TerminalElement {
}
});
let interactive_text_bounds = InteractiveBounds {
bounds,
stacking_order: cx.stacking_order().clone(),
};
if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
if bounds.contains(&cx.mouse_position()) {
let stacking_order = cx.stacking_order().clone();
if self.can_navigate_to_selected_word && last_hovered_word.is_some() {
cx.set_cursor_style(gpui::CursorStyle::PointingHand)
cx.set_cursor_style(gpui::CursorStyle::PointingHand, stacking_order);
} else {
cx.set_cursor_style(gpui::CursorStyle::IBeam)
cx.set_cursor_style(gpui::CursorStyle::IBeam, stacking_order);
}
}

View File

@@ -192,8 +192,8 @@ impl TerminalPanel {
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.height = serialized_panel.height.map(|h| h.round());
panel.width = serialized_panel.width.map(|w| w.round());
panel.pane.update(cx, |_, cx| {
serialized_panel
.items

View File

@@ -7,6 +7,7 @@ use crate::prelude::*;
pub enum LabelSize {
#[default]
Default,
Large,
Small,
XSmall,
}
@@ -97,6 +98,7 @@ impl RenderOnce for LabelLike {
)
})
.map(|this| match self.size {
LabelSize::Large => this.text_ui_lg(),
LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(),
LabelSize::XSmall => this.text_ui_xs(),

View File

@@ -35,6 +35,17 @@ pub trait StyledExt: Styled + Sized {
self.text_size(size.rems())
}
/// The large size for UI text.
///
/// `1rem` or `16px` at the default scale of `1rem` = `16px`.
///
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
///
/// Use `text_ui` for regular-sized text.
fn text_ui_lg(self) -> Self {
self.text_size(UiTextSize::Large.rems())
}
/// The default size for UI text.
///
/// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.

View File

@@ -13,6 +13,13 @@ pub enum UiTextSize {
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
#[default]
Default,
/// The large size for UI text.
///
/// `1rem` or `16px` at the default scale of `1rem` = `16px`.
///
/// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
Large,
/// The small size for UI text.
///
/// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
@@ -31,6 +38,7 @@ pub enum UiTextSize {
impl UiTextSize {
pub fn rems(self) -> Rems {
match self {
Self::Large => rems(16. / 16.),
Self::Default => rems(14. / 16.),
Self::Small => rems(12. / 16.),
Self::XSmall => rems(10. / 16.),

View File

@@ -23,6 +23,19 @@ impl ZedHttpClient {
pub fn zed_url(&self, path: &str) -> String {
format!("{}{}", self.zed_host.lock(), path)
}
pub fn zed_api_url(&self, path: &str) -> String {
let zed_host = self.zed_host.lock().clone();
let host = match zed_host.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,
};
format!("{}{}", host, path)
}
}
impl HttpClient for Arc<ZedHttpClient> {

View File

@@ -828,7 +828,7 @@ fn next_word_end(
let mut new_point = point;
if new_point.column() < map.line_len(new_point.row()) {
*new_point.column_mut() += 1;
} else if new_point.row() < map.max_buffer_row() {
} else if new_point < map.max_point() {
*new_point.row_mut() += 1;
*new_point.column_mut() = 0;
}
@@ -1110,13 +1110,15 @@ fn window_top(
if let Some(visible_rows) = text_layout_details.visible_rows {
let bottom_row = first_visible_line.row() + visible_rows as u32;
let new_row = (first_visible_line.row() + (times as u32)).min(bottom_row);
let new_row = (first_visible_line.row() + (times as u32))
.min(bottom_row)
.min(map.max_point().row());
let new_col = point.column().min(map.line_len(first_visible_line.row()));
let new_point = DisplayPoint::new(new_row, new_col);
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
} else {
let new_row = first_visible_line.row() + (times as u32);
let new_row = (first_visible_line.row() + (times as u32)).min(map.max_point().row());
let new_col = point.column().min(map.line_len(first_visible_line.row()));
let new_point = DisplayPoint::new(new_row, new_col);
@@ -1134,8 +1136,12 @@ fn window_middle(
.scroll_anchor
.anchor
.to_display_point(map);
let max_rows = (visible_rows as u32).min(map.max_buffer_row());
let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
let max_visible_rows =
(visible_rows as u32).min(map.max_point().row() - first_visible_line.row());
let new_row =
(first_visible_line.row() + (max_visible_rows / 2) as u32).min(map.max_point().row());
let new_col = point.column().min(map.line_len(new_row));
let new_point = DisplayPoint::new(new_row, new_col);
(map.clip_point(new_point, Bias::Left), SelectionGoal::None)
@@ -1157,12 +1163,12 @@ fn window_bottom(
.to_display_point(map);
let bottom_row = first_visible_line.row()
+ (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
if bottom_row < map.max_buffer_row()
if bottom_row < map.max_point().row()
&& text_layout_details.vertical_scroll_margin as usize > times
{
times = text_layout_details.vertical_scroll_margin.ceil() as usize;
}
let bottom_row_capped = bottom_row.min(map.max_buffer_row());
let bottom_row_capped = bottom_row.min(map.max_point().row());
let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row() {
first_visible_line.row()
} else {

View File

@@ -950,4 +950,16 @@ async fn test_remap(cx: &mut gpui::TestAppContext) {
cx.set_state("ˇ1234\n56789", Mode::Normal);
cx.simulate_keystrokes(["g", "u"]);
cx.assert_state("1234 567ˇ89", Mode::Normal);
// test leaving command
cx.update(|cx| {
cx.bind_keys([KeyBinding::new(
"g t",
workspace::SendKeystrokes("i space escape".to_string()),
None,
)])
});
cx.set_state("12ˇ34", Mode::Normal);
cx.simulate_keystrokes(["g", "t"]);
cx.assert_state("12ˇ 34", Mode::Normal);
}

View File

@@ -25,6 +25,7 @@ async-recursion = "1.0.0"
bincode = "1.2.1"
call.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
db.workspace = true
derive_more.workspace = true

View File

@@ -522,7 +522,7 @@ impl Dock {
pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE));
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
entry.panel.set_size(size, cx);
cx.notify();
}

View File

@@ -1,10 +1,14 @@
use crate::{Toast, Workspace};
use collections::HashMap;
use gpui::{
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Global,
PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
svg, AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
Global, PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
};
use language::DiagnosticSeverity;
use std::{any::TypeId, ops::DerefMut};
use ui::prelude::*;
use util::ResultExt;
pub fn init(cx: &mut AppContext) {
cx.set_global(NotificationTracker::new());
@@ -168,6 +172,105 @@ impl Workspace {
}
}
pub struct LanguageServerPrompt {
request: Option<project::LanguageServerPromptRequest>,
}
impl LanguageServerPrompt {
pub fn new(request: project::LanguageServerPromptRequest) -> Self {
Self {
request: Some(request),
}
}
async fn select_option(this: View<Self>, ix: usize, mut cx: AsyncWindowContext) {
util::async_maybe!({
let potential_future = this.update(&mut cx, |this, _| {
this.request.take().map(|request| request.respond(ix))
});
potential_future? // App Closed
.ok_or_else(|| anyhow::anyhow!("Response already sent"))?
.await
.ok_or_else(|| anyhow::anyhow!("Stream already closed"))?;
this.update(&mut cx, |_, cx| cx.emit(DismissEvent))?;
anyhow::Ok(())
})
.await
.log_err();
}
}
impl Render for LanguageServerPrompt {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(request) = &self.request else {
return div().id("language_server_prompt_notification");
};
h_flex()
.id("language_server_prompt_notification")
.elevation_3(cx)
.items_start()
.p_2()
.gap_2()
.w_full()
.child(
v_flex()
.overflow_hidden()
.child(
h_flex()
.children(
match request.level {
PromptLevel::Info => None,
PromptLevel::Warning => Some(DiagnosticSeverity::WARNING),
PromptLevel::Critical => Some(DiagnosticSeverity::ERROR),
}
.map(|severity| {
svg()
.size(cx.text_style().font_size)
.flex_none()
.mr_1()
.map(|icon| {
if severity == DiagnosticSeverity::ERROR {
icon.path(IconName::ExclamationTriangle.path())
.text_color(Color::Error.color(cx))
} else {
icon.path(IconName::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx))
}
})
}),
)
.child(
Label::new(format!("{}:", request.lsp_name))
.size(LabelSize::Default),
),
)
.child(Label::new(request.message.to_string()))
.children(request.actions.iter().enumerate().map(|(ix, action)| {
let this_handle = cx.view().clone();
ui::Button::new(ix, action.title.clone())
.size(ButtonSize::Large)
.on_click(move |_, cx| {
let this_handle = this_handle.clone();
cx.spawn(|cx| async move {
LanguageServerPrompt::select_option(this_handle, ix, cx).await
})
.detach()
})
})),
)
.child(
ui::IconButton::new("close", ui::IconName::Close)
.on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
)
}
}
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
pub mod simple_message_notification {
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,

View File

@@ -588,9 +588,9 @@ mod element {
use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
use gpui::{
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
Size, Style, WeakView, WindowContext,
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style,
WeakView, WindowContext,
};
use parking_lot::Mutex;
use settings::Settings;
@@ -754,15 +754,13 @@ mod element {
};
cx.with_z_index(3, |cx| {
let interactive_handle_bounds = InteractiveBounds {
bounds: handle_bounds,
stacking_order: cx.stacking_order().clone(),
};
if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) {
cx.set_cursor_style(match axis {
if handle_bounds.contains(&cx.mouse_position()) {
let stacking_order = cx.stacking_order().clone();
let cursor_style = match axis {
Axis::Vertical => CursorStyle::ResizeUpDown,
Axis::Horizontal => CursorStyle::ResizeLeftRight,
})
};
cx.set_cursor_style(cursor_style, stacking_order);
}
cx.add_opaque_layer(handle_bounds);
@@ -885,7 +883,8 @@ mod element {
let child_size = bounds
.size
.apply_along(self.axis, |_| space_per_flex * child_flex);
.apply_along(self.axis, |_| space_per_flex * child_flex)
.map(|d| d.round());
let child_bounds = Bounds {
origin,

View File

@@ -59,7 +59,10 @@ use std::{
any::TypeId,
borrow::Cow,
cell::RefCell,
cmp, env,
cmp,
collections::hash_map::DefaultHasher,
env,
hash::{Hash, Hasher},
path::{Path, PathBuf},
rc::Rc,
sync::{atomic::AtomicUsize, Arc, Weak},
@@ -119,7 +122,6 @@ actions!(
ToggleRightDock,
ToggleBottomDock,
CloseAllDocks,
ToggleGraphicsProfiler,
]
);
@@ -397,8 +399,9 @@ impl AppState {
let fs = fs::FakeFs::new(cx.background_executor().clone());
let languages = Arc::new(LanguageRegistry::test());
let clock = Arc::new(clock::FakeSystemClock::default());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let client = Client::new(clock, http_client.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
@@ -578,24 +581,13 @@ impl Workspace {
}),
project::Event::LanguageServerPrompt(request) => {
let request = request.clone();
let mut hasher = DefaultHasher::new();
request.message.as_str().hash(&mut hasher);
let id = hasher.finish();
cx.spawn(|_, mut cx| async move {
let messages = request
.actions
.iter()
.map(|action| action.title.as_str())
.collect::<Vec<_>>();
let index = cx
.update(|cx| {
cx.prompt(request.level, "", Some(&request.message), &messages)
})?
.await?;
request.respond(index).await;
Result::<(), anyhow::Error>::Ok(())
})
.detach()
this.show_notification(id as usize, cx, |cx| {
cx.new_view(|_| notifications::LanguageServerPrompt::new(request.clone()))
});
}
_ => {}
@@ -2759,7 +2751,7 @@ impl Workspace {
.z_index(100)
.right_3()
.bottom_3()
.w_96()
.w_112()
.h_full()
.flex()
.flex_col()
@@ -3567,7 +3559,6 @@ impl Workspace {
workspace.reopen_closed_item(cx).detach();
}),
)
.on_action(|_: &ToggleGraphicsProfiler, cx| cx.toggle_graphics_profiler())
}
#[cfg(any(test, feature = "test-support"))]

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.124.0"
version = "0.124.8"
publish = false
license = "GPL-3.0-or-later"
@@ -34,6 +34,7 @@ channel.workspace = true
chrono = "0.4"
cli.workspace = true
client.workspace = true
clock.workspace = true
collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true

View File

@@ -1 +1 @@
dev
stable

View File

@@ -22,5 +22,3 @@
<string>An application in Zed wants to use speech recognition.</string>
<key>NSRemindersUsageDescription</key>
<string>An application in Zed wants to use your reminders.</string>
<key>MetalHudEnabled</key>
<true />

View File

@@ -156,10 +156,6 @@ pub fn app_menus() -> Vec<Menu<'static>> {
MenuItem::action("View Telemetry", crate::OpenTelemetryLog),
MenuItem::action("View Dependency Licenses", crate::OpenLicenses),
MenuItem::action("Show Welcome", workspace::Welcome),
MenuItem::action(
"Toggle Graphics Profiler",
workspace::ToggleGraphicsProfiler,
),
MenuItem::separator(),
MenuItem::separator(),
MenuItem::action(

View File

@@ -71,6 +71,7 @@ impl LspAdapter for AstroLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -122,6 +123,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -84,6 +84,7 @@ impl super::LspAdapter for CLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
})
}
@@ -260,6 +261,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
if clangd_bin.exists() {
Ok(LanguageServerBinary {
path: clangd_bin,
env: None,
arguments: vec![],
})
} else {

View File

@@ -105,6 +105,7 @@ impl super::LspAdapter for ClojureLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
})
}
@@ -118,6 +119,7 @@ impl super::LspAdapter for ClojureLspAdapter {
if binary_path.exists() {
Some(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
})
} else {
@@ -133,6 +135,7 @@ impl super::LspAdapter for ClojureLspAdapter {
if binary_path.exists() {
Some(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec!["--version".into()],
})
} else {

View File

@@ -92,6 +92,7 @@ impl super::LspAdapter for OmniSharpAdapter {
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: server_binary_arguments(),
})
}
@@ -136,6 +137,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
if let Some(path) = last_binary_path {
Ok(LanguageServerBinary {
path,
env: None,
arguments: server_binary_arguments(),
})
} else {

View File

@@ -72,6 +72,7 @@ impl LspAdapter for CssLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -116,6 +117,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -39,6 +39,7 @@ impl LspAdapter for DartLanguageServer {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "dart".into(),
env: None,
arguments: vec!["language-server".into(), "--protocol=lsp".into()],
})
}

View File

@@ -134,6 +134,7 @@ impl LspAdapter for DenoLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: deno_server_binary_arguments(),
})
}
@@ -220,6 +221,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
if fs::metadata(&binary).await.is_ok() {
return Ok(LanguageServerBinary {
path: binary,
env: None,
arguments: deno_server_binary_arguments(),
});
}

View File

@@ -71,6 +71,7 @@ impl LspAdapter for DockerfileLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -110,6 +111,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -174,6 +174,7 @@ impl LspAdapter for ElixirLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
})
}
@@ -284,6 +285,7 @@ async fn get_cached_server_binary_elixir_ls(
if server_path.exists() {
Some(LanguageServerBinary {
path: server_path,
env: None,
arguments: vec![],
})
} else {
@@ -369,6 +371,7 @@ impl LspAdapter for NextLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec!["--stdio".into()],
})
}
@@ -435,6 +438,7 @@ async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<Languag
if let Some(path) = last_binary_path {
Ok(LanguageServerBinary {
path,
env: None,
arguments: Vec::new(),
})
} else {
@@ -476,6 +480,7 @@ impl LspAdapter for LocalLspAdapter {
let path = shellexpand::full(&self.path)?;
Ok(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}
@@ -488,6 +493,7 @@ impl LspAdapter for LocalLspAdapter {
let path = shellexpand::full(&self.path).ok()?;
Some(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}
@@ -496,6 +502,7 @@ impl LspAdapter for LocalLspAdapter {
let path = shellexpand::full(&self.path).ok()?;
Some(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}

View File

@@ -75,6 +75,7 @@ impl LspAdapter for ElmLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -134,6 +135,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -41,6 +41,7 @@ impl LspAdapter for ErlangLspAdapter {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "erlang_ls".into(),
env: None,
arguments: vec![],
})
}
@@ -52,6 +53,7 @@ impl LspAdapter for ErlangLspAdapter {
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "erlang_ls".into(),
env: None,
arguments: vec!["--version".into()],
})
}

View File

@@ -81,6 +81,7 @@ impl LspAdapter for GleamLspAdapter {
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: server_binary_arguments(),
})
}
@@ -116,6 +117,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
anyhow::Ok(LanguageServerBinary {
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
env: None,
arguments: server_binary_arguments(),
})
})

View File

@@ -58,6 +58,25 @@ impl super::LspAdapter for GoLspAdapter {
Ok(Box::new(version) as Box<_>)
}
fn check_if_user_installed(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
let delegate = delegate.clone();
Some(cx.spawn(|cx| async move {
match cx.update(|cx| delegate.which_command(OsString::from("gopls"), cx)) {
Ok(task) => task.await.map(|(path, env)| LanguageServerBinary {
path,
arguments: server_binary_arguments(),
env: Some(env),
}),
Err(_) => None,
}
}))
}
fn will_fetch_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
@@ -107,6 +126,7 @@ impl super::LspAdapter for GoLspAdapter {
return Ok(LanguageServerBinary {
path: binary_path.to_path_buf(),
arguments: server_binary_arguments(),
env: None,
});
}
}
@@ -154,6 +174,7 @@ impl super::LspAdapter for GoLspAdapter {
Ok(LanguageServerBinary {
path: binary_path.to_path_buf(),
arguments: server_binary_arguments(),
env: None,
})
}
@@ -372,6 +393,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
Ok(LanguageServerBinary {
path,
arguments: server_binary_arguments(),
env: None,
})
} else {
Err(anyhow!("no cached binary"))

View File

@@ -41,6 +41,7 @@ impl LspAdapter for HaskellLanguageServer {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "haskell-language-server-wrapper".into(),
env: None,
arguments: vec!["lsp".into()],
})
}

View File

@@ -72,6 +72,7 @@ impl LspAdapter for HtmlLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -116,6 +117,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -122,6 +122,7 @@ impl LspAdapter for JsonLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -177,6 +178,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -94,6 +94,7 @@ impl super::LspAdapter for LuaLspAdapter {
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: Vec::new(),
})
}
@@ -138,6 +139,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
if let Some(path) = last_binary_path {
Ok(LanguageServerBinary {
path,
env: None,
arguments: Vec::new(),
})
} else {

View File

@@ -41,6 +41,7 @@ impl LspAdapter for NuLanguageServer {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "nu".into(),
env: None,
arguments: vec!["--lsp".into()],
})
}

View File

@@ -47,6 +47,7 @@ impl LspAdapter for OCamlLspAdapter {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "ocamllsp".into(),
env: None,
arguments: vec![],
})
}

View File

@@ -69,6 +69,7 @@ impl LspAdapter for IntelephenseLspAdapter {
}
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: intelephense_server_binary_arguments(&server_path),
})
}
@@ -126,6 +127,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: intelephense_server_binary_arguments(&server_path),
})
} else {

View File

@@ -70,6 +70,7 @@ impl LspAdapter for PrismaLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -112,6 +113,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -74,6 +74,7 @@ impl LspAdapter for PurescriptLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -127,6 +128,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -62,6 +62,7 @@ impl LspAdapter for PythonLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -167,6 +168,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Some(LanguageServerBinary {
path: node.binary_path().await.log_err()?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -39,6 +39,7 @@ impl LspAdapter for RubyLanguageServer {
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "solargraph".into(),
env: None,
arguments: vec!["stdio".into()],
})
}

View File

@@ -89,6 +89,7 @@ impl LspAdapter for RustLspAdapter {
Ok(LanguageServerBinary {
path: destination_path,
env: None,
arguments: Default::default(),
})
}
@@ -296,6 +297,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
anyhow::Ok(LanguageServerBinary {
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
env: None,
arguments: Default::default(),
})
})

View File

@@ -71,6 +71,7 @@ impl LspAdapter for SvelteLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -148,6 +149,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

View File

@@ -73,6 +73,7 @@ impl LspAdapter for TailwindLspAdapter {
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
}
@@ -150,6 +151,7 @@ async fn get_cached_server_binary(
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: server_binary_arguments(&server_path),
})
} else {

Some files were not shown because too many files have changed in this diff Show More