Merge remote-tracking branch 'origin/main' into room
This commit is contained in:
commit
afaacba41f
92 changed files with 10800 additions and 6586 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -56,6 +56,7 @@ jobs:
|
||||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||||
|
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
run: |
|
run: |
|
||||||
|
|
22
.github/workflows/discord_webhook.yml
vendored
Normal file
22
.github/workflows/discord_webhook.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
message:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Discord Webhook Action
|
||||||
|
uses: tsickert/discord-webhook@v5.3.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
content: |
|
||||||
|
📣 Zed ${{ github.event.release.name }} was just released!
|
||||||
|
|
||||||
|
Restart your Zed or head to https://zed.dev/releases to grab it.
|
||||||
|
|
||||||
|
```md
|
||||||
|
### Changelog
|
||||||
|
|
||||||
|
${{ github.event.release.body }}
|
||||||
|
```
|
110
Cargo.lock
generated
110
Cargo.lock
generated
|
@ -959,6 +959,7 @@ dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
"collections",
|
"collections",
|
||||||
|
"db",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
|
@ -969,13 +970,16 @@ dependencies = [
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"rpc",
|
"rpc",
|
||||||
|
"serde",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.11",
|
"time 0.3.11",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
|
"uuid 1.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1042,6 +1046,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"envy",
|
"envy",
|
||||||
"futures",
|
"futures",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"hyper",
|
"hyper",
|
||||||
"language",
|
"language",
|
||||||
|
@ -1072,6 +1077,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
@ -1495,6 +1501,19 @@ dependencies = [
|
||||||
"matches",
|
"matches",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"collections",
|
||||||
|
"gpui",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"rocksdb",
|
||||||
|
"tempdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deflate"
|
name = "deflate"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
@ -1672,6 +1691,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
@ -1694,6 +1714,8 @@ dependencies = [
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
|
"tree-sitter-html",
|
||||||
|
"tree-sitter-javascript",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"unindent",
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
|
@ -2199,6 +2221,39 @@ dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"clock",
|
||||||
|
"collections",
|
||||||
|
"futures",
|
||||||
|
"git2",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"smol",
|
||||||
|
"sum_tree",
|
||||||
|
"text",
|
||||||
|
"unindent",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "git2"
|
||||||
|
version = "0.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"libc",
|
||||||
|
"libgit2-sys",
|
||||||
|
"log",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -2815,6 +2870,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2834,6 +2890,8 @@ dependencies = [
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
|
"tree-sitter-html",
|
||||||
|
"tree-sitter-javascript",
|
||||||
"tree-sitter-json 0.19.0",
|
"tree-sitter-json 0.19.0",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
|
@ -2869,6 +2927,18 @@ version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libgit2-sys"
|
||||||
|
version = "0.14.0+1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"libz-sys",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -3941,9 +4011,11 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
|
"db",
|
||||||
"fsevent",
|
"fsevent",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"ignore",
|
"ignore",
|
||||||
"language",
|
"language",
|
||||||
|
@ -5999,6 +6071,15 @@ dependencies = [
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-css"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "git+https://github.com/tree-sitter/tree-sitter-css?rev=769203d0f9abe1a9a691ac2b9fe4bb4397a73c51#769203d0f9abe1a9a691ac2b9fe4bb4397a73c51"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-elixir"
|
name = "tree-sitter-elixir"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
@ -6017,6 +6098,26 @@ dependencies = [
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-html"
|
||||||
|
version = "0.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "184e6b77953a354303dc87bf5fe36558c83569ce92606e7b382a0dc1b7443443"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter-javascript"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2490fab08630b2c8943c320f7b63473cbf65511c8d83aec551beb9b4375906ed"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter-json"
|
name = "tree-sitter-json"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
@ -6306,6 +6407,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures",
|
"futures",
|
||||||
|
"git2",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -6326,6 +6429,9 @@ name = "uuid"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
|
@ -7122,7 +7228,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.55.0"
|
version = "0.59.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -7198,8 +7304,10 @@ dependencies = [
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-c",
|
"tree-sitter-c",
|
||||||
"tree-sitter-cpp",
|
"tree-sitter-cpp",
|
||||||
|
"tree-sitter-css",
|
||||||
"tree-sitter-elixir",
|
"tree-sitter-elixir",
|
||||||
"tree-sitter-go",
|
"tree-sitter-go",
|
||||||
|
"tree-sitter-html",
|
||||||
"tree-sitter-json 0.20.0",
|
"tree-sitter-json 0.20.0",
|
||||||
"tree-sitter-markdown",
|
"tree-sitter-markdown",
|
||||||
"tree-sitter-python",
|
"tree-sitter-python",
|
||||||
|
|
|
@ -74,6 +74,15 @@
|
||||||
"hard_tabs": false,
|
"hard_tabs": false,
|
||||||
// How many columns a tab should occupy.
|
// How many columns a tab should occupy.
|
||||||
"tab_size": 4,
|
"tab_size": 4,
|
||||||
|
// Git gutter behavior configuration.
|
||||||
|
"git": {
|
||||||
|
// Control whether the git gutter is shown. May take 2 values:
|
||||||
|
// 1. Show the gutter
|
||||||
|
// "git_gutter": "tracked_files"
|
||||||
|
// 2. Hide the gutter
|
||||||
|
// "git_gutter": "hide"
|
||||||
|
"git_gutter": "tracked_files"
|
||||||
|
},
|
||||||
// Settings specific to the terminal
|
// Settings specific to the terminal
|
||||||
"terminal": {
|
"terminal": {
|
||||||
// What shell to use when opening a terminal. May take 3 values:
|
// What shell to use when opening a terminal. May take 3 values:
|
||||||
|
|
|
@ -12,6 +12,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
@ -31,7 +32,10 @@ smol = "1.2.5"
|
||||||
thiserror = "1.0.29"
|
thiserror = "1.0.29"
|
||||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||||
tiny_http = "0.8"
|
tiny_http = "0.8"
|
||||||
|
uuid = { version = "1.1.2", features = ["v4"] }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
serde = { version = "*", features = ["derive"] }
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
|
|
@ -601,7 +601,7 @@ mod tests {
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
Channel::init(&client);
|
Channel::init(&client);
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod test;
|
||||||
|
|
||||||
pub mod channel;
|
pub mod channel;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
|
pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
@ -11,10 +12,12 @@ use async_tungstenite::tungstenite::{
|
||||||
error::Error as WebsocketError,
|
error::Error as WebsocketError,
|
||||||
http::{Request, StatusCode},
|
http::{Request, StatusCode},
|
||||||
};
|
};
|
||||||
|
use db::Db;
|
||||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
|
||||||
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
|
MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use http::HttpClient;
|
use http::HttpClient;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -28,9 +31,11 @@ use std::{
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
fmt::Write as _,
|
fmt::Write as _,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
path::PathBuf,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
use telemetry::Telemetry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
@ -51,11 +56,16 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||||
|
|
||||||
actions!(client, [Authenticate]);
|
actions!(client, [Authenticate]);
|
||||||
|
|
||||||
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
|
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||||
cx.add_global_action(move |_: &Authenticate, cx| {
|
cx.add_global_action({
|
||||||
let rpc = rpc.clone();
|
let client = client.clone();
|
||||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
|
move |_: &Authenticate, cx| {
|
||||||
|
let client = client.clone();
|
||||||
|
cx.spawn(
|
||||||
|
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||||
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +73,7 @@ pub struct Client {
|
||||||
id: usize,
|
id: usize,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
|
telemetry: Arc<Telemetry>,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
|
@ -232,10 +243,11 @@ impl Drop for Subscription {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> {
|
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
id: 0,
|
id: 0,
|
||||||
peer: Peer::new(),
|
peer: Peer::new(),
|
||||||
|
telemetry: Telemetry::new(http.clone(), cx),
|
||||||
http,
|
http,
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
|
||||||
|
@ -339,6 +351,7 @@ impl Client {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Status::SignedOut | Status::UpgradeRequired => {
|
Status::SignedOut | Status::UpgradeRequired => {
|
||||||
|
self.telemetry.set_authenticated_user_info(None, false);
|
||||||
state._reconnect_task.take();
|
state._reconnect_task.take();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -618,6 +631,9 @@ impl Client {
|
||||||
if credentials.is_none() && try_keychain {
|
if credentials.is_none() && try_keychain {
|
||||||
credentials = read_credentials_from_keychain(cx);
|
credentials = read_credentials_from_keychain(cx);
|
||||||
read_from_keychain = credentials.is_some();
|
read_from_keychain = credentials.is_some();
|
||||||
|
if read_from_keychain {
|
||||||
|
self.report_event("read credentials from keychain", Default::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if credentials.is_none() {
|
if credentials.is_none() {
|
||||||
let mut status_rx = self.status();
|
let mut status_rx = self.status();
|
||||||
|
@ -901,6 +917,7 @@ impl Client {
|
||||||
) -> Task<Result<Credentials>> {
|
) -> Task<Result<Credentials>> {
|
||||||
let platform = cx.platform();
|
let platform = cx.platform();
|
||||||
let executor = cx.background();
|
let executor = cx.background();
|
||||||
|
let telemetry = self.telemetry.clone();
|
||||||
executor.clone().spawn(async move {
|
executor.clone().spawn(async move {
|
||||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||||
|
@ -979,6 +996,8 @@ impl Client {
|
||||||
.context("failed to decrypt access token")?;
|
.context("failed to decrypt access token")?;
|
||||||
platform.activate(true);
|
platform.activate(true);
|
||||||
|
|
||||||
|
telemetry.report_event("authenticate with browser", Default::default());
|
||||||
|
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: user_id.parse()?,
|
user_id: user_id.parse()?,
|
||||||
access_token,
|
access_token,
|
||||||
|
@ -1043,6 +1062,18 @@ impl Client {
|
||||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||||
self.peer.respond_with_error(receipt, error)
|
self.peer.respond_with_error(receipt, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn start_telemetry(&self, db: Arc<Db>) {
|
||||||
|
self.telemetry.start(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||||
|
self.telemetry.report_event(kind, properties)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
self.telemetry.log_file_path()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWeakEntityHandle {
|
impl AnyWeakEntityHandle {
|
||||||
|
@ -1108,7 +1139,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
let mut status = client.status();
|
let mut status = client.status();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
@ -1147,7 +1178,7 @@ mod tests {
|
||||||
|
|
||||||
let auth_count = Arc::new(Mutex::new(0));
|
let auth_count = Arc::new(Mutex::new(0));
|
||||||
let dropped_auth_count = Arc::new(Mutex::new(0));
|
let dropped_auth_count = Arc::new(Mutex::new(0));
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
client.override_authenticate({
|
client.override_authenticate({
|
||||||
let auth_count = auth_count.clone();
|
let auth_count = auth_count.clone();
|
||||||
let dropped_auth_count = dropped_auth_count.clone();
|
let dropped_auth_count = dropped_auth_count.clone();
|
||||||
|
@ -1196,7 +1227,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||||
|
@ -1242,7 +1273,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
|
@ -1270,7 +1301,7 @@ mod tests {
|
||||||
cx.foreground().forbid_parking();
|
cx.foreground().forbid_parking();
|
||||||
|
|
||||||
let user_id = 5;
|
let user_id = 5;
|
||||||
let client = Client::new(FakeHttpClient::with_404_response());
|
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||||
|
|
||||||
let model = cx.add_model(|_| Model::default());
|
let model = cx.add_model(|_| Model::default());
|
||||||
|
|
283
crates/client/src/telemetry.rs
Normal file
283
crates/client/src/telemetry.rs
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
use crate::http::HttpClient;
|
||||||
|
use db::Db;
|
||||||
|
use gpui::{
|
||||||
|
executor::Background,
|
||||||
|
serde_json::{self, value::Map, Value},
|
||||||
|
AppContext, Task,
|
||||||
|
};
|
||||||
|
use isahc::Request;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::{
|
||||||
|
io::Write,
|
||||||
|
mem,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct Telemetry {
|
||||||
|
http_client: Arc<dyn HttpClient>,
|
||||||
|
executor: Arc<Background>,
|
||||||
|
session_id: u128,
|
||||||
|
state: Mutex<TelemetryState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TelemetryState {
|
||||||
|
metrics_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
queue: Vec<AmplitudeEvent>,
|
||||||
|
next_event_id: usize,
|
||||||
|
flush_task: Option<Task<()>>,
|
||||||
|
log_file: Option<NamedTempFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AMPLITUDE_EVENTS_URL: &'static str = "https://api2.amplitude.com/batch";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref AMPLITUDE_API_KEY: Option<String> = std::env::var("ZED_AMPLITUDE_API_KEY")
|
||||||
|
.ok()
|
||||||
|
.or_else(|| option_env!("ZED_AMPLITUDE_API_KEY").map(|key| key.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEventBatch {
|
||||||
|
api_key: &'static str,
|
||||||
|
events: Vec<AmplitudeEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AmplitudeEvent {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_id: Option<Arc<str>>,
|
||||||
|
device_id: Option<Arc<str>>,
|
||||||
|
event_type: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
event_properties: Option<Map<String, Value>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_properties: Option<Map<String, Value>>,
|
||||||
|
os_name: &'static str,
|
||||||
|
os_version: Option<Arc<str>>,
|
||||||
|
app_version: Option<Arc<str>>,
|
||||||
|
platform: &'static str,
|
||||||
|
event_id: usize,
|
||||||
|
session_id: u128,
|
||||||
|
time: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const MAX_QUEUE_LEN: usize = 1;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const MAX_QUEUE_LEN: usize = 10;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
impl Telemetry {
|
||||||
|
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||||
|
let platform = cx.platform();
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
http_client: client,
|
||||||
|
executor: cx.background().clone(),
|
||||||
|
session_id: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
state: Mutex::new(TelemetryState {
|
||||||
|
os_version: platform
|
||||||
|
.os_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
os_name: platform.os_name().into(),
|
||||||
|
app_version: platform
|
||||||
|
.app_version()
|
||||||
|
.log_err()
|
||||||
|
.map(|v| v.to_string().into()),
|
||||||
|
device_id: None,
|
||||||
|
queue: Default::default(),
|
||||||
|
flush_task: Default::default(),
|
||||||
|
next_event_id: 0,
|
||||||
|
log_file: None,
|
||||||
|
metrics_id: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if AMPLITUDE_API_KEY.is_some() {
|
||||||
|
this.executor
|
||||||
|
.spawn({
|
||||||
|
let this = this.clone();
|
||||||
|
async move {
|
||||||
|
if let Some(tempfile) = NamedTempFile::new().log_err() {
|
||||||
|
this.state.lock().log_file = Some(tempfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||||
|
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(self: &Arc<Self>, db: Arc<Db>) {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let device_id = if let Some(device_id) = db
|
||||||
|
.read(["device_id"])?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.next()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
{
|
||||||
|
device_id
|
||||||
|
} else {
|
||||||
|
let device_id = Uuid::new_v4().to_string();
|
||||||
|
db.write([("device_id", device_id.as_bytes())])?;
|
||||||
|
device_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let device_id = Some(Arc::from(device_id));
|
||||||
|
let mut state = this.state.lock();
|
||||||
|
state.device_id = device_id.clone();
|
||||||
|
for event in &mut state.queue {
|
||||||
|
event.device_id = device_id.clone();
|
||||||
|
}
|
||||||
|
if !state.queue.is_empty() {
|
||||||
|
drop(state);
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authenticated_user_info(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
metrics_id: Option<String>,
|
||||||
|
is_staff: bool,
|
||||||
|
) {
|
||||||
|
let is_signed_in = metrics_id.is_some();
|
||||||
|
self.state.lock().metrics_id = metrics_id.map(|s| s.into());
|
||||||
|
if is_signed_in {
|
||||||
|
self.report_event_with_user_properties(
|
||||||
|
"$identify",
|
||||||
|
Default::default(),
|
||||||
|
json!({ "$set": { "staff": is_staff } }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||||
|
self.report_event_with_user_properties(kind, properties, Default::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_event_with_user_properties(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
kind: &str,
|
||||||
|
properties: Value,
|
||||||
|
user_properties: Value,
|
||||||
|
) {
|
||||||
|
if AMPLITUDE_API_KEY.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let event = AmplitudeEvent {
|
||||||
|
event_type: kind.to_string(),
|
||||||
|
time: SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis(),
|
||||||
|
session_id: self.session_id,
|
||||||
|
event_properties: if let Value::Object(properties) = properties {
|
||||||
|
Some(properties)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
user_properties: if let Value::Object(user_properties) = user_properties {
|
||||||
|
Some(user_properties)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
user_id: state.metrics_id.clone(),
|
||||||
|
device_id: state.device_id.clone(),
|
||||||
|
os_name: state.os_name,
|
||||||
|
platform: "Zed",
|
||||||
|
os_version: state.os_version.clone(),
|
||||||
|
app_version: state.app_version.clone(),
|
||||||
|
event_id: post_inc(&mut state.next_event_id),
|
||||||
|
};
|
||||||
|
state.queue.push(event);
|
||||||
|
if state.device_id.is_some() {
|
||||||
|
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||||
|
drop(state);
|
||||||
|
self.flush();
|
||||||
|
} else {
|
||||||
|
let this = self.clone();
|
||||||
|
let executor = self.executor.clone();
|
||||||
|
state.flush_task = Some(self.executor.spawn(async move {
|
||||||
|
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||||
|
this.flush();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(self: &Arc<Self>) {
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
let events = mem::take(&mut state.queue);
|
||||||
|
state.flush_task.take();
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
if let Some(api_key) = AMPLITUDE_API_KEY.as_ref() {
|
||||||
|
let this = self.clone();
|
||||||
|
self.executor
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
|
let mut json_bytes = Vec::new();
|
||||||
|
|
||||||
|
if let Some(file) = &mut this.state.lock().log_file {
|
||||||
|
let file = file.as_file_mut();
|
||||||
|
for event in &events {
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, event)?;
|
||||||
|
file.write_all(&json_bytes)?;
|
||||||
|
file.write(b"\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let batch = AmplitudeEventBatch { api_key, events };
|
||||||
|
json_bytes.clear();
|
||||||
|
serde_json::to_writer(&mut json_bytes, &batch)?;
|
||||||
|
let request =
|
||||||
|
Request::post(AMPLITUDE_EVENTS_URL).body(json_bytes.into())?;
|
||||||
|
this.http_client.send(request).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.log_err(),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,10 @@ use anyhow::{anyhow, Result};
|
||||||
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
|
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
|
||||||
use gpui::{executor, ModelHandle, TestAppContext};
|
use gpui::{executor, ModelHandle, TestAppContext};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
|
use rpc::{
|
||||||
|
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
|
||||||
|
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
|
};
|
||||||
use std::{fmt, rc::Rc, sync::Arc};
|
use std::{fmt, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
pub struct FakeServer {
|
pub struct FakeServer {
|
||||||
|
@ -93,6 +96,7 @@ impl FakeServer {
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.authenticate_and_connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
server
|
server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,26 +130,45 @@ impl FakeServer {
|
||||||
#[allow(clippy::await_holding_lock)]
|
#[allow(clippy::await_holding_lock)]
|
||||||
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
||||||
self.executor.start_waiting();
|
self.executor.start_waiting();
|
||||||
let message = self
|
|
||||||
.state
|
loop {
|
||||||
.lock()
|
let message = self
|
||||||
.incoming
|
.state
|
||||||
.as_mut()
|
.lock()
|
||||||
.expect("not connected")
|
.incoming
|
||||||
.next()
|
.as_mut()
|
||||||
.await
|
.expect("not connected")
|
||||||
.ok_or_else(|| anyhow!("other half hung up"))?;
|
.next()
|
||||||
self.executor.finish_waiting();
|
.await
|
||||||
let type_name = message.payload_type_name();
|
.ok_or_else(|| anyhow!("other half hung up"))?;
|
||||||
Ok(*message
|
self.executor.finish_waiting();
|
||||||
.into_any()
|
let type_name = message.payload_type_name();
|
||||||
.downcast::<TypedEnvelope<M>>()
|
let message = message.into_any();
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
panic!(
|
if message.is::<TypedEnvelope<M>>() {
|
||||||
"fake server received unexpected message type: {:?}",
|
return Ok(*message.downcast().unwrap());
|
||||||
type_name
|
}
|
||||||
);
|
|
||||||
}))
|
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
|
||||||
|
self.respond(
|
||||||
|
message
|
||||||
|
.downcast::<TypedEnvelope<GetPrivateUserInfo>>()
|
||||||
|
.unwrap()
|
||||||
|
.receipt(),
|
||||||
|
GetPrivateUserInfoResponse {
|
||||||
|
metrics_id: "the-metrics-id".into(),
|
||||||
|
staff: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!(
|
||||||
|
"fake server received unexpected message type: {:?}",
|
||||||
|
type_name
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn respond<T: proto::RequestMessage>(
|
pub async fn respond<T: proto::RequestMessage>(
|
||||||
|
|
|
@ -135,10 +135,21 @@ impl UserStore {
|
||||||
match status {
|
match status {
|
||||||
Status::Connected { .. } => {
|
Status::Connected { .. } => {
|
||||||
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
|
||||||
let user = this
|
let fetch_user = this
|
||||||
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
|
.update(&mut cx, |this, cx| this.get_user(user_id, cx))
|
||||||
.log_err()
|
.log_err();
|
||||||
.await;
|
let fetch_metrics_id =
|
||||||
|
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||||
|
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||||
|
if let Some(info) = info {
|
||||||
|
client.telemetry.set_authenticated_user_info(
|
||||||
|
Some(info.metrics_id),
|
||||||
|
info.staff,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
client.telemetry.set_authenticated_user_info(None, false);
|
||||||
|
}
|
||||||
|
client.telemetry.report_event("sign in", Default::default());
|
||||||
current_user_tx.send(user).await.ok();
|
current_user_tx.send(user).await.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Nathan Sobo <nathan@warp.dev>"]
|
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
|
@ -26,6 +26,7 @@ base64 = "0.13"
|
||||||
clap = { version = "3.1", features = ["derive"], optional = true }
|
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||||
envy = "0.4.2"
|
envy = "0.4.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
git = { path = "../git" }
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
lipsum = { version = "0.8", optional = true }
|
lipsum = { version = "0.8", optional = true }
|
||||||
|
@ -66,11 +67,13 @@ rpc = { path = "../rpc", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
git = { path = "../git", features = ["test-support"] }
|
||||||
ctor = "0.1"
|
ctor = "0.1"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
unindent = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
seed-support = ["clap", "lipsum", "reqwest"]
|
seed-support = ["clap", "lipsum", "reqwest"]
|
||||||
|
|
27
crates/collab/migrations/20220913211150_create_signups.sql
Normal file
27
crates/collab/migrations/20220913211150_create_signups.sql
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS "signups" (
|
||||||
|
"id" SERIAL PRIMARY KEY,
|
||||||
|
"email_address" VARCHAR NOT NULL,
|
||||||
|
"email_confirmation_code" VARCHAR(64) NOT NULL,
|
||||||
|
"email_confirmation_sent" BOOLEAN NOT NULL,
|
||||||
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"device_id" VARCHAR,
|
||||||
|
"user_id" INTEGER REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
"inviting_user_id" INTEGER REFERENCES users (id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
"platform_mac" BOOLEAN NOT NULL,
|
||||||
|
"platform_linux" BOOLEAN NOT NULL,
|
||||||
|
"platform_windows" BOOLEAN NOT NULL,
|
||||||
|
"platform_unknown" BOOLEAN NOT NULL,
|
||||||
|
|
||||||
|
"editor_features" VARCHAR[],
|
||||||
|
"programming_languages" VARCHAR[]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "index_signups_on_email_address" ON "signups" ("email_address");
|
||||||
|
CREATE INDEX "index_signups_on_email_confirmation_sent" ON "signups" ("email_confirmation_sent");
|
||||||
|
|
||||||
|
ALTER TABLE "users"
|
||||||
|
ADD "github_user_id" INTEGER;
|
||||||
|
|
||||||
|
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||||
|
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE "users"
|
||||||
|
ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid();
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{ProjectId, User, UserId},
|
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||||
rpc::{self, ResultExt},
|
rpc::{self, ResultExt},
|
||||||
AppState, Error, Result,
|
AppState, Error, Result,
|
||||||
};
|
};
|
||||||
|
@ -24,13 +24,10 @@ use tracing::instrument;
|
||||||
|
|
||||||
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/user", get(get_authenticated_user))
|
||||||
.route("/users", get(get_users).post(create_user))
|
.route("/users", get(get_users).post(create_user))
|
||||||
.route(
|
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||||
"/users/:id",
|
|
||||||
put(update_user).delete(destroy_user).get(get_user),
|
|
||||||
)
|
|
||||||
.route("/users/:id/access_tokens", post(create_access_token))
|
.route("/users/:id/access_tokens", post(create_access_token))
|
||||||
.route("/bulk_users", post(create_users))
|
|
||||||
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
.route("/users_with_no_invites", get(get_users_with_no_invites))
|
||||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||||
.route("/panic", post(trace_panic))
|
.route("/panic", post(trace_panic))
|
||||||
|
@ -45,6 +42,11 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
|
||||||
)
|
)
|
||||||
.route("/user_activity/counts", get(get_active_user_counts))
|
.route("/user_activity/counts", get(get_active_user_counts))
|
||||||
.route("/project_metadata", get(get_project_metadata))
|
.route("/project_metadata", get(get_project_metadata))
|
||||||
|
.route("/signups", post(create_signup))
|
||||||
|
.route("/signups_summary", get(get_waitlist_summary))
|
||||||
|
.route("/user_invites", post(create_invite_from_code))
|
||||||
|
.route("/unsent_invites", get(get_unsent_invites))
|
||||||
|
.route("/sent_invites", post(record_sent_invites))
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(Extension(state))
|
.layer(Extension(state))
|
||||||
|
@ -84,6 +86,31 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||||
Ok::<_, Error>(next.run(req).await)
|
Ok::<_, Error>(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthenticatedUserParams {
|
||||||
|
github_user_id: i32,
|
||||||
|
github_login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AuthenticatedUserResponse {
|
||||||
|
user: User,
|
||||||
|
metrics_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_authenticated_user(
|
||||||
|
Query(params): Query<AuthenticatedUserParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||||
|
let user = app
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(¶ms.github_login, Some(params.github_user_id))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
|
||||||
|
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||||
|
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetUsersQueryParams {
|
struct GetUsersQueryParams {
|
||||||
query: Option<String>,
|
query: Option<String>,
|
||||||
|
@ -108,48 +135,76 @@ async fn get_users(
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct CreateUserParams {
|
struct CreateUserParams {
|
||||||
|
github_user_id: i32,
|
||||||
github_login: String,
|
github_login: String,
|
||||||
invite_code: Option<String>,
|
email_address: String,
|
||||||
email_address: Option<String>,
|
email_confirmation_code: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
admin: bool,
|
admin: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
invite_count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct CreateUserResponse {
|
||||||
|
user: User,
|
||||||
|
signup_device_id: Option<String>,
|
||||||
|
metrics_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
Json(params): Json<CreateUserParams>,
|
Json(params): Json<CreateUserParams>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||||
) -> Result<Json<User>> {
|
) -> Result<Json<CreateUserResponse>> {
|
||||||
let user_id = if let Some(invite_code) = params.invite_code {
|
let user = NewUserParams {
|
||||||
let invitee_id = app
|
github_login: params.github_login,
|
||||||
.db
|
github_user_id: params.github_user_id,
|
||||||
.redeem_invite_code(
|
invite_count: params.invite_count,
|
||||||
&invite_code,
|
};
|
||||||
¶ms.github_login,
|
|
||||||
params.email_address.as_deref(),
|
// Creating a user via the normal signup process
|
||||||
)
|
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
|
||||||
.await?;
|
|
||||||
rpc_server
|
|
||||||
.invite_code_redeemed(&invite_code, invitee_id)
|
|
||||||
.await
|
|
||||||
.trace_err();
|
|
||||||
invitee_id
|
|
||||||
} else {
|
|
||||||
app.db
|
app.db
|
||||||
.create_user(
|
.create_user_from_invite(
|
||||||
¶ms.github_login,
|
&Invite {
|
||||||
params.email_address.as_deref(),
|
email_address: params.email_address,
|
||||||
params.admin,
|
email_confirmation_code,
|
||||||
|
},
|
||||||
|
user,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
}
|
||||||
|
// Creating a user as an admin
|
||||||
|
else if params.admin {
|
||||||
|
app.db
|
||||||
|
.create_user(¶ms.email_address, false, user)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
Err(Error::Http(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"email confirmation code is required".into(),
|
||||||
|
))?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(inviter_id) = result.inviting_user_id {
|
||||||
|
rpc_server
|
||||||
|
.invite_code_redeemed(inviter_id, result.user_id)
|
||||||
|
.await
|
||||||
|
.trace_err();
|
||||||
|
}
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_id(user_id)
|
.get_user_by_id(result.user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
|
||||||
|
|
||||||
Ok(Json(user))
|
Ok(Json(CreateUserResponse {
|
||||||
|
user,
|
||||||
|
metrics_id: result.metrics_id,
|
||||||
|
signup_device_id: result.signup_device_id,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -171,7 +226,9 @@ async fn update_user(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(invite_count) = params.invite_count {
|
if let Some(invite_count) = params.invite_count {
|
||||||
app.db.set_invite_count(user_id, invite_count).await?;
|
app.db
|
||||||
|
.set_invite_count_for_user(user_id, invite_count)
|
||||||
|
.await?;
|
||||||
rpc_server.invite_count_updated(user_id).await.trace_err();
|
rpc_server.invite_count_updated(user_id).await.trace_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,54 +243,6 @@ async fn destroy_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(
|
|
||||||
Path(login): Path<String>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<User>> {
|
|
||||||
let user = app
|
|
||||||
.db
|
|
||||||
.get_user_by_github_login(&login)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
|
|
||||||
Ok(Json(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersParams {
|
|
||||||
users: Vec<CreateUsersEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateUsersEntry {
|
|
||||||
github_login: String,
|
|
||||||
email_address: String,
|
|
||||||
invite_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_users(
|
|
||||||
Json(params): Json<CreateUsersParams>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<Vec<User>>> {
|
|
||||||
let user_ids = app
|
|
||||||
.db
|
|
||||||
.create_users(
|
|
||||||
params
|
|
||||||
.users
|
|
||||||
.into_iter()
|
|
||||||
.map(|params| {
|
|
||||||
(
|
|
||||||
params.github_login,
|
|
||||||
params.email_address,
|
|
||||||
params.invite_count,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let users = app.db.get_users_by_ids(user_ids).await?;
|
|
||||||
Ok(Json(users))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetUsersWithNoInvites {
|
struct GetUsersWithNoInvites {
|
||||||
invited_by_another_user: bool,
|
invited_by_another_user: bool,
|
||||||
|
@ -368,22 +377,24 @@ struct CreateAccessTokenResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_access_token(
|
async fn create_access_token(
|
||||||
Path(login): Path<String>,
|
Path(user_id): Path<UserId>,
|
||||||
Query(params): Query<CreateAccessTokenQueryParams>,
|
Query(params): Query<CreateAccessTokenQueryParams>,
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
) -> Result<Json<CreateAccessTokenResponse>> {
|
) -> Result<Json<CreateAccessTokenResponse>> {
|
||||||
// request.require_token().await?;
|
|
||||||
|
|
||||||
let user = app
|
let user = app
|
||||||
.db
|
.db
|
||||||
.get_user_by_github_login(&login)
|
.get_user_by_id(user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| anyhow!("user not found"))?;
|
.ok_or_else(|| anyhow!("user not found"))?;
|
||||||
|
|
||||||
let mut user_id = user.id;
|
let mut user_id = user.id;
|
||||||
if let Some(impersonate) = params.impersonate {
|
if let Some(impersonate) = params.impersonate {
|
||||||
if user.admin {
|
if user.admin {
|
||||||
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? {
|
if let Some(impersonated_user) = app
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(&impersonate, None)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
user_id = impersonated_user.id;
|
user_id = impersonated_user.id;
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::Http(
|
return Err(Error::Http(
|
||||||
|
@ -415,3 +426,59 @@ async fn get_user_for_invite_code(
|
||||||
) -> Result<Json<User>> {
|
) -> Result<Json<User>> {
|
||||||
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
Ok(Json(app.db.get_user_for_invite_code(&code).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_signup(
|
||||||
|
Json(params): Json<Signup>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.create_signup(params).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_waitlist_summary(
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<WaitlistSummary>> {
|
||||||
|
Ok(Json(app.db.get_waitlist_summary().await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateInviteFromCodeParams {
|
||||||
|
invite_code: String,
|
||||||
|
email_address: String,
|
||||||
|
device_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_invite_from_code(
|
||||||
|
Json(params): Json<CreateInviteFromCodeParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Invite>> {
|
||||||
|
Ok(Json(
|
||||||
|
app.db
|
||||||
|
.create_invite_from_code(
|
||||||
|
¶ms.invite_code,
|
||||||
|
¶ms.email_address,
|
||||||
|
params.device_id.as_deref(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GetUnsentInvitesParams {
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_unsent_invites(
|
||||||
|
Query(params): Query<GetUnsentInvitesParams>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<Json<Vec<Invite>>> {
|
||||||
|
Ok(Json(app.db.get_unsent_invites(params.count).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_sent_invites(
|
||||||
|
Json(params): Json<Vec<Invite>>,
|
||||||
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
app.db.record_sent_invites(¶ms).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ mod db;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GitHubUser {
|
struct GitHubUser {
|
||||||
id: usize,
|
id: i32,
|
||||||
login: String,
|
login: String,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,11 @@ async fn main() {
|
||||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
let current_user =
|
let mut current_user =
|
||||||
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
|
||||||
|
current_user
|
||||||
|
.email
|
||||||
|
.get_or_insert_with(|| "placeholder@example.com".to_string());
|
||||||
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
let staff_users = fetch_github::<Vec<GitHubUser>>(
|
||||||
&client,
|
&client,
|
||||||
&github_token,
|
&github_token,
|
||||||
|
@ -64,16 +67,24 @@ async fn main() {
|
||||||
let mut zed_user_ids = Vec::<UserId>::new();
|
let mut zed_user_ids = Vec::<UserId>::new();
|
||||||
for (github_user, admin) in zed_users {
|
for (github_user, admin) in zed_users {
|
||||||
if let Some(user) = db
|
if let Some(user) = db
|
||||||
.get_user_by_github_login(&github_user.login)
|
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||||
.await
|
.await
|
||||||
.expect("failed to fetch user")
|
.expect("failed to fetch user")
|
||||||
{
|
{
|
||||||
zed_user_ids.push(user.id);
|
zed_user_ids.push(user.id);
|
||||||
} else {
|
} else if let Some(email) = &github_user.email {
|
||||||
zed_user_ids.push(
|
zed_user_ids.push(
|
||||||
db.create_user(&github_user.login, github_user.email.as_deref(), admin)
|
db.create_user(
|
||||||
.await
|
email,
|
||||||
.expect("failed to insert user"),
|
admin,
|
||||||
|
db::NewUserParams {
|
||||||
|
github_login: github_user.login,
|
||||||
|
github_user_id: github_user.id,
|
||||||
|
invite_count: 5,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("failed to insert user"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
1186
crates/collab/src/db_tests.rs
Normal file
1186
crates/collab/src/db_tests.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{tests::TestDb, ProjectId, UserId},
|
db::{NewUserParams, ProjectId, TestDb, UserId},
|
||||||
rpc::{Executor, Server, Store},
|
rpc::{Executor, Server, Store},
|
||||||
AppState,
|
AppState,
|
||||||
};
|
};
|
||||||
|
@ -52,6 +52,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
|
use unindent::Unindent as _;
|
||||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -329,6 +330,7 @@ async fn test_room_uniqueness(
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
deterministic.run_until_parked();
|
||||||
let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
|
let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
|
||||||
assert_eq!(call_b2.caller.github_login, "user_c");
|
assert_eq!(call_b2.caller.github_login, "user_c");
|
||||||
}
|
}
|
||||||
|
@ -1174,6 +1176,258 @@ async fn test_propagate_saves_and_fs_changes(
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test(iterations = 10)]
|
||||||
|
async fn test_git_diff_base_change(
|
||||||
|
executor: Arc<Deterministic>,
|
||||||
|
cx_a: &mut TestAppContext,
|
||||||
|
cx_b: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
executor.forbid_parking();
|
||||||
|
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||||
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
server
|
||||||
|
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||||
|
.await;
|
||||||
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.insert_tree(
|
||||||
|
"/dir",
|
||||||
|
json!({
|
||||||
|
".git": {},
|
||||||
|
"sub": {
|
||||||
|
".git": {},
|
||||||
|
"b.txt": "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
".unindent(),
|
||||||
|
},
|
||||||
|
"a.txt": "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
".unindent(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||||
|
let project_id = active_call_a
|
||||||
|
.update(cx_a, |call, cx| {
|
||||||
|
call.share_project(project_local.clone(), cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let new_diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.set_index_for_repo(
|
||||||
|
Path::new("/dir/.git"),
|
||||||
|
&[(Path::new("a.txt"), diff_base.clone())],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create the buffer
|
||||||
|
let buffer_local_a = project_local
|
||||||
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait for it to catch up to the new diff
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test diffing
|
||||||
|
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "", "two\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create remote buffer
|
||||||
|
let buffer_remote_a = project_remote
|
||||||
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait remote buffer to catch up to the new diff
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test diffing
|
||||||
|
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "", "two\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.set_index_for_repo(
|
||||||
|
Path::new("/dir/.git"),
|
||||||
|
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Wait for buffer_local_a to receive it
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test new diffing
|
||||||
|
buffer_local_a.read_with(cx_a, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||||
|
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(2..3, "", "three\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smoke test B
|
||||||
|
buffer_remote_a.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(2..3, "", "three\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
//Nested git dir
|
||||||
|
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let new_diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.set_index_for_repo(
|
||||||
|
Path::new("/dir/sub/.git"),
|
||||||
|
&[(Path::new("b.txt"), diff_base.clone())],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Create the buffer
|
||||||
|
let buffer_local_b = project_local
|
||||||
|
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait for it to catch up to the new diff
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test diffing
|
||||||
|
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "", "two\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create remote buffer
|
||||||
|
let buffer_remote_b = project_remote
|
||||||
|
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Wait remote buffer to catch up to the new diff
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test diffing
|
||||||
|
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "", "two\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.set_index_for_repo(
|
||||||
|
Path::new("/dir/sub/.git"),
|
||||||
|
&[(Path::new("b.txt"), new_diff_base.clone())],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Wait for buffer_local_b to receive it
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
// Smoke test new diffing
|
||||||
|
buffer_local_b.read_with(cx_a, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||||
|
println!("{:?}", buffer.as_rope().to_string());
|
||||||
|
println!("{:?}", buffer.diff_base());
|
||||||
|
println!(
|
||||||
|
"{:?}",
|
||||||
|
buffer
|
||||||
|
.snapshot()
|
||||||
|
.git_diff_hunks_in_range(0..4)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(2..3, "", "three\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smoke test B
|
||||||
|
buffer_remote_b.read_with(cx_b, |buffer, _| {
|
||||||
|
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||||
|
git::diff::assert_hunks(
|
||||||
|
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(2..3, "", "three\n")],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_fs_operations(
|
async fn test_fs_operations(
|
||||||
executor: Arc<Deterministic>,
|
executor: Arc<Deterministic>,
|
||||||
|
@ -5092,7 +5346,19 @@ async fn test_random_collaboration(
|
||||||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||||
let db = server.app_state.db.clone();
|
let db = server.app_state.db.clone();
|
||||||
|
|
||||||
let room_creator_user_id = db.create_user("room-creator", None, false).await.unwrap();
|
let room_creator_user_id = db
|
||||||
|
.create_user(
|
||||||
|
"room-creator@example.com",
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: "room-creator".into(),
|
||||||
|
github_user_id: 0,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
let mut available_guests = vec![
|
let mut available_guests = vec![
|
||||||
"guest-1".to_string(),
|
"guest-1".to_string(),
|
||||||
"guest-2".to_string(),
|
"guest-2".to_string(),
|
||||||
|
@ -5100,11 +5366,24 @@ async fn test_random_collaboration(
|
||||||
"guest-4".to_string(),
|
"guest-4".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for username in Some(&"host".to_string())
|
for (ix, username) in Some(&"host".to_string())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(&available_guests)
|
.chain(&available_guests)
|
||||||
|
.enumerate()
|
||||||
{
|
{
|
||||||
let user_id = db.create_user(username, None, false).await.unwrap();
|
let user_id = db
|
||||||
|
.create_user(
|
||||||
|
&format!("{username}@example.com"),
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: username.into(),
|
||||||
|
github_user_id: (ix + 1) as i32,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id;
|
||||||
server
|
server
|
||||||
.app_state
|
.app_state
|
||||||
.db
|
.db
|
||||||
|
@ -5632,18 +5911,31 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
let http = FakeHttpClient::with_404_response();
|
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
|
let user_id = if let Ok(Some(user)) = self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.get_user_by_github_account(name, None)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
user.id
|
user.id
|
||||||
} else {
|
} else {
|
||||||
self.app_state
|
self.app_state
|
||||||
.db
|
.db
|
||||||
.create_user(name, None, false)
|
.create_user(
|
||||||
|
&format!("{name}@example.com"),
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: name.into(),
|
||||||
|
github_user_id: 0,
|
||||||
|
invite_count: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.user_id
|
||||||
};
|
};
|
||||||
let client_name = name.to_string();
|
let client_name = name.to_string();
|
||||||
let mut client = Client::new(http.clone());
|
let mut client = cx.read(|cx| Client::new(http.clone(), cx));
|
||||||
let server = self.server.clone();
|
let server = self.server.clone();
|
||||||
let db = self.app_state.db.clone();
|
let db = self.app_state.db.clone();
|
||||||
let connection_killers = self.connection_killers.clone();
|
let connection_killers = self.connection_killers.clone();
|
||||||
|
|
|
@ -4,6 +4,8 @@ mod db;
|
||||||
mod env;
|
mod env;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod db_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod integration_tests;
|
mod integration_tests;
|
||||||
|
|
||||||
|
|
|
@ -206,7 +206,9 @@ impl Server {
|
||||||
.add_request_handler(Server::follow)
|
.add_request_handler(Server::follow)
|
||||||
.add_message_handler(Server::unfollow)
|
.add_message_handler(Server::unfollow)
|
||||||
.add_message_handler(Server::update_followers)
|
.add_message_handler(Server::update_followers)
|
||||||
.add_request_handler(Server::get_channel_messages);
|
.add_request_handler(Server::get_channel_messages)
|
||||||
|
.add_message_handler(Server::update_diff_base)
|
||||||
|
.add_request_handler(Server::get_private_user_info);
|
||||||
|
|
||||||
Arc::new(server)
|
Arc::new(server)
|
||||||
}
|
}
|
||||||
|
@ -528,27 +530,30 @@ impl Server {
|
||||||
|
|
||||||
pub async fn invite_code_redeemed(
|
pub async fn invite_code_redeemed(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
code: &str,
|
inviter_id: UserId,
|
||||||
invitee_id: UserId,
|
invitee_id: UserId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let user = self.app_state.db.get_user_for_invite_code(code).await?;
|
if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
|
||||||
let store = self.store().await;
|
if let Some(code) = &user.invite_code {
|
||||||
let invitee_contact = store.contact_for_user(invitee_id, true);
|
let store = self.store().await;
|
||||||
for connection_id in store.connection_ids_for_user(user.id) {
|
let invitee_contact = store.contact_for_user(invitee_id, true);
|
||||||
self.peer.send(
|
for connection_id in store.connection_ids_for_user(inviter_id) {
|
||||||
connection_id,
|
self.peer.send(
|
||||||
proto::UpdateContacts {
|
connection_id,
|
||||||
contacts: vec![invitee_contact.clone()],
|
proto::UpdateContacts {
|
||||||
..Default::default()
|
contacts: vec![invitee_contact.clone()],
|
||||||
},
|
..Default::default()
|
||||||
)?;
|
},
|
||||||
self.peer.send(
|
)?;
|
||||||
connection_id,
|
self.peer.send(
|
||||||
proto::UpdateInviteInfo {
|
connection_id,
|
||||||
url: format!("{}{}", self.app_state.invite_link_prefix, code),
|
proto::UpdateInviteInfo {
|
||||||
count: user.invite_count as u32,
|
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
|
||||||
},
|
count: user.invite_count as u32,
|
||||||
)?;
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1427,7 +1432,7 @@ impl Server {
|
||||||
let users = match query.len() {
|
let users = match query.len() {
|
||||||
0 => vec![],
|
0 => vec![],
|
||||||
1 | 2 => db
|
1 | 2 => db
|
||||||
.get_user_by_github_login(&query)
|
.get_user_by_github_account(&query, None)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -1750,6 +1755,44 @@ impl Server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_diff_base(
|
||||||
|
self: Arc<Server>,
|
||||||
|
request: TypedEnvelope<proto::UpdateDiffBase>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let receiver_ids = self.store().await.project_connection_ids(
|
||||||
|
ProjectId::from_proto(request.payload.project_id),
|
||||||
|
request.sender_id,
|
||||||
|
)?;
|
||||||
|
broadcast(request.sender_id, receiver_ids, |connection_id| {
|
||||||
|
self.peer
|
||||||
|
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_private_user_info(
|
||||||
|
self: Arc<Self>,
|
||||||
|
request: TypedEnvelope<proto::GetPrivateUserInfo>,
|
||||||
|
response: Response<proto::GetPrivateUserInfo>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let user_id = self
|
||||||
|
.store()
|
||||||
|
.await
|
||||||
|
.user_id_for_connection(request.sender_id)?;
|
||||||
|
let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?;
|
||||||
|
let user = self
|
||||||
|
.app_state
|
||||||
|
.db
|
||||||
|
.get_user_by_id(user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("user not found"))?;
|
||||||
|
response.send(proto::GetPrivateUserInfoResponse {
|
||||||
|
metrics_id,
|
||||||
|
staff: user.admin,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn store(&self) -> StoreGuard<'_> {
|
pub(crate) async fn store(&self) -> StoreGuard<'_> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
|
|
22
crates/db/Cargo.toml
Normal file
22
crates/db/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/db.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
anyhow = "1.0.57"
|
||||||
|
async-trait = "0.1"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
rocksdb = "0.18"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
tempdir = { version = "0.3.7" }
|
|
@ -25,6 +25,7 @@ clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
@ -51,6 +52,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
smol = "1.2"
|
smol = "1.2"
|
||||||
tree-sitter-rust = { version = "*", optional = true }
|
tree-sitter-rust = { version = "*", optional = true }
|
||||||
|
tree-sitter-html = { version = "*", optional = true }
|
||||||
|
tree-sitter-javascript = { version = "*", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
text = { path = "../text", features = ["test-support"] }
|
text = { path = "../text", features = ["test-support"] }
|
||||||
|
@ -67,3 +70,5 @@ rand = "0.8"
|
||||||
unindent = "0.1.7"
|
unindent = "0.1.7"
|
||||||
tree-sitter = "0.20"
|
tree-sitter = "0.20"
|
||||||
tree-sitter-rust = "0.20"
|
tree-sitter-rust = "0.20"
|
||||||
|
tree-sitter-html = "0.19"
|
||||||
|
tree-sitter-javascript = "0.20"
|
||||||
|
|
|
@ -274,6 +274,7 @@ impl FoldMap {
|
||||||
if buffer.edit_count() != new_buffer.edit_count()
|
if buffer.edit_count() != new_buffer.edit_count()
|
||||||
|| buffer.parse_count() != new_buffer.parse_count()
|
|| buffer.parse_count() != new_buffer.parse_count()
|
||||||
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
||||||
|
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|
||||||
|| buffer.trailing_excerpt_update_count()
|
|| buffer.trailing_excerpt_update_count()
|
||||||
!= new_buffer.trailing_excerpt_update_count()
|
!= new_buffer.trailing_excerpt_update_count()
|
||||||
{
|
{
|
||||||
|
|
File diff suppressed because it is too large
Load diff
4936
crates/editor/src/editor_tests.rs
Normal file
4936
crates/editor/src/editor_tests.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
|
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::*,
|
elements::*,
|
||||||
|
@ -36,7 +37,7 @@ use gpui::{
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use settings::Settings;
|
use settings::{GitGutter, Settings};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
|
@ -45,6 +46,7 @@ use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use theme::DiffStyle;
|
||||||
|
|
||||||
struct SelectionLayout {
|
struct SelectionLayout {
|
||||||
head: DisplayPoint,
|
head: DisplayPoint,
|
||||||
|
@ -524,30 +526,141 @@ impl EditorElement {
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
let scroll_top =
|
struct GutterLayout {
|
||||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
line_height: f32,
|
||||||
|
// scroll_position: Vector2F,
|
||||||
|
scroll_top: f32,
|
||||||
|
bounds: RectF,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DiffLayout<'a> {
|
||||||
|
buffer_row: u32,
|
||||||
|
last_diff: Option<&'a DiffHunk<u32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff_quad(
|
||||||
|
hunk: &DiffHunk<u32>,
|
||||||
|
gutter_layout: &GutterLayout,
|
||||||
|
diff_style: &DiffStyle,
|
||||||
|
) -> Quad {
|
||||||
|
let color = match hunk.status() {
|
||||||
|
DiffHunkStatus::Added => diff_style.inserted,
|
||||||
|
DiffHunkStatus::Modified => diff_style.modified,
|
||||||
|
|
||||||
|
//TODO: This rendering is entirely a horrible hack
|
||||||
|
DiffHunkStatus::Removed => {
|
||||||
|
let row = hunk.buffer_range.start;
|
||||||
|
|
||||||
|
let offset = gutter_layout.line_height / 2.;
|
||||||
|
let start_y =
|
||||||
|
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
|
||||||
|
let end_y = start_y + gutter_layout.line_height;
|
||||||
|
|
||||||
|
let width = diff_style.removed_width_em * gutter_layout.line_height;
|
||||||
|
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||||
|
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||||
|
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||||
|
|
||||||
|
return Quad {
|
||||||
|
bounds: highlight_bounds,
|
||||||
|
background: Some(diff_style.deleted),
|
||||||
|
border: Border::new(0., Color::transparent_black()),
|
||||||
|
corner_radius: 1. * gutter_layout.line_height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_row = hunk.buffer_range.start;
|
||||||
|
let end_row = hunk.buffer_range.end;
|
||||||
|
|
||||||
|
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||||
|
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||||
|
|
||||||
|
let width = diff_style.width_em * gutter_layout.line_height;
|
||||||
|
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||||
|
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||||
|
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||||
|
|
||||||
|
Quad {
|
||||||
|
bounds: highlight_bounds,
|
||||||
|
background: Some(color),
|
||||||
|
border: Border::new(0., Color::transparent_black()),
|
||||||
|
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||||
|
let gutter_layout = {
|
||||||
|
let line_height = layout.position_map.line_height;
|
||||||
|
GutterLayout {
|
||||||
|
scroll_top: scroll_position.y() * line_height,
|
||||||
|
line_height,
|
||||||
|
bounds,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut diff_layout = DiffLayout {
|
||||||
|
buffer_row: scroll_position.y() as u32,
|
||||||
|
last_diff: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
|
||||||
|
let show_gutter = matches!(
|
||||||
|
&cx.global::<Settings>()
|
||||||
|
.git_overrides
|
||||||
|
.git_gutter
|
||||||
|
.unwrap_or_default(),
|
||||||
|
GitGutter::TrackedFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
// line is `None` when there's a line wrap
|
||||||
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
||||||
if let Some(line) = line {
|
if let Some(line) = line {
|
||||||
let line_origin = bounds.origin()
|
let line_origin = bounds.origin()
|
||||||
+ vec2f(
|
+ vec2f(
|
||||||
bounds.width() - line.width() - layout.gutter_padding,
|
bounds.width() - line.width() - layout.gutter_padding,
|
||||||
ix as f32 * layout.position_map.line_height
|
ix as f32 * gutter_layout.line_height
|
||||||
- (scroll_top % layout.position_map.line_height),
|
- (gutter_layout.scroll_top % gutter_layout.line_height),
|
||||||
);
|
);
|
||||||
line.paint(
|
|
||||||
line_origin,
|
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
|
||||||
visible_bounds,
|
|
||||||
layout.position_map.line_height,
|
if show_gutter {
|
||||||
cx,
|
//This line starts a buffer line, so let's do the diff calculation
|
||||||
);
|
let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks);
|
||||||
|
|
||||||
|
let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
|
||||||
|
(Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => (false, false),
|
||||||
|
(a, b) => (a.is_some(), b.is_some()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_ending {
|
||||||
|
let last_hunk = diff_layout.last_diff.take().unwrap();
|
||||||
|
cx.scene
|
||||||
|
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_starting {
|
||||||
|
let new_hunk = new_hunk.unwrap();
|
||||||
|
diff_layout.last_diff = Some(new_hunk);
|
||||||
|
};
|
||||||
|
|
||||||
|
diff_layout.buffer_row += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we ran out with a diff hunk still being prepped, paint it now
|
||||||
|
if let Some(last_hunk) = diff_layout.last_diff {
|
||||||
|
cx.scene
|
||||||
|
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style))
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||||
let mut x = bounds.width() - layout.gutter_padding;
|
let mut x = bounds.width() - layout.gutter_padding;
|
||||||
let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
|
let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||||
y += (layout.position_map.line_height - indicator.size().y()) / 2.;
|
y += (gutter_layout.line_height - indicator.size().y()) / 2.;
|
||||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1252,6 +1365,27 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the hunk that contains buffer_line, starting from start_idx
|
||||||
|
/// Returns none if there is none found, and
|
||||||
|
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
|
||||||
|
for i in 0..hunks.len() {
|
||||||
|
// Safety: Index out of bounds is handled by the check above
|
||||||
|
let hunk = hunks.get(i).unwrap();
|
||||||
|
if hunk.buffer_range.contains(&(buffer_line as u32)) {
|
||||||
|
return Some(hunk);
|
||||||
|
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
|
||||||
|
{
|
||||||
|
return Some(hunk);
|
||||||
|
} else if hunk.buffer_range.start > buffer_line as u32 {
|
||||||
|
// If we've passed the buffer_line, just stop
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We reached the end of the array without finding a hunk, just return none.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
impl Element for EditorElement {
|
impl Element for EditorElement {
|
||||||
type LayoutState = LayoutState;
|
type LayoutState = LayoutState;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
@ -1425,6 +1559,11 @@ impl Element for EditorElement {
|
||||||
let line_number_layouts =
|
let line_number_layouts =
|
||||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||||
|
|
||||||
|
let diff_hunks = snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.git_diff_hunks_in_range(start_row..end_row)
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut max_visible_line_width = 0.0;
|
let mut max_visible_line_width = 0.0;
|
||||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||||
for line in &line_layouts {
|
for line in &line_layouts {
|
||||||
|
@ -1573,6 +1712,7 @@ impl Element for EditorElement {
|
||||||
highlighted_rows,
|
highlighted_rows,
|
||||||
highlighted_ranges,
|
highlighted_ranges,
|
||||||
line_number_layouts,
|
line_number_layouts,
|
||||||
|
diff_hunks,
|
||||||
blocks,
|
blocks,
|
||||||
selections,
|
selections,
|
||||||
context_menu,
|
context_menu,
|
||||||
|
@ -1710,6 +1850,7 @@ pub struct LayoutState {
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
|
diff_hunks: Vec<DiffHunk<u32>>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,6 +404,8 @@ impl Item for Editor {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
|
self.report_event("save editor", cx);
|
||||||
|
|
||||||
let buffer = self.buffer().clone();
|
let buffer = self.buffer().clone();
|
||||||
let buffers = buffer.read(cx).all_buffers();
|
let buffers = buffer.read(cx).all_buffers();
|
||||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||||
|
@ -476,6 +478,17 @@ impl Item for Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn git_diff_recalc(
|
||||||
|
&mut self,
|
||||||
|
_project: ModelHandle<Project>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.buffer().update(cx, |multibuffer, cx| {
|
||||||
|
multibuffer.git_diff_recalc(cx);
|
||||||
|
});
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
match event {
|
match event {
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
|
use git::diff::DiffHunk;
|
||||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
pub use language::Completion;
|
pub use language::Completion;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -90,6 +91,7 @@ struct BufferState {
|
||||||
last_selections_update_count: usize,
|
last_selections_update_count: usize,
|
||||||
last_diagnostics_update_count: usize,
|
last_diagnostics_update_count: usize,
|
||||||
last_file_update_count: usize,
|
last_file_update_count: usize,
|
||||||
|
last_git_diff_update_count: usize,
|
||||||
excerpts: Vec<ExcerptId>,
|
excerpts: Vec<ExcerptId>,
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
}
|
}
|
||||||
|
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
|
||||||
parse_count: usize,
|
parse_count: usize,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
trailing_excerpt_update_count: usize,
|
trailing_excerpt_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
edit_count: usize,
|
edit_count: usize,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
|
@ -202,6 +205,7 @@ impl MultiBuffer {
|
||||||
last_selections_update_count: buffer_state.last_selections_update_count,
|
last_selections_update_count: buffer_state.last_selections_update_count,
|
||||||
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
||||||
last_file_update_count: buffer_state.last_file_update_count,
|
last_file_update_count: buffer_state.last_file_update_count,
|
||||||
|
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
|
||||||
excerpts: buffer_state.excerpts.clone(),
|
excerpts: buffer_state.excerpts.clone(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||||
|
@ -308,6 +312,17 @@ impl MultiBuffer {
|
||||||
self.read(cx).symbols_containing(offset, theme)
|
self.read(cx).symbols_containing(offset, theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
let buffers = self.buffers.borrow();
|
||||||
|
for buffer_state in buffers.values() {
|
||||||
|
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
|
||||||
|
buffer_state
|
||||||
|
.buffer
|
||||||
|
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit<I, S, T>(
|
pub fn edit<I, S, T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
edits: I,
|
edits: I,
|
||||||
|
@ -827,6 +842,7 @@ impl MultiBuffer {
|
||||||
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
||||||
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
||||||
last_file_update_count: buffer_snapshot.file_update_count(),
|
last_file_update_count: buffer_snapshot.file_update_count(),
|
||||||
|
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
|
||||||
excerpts: Default::default(),
|
excerpts: Default::default(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||||
|
@ -1212,9 +1228,9 @@ impl MultiBuffer {
|
||||||
&self,
|
&self,
|
||||||
point: T,
|
point: T,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> Option<&'a Arc<Language>> {
|
) -> Option<Arc<Language>> {
|
||||||
self.point_to_buffer_offset(point, cx)
|
self.point_to_buffer_offset(point, cx)
|
||||||
.and_then(|(buffer, _)| buffer.read(cx).language())
|
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
|
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
|
||||||
|
@ -1249,6 +1265,7 @@ impl MultiBuffer {
|
||||||
let mut excerpts_to_edit = Vec::new();
|
let mut excerpts_to_edit = Vec::new();
|
||||||
let mut reparsed = false;
|
let mut reparsed = false;
|
||||||
let mut diagnostics_updated = false;
|
let mut diagnostics_updated = false;
|
||||||
|
let mut git_diff_updated = false;
|
||||||
let mut is_dirty = false;
|
let mut is_dirty = false;
|
||||||
let mut has_conflict = false;
|
let mut has_conflict = false;
|
||||||
let mut edited = false;
|
let mut edited = false;
|
||||||
|
@ -1260,6 +1277,7 @@ impl MultiBuffer {
|
||||||
let selections_update_count = buffer.selections_update_count();
|
let selections_update_count = buffer.selections_update_count();
|
||||||
let diagnostics_update_count = buffer.diagnostics_update_count();
|
let diagnostics_update_count = buffer.diagnostics_update_count();
|
||||||
let file_update_count = buffer.file_update_count();
|
let file_update_count = buffer.file_update_count();
|
||||||
|
let git_diff_update_count = buffer.git_diff_update_count();
|
||||||
|
|
||||||
let buffer_edited = version.changed_since(&buffer_state.last_version);
|
let buffer_edited = version.changed_since(&buffer_state.last_version);
|
||||||
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
||||||
|
@ -1268,17 +1286,21 @@ impl MultiBuffer {
|
||||||
let buffer_diagnostics_updated =
|
let buffer_diagnostics_updated =
|
||||||
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
||||||
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
|
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
|
||||||
|
let buffer_git_diff_updated =
|
||||||
|
git_diff_update_count > buffer_state.last_git_diff_update_count;
|
||||||
if buffer_edited
|
if buffer_edited
|
||||||
|| buffer_reparsed
|
|| buffer_reparsed
|
||||||
|| buffer_selections_updated
|
|| buffer_selections_updated
|
||||||
|| buffer_diagnostics_updated
|
|| buffer_diagnostics_updated
|
||||||
|| buffer_file_updated
|
|| buffer_file_updated
|
||||||
|
|| buffer_git_diff_updated
|
||||||
{
|
{
|
||||||
buffer_state.last_version = version;
|
buffer_state.last_version = version;
|
||||||
buffer_state.last_parse_count = parse_count;
|
buffer_state.last_parse_count = parse_count;
|
||||||
buffer_state.last_selections_update_count = selections_update_count;
|
buffer_state.last_selections_update_count = selections_update_count;
|
||||||
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
||||||
buffer_state.last_file_update_count = file_update_count;
|
buffer_state.last_file_update_count = file_update_count;
|
||||||
|
buffer_state.last_git_diff_update_count = git_diff_update_count;
|
||||||
excerpts_to_edit.extend(
|
excerpts_to_edit.extend(
|
||||||
buffer_state
|
buffer_state
|
||||||
.excerpts
|
.excerpts
|
||||||
|
@ -1290,6 +1312,7 @@ impl MultiBuffer {
|
||||||
edited |= buffer_edited;
|
edited |= buffer_edited;
|
||||||
reparsed |= buffer_reparsed;
|
reparsed |= buffer_reparsed;
|
||||||
diagnostics_updated |= buffer_diagnostics_updated;
|
diagnostics_updated |= buffer_diagnostics_updated;
|
||||||
|
git_diff_updated |= buffer_git_diff_updated;
|
||||||
is_dirty |= buffer.is_dirty();
|
is_dirty |= buffer.is_dirty();
|
||||||
has_conflict |= buffer.has_conflict();
|
has_conflict |= buffer.has_conflict();
|
||||||
}
|
}
|
||||||
|
@ -1302,6 +1325,9 @@ impl MultiBuffer {
|
||||||
if diagnostics_updated {
|
if diagnostics_updated {
|
||||||
snapshot.diagnostics_update_count += 1;
|
snapshot.diagnostics_update_count += 1;
|
||||||
}
|
}
|
||||||
|
if git_diff_updated {
|
||||||
|
snapshot.git_diff_update_count += 1;
|
||||||
|
}
|
||||||
snapshot.is_dirty = is_dirty;
|
snapshot.is_dirty = is_dirty;
|
||||||
snapshot.has_conflict = has_conflict;
|
snapshot.has_conflict = has_conflict;
|
||||||
|
|
||||||
|
@ -1940,6 +1966,24 @@ impl MultiBufferSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn point_to_buffer_offset<T: ToOffset>(
|
||||||
|
&self,
|
||||||
|
point: T,
|
||||||
|
) -> Option<(&BufferSnapshot, usize)> {
|
||||||
|
let offset = point.to_offset(&self);
|
||||||
|
let mut cursor = self.excerpts.cursor::<usize>();
|
||||||
|
cursor.seek(&offset, Bias::Right, &());
|
||||||
|
if cursor.item().is_none() {
|
||||||
|
cursor.prev(&());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.item().map(|excerpt| {
|
||||||
|
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||||
|
let buffer_point = excerpt_start + offset - *cursor.start();
|
||||||
|
(&excerpt.buffer, buffer_point)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn suggested_indents(
|
pub fn suggested_indents(
|
||||||
&self,
|
&self,
|
||||||
rows: impl IntoIterator<Item = u32>,
|
rows: impl IntoIterator<Item = u32>,
|
||||||
|
@ -1949,8 +1993,10 @@ impl MultiBufferSnapshot {
|
||||||
|
|
||||||
let mut rows_for_excerpt = Vec::new();
|
let mut rows_for_excerpt = Vec::new();
|
||||||
let mut cursor = self.excerpts.cursor::<Point>();
|
let mut cursor = self.excerpts.cursor::<Point>();
|
||||||
|
|
||||||
let mut rows = rows.into_iter().peekable();
|
let mut rows = rows.into_iter().peekable();
|
||||||
|
let mut prev_row = u32::MAX;
|
||||||
|
let mut prev_language_indent_size = IndentSize::default();
|
||||||
|
|
||||||
while let Some(row) = rows.next() {
|
while let Some(row) = rows.next() {
|
||||||
cursor.seek(&Point::new(row, 0), Bias::Right, &());
|
cursor.seek(&Point::new(row, 0), Bias::Right, &());
|
||||||
let excerpt = match cursor.item() {
|
let excerpt = match cursor.item() {
|
||||||
|
@ -1958,7 +2004,17 @@ impl MultiBufferSnapshot {
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let single_indent_size = excerpt.buffer.single_indent_size(cx);
|
// Retrieve the language and indent size once for each disjoint region being indented.
|
||||||
|
let single_indent_size = if row.saturating_sub(1) == prev_row {
|
||||||
|
prev_language_indent_size
|
||||||
|
} else {
|
||||||
|
excerpt
|
||||||
|
.buffer
|
||||||
|
.language_indent_size_at(Point::new(row, 0), cx)
|
||||||
|
};
|
||||||
|
prev_language_indent_size = single_indent_size;
|
||||||
|
prev_row = row;
|
||||||
|
|
||||||
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
|
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
|
||||||
let start_multibuffer_row = cursor.start().row;
|
let start_multibuffer_row = cursor.start().row;
|
||||||
|
|
||||||
|
@ -2479,15 +2535,17 @@ impl MultiBufferSnapshot {
|
||||||
self.diagnostics_update_count
|
self.diagnostics_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_update_count(&self) -> usize {
|
||||||
|
self.git_diff_update_count
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trailing_excerpt_update_count(&self) -> usize {
|
pub fn trailing_excerpt_update_count(&self) -> usize {
|
||||||
self.trailing_excerpt_update_count
|
self.trailing_excerpt_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
|
||||||
self.excerpts
|
self.point_to_buffer_offset(point)
|
||||||
.iter()
|
.and_then(|(buffer, offset)| buffer.language_at(offset))
|
||||||
.next()
|
|
||||||
.and_then(|excerpt| excerpt.buffer.language())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_dirty(&self) -> bool {
|
pub fn is_dirty(&self) -> bool {
|
||||||
|
@ -2529,6 +2587,15 @@ impl MultiBufferSnapshot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_hunks_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
row_range: Range<u32>,
|
||||||
|
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
self.as_singleton()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
|
|
28
crates/git/Cargo.toml
Normal file
28
crates/git/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[package]
|
||||||
|
name = "git"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/git.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.38"
|
||||||
|
clock = { path = "../clock" }
|
||||||
|
git2 = { version = "0.15", default-features = false }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
text = { path = "../text" }
|
||||||
|
collections = { path = "../collections" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
smol = "1.2"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
async-trait = "0.1"
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
unindent = "0.1.7"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
362
crates/git/src/diff.rs
Normal file
362
crates/git/src/diff.rs
Normal file
|
@ -0,0 +1,362 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use sum_tree::SumTree;
|
||||||
|
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||||
|
|
||||||
|
pub use git2 as libgit;
|
||||||
|
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum DiffHunkStatus {
|
||||||
|
Added,
|
||||||
|
Modified,
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DiffHunk<T> {
|
||||||
|
pub buffer_range: Range<T>,
|
||||||
|
pub head_byte_range: Range<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiffHunk<u32> {
|
||||||
|
pub fn status(&self) -> DiffHunkStatus {
|
||||||
|
if self.head_byte_range.is_empty() {
|
||||||
|
DiffHunkStatus::Added
|
||||||
|
} else if self.buffer_range.is_empty() {
|
||||||
|
DiffHunkStatus::Removed
|
||||||
|
} else {
|
||||||
|
DiffHunkStatus::Modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Item for DiffHunk<Anchor> {
|
||||||
|
type Summary = DiffHunkSummary;
|
||||||
|
|
||||||
|
fn summary(&self) -> Self::Summary {
|
||||||
|
DiffHunkSummary {
|
||||||
|
buffer_range: self.buffer_range.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct DiffHunkSummary {
|
||||||
|
buffer_range: Range<Anchor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sum_tree::Summary for DiffHunkSummary {
|
||||||
|
type Context = text::BufferSnapshot;
|
||||||
|
|
||||||
|
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||||
|
self.buffer_range.start = self
|
||||||
|
.buffer_range
|
||||||
|
.start
|
||||||
|
.min(&other.buffer_range.start, buffer);
|
||||||
|
self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BufferDiff {
|
||||||
|
last_buffer_version: Option<clock::Global>,
|
||||||
|
tree: SumTree<DiffHunk<Anchor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferDiff {
|
||||||
|
pub fn new() -> BufferDiff {
|
||||||
|
BufferDiff {
|
||||||
|
last_buffer_version: None,
|
||||||
|
tree: SumTree::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hunks_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
query_row_range: Range<u32>,
|
||||||
|
buffer: &'a BufferSnapshot,
|
||||||
|
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
|
||||||
|
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
|
||||||
|
|
||||||
|
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
|
||||||
|
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
|
||||||
|
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
|
||||||
|
!before_start && !after_end
|
||||||
|
});
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
cursor.next(buffer);
|
||||||
|
let hunk = cursor.item()?;
|
||||||
|
|
||||||
|
let range = hunk.buffer_range.to_point(buffer);
|
||||||
|
let end_row = if range.end.column > 0 {
|
||||||
|
range.end.row + 1
|
||||||
|
} else {
|
||||||
|
range.end.row
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(DiffHunk {
|
||||||
|
buffer_range: range.start.row..end_row,
|
||||||
|
head_byte_range: hunk.head_byte_range.clone(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, buffer: &text::BufferSnapshot) {
|
||||||
|
self.last_buffer_version = Some(buffer.version().clone());
|
||||||
|
self.tree = SumTree::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
|
||||||
|
match &self.last_buffer_version {
|
||||||
|
Some(last) => buffer.version().changed_since(last),
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
|
||||||
|
let mut tree = SumTree::new();
|
||||||
|
|
||||||
|
let buffer_text = buffer.as_rope().to_string();
|
||||||
|
let patch = Self::diff(&diff_base, &buffer_text);
|
||||||
|
|
||||||
|
if let Some(patch) = patch {
|
||||||
|
let mut divergence = 0;
|
||||||
|
for hunk_index in 0..patch.num_hunks() {
|
||||||
|
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||||
|
tree.push(hunk, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tree = tree;
|
||||||
|
self.last_buffer_version = Some(buffer.version().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||||
|
self.hunks_in_range(0..u32::MAX, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
|
||||||
|
let mut options = GitOptions::default();
|
||||||
|
options.context_lines(0);
|
||||||
|
|
||||||
|
let patch = GitPatch::from_buffers(
|
||||||
|
head.as_bytes(),
|
||||||
|
None,
|
||||||
|
current.as_bytes(),
|
||||||
|
None,
|
||||||
|
Some(&mut options),
|
||||||
|
);
|
||||||
|
|
||||||
|
match patch {
|
||||||
|
Ok(patch) => Some(patch),
|
||||||
|
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("`GitPatch::from_buffers` failed: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_patch_hunk<'a>(
|
||||||
|
patch: &GitPatch<'a>,
|
||||||
|
hunk_index: usize,
|
||||||
|
buffer: &text::BufferSnapshot,
|
||||||
|
buffer_row_divergence: &mut i64,
|
||||||
|
) -> DiffHunk<Anchor> {
|
||||||
|
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||||
|
assert!(line_item_count > 0);
|
||||||
|
|
||||||
|
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||||
|
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||||
|
let mut head_byte_range: Option<Range<usize>> = None;
|
||||||
|
|
||||||
|
for line_index in 0..line_item_count {
|
||||||
|
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||||
|
let kind = line.origin_value();
|
||||||
|
let content_offset = line.content_offset() as isize;
|
||||||
|
let content_len = line.content().len() as isize;
|
||||||
|
|
||||||
|
if kind == GitDiffLineType::Addition {
|
||||||
|
*buffer_row_divergence += 1;
|
||||||
|
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||||
|
|
||||||
|
match &mut buffer_row_range {
|
||||||
|
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||||
|
None => buffer_row_range = Some(row..row + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == GitDiffLineType::Deletion {
|
||||||
|
*buffer_row_divergence -= 1;
|
||||||
|
let end = content_offset + content_len;
|
||||||
|
|
||||||
|
match &mut head_byte_range {
|
||||||
|
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||||
|
None => head_byte_range = Some(content_offset as usize..end as usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
if first_deletion_buffer_row.is_none() {
|
||||||
|
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||||
|
let row = old_row as i64 + *buffer_row_divergence;
|
||||||
|
first_deletion_buffer_row = Some(row as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//unwrap_or deletion without addition
|
||||||
|
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||||
|
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||||
|
let row = first_deletion_buffer_row.unwrap();
|
||||||
|
row..row
|
||||||
|
});
|
||||||
|
|
||||||
|
//unwrap_or addition without deletion
|
||||||
|
let head_byte_range = head_byte_range.unwrap_or(0..0);
|
||||||
|
|
||||||
|
let start = Point::new(buffer_row_range.start, 0);
|
||||||
|
let end = Point::new(buffer_row_range.end, 0);
|
||||||
|
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||||
|
DiffHunk {
|
||||||
|
buffer_range,
|
||||||
|
head_byte_range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Range (crossing new lines), old, new
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[track_caller]
|
||||||
|
pub fn assert_hunks<Iter>(
|
||||||
|
diff_hunks: Iter,
|
||||||
|
buffer: &BufferSnapshot,
|
||||||
|
diff_base: &str,
|
||||||
|
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||||
|
) where
|
||||||
|
Iter: Iterator<Item = DiffHunk<u32>>,
|
||||||
|
{
|
||||||
|
let actual_hunks = diff_hunks
|
||||||
|
.map(|hunk| {
|
||||||
|
(
|
||||||
|
hunk.buffer_range.clone(),
|
||||||
|
&diff_base[hunk.head_byte_range],
|
||||||
|
buffer
|
||||||
|
.text_for_range(
|
||||||
|
Point::new(hunk.buffer_range.start, 0)
|
||||||
|
..Point::new(hunk.buffer_range.end, 0),
|
||||||
|
)
|
||||||
|
.collect::<String>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let expected_hunks: Vec<_> = expected_hunks
|
||||||
|
.iter()
|
||||||
|
.map(|(r, s, h)| (r.clone(), *s, h.to_string()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(actual_hunks, expected_hunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use text::Buffer;
|
||||||
|
use unindent::Unindent as _;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_buffer_diff_simple() {
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_text = "
|
||||||
|
one
|
||||||
|
HELLO
|
||||||
|
three
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(0, 0, buffer_text);
|
||||||
|
let mut diff = BufferDiff::new();
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(1..2, "two\n", "HELLO\n")],
|
||||||
|
);
|
||||||
|
|
||||||
|
buffer.edit([(0..0, "point five\n")]);
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks(&buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
|
||||||
|
);
|
||||||
|
|
||||||
|
diff.clear(&buffer);
|
||||||
|
assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_buffer_diff_range() {
|
||||||
|
let diff_base = "
|
||||||
|
one
|
||||||
|
two
|
||||||
|
three
|
||||||
|
four
|
||||||
|
five
|
||||||
|
six
|
||||||
|
seven
|
||||||
|
eight
|
||||||
|
nine
|
||||||
|
ten
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer_text = "
|
||||||
|
A
|
||||||
|
one
|
||||||
|
B
|
||||||
|
two
|
||||||
|
C
|
||||||
|
three
|
||||||
|
HELLO
|
||||||
|
four
|
||||||
|
five
|
||||||
|
SIXTEEN
|
||||||
|
seven
|
||||||
|
eight
|
||||||
|
WORLD
|
||||||
|
nine
|
||||||
|
|
||||||
|
ten
|
||||||
|
|
||||||
|
"
|
||||||
|
.unindent();
|
||||||
|
|
||||||
|
let buffer = Buffer::new(0, 0, buffer_text);
|
||||||
|
let mut diff = BufferDiff::new();
|
||||||
|
smol::block_on(diff.update(&diff_base, &buffer));
|
||||||
|
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||||
|
|
||||||
|
assert_hunks(
|
||||||
|
diff.hunks_in_range(7..12, &buffer),
|
||||||
|
&buffer,
|
||||||
|
&diff_base,
|
||||||
|
&[
|
||||||
|
(6..7, "", "HELLO\n"),
|
||||||
|
(9..10, "six\n", "SIXTEEN\n"),
|
||||||
|
(12..13, "", "WORLD\n"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
12
crates/git/src/git.rs
Normal file
12
crates/git/src/git.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
pub use git2 as libgit;
|
||||||
|
pub use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
pub mod diff;
|
||||||
|
pub mod repository;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||||
|
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||||
|
}
|
71
crates/git/src/repository.rs
Normal file
71
crates/git/src/repository.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use git2::Repository as LibGitRepository;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait GitRepository: Send {
|
||||||
|
fn reload_index(&self);
|
||||||
|
|
||||||
|
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl GitRepository for LibGitRepository {
|
||||||
|
fn reload_index(&self) {
|
||||||
|
if let Ok(mut index) = self.index() {
|
||||||
|
_ = index.read(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
|
||||||
|
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
|
||||||
|
const STAGE_NORMAL: i32 = 0;
|
||||||
|
let index = repo.index()?;
|
||||||
|
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
|
||||||
|
Some(entry) => entry.id,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = repo.find_blob(oid)?.content().to_owned();
|
||||||
|
Ok(Some(String::from_utf8(content)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
match logic(&self, relative_file_path) {
|
||||||
|
Ok(value) => return value,
|
||||||
|
Err(err) => log::error!("Error loading head text: {:?}", err),
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FakeGitRepository {
|
||||||
|
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FakeGitRepositoryState {
|
||||||
|
pub index_contents: HashMap<PathBuf, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeGitRepository {
|
||||||
|
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
|
||||||
|
Arc::new(Mutex::new(FakeGitRepository { state }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl GitRepository for FakeGitRepository {
|
||||||
|
fn reload_index(&self) {}
|
||||||
|
|
||||||
|
fn load_index_text(&self, path: &Path) -> Option<String> {
|
||||||
|
let state = self.state.lock();
|
||||||
|
state.index_contents.get(path).cloned()
|
||||||
|
}
|
||||||
|
}
|
|
@ -325,7 +325,12 @@ impl Deterministic {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let wakeup_at = state.now + duration;
|
let wakeup_at = state.now + duration;
|
||||||
let id = util::post_inc(&mut state.next_timer_id);
|
let id = util::post_inc(&mut state.next_timer_id);
|
||||||
state.pending_timers.push((id, wakeup_at, tx));
|
match state
|
||||||
|
.pending_timers
|
||||||
|
.binary_search_by_key(&wakeup_at, |e| e.1)
|
||||||
|
{
|
||||||
|
Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
|
||||||
|
}
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
Timer::Deterministic(DeterministicTimer { rx, id, state })
|
Timer::Deterministic(DeterministicTimer { rx, id, state })
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,8 @@ pub trait Platform: Send + Sync {
|
||||||
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
|
||||||
fn app_path(&self) -> Result<PathBuf>;
|
fn app_path(&self) -> Result<PathBuf>;
|
||||||
fn app_version(&self) -> Result<AppVersion>;
|
fn app_version(&self) -> Result<AppVersion>;
|
||||||
|
fn os_name(&self) -> &'static str;
|
||||||
|
fn os_version(&self) -> Result<AppVersion>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait ForegroundPlatform {
|
pub(crate) trait ForegroundPlatform {
|
||||||
|
|
|
@ -14,8 +14,10 @@ use core_graphics::{
|
||||||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||||
event_source::{CGEventSource, CGEventSourceStateID},
|
event_source::{CGEventSource, CGEventSourceStateID},
|
||||||
};
|
};
|
||||||
|
use ctor::ctor;
|
||||||
|
use foreign_types::ForeignType;
|
||||||
use objc::{class, msg_send, sel, sel_impl};
|
use objc::{class, msg_send, sel, sel_impl};
|
||||||
use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
|
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
|
||||||
|
|
||||||
const BACKSPACE_KEY: u16 = 0x7f;
|
const BACKSPACE_KEY: u16 = 0x7f;
|
||||||
const SPACE_KEY: u16 = b' ' as u16;
|
const SPACE_KEY: u16 = b' ' as u16;
|
||||||
|
@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b;
|
||||||
const TAB_KEY: u16 = 0x09;
|
const TAB_KEY: u16 = 0x09;
|
||||||
const SHIFT_TAB_KEY: u16 = 0x19;
|
const SHIFT_TAB_KEY: u16 = 0x19;
|
||||||
|
|
||||||
|
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
|
||||||
|
|
||||||
|
#[ctor]
|
||||||
|
unsafe fn build_event_source() {
|
||||||
|
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
|
||||||
|
EVENT_SOURCE = source.as_ptr();
|
||||||
|
mem::forget(source);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn key_to_native(key: &str) -> Cow<str> {
|
pub fn key_to_native(key: &str) -> Cow<str> {
|
||||||
use cocoa::appkit::*;
|
use cocoa::appkit::*;
|
||||||
let code = match key {
|
let code = match key {
|
||||||
|
@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
let mut chars_ignoring_modifiers =
|
let mut chars_ignoring_modifiers =
|
||||||
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.to_string();
|
||||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||||
let modifiers = native_event.modifierFlags();
|
let modifiers = native_event.modifierFlags();
|
||||||
|
|
||||||
|
@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
|
|
||||||
#[allow(non_upper_case_globals)]
|
#[allow(non_upper_case_globals)]
|
||||||
let key = match first_char {
|
let key = match first_char {
|
||||||
Some(SPACE_KEY) => "space",
|
Some(SPACE_KEY) => "space".to_string(),
|
||||||
Some(BACKSPACE_KEY) => "backspace",
|
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
|
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
|
||||||
Some(ESCAPE_KEY) => "escape",
|
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||||
Some(TAB_KEY) => "tab",
|
Some(TAB_KEY) => "tab".to_string(),
|
||||||
Some(SHIFT_TAB_KEY) => "tab",
|
Some(SHIFT_TAB_KEY) => "tab".to_string(),
|
||||||
Some(NSUpArrowFunctionKey) => "up",
|
Some(NSUpArrowFunctionKey) => "up".to_string(),
|
||||||
Some(NSDownArrowFunctionKey) => "down",
|
Some(NSDownArrowFunctionKey) => "down".to_string(),
|
||||||
Some(NSLeftArrowFunctionKey) => "left",
|
Some(NSLeftArrowFunctionKey) => "left".to_string(),
|
||||||
Some(NSRightArrowFunctionKey) => "right",
|
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
||||||
Some(NSPageUpFunctionKey) => "pageup",
|
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
||||||
Some(NSPageDownFunctionKey) => "pagedown",
|
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
||||||
Some(NSDeleteFunctionKey) => "delete",
|
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
||||||
Some(NSF1FunctionKey) => "f1",
|
Some(NSF1FunctionKey) => "f1".to_string(),
|
||||||
Some(NSF2FunctionKey) => "f2",
|
Some(NSF2FunctionKey) => "f2".to_string(),
|
||||||
Some(NSF3FunctionKey) => "f3",
|
Some(NSF3FunctionKey) => "f3".to_string(),
|
||||||
Some(NSF4FunctionKey) => "f4",
|
Some(NSF4FunctionKey) => "f4".to_string(),
|
||||||
Some(NSF5FunctionKey) => "f5",
|
Some(NSF5FunctionKey) => "f5".to_string(),
|
||||||
Some(NSF6FunctionKey) => "f6",
|
Some(NSF6FunctionKey) => "f6".to_string(),
|
||||||
Some(NSF7FunctionKey) => "f7",
|
Some(NSF7FunctionKey) => "f7".to_string(),
|
||||||
Some(NSF8FunctionKey) => "f8",
|
Some(NSF8FunctionKey) => "f8".to_string(),
|
||||||
Some(NSF9FunctionKey) => "f9",
|
Some(NSF9FunctionKey) => "f9".to_string(),
|
||||||
Some(NSF10FunctionKey) => "f10",
|
Some(NSF10FunctionKey) => "f10".to_string(),
|
||||||
Some(NSF11FunctionKey) => "f11",
|
Some(NSF11FunctionKey) => "f11".to_string(),
|
||||||
Some(NSF12FunctionKey) => "f12",
|
Some(NSF12FunctionKey) => "f12".to_string(),
|
||||||
_ => {
|
_ => {
|
||||||
let mut chars_ignoring_modifiers_and_shift =
|
let mut chars_ignoring_modifiers_and_shift =
|
||||||
chars_for_modified_key(native_event.keyCode(), false, false);
|
chars_for_modified_key(native_event.keyCode(), false, false);
|
||||||
|
@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
shift,
|
shift,
|
||||||
cmd,
|
cmd,
|
||||||
function,
|
function,
|
||||||
key: key.into(),
|
key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str {
|
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
||||||
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
||||||
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
||||||
// an event with the given flags instead lets us access `characters`, which always
|
// an event with the given flags instead lets us access `characters`, which always
|
||||||
// returns a valid string.
|
// returns a valid string.
|
||||||
let event = CGEvent::new_keyboard_event(
|
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
|
||||||
CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
|
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
|
||||||
code,
|
mem::forget(source);
|
||||||
true,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let mut flags = CGEventFlags::empty();
|
let mut flags = CGEventFlags::empty();
|
||||||
if cmd {
|
if cmd {
|
||||||
flags |= CGEventFlags::CGEventFlagCommand;
|
flags |= CGEventFlags::CGEventFlagCommand;
|
||||||
|
@ -327,10 +337,11 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a st
|
||||||
}
|
}
|
||||||
event.set_flags(flags);
|
event.set_flags(flags);
|
||||||
|
|
||||||
let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||||
CStr::from_ptr(event.characters().UTF8String())
|
CStr::from_ptr(event.characters().UTF8String())
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap,
|
keymap,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
|
@ -18,7 +18,8 @@ use cocoa::{
|
||||||
},
|
},
|
||||||
base::{id, nil, selector, YES},
|
base::{id, nil, selector, YES},
|
||||||
foundation::{
|
foundation::{
|
||||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||||
|
NSUInteger, NSURL,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use core_foundation::{
|
use core_foundation::{
|
||||||
|
@ -758,6 +759,22 @@ impl platform::Platform for MacPlatform {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"macOS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<crate::AppVersion> {
|
||||||
|
unsafe {
|
||||||
|
let process_info = NSProcessInfo::processInfo(nil);
|
||||||
|
let version = process_info.operatingSystemVersion();
|
||||||
|
Ok(AppVersion {
|
||||||
|
major: version.majorVersion as usize,
|
||||||
|
minor: version.minorVersion as usize,
|
||||||
|
patch: version.patchVersion as usize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||||
|
|
|
@ -200,6 +200,18 @@ impl super::Platform for Platform {
|
||||||
patch: 0,
|
patch: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn os_name(&self) -> &'static str {
|
||||||
|
"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn os_version(&self) -> Result<AppVersion> {
|
||||||
|
Ok(AppVersion {
|
||||||
|
major: 1,
|
||||||
|
minor: 0,
|
||||||
|
patch: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
|
|
@ -25,6 +25,7 @@ client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
@ -63,6 +64,8 @@ util = { path = "../util", features = ["test-support"] }
|
||||||
ctor = "0.1"
|
ctor = "0.1"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
rand = "0.8.3"
|
rand = "0.8.3"
|
||||||
|
tree-sitter-html = "*"
|
||||||
|
tree-sitter-javascript = "*"
|
||||||
tree-sitter-json = "*"
|
tree-sitter-json = "*"
|
||||||
tree-sitter-rust = "*"
|
tree-sitter-rust = "*"
|
||||||
tree-sitter-python = "*"
|
tree-sitter-python = "*"
|
||||||
|
|
|
@ -45,8 +45,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||||
|
|
||||||
pub use lsp::DiagnosticSeverity;
|
pub use lsp::DiagnosticSeverity;
|
||||||
|
|
||||||
|
struct GitDiffStatus {
|
||||||
|
diff: git::diff::BufferDiff,
|
||||||
|
update_in_progress: bool,
|
||||||
|
update_requested: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
text: TextBuffer,
|
text: TextBuffer,
|
||||||
|
diff_base: Option<String>,
|
||||||
|
git_diff_status: GitDiffStatus,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
saved_version: clock::Global,
|
saved_version: clock::Global,
|
||||||
saved_version_fingerprint: String,
|
saved_version_fingerprint: String,
|
||||||
|
@ -66,6 +74,7 @@ pub struct Buffer {
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
diagnostics_timestamp: clock::Lamport,
|
diagnostics_timestamp: clock::Lamport,
|
||||||
file_update_count: usize,
|
file_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
completion_triggers: Vec<String>,
|
completion_triggers: Vec<String>,
|
||||||
completion_triggers_timestamp: clock::Lamport,
|
completion_triggers_timestamp: clock::Lamport,
|
||||||
deferred_ops: OperationQueue<Operation>,
|
deferred_ops: OperationQueue<Operation>,
|
||||||
|
@ -73,25 +82,28 @@ pub struct Buffer {
|
||||||
|
|
||||||
pub struct BufferSnapshot {
|
pub struct BufferSnapshot {
|
||||||
text: text::BufferSnapshot,
|
text: text::BufferSnapshot,
|
||||||
|
pub git_diff: git::diff::BufferDiff,
|
||||||
pub(crate) syntax: SyntaxSnapshot,
|
pub(crate) syntax: SyntaxSnapshot,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
diagnostics: DiagnosticSet,
|
diagnostics: DiagnosticSet,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
file_update_count: usize,
|
file_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
||||||
selections_update_count: usize,
|
selections_update_count: usize,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
parse_count: usize,
|
parse_count: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
pub struct IndentSize {
|
pub struct IndentSize {
|
||||||
pub len: u32,
|
pub len: u32,
|
||||||
pub kind: IndentKind,
|
pub kind: IndentKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
pub enum IndentKind {
|
pub enum IndentKind {
|
||||||
|
#[default]
|
||||||
Space,
|
Space,
|
||||||
Tab,
|
Tab,
|
||||||
}
|
}
|
||||||
|
@ -236,7 +248,6 @@ pub enum AutoindentMode {
|
||||||
struct AutoindentRequest {
|
struct AutoindentRequest {
|
||||||
before_edit: BufferSnapshot,
|
before_edit: BufferSnapshot,
|
||||||
entries: Vec<AutoindentRequestEntry>,
|
entries: Vec<AutoindentRequestEntry>,
|
||||||
indent_size: IndentSize,
|
|
||||||
is_block_mode: bool,
|
is_block_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +260,7 @@ struct AutoindentRequestEntry {
|
||||||
/// only be adjusted if the suggested indentation level has *changed*
|
/// only be adjusted if the suggested indentation level has *changed*
|
||||||
/// since the edit was made.
|
/// since the edit was made.
|
||||||
first_line_is_new: bool,
|
first_line_is_new: bool,
|
||||||
|
indent_size: IndentSize,
|
||||||
original_indent_column: Option<u32>,
|
original_indent_column: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,10 +300,8 @@ pub struct Chunk<'a> {
|
||||||
|
|
||||||
pub struct Diff {
|
pub struct Diff {
|
||||||
base_version: clock::Global,
|
base_version: clock::Global,
|
||||||
new_text: Arc<str>,
|
|
||||||
changes: Vec<(ChangeTag, usize)>,
|
|
||||||
line_ending: LineEnding,
|
line_ending: LineEnding,
|
||||||
start_offset: usize,
|
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -328,17 +338,20 @@ impl Buffer {
|
||||||
Self::build(
|
Self::build(
|
||||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_file<T: Into<String>>(
|
pub fn from_file<T: Into<String>>(
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
base_text: T,
|
base_text: T,
|
||||||
|
diff_base: Option<T>,
|
||||||
file: Arc<dyn File>,
|
file: Arc<dyn File>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::build(
|
Self::build(
|
||||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||||
|
diff_base.map(|h| h.into().into_boxed_str().into()),
|
||||||
Some(file),
|
Some(file),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -349,7 +362,11 @@ impl Buffer {
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
|
let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
|
||||||
let mut this = Self::build(buffer, file);
|
let mut this = Self::build(
|
||||||
|
buffer,
|
||||||
|
message.diff_base.map(|text| text.into_boxed_str().into()),
|
||||||
|
file,
|
||||||
|
);
|
||||||
this.text.set_line_ending(proto::deserialize_line_ending(
|
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||||
proto::LineEnding::from_i32(message.line_ending)
|
proto::LineEnding::from_i32(message.line_ending)
|
||||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||||
|
@ -362,6 +379,7 @@ impl Buffer {
|
||||||
id: self.remote_id(),
|
id: self.remote_id(),
|
||||||
file: self.file.as_ref().map(|f| f.to_proto()),
|
file: self.file.as_ref().map(|f| f.to_proto()),
|
||||||
base_text: self.base_text().to_string(),
|
base_text: self.base_text().to_string(),
|
||||||
|
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -404,7 +422,7 @@ impl Buffer {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self {
|
fn build(buffer: TextBuffer, diff_base: Option<String>, file: Option<Arc<dyn File>>) -> Self {
|
||||||
let saved_mtime = if let Some(file) = file.as_ref() {
|
let saved_mtime = if let Some(file) = file.as_ref() {
|
||||||
file.mtime()
|
file.mtime()
|
||||||
} else {
|
} else {
|
||||||
|
@ -418,6 +436,12 @@ impl Buffer {
|
||||||
transaction_depth: 0,
|
transaction_depth: 0,
|
||||||
was_dirty_before_starting_transaction: None,
|
was_dirty_before_starting_transaction: None,
|
||||||
text: buffer,
|
text: buffer,
|
||||||
|
diff_base,
|
||||||
|
git_diff_status: GitDiffStatus {
|
||||||
|
diff: git::diff::BufferDiff::new(),
|
||||||
|
update_in_progress: false,
|
||||||
|
update_requested: false,
|
||||||
|
},
|
||||||
file,
|
file,
|
||||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||||
parsing_in_background: false,
|
parsing_in_background: false,
|
||||||
|
@ -432,6 +456,7 @@ impl Buffer {
|
||||||
diagnostics_update_count: 0,
|
diagnostics_update_count: 0,
|
||||||
diagnostics_timestamp: Default::default(),
|
diagnostics_timestamp: Default::default(),
|
||||||
file_update_count: 0,
|
file_update_count: 0,
|
||||||
|
git_diff_update_count: 0,
|
||||||
completion_triggers: Default::default(),
|
completion_triggers: Default::default(),
|
||||||
completion_triggers_timestamp: Default::default(),
|
completion_triggers_timestamp: Default::default(),
|
||||||
deferred_ops: OperationQueue::new(),
|
deferred_ops: OperationQueue::new(),
|
||||||
|
@ -447,11 +472,13 @@ impl Buffer {
|
||||||
BufferSnapshot {
|
BufferSnapshot {
|
||||||
text,
|
text,
|
||||||
syntax,
|
syntax,
|
||||||
|
git_diff: self.git_diff_status.diff.clone(),
|
||||||
file: self.file.clone(),
|
file: self.file.clone(),
|
||||||
remote_selections: self.remote_selections.clone(),
|
remote_selections: self.remote_selections.clone(),
|
||||||
diagnostics: self.diagnostics.clone(),
|
diagnostics: self.diagnostics.clone(),
|
||||||
diagnostics_update_count: self.diagnostics_update_count,
|
diagnostics_update_count: self.diagnostics_update_count,
|
||||||
file_update_count: self.file_update_count,
|
file_update_count: self.file_update_count,
|
||||||
|
git_diff_update_count: self.git_diff_update_count,
|
||||||
language: self.language.clone(),
|
language: self.language.clone(),
|
||||||
parse_count: self.parse_count,
|
parse_count: self.parse_count,
|
||||||
selections_update_count: self.selections_update_count,
|
selections_update_count: self.selections_update_count,
|
||||||
|
@ -584,6 +611,7 @@ impl Buffer {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
self.git_diff_recalc(cx);
|
||||||
cx.emit(Event::Reloaded);
|
cx.emit(Event::Reloaded);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -633,6 +661,60 @@ impl Buffer {
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn diff_base(&self) -> Option<&str> {
|
||||||
|
self.diff_base.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
||||||
|
self.diff_base = diff_base;
|
||||||
|
self.git_diff_recalc(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_git_diff_recalc(&self) -> bool {
|
||||||
|
self.git_diff_status.diff.needs_update(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
|
if self.git_diff_status.update_in_progress {
|
||||||
|
self.git_diff_status.update_requested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(diff_base) = &self.diff_base {
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
let diff_base = diff_base.clone();
|
||||||
|
|
||||||
|
let mut diff = self.git_diff_status.diff.clone();
|
||||||
|
let diff = cx.background().spawn(async move {
|
||||||
|
diff.update(&diff_base, &snapshot).await;
|
||||||
|
diff
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
let buffer_diff = diff.await;
|
||||||
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.git_diff_status.diff = buffer_diff;
|
||||||
|
this.git_diff_update_count += 1;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
this.git_diff_status.update_in_progress = false;
|
||||||
|
if this.git_diff_status.update_requested {
|
||||||
|
this.git_diff_recalc(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
} else {
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
self.git_diff_status.diff.clear(&snapshot);
|
||||||
|
self.git_diff_update_count += 1;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
cx.emit(Event::Closed);
|
cx.emit(Event::Closed);
|
||||||
}
|
}
|
||||||
|
@ -641,6 +723,16 @@ impl Buffer {
|
||||||
self.language.as_ref()
|
self.language.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<Arc<Language>> {
|
||||||
|
let offset = position.to_offset(self);
|
||||||
|
self.syntax_map
|
||||||
|
.lock()
|
||||||
|
.layers_for_range(offset..offset, &self.text)
|
||||||
|
.last()
|
||||||
|
.map(|info| info.language.clone())
|
||||||
|
.or_else(|| self.language.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_count(&self) -> usize {
|
pub fn parse_count(&self) -> usize {
|
||||||
self.parse_count
|
self.parse_count
|
||||||
}
|
}
|
||||||
|
@ -657,6 +749,10 @@ impl Buffer {
|
||||||
self.file_update_count
|
self.file_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_update_count(&self) -> usize {
|
||||||
|
self.git_diff_update_count
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn is_parsing(&self) -> bool {
|
pub fn is_parsing(&self) -> bool {
|
||||||
self.parsing_in_background
|
self.parsing_in_background
|
||||||
|
@ -784,10 +880,13 @@ impl Buffer {
|
||||||
// buffer before this batch of edits.
|
// buffer before this batch of edits.
|
||||||
let mut row_ranges = Vec::new();
|
let mut row_ranges = Vec::new();
|
||||||
let mut old_to_new_rows = BTreeMap::new();
|
let mut old_to_new_rows = BTreeMap::new();
|
||||||
|
let mut language_indent_sizes_by_new_row = Vec::new();
|
||||||
for entry in &request.entries {
|
for entry in &request.entries {
|
||||||
let position = entry.range.start;
|
let position = entry.range.start;
|
||||||
let new_row = position.to_point(&snapshot).row;
|
let new_row = position.to_point(&snapshot).row;
|
||||||
let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
|
let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
|
||||||
|
language_indent_sizes_by_new_row.push((new_row, entry.indent_size));
|
||||||
|
|
||||||
if !entry.first_line_is_new {
|
if !entry.first_line_is_new {
|
||||||
let old_row = position.to_point(&request.before_edit).row;
|
let old_row = position.to_point(&request.before_edit).row;
|
||||||
old_to_new_rows.insert(old_row, new_row);
|
old_to_new_rows.insert(old_row, new_row);
|
||||||
|
@ -801,6 +900,8 @@ impl Buffer {
|
||||||
let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
|
let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
|
||||||
let old_edited_ranges =
|
let old_edited_ranges =
|
||||||
contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
|
contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
|
||||||
|
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
|
||||||
|
let mut language_indent_size = IndentSize::default();
|
||||||
for old_edited_range in old_edited_ranges {
|
for old_edited_range in old_edited_ranges {
|
||||||
let suggestions = request
|
let suggestions = request
|
||||||
.before_edit
|
.before_edit
|
||||||
|
@ -809,6 +910,17 @@ impl Buffer {
|
||||||
.flatten();
|
.flatten();
|
||||||
for (old_row, suggestion) in old_edited_range.zip(suggestions) {
|
for (old_row, suggestion) in old_edited_range.zip(suggestions) {
|
||||||
if let Some(suggestion) = suggestion {
|
if let Some(suggestion) = suggestion {
|
||||||
|
let new_row = *old_to_new_rows.get(&old_row).unwrap();
|
||||||
|
|
||||||
|
// Find the indent size based on the language for this row.
|
||||||
|
while let Some((row, size)) = language_indent_sizes.peek() {
|
||||||
|
if *row > new_row {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
language_indent_size = *size;
|
||||||
|
language_indent_sizes.next();
|
||||||
|
}
|
||||||
|
|
||||||
let suggested_indent = old_to_new_rows
|
let suggested_indent = old_to_new_rows
|
||||||
.get(&suggestion.basis_row)
|
.get(&suggestion.basis_row)
|
||||||
.and_then(|from_row| old_suggestions.get(from_row).copied())
|
.and_then(|from_row| old_suggestions.get(from_row).copied())
|
||||||
|
@ -817,9 +929,8 @@ impl Buffer {
|
||||||
.before_edit
|
.before_edit
|
||||||
.indent_size_for_line(suggestion.basis_row)
|
.indent_size_for_line(suggestion.basis_row)
|
||||||
})
|
})
|
||||||
.with_delta(suggestion.delta, request.indent_size);
|
.with_delta(suggestion.delta, language_indent_size);
|
||||||
old_suggestions
|
old_suggestions.insert(new_row, suggested_indent);
|
||||||
.insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield_now().await;
|
yield_now().await;
|
||||||
|
@ -840,6 +951,8 @@ impl Buffer {
|
||||||
|
|
||||||
// Compute new suggestions for each line, but only include them in the result
|
// Compute new suggestions for each line, but only include them in the result
|
||||||
// if they differ from the old suggestion for that line.
|
// if they differ from the old suggestion for that line.
|
||||||
|
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
|
||||||
|
let mut language_indent_size = IndentSize::default();
|
||||||
for new_edited_row_range in new_edited_row_ranges {
|
for new_edited_row_range in new_edited_row_ranges {
|
||||||
let suggestions = snapshot
|
let suggestions = snapshot
|
||||||
.suggest_autoindents(new_edited_row_range.clone())
|
.suggest_autoindents(new_edited_row_range.clone())
|
||||||
|
@ -847,13 +960,22 @@ impl Buffer {
|
||||||
.flatten();
|
.flatten();
|
||||||
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
|
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
|
||||||
if let Some(suggestion) = suggestion {
|
if let Some(suggestion) = suggestion {
|
||||||
|
// Find the indent size based on the language for this row.
|
||||||
|
while let Some((row, size)) = language_indent_sizes.peek() {
|
||||||
|
if *row > new_row {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
language_indent_size = *size;
|
||||||
|
language_indent_sizes.next();
|
||||||
|
}
|
||||||
|
|
||||||
let suggested_indent = indent_sizes
|
let suggested_indent = indent_sizes
|
||||||
.get(&suggestion.basis_row)
|
.get(&suggestion.basis_row)
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
snapshot.indent_size_for_line(suggestion.basis_row)
|
snapshot.indent_size_for_line(suggestion.basis_row)
|
||||||
})
|
})
|
||||||
.with_delta(suggestion.delta, request.indent_size);
|
.with_delta(suggestion.delta, language_indent_size);
|
||||||
if old_suggestions
|
if old_suggestions
|
||||||
.get(&new_row)
|
.get(&new_row)
|
||||||
.map_or(true, |old_indentation| {
|
.map_or(true, |old_indentation| {
|
||||||
|
@ -965,16 +1087,30 @@ impl Buffer {
|
||||||
let old_text = old_text.to_string();
|
let old_text = old_text.to_string();
|
||||||
let line_ending = LineEnding::detect(&new_text);
|
let line_ending = LineEnding::detect(&new_text);
|
||||||
LineEnding::normalize(&mut new_text);
|
LineEnding::normalize(&mut new_text);
|
||||||
let changes = TextDiff::from_chars(old_text.as_str(), new_text.as_str())
|
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||||
.iter_all_changes()
|
let mut edits = Vec::new();
|
||||||
.map(|c| (c.tag(), c.value().len()))
|
let mut offset = 0;
|
||||||
.collect::<Vec<_>>();
|
let empty: Arc<str> = "".into();
|
||||||
|
for change in diff.iter_all_changes() {
|
||||||
|
let value = change.value();
|
||||||
|
let end_offset = offset + value.len();
|
||||||
|
match change.tag() {
|
||||||
|
ChangeTag::Equal => {
|
||||||
|
offset = end_offset;
|
||||||
|
}
|
||||||
|
ChangeTag::Delete => {
|
||||||
|
edits.push((offset..end_offset, empty.clone()));
|
||||||
|
offset = end_offset;
|
||||||
|
}
|
||||||
|
ChangeTag::Insert => {
|
||||||
|
edits.push((offset..offset, value.into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Diff {
|
Diff {
|
||||||
base_version,
|
base_version,
|
||||||
new_text: new_text.into(),
|
|
||||||
changes,
|
|
||||||
line_ending,
|
line_ending,
|
||||||
start_offset: 0,
|
edits,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -984,28 +1120,7 @@ impl Buffer {
|
||||||
self.finalize_last_transaction();
|
self.finalize_last_transaction();
|
||||||
self.start_transaction();
|
self.start_transaction();
|
||||||
self.text.set_line_ending(diff.line_ending);
|
self.text.set_line_ending(diff.line_ending);
|
||||||
let mut offset = diff.start_offset;
|
self.edit(diff.edits, None, cx);
|
||||||
for (tag, len) in diff.changes {
|
|
||||||
let range = offset..(offset + len);
|
|
||||||
match tag {
|
|
||||||
ChangeTag::Equal => offset += len,
|
|
||||||
ChangeTag::Delete => {
|
|
||||||
self.edit([(range, "")], None, cx);
|
|
||||||
}
|
|
||||||
ChangeTag::Insert => {
|
|
||||||
self.edit(
|
|
||||||
[(
|
|
||||||
offset..offset,
|
|
||||||
&diff.new_text[range.start - diff.start_offset
|
|
||||||
..range.end - diff.start_offset],
|
|
||||||
)],
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
offset += len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.end_transaction(cx).is_some() {
|
if self.end_transaction(cx).is_some() {
|
||||||
self.finalize_last_transaction()
|
self.finalize_last_transaction()
|
||||||
} else {
|
} else {
|
||||||
|
@ -1184,7 +1299,6 @@ impl Buffer {
|
||||||
let edit_id = edit_operation.local_timestamp();
|
let edit_id = edit_operation.local_timestamp();
|
||||||
|
|
||||||
if let Some((before_edit, mode)) = autoindent_request {
|
if let Some((before_edit, mode)) = autoindent_request {
|
||||||
let indent_size = before_edit.single_indent_size(cx);
|
|
||||||
let (start_columns, is_block_mode) = match mode {
|
let (start_columns, is_block_mode) = match mode {
|
||||||
AutoindentMode::Block {
|
AutoindentMode::Block {
|
||||||
original_indent_columns: start_columns,
|
original_indent_columns: start_columns,
|
||||||
|
@ -1233,6 +1347,7 @@ impl Buffer {
|
||||||
AutoindentRequestEntry {
|
AutoindentRequestEntry {
|
||||||
first_line_is_new,
|
first_line_is_new,
|
||||||
original_indent_column: start_column,
|
original_indent_column: start_column,
|
||||||
|
indent_size: before_edit.language_indent_size_at(range.start, cx),
|
||||||
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
|
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
|
||||||
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
|
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
|
||||||
}
|
}
|
||||||
|
@ -1242,7 +1357,6 @@ impl Buffer {
|
||||||
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||||
before_edit,
|
before_edit,
|
||||||
entries,
|
entries,
|
||||||
indent_size,
|
|
||||||
is_block_mode,
|
is_block_mode,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -1560,8 +1674,8 @@ impl BufferSnapshot {
|
||||||
indent_size_for_line(self, row)
|
indent_size_for_line(self, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize {
|
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
|
||||||
let language_name = self.language().map(|language| language.name());
|
let language_name = self.language_at(position).map(|language| language.name());
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
if settings.hard_tabs(language_name.as_deref()) {
|
if settings.hard_tabs(language_name.as_deref()) {
|
||||||
IndentSize::tab()
|
IndentSize::tab()
|
||||||
|
@ -1631,6 +1745,8 @@ impl BufferSnapshot {
|
||||||
if capture.index == config.indent_capture_ix {
|
if capture.index == config.indent_capture_ix {
|
||||||
start.get_or_insert(Point::from_ts_point(capture.node.start_position()));
|
start.get_or_insert(Point::from_ts_point(capture.node.start_position()));
|
||||||
end.get_or_insert(Point::from_ts_point(capture.node.end_position()));
|
end.get_or_insert(Point::from_ts_point(capture.node.end_position()));
|
||||||
|
} else if Some(capture.index) == config.start_capture_ix {
|
||||||
|
start = Some(Point::from_ts_point(capture.node.end_position()));
|
||||||
} else if Some(capture.index) == config.end_capture_ix {
|
} else if Some(capture.index) == config.end_capture_ix {
|
||||||
end = Some(Point::from_ts_point(capture.node.start_position()));
|
end = Some(Point::from_ts_point(capture.node.start_position()));
|
||||||
}
|
}
|
||||||
|
@ -1820,8 +1936,14 @@ impl BufferSnapshot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||||
self.language.as_ref()
|
let offset = position.to_offset(self);
|
||||||
|
self.syntax
|
||||||
|
.layers_for_range(offset..offset, &self.text)
|
||||||
|
.filter(|l| l.node.end_byte() > offset)
|
||||||
|
.last()
|
||||||
|
.map(|info| info.language)
|
||||||
|
.or(self.language.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
|
pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
|
||||||
|
@ -1856,8 +1978,8 @@ impl BufferSnapshot {
|
||||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
let mut result: Option<Range<usize>> = None;
|
let mut result: Option<Range<usize>> = None;
|
||||||
'outer: for (_, _, node) in self.syntax.layers_for_range(range.clone(), &self.text) {
|
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
|
||||||
let mut cursor = node.walk();
|
let mut cursor = layer.node.walk();
|
||||||
|
|
||||||
// Descend to the first leaf that touches the start of the range,
|
// Descend to the first leaf that touches the start of the range,
|
||||||
// and if the range is non-empty, extends beyond the start.
|
// and if the range is non-empty, extends beyond the start.
|
||||||
|
@ -2139,6 +2261,13 @@ impl BufferSnapshot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_hunks_in_range<'a>(
|
||||||
|
&'a self,
|
||||||
|
query_row_range: Range<u32>,
|
||||||
|
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||||
|
self.git_diff.hunks_in_range(query_row_range, self)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diagnostics_in_range<'a, T, O>(
|
pub fn diagnostics_in_range<'a, T, O>(
|
||||||
&'a self,
|
&'a self,
|
||||||
search_range: Range<T>,
|
search_range: Range<T>,
|
||||||
|
@ -2186,6 +2315,10 @@ impl BufferSnapshot {
|
||||||
pub fn file_update_count(&self) -> usize {
|
pub fn file_update_count(&self) -> usize {
|
||||||
self.file_update_count
|
self.file_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_update_count(&self) -> usize {
|
||||||
|
self.git_diff_update_count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||||
|
@ -2212,6 +2345,7 @@ impl Clone for BufferSnapshot {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: self.text.clone(),
|
text: self.text.clone(),
|
||||||
|
git_diff: self.git_diff.clone(),
|
||||||
syntax: self.syntax.clone(),
|
syntax: self.syntax.clone(),
|
||||||
file: self.file.clone(),
|
file: self.file.clone(),
|
||||||
remote_selections: self.remote_selections.clone(),
|
remote_selections: self.remote_selections.clone(),
|
||||||
|
@ -2219,6 +2353,7 @@ impl Clone for BufferSnapshot {
|
||||||
selections_update_count: self.selections_update_count,
|
selections_update_count: self.selections_update_count,
|
||||||
diagnostics_update_count: self.diagnostics_update_count,
|
diagnostics_update_count: self.diagnostics_update_count,
|
||||||
file_update_count: self.file_update_count,
|
file_update_count: self.file_update_count,
|
||||||
|
git_diff_update_count: self.git_diff_update_count,
|
||||||
language: self.language.clone(),
|
language: self.language.clone(),
|
||||||
parse_count: self.parse_count,
|
parse_count: self.parse_count,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use text::network::Network;
|
use text::network::Network;
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::post_inc;
|
use util::{post_inc, test::marked_text_ranges};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -1035,6 +1035,120 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) {
|
||||||
|
cx.set_global({
|
||||||
|
let mut settings = Settings::test(cx);
|
||||||
|
settings.language_overrides.extend([
|
||||||
|
(
|
||||||
|
"HTML".into(),
|
||||||
|
settings::EditorSettings {
|
||||||
|
tab_size: Some(2.try_into().unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"JavaScript".into(),
|
||||||
|
settings::EditorSettings {
|
||||||
|
tab_size: Some(8.try_into().unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
settings
|
||||||
|
});
|
||||||
|
|
||||||
|
let html_language = Arc::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "HTML".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_html::language()),
|
||||||
|
)
|
||||||
|
.with_indents_query(
|
||||||
|
"
|
||||||
|
(element
|
||||||
|
(start_tag) @start
|
||||||
|
(end_tag)? @end) @indent
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_injection_query(
|
||||||
|
r#"
|
||||||
|
(script_element
|
||||||
|
(raw_text) @content
|
||||||
|
(#set! "language" "javascript"))
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let javascript_language = Arc::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "JavaScript".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Some(tree_sitter_javascript::language()),
|
||||||
|
)
|
||||||
|
.with_indents_query(
|
||||||
|
r#"
|
||||||
|
(object "}" @end) @indent
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let language_registry = Arc::new(LanguageRegistry::test());
|
||||||
|
language_registry.add(html_language.clone());
|
||||||
|
language_registry.add(javascript_language.clone());
|
||||||
|
|
||||||
|
cx.add_model(|cx| {
|
||||||
|
let (text, ranges) = marked_text_ranges(
|
||||||
|
&"
|
||||||
|
<div>ˇ
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
init({ˇ
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<span>ˇ
|
||||||
|
</span>
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut buffer = Buffer::new(0, text, cx);
|
||||||
|
buffer.set_language_registry(language_registry);
|
||||||
|
buffer.set_language(Some(html_language), cx);
|
||||||
|
buffer.edit(
|
||||||
|
ranges.into_iter().map(|range| (range, "\na")),
|
||||||
|
Some(AutoindentMode::EachLine),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
buffer.text(),
|
||||||
|
"
|
||||||
|
<div>
|
||||||
|
a
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
init({
|
||||||
|
a
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<span>
|
||||||
|
a
|
||||||
|
</span>
|
||||||
|
"
|
||||||
|
.unindent()
|
||||||
|
);
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
||||||
let mut now = Instant::now();
|
let mut now = Instant::now();
|
||||||
|
@ -1449,7 +1563,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
|
||||||
buffer.read_with(cx, |buffer, _| {
|
buffer.read_with(cx, |buffer, _| {
|
||||||
let snapshot = buffer.snapshot();
|
let snapshot = buffer.snapshot();
|
||||||
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
|
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
|
||||||
layers[0].2.to_sexp()
|
layers[0].node.to_sexp()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ mod highlight_map;
|
||||||
mod outline;
|
mod outline;
|
||||||
pub mod proto;
|
pub mod proto;
|
||||||
mod syntax_map;
|
mod syntax_map;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod buffer_tests;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -26,6 +27,7 @@ use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
fmt::Debug,
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -135,7 +137,7 @@ impl CachedLspAdapter {
|
||||||
pub async fn label_for_completion(
|
pub async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
completion_item: &lsp::CompletionItem,
|
completion_item: &lsp::CompletionItem,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
self.adapter
|
self.adapter
|
||||||
.label_for_completion(completion_item, language)
|
.label_for_completion(completion_item, language)
|
||||||
|
@ -146,7 +148,7 @@ impl CachedLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: lsp::SymbolKind,
|
kind: lsp::SymbolKind,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
self.adapter.label_for_symbol(name, kind, language).await
|
self.adapter.label_for_symbol(name, kind, language).await
|
||||||
}
|
}
|
||||||
|
@ -175,7 +177,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
_: &lsp::CompletionItem,
|
_: &lsp::CompletionItem,
|
||||||
_: &Language,
|
_: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -184,7 +186,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
&self,
|
&self,
|
||||||
_: &str,
|
_: &str,
|
||||||
_: lsp::SymbolKind,
|
_: lsp::SymbolKind,
|
||||||
_: &Language,
|
_: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -230,7 +232,10 @@ pub struct LanguageConfig {
|
||||||
pub decrease_indent_pattern: Option<Regex>,
|
pub decrease_indent_pattern: Option<Regex>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub autoclose_before: String,
|
pub autoclose_before: String,
|
||||||
pub line_comment: Option<String>,
|
#[serde(default)]
|
||||||
|
pub line_comment: Option<Arc<str>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LanguageConfig {
|
impl Default for LanguageConfig {
|
||||||
|
@ -244,6 +249,7 @@ impl Default for LanguageConfig {
|
||||||
decrease_indent_pattern: Default::default(),
|
decrease_indent_pattern: Default::default(),
|
||||||
autoclose_before: Default::default(),
|
autoclose_before: Default::default(),
|
||||||
line_comment: Default::default(),
|
line_comment: Default::default(),
|
||||||
|
block_comment: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +276,7 @@ pub struct FakeLspAdapter {
|
||||||
pub disk_based_diagnostics_sources: Vec<String>,
|
pub disk_based_diagnostics_sources: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct BracketPair {
|
pub struct BracketPair {
|
||||||
pub start: String,
|
pub start: String,
|
||||||
pub end: String,
|
pub end: String,
|
||||||
|
@ -304,6 +310,7 @@ pub struct Grammar {
|
||||||
struct IndentConfig {
|
struct IndentConfig {
|
||||||
query: Query,
|
query: Query,
|
||||||
indent_capture_ix: u32,
|
indent_capture_ix: u32,
|
||||||
|
start_capture_ix: Option<u32>,
|
||||||
end_capture_ix: Option<u32>,
|
end_capture_ix: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -661,11 +668,13 @@ impl Language {
|
||||||
let grammar = self.grammar_mut();
|
let grammar = self.grammar_mut();
|
||||||
let query = Query::new(grammar.ts_language, source)?;
|
let query = Query::new(grammar.ts_language, source)?;
|
||||||
let mut indent_capture_ix = None;
|
let mut indent_capture_ix = None;
|
||||||
|
let mut start_capture_ix = None;
|
||||||
let mut end_capture_ix = None;
|
let mut end_capture_ix = None;
|
||||||
get_capture_indices(
|
get_capture_indices(
|
||||||
&query,
|
&query,
|
||||||
&mut [
|
&mut [
|
||||||
("indent", &mut indent_capture_ix),
|
("indent", &mut indent_capture_ix),
|
||||||
|
("start", &mut start_capture_ix),
|
||||||
("end", &mut end_capture_ix),
|
("end", &mut end_capture_ix),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -673,6 +682,7 @@ impl Language {
|
||||||
grammar.indents_config = Some(IndentConfig {
|
grammar.indents_config = Some(IndentConfig {
|
||||||
query,
|
query,
|
||||||
indent_capture_ix,
|
indent_capture_ix,
|
||||||
|
start_capture_ix,
|
||||||
end_capture_ix,
|
end_capture_ix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -763,8 +773,15 @@ impl Language {
|
||||||
self.config.name.clone()
|
self.config.name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn line_comment_prefix(&self) -> Option<&str> {
|
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
|
||||||
self.config.line_comment.as_deref()
|
self.config.line_comment.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
|
||||||
|
self.config
|
||||||
|
.block_comment
|
||||||
|
.as_ref()
|
||||||
|
.map(|(start, end)| (start, end))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
|
pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
|
||||||
|
@ -789,7 +806,7 @@ impl Language {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn label_for_completion(
|
pub async fn label_for_completion(
|
||||||
&self,
|
self: &Arc<Self>,
|
||||||
completion: &lsp::CompletionItem,
|
completion: &lsp::CompletionItem,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
self.adapter
|
self.adapter
|
||||||
|
@ -798,7 +815,11 @@ impl Language {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> {
|
pub async fn label_for_symbol(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
name: &str,
|
||||||
|
kind: lsp::SymbolKind,
|
||||||
|
) -> Option<CodeLabel> {
|
||||||
self.adapter
|
self.adapter
|
||||||
.as_ref()?
|
.as_ref()?
|
||||||
.label_for_symbol(name, kind, self)
|
.label_for_symbol(name, kind, self)
|
||||||
|
@ -806,20 +827,17 @@ impl Language {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_text<'a>(
|
pub fn highlight_text<'a>(
|
||||||
&'a self,
|
self: &'a Arc<Self>,
|
||||||
text: &'a Rope,
|
text: &'a Rope,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
) -> Vec<(Range<usize>, HighlightId)> {
|
) -> Vec<(Range<usize>, HighlightId)> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
if let Some(grammar) = &self.grammar {
|
if let Some(grammar) = &self.grammar {
|
||||||
let tree = grammar.parse_text(text, None);
|
let tree = grammar.parse_text(text, None);
|
||||||
let captures = SyntaxSnapshot::single_tree_captures(
|
let captures =
|
||||||
range.clone(),
|
SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| {
|
||||||
text,
|
grammar.highlights_query.as_ref()
|
||||||
&tree,
|
});
|
||||||
grammar,
|
|
||||||
|grammar| grammar.highlights_query.as_ref(),
|
|
||||||
);
|
|
||||||
let highlight_maps = vec![grammar.highlight_map()];
|
let highlight_maps = vec![grammar.highlight_map()];
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
|
for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
|
||||||
|
@ -861,6 +879,14 @@ impl Language {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for Language {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Language")
|
||||||
|
.field("name", &self.config.name)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Grammar {
|
impl Grammar {
|
||||||
pub fn id(&self) -> usize {
|
pub fn id(&self) -> usize {
|
||||||
self.id
|
self.id
|
||||||
|
|
|
@ -92,6 +92,13 @@ struct SyntaxLayer {
|
||||||
language: Arc<Language>,
|
language: Arc<Language>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyntaxLayerInfo<'a> {
|
||||||
|
pub depth: usize,
|
||||||
|
pub node: Node<'a>,
|
||||||
|
pub language: &'a Arc<Language>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct SyntaxLayerSummary {
|
struct SyntaxLayerSummary {
|
||||||
min_depth: usize,
|
min_depth: usize,
|
||||||
|
@ -473,13 +480,18 @@ impl SyntaxSnapshot {
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
text: &'a Rope,
|
text: &'a Rope,
|
||||||
tree: &'a Tree,
|
tree: &'a Tree,
|
||||||
grammar: &'a Grammar,
|
language: &'a Arc<Language>,
|
||||||
query: fn(&Grammar) -> Option<&Query>,
|
query: fn(&Grammar) -> Option<&Query>,
|
||||||
) -> SyntaxMapCaptures<'a> {
|
) -> SyntaxMapCaptures<'a> {
|
||||||
SyntaxMapCaptures::new(
|
SyntaxMapCaptures::new(
|
||||||
range.clone(),
|
range.clone(),
|
||||||
text,
|
text,
|
||||||
[(grammar, 0, tree.root_node())].into_iter(),
|
[SyntaxLayerInfo {
|
||||||
|
language,
|
||||||
|
depth: 0,
|
||||||
|
node: tree.root_node(),
|
||||||
|
}]
|
||||||
|
.into_iter(),
|
||||||
query,
|
query,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -513,19 +525,19 @@ impl SyntaxSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn layers(&self, buffer: &BufferSnapshot) -> Vec<(&Grammar, usize, Node)> {
|
pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayerInfo> {
|
||||||
self.layers_for_range(0..buffer.len(), buffer)
|
self.layers_for_range(0..buffer.len(), buffer).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layers_for_range<'a, T: ToOffset>(
|
pub fn layers_for_range<'a, T: ToOffset>(
|
||||||
&self,
|
&'a self,
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
buffer: &BufferSnapshot,
|
buffer: &'a BufferSnapshot,
|
||||||
) -> Vec<(&Grammar, usize, Node)> {
|
) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
|
||||||
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
||||||
let end = buffer.anchor_after(range.end.to_offset(buffer));
|
let end = buffer.anchor_after(range.end.to_offset(buffer));
|
||||||
|
|
||||||
let mut cursor = self.layers.filter::<_, ()>(|summary| {
|
let mut cursor = self.layers.filter::<_, ()>(move |summary| {
|
||||||
if summary.max_depth > summary.min_depth {
|
if summary.max_depth > summary.min_depth {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
@ -535,23 +547,26 @@ impl SyntaxSnapshot {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut result = Vec::new();
|
// let mut result = Vec::new();
|
||||||
cursor.next(buffer);
|
cursor.next(buffer);
|
||||||
while let Some(layer) = cursor.item() {
|
std::iter::from_fn(move || {
|
||||||
if let Some(grammar) = &layer.language.grammar {
|
if let Some(layer) = cursor.item() {
|
||||||
result.push((
|
let info = SyntaxLayerInfo {
|
||||||
grammar.as_ref(),
|
language: &layer.language,
|
||||||
layer.depth,
|
depth: layer.depth,
|
||||||
layer.tree.root_node_with_offset(
|
node: layer.tree.root_node_with_offset(
|
||||||
layer.range.start.to_offset(buffer),
|
layer.range.start.to_offset(buffer),
|
||||||
layer.range.start.to_point(buffer).to_ts_point(),
|
layer.range.start.to_point(buffer).to_ts_point(),
|
||||||
),
|
),
|
||||||
));
|
};
|
||||||
|
cursor.next(buffer);
|
||||||
|
Some(info)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
cursor.next(buffer)
|
})
|
||||||
}
|
|
||||||
|
|
||||||
result
|
// result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,7 +574,7 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||||
fn new(
|
fn new(
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
text: &'a Rope,
|
text: &'a Rope,
|
||||||
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
|
layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
|
||||||
query: fn(&Grammar) -> Option<&Query>,
|
query: fn(&Grammar) -> Option<&Query>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
|
@ -567,11 +582,19 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||||
grammars: Vec::new(),
|
grammars: Vec::new(),
|
||||||
active_layer_count: 0,
|
active_layer_count: 0,
|
||||||
};
|
};
|
||||||
for (grammar, depth, node) in layers {
|
for SyntaxLayerInfo {
|
||||||
let query = if let Some(query) = query(grammar) {
|
language,
|
||||||
query
|
depth,
|
||||||
} else {
|
node,
|
||||||
continue;
|
} in layers
|
||||||
|
{
|
||||||
|
let grammar = match &language.grammar {
|
||||||
|
Some(grammer) => grammer,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let query = match query(&grammar) {
|
||||||
|
Some(query) => query,
|
||||||
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut query_cursor = QueryCursorHandle::new();
|
let mut query_cursor = QueryCursorHandle::new();
|
||||||
|
@ -678,15 +701,23 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||||
fn new(
|
fn new(
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
text: &'a Rope,
|
text: &'a Rope,
|
||||||
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
|
layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
|
||||||
query: fn(&Grammar) -> Option<&Query>,
|
query: fn(&Grammar) -> Option<&Query>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut result = Self::default();
|
let mut result = Self::default();
|
||||||
for (grammar, depth, node) in layers {
|
for SyntaxLayerInfo {
|
||||||
let query = if let Some(query) = query(grammar) {
|
language,
|
||||||
query
|
depth,
|
||||||
} else {
|
node,
|
||||||
continue;
|
} in layers
|
||||||
|
{
|
||||||
|
let grammar = match &language.grammar {
|
||||||
|
Some(grammer) => grammer,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let query = match query(&grammar) {
|
||||||
|
Some(query) => query,
|
||||||
|
None => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut query_cursor = QueryCursorHandle::new();
|
let mut query_cursor = QueryCursorHandle::new();
|
||||||
|
@ -1624,8 +1655,8 @@ mod tests {
|
||||||
let reference_layers = reference_syntax_map.layers(&buffer);
|
let reference_layers = reference_syntax_map.layers(&buffer);
|
||||||
for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter())
|
for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter())
|
||||||
{
|
{
|
||||||
assert_eq!(edited_layer.2.to_sexp(), reference_layer.2.to_sexp());
|
assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp());
|
||||||
assert_eq!(edited_layer.2.range(), reference_layer.2.range());
|
assert_eq!(edited_layer.node.range(), reference_layer.node.range());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1770,13 +1801,13 @@ mod tests {
|
||||||
mutated_layers.into_iter().zip(reference_layers.into_iter())
|
mutated_layers.into_iter().zip(reference_layers.into_iter())
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edited_layer.2.to_sexp(),
|
edited_layer.node.to_sexp(),
|
||||||
reference_layer.2.to_sexp(),
|
reference_layer.node.to_sexp(),
|
||||||
"different layer at step {i}"
|
"different layer at step {i}"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edited_layer.2.range(),
|
edited_layer.node.range(),
|
||||||
reference_layer.2.range(),
|
reference_layer.node.range(),
|
||||||
"different layer at step {i}"
|
"different layer at step {i}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1822,13 +1853,15 @@ mod tests {
|
||||||
range: Range<Point>,
|
range: Range<Point>,
|
||||||
expected_layers: &[&str],
|
expected_layers: &[&str],
|
||||||
) {
|
) {
|
||||||
let layers = syntax_map.layers_for_range(range, &buffer);
|
let layers = syntax_map
|
||||||
|
.layers_for_range(range, &buffer)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
layers.len(),
|
layers.len(),
|
||||||
expected_layers.len(),
|
expected_layers.len(),
|
||||||
"wrong number of layers"
|
"wrong number of layers"
|
||||||
);
|
);
|
||||||
for (i, ((_, _, node), expected_s_exp)) in
|
for (i, (SyntaxLayerInfo { node, .. }, expected_s_exp)) in
|
||||||
layers.iter().zip(expected_layers.iter()).enumerate()
|
layers.iter().zip(expected_layers.iter()).enumerate()
|
||||||
{
|
{
|
||||||
let actual_s_exp = node.to_sexp();
|
let actual_s_exp = node.to_sexp();
|
||||||
|
|
|
@ -10,6 +10,7 @@ doctest = false
|
||||||
[features]
|
[features]
|
||||||
test-support = [
|
test-support = [
|
||||||
"client/test-support",
|
"client/test-support",
|
||||||
|
"db/test-support",
|
||||||
"language/test-support",
|
"language/test-support",
|
||||||
"settings/test-support",
|
"settings/test-support",
|
||||||
"text/test-support",
|
"text/test-support",
|
||||||
|
@ -20,8 +21,10 @@ text = { path = "../text" }
|
||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
|
db = { path = "../db" }
|
||||||
fsevent = { path = "../fsevent" }
|
fsevent = { path = "../fsevent" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
@ -54,6 +57,7 @@ rocksdb = "0.18"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
client = { path = "../client", features = ["test-support"] }
|
client = { path = "../client", features = ["test-support"] }
|
||||||
collections = { path = "../collections", features = ["test-support"] }
|
collections = { path = "../collections", features = ["test-support"] }
|
||||||
|
db = { path = "../db", features = ["test-support"] }
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
language = { path = "../language", features = ["test-support"] }
|
language = { path = "../language", features = ["test-support"] }
|
||||||
lsp = { path = "../lsp", features = ["test-support"] }
|
lsp = { path = "../lsp", features = ["test-support"] }
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use fsevent::EventStream;
|
use fsevent::EventStream;
|
||||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||||
|
use git::repository::{GitRepository, LibGitRepository};
|
||||||
use language::LineEnding;
|
use language::LineEnding;
|
||||||
|
use parking_lot::Mutex as SyncMutex;
|
||||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
|
@ -11,13 +14,16 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use text::Rope;
|
use text::Rope;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use collections::{btree_map, BTreeMap};
|
use collections::{btree_map, BTreeMap};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use std::sync::{Arc, Weak};
|
use git::repository::FakeGitRepositoryState;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use std::sync::Weak;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait Fs: Send + Sync {
|
pub trait Fs: Send + Sync {
|
||||||
|
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
|
||||||
path: &Path,
|
path: &Path,
|
||||||
latency: Duration,
|
latency: Duration,
|
||||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
||||||
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
|
||||||
fn is_fake(&self) -> bool;
|
fn is_fake(&self) -> bool;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_fake(&self) -> &FakeFs;
|
fn as_fake(&self) -> &FakeFs;
|
||||||
|
@ -235,6 +242,14 @@ impl Fs for RealFs {
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||||
|
LibGitRepository::open(&dotgit_path)
|
||||||
|
.log_err()
|
||||||
|
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
|
||||||
|
Some(Arc::new(SyncMutex::new(libgit_repository)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -270,6 +285,7 @@ enum FakeFsEntry {
|
||||||
inode: u64,
|
inode: u64,
|
||||||
mtime: SystemTime,
|
mtime: SystemTime,
|
||||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||||
|
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
|
||||||
},
|
},
|
||||||
Symlink {
|
Symlink {
|
||||||
target: PathBuf,
|
target: PathBuf,
|
||||||
|
@ -384,6 +400,7 @@ impl FakeFs {
|
||||||
inode: 0,
|
inode: 0,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
})),
|
})),
|
||||||
next_inode: 1,
|
next_inode: 1,
|
||||||
event_txs: Default::default(),
|
event_txs: Default::default(),
|
||||||
|
@ -473,6 +490,28 @@ impl FakeFs {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||||
|
let mut state = self.state.lock().await;
|
||||||
|
let entry = state.read_path(dot_git).await.unwrap();
|
||||||
|
let mut entry = entry.lock().await;
|
||||||
|
|
||||||
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
|
let repo_state = git_repo_state.get_or_insert_with(Default::default);
|
||||||
|
let mut repo_state = repo_state.lock();
|
||||||
|
|
||||||
|
repo_state.index_contents.clear();
|
||||||
|
repo_state.index_contents.extend(
|
||||||
|
head_state
|
||||||
|
.iter()
|
||||||
|
.map(|(path, content)| (path.to_path_buf(), content.clone())),
|
||||||
|
);
|
||||||
|
|
||||||
|
state.emit_event([dot_git]);
|
||||||
|
} else {
|
||||||
|
panic!("not a directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn files(&self) -> Vec<PathBuf> {
|
pub async fn files(&self) -> Vec<PathBuf> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut queue = collections::VecDeque::new();
|
let mut queue = collections::VecDeque::new();
|
||||||
|
@ -562,6 +601,7 @@ impl Fs for FakeFs {
|
||||||
inode,
|
inode,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -846,6 +886,24 @@ impl Fs for FakeFs {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||||
|
smol::block_on(async move {
|
||||||
|
let state = self.state.lock().await;
|
||||||
|
let entry = state.read_path(abs_dot_git).await.unwrap();
|
||||||
|
let mut entry = entry.lock().await;
|
||||||
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||||
|
let state = git_repo_state
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
|
||||||
|
})
|
||||||
|
.clone();
|
||||||
|
Some(git::repository::FakeGitRepository::open(state))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn is_fake(&self) -> bool {
|
fn is_fake(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
mod db;
|
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod lsp_command;
|
mod lsp_command;
|
||||||
|
@ -13,6 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||||
|
@ -62,7 +62,7 @@ use std::{
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
|
||||||
|
|
||||||
pub use db::Db;
|
pub use db::Db;
|
||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
|
@ -123,6 +123,7 @@ pub struct Project {
|
||||||
opened_buffers: HashMap<u64, OpenBuffer>,
|
opened_buffers: HashMap<u64, OpenBuffer>,
|
||||||
incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>,
|
incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>,
|
||||||
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
||||||
|
buffers_being_formatted: HashSet<usize>,
|
||||||
nonce: u128,
|
nonce: u128,
|
||||||
_maintain_buffer_languages: Task<()>,
|
_maintain_buffer_languages: Task<()>,
|
||||||
}
|
}
|
||||||
|
@ -407,6 +408,7 @@ impl Project {
|
||||||
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
||||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||||
client.add_model_request_handler(Self::handle_save_buffer);
|
client.add_model_request_handler(Self::handle_save_buffer);
|
||||||
|
client.add_model_message_handler(Self::handle_update_diff_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
|
@ -466,6 +468,7 @@ impl Project {
|
||||||
language_server_statuses: Default::default(),
|
language_server_statuses: Default::default(),
|
||||||
last_workspace_edits_by_language_server: Default::default(),
|
last_workspace_edits_by_language_server: Default::default(),
|
||||||
language_server_settings: Default::default(),
|
language_server_settings: Default::default(),
|
||||||
|
buffers_being_formatted: Default::default(),
|
||||||
next_language_server_id: 0,
|
next_language_server_id: 0,
|
||||||
nonce: StdRng::from_entropy().gen(),
|
nonce: StdRng::from_entropy().gen(),
|
||||||
}
|
}
|
||||||
|
@ -562,6 +565,7 @@ impl Project {
|
||||||
last_workspace_edits_by_language_server: Default::default(),
|
last_workspace_edits_by_language_server: Default::default(),
|
||||||
next_language_server_id: 0,
|
next_language_server_id: 0,
|
||||||
opened_buffers: Default::default(),
|
opened_buffers: Default::default(),
|
||||||
|
buffers_being_formatted: Default::default(),
|
||||||
buffer_snapshots: Default::default(),
|
buffer_snapshots: Default::default(),
|
||||||
nonce: StdRng::from_entropy().gen(),
|
nonce: StdRng::from_entropy().gen(),
|
||||||
};
|
};
|
||||||
|
@ -604,7 +608,7 @@ impl Project {
|
||||||
|
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = client::Client::new(http_client.clone());
|
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
let project =
|
let project =
|
||||||
|
@ -2804,7 +2808,26 @@ impl Project {
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
// Do not allow multiple concurrent formatting requests for the
|
||||||
|
// same buffer.
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
local_buffers
|
||||||
|
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
|
||||||
|
});
|
||||||
|
let _cleanup = defer({
|
||||||
|
let this = this.clone();
|
||||||
|
let mut cx = cx.clone();
|
||||||
|
let local_buffers = &local_buffers;
|
||||||
|
move || {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
for (buffer, _, _) in local_buffers {
|
||||||
|
this.buffers_being_formatted.remove(&buffer.id());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (buffer, buffer_abs_path, language_server) in &local_buffers {
|
||||||
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||||
let settings = cx.global::<Settings>();
|
let settings = cx.global::<Settings>();
|
||||||
let language_name = buffer.language().map(|language| language.name());
|
let language_name = buffer.language().map(|language| language.name());
|
||||||
|
@ -2856,7 +2879,7 @@ impl Project {
|
||||||
buffer.forget_transaction(transaction.id)
|
buffer.forget_transaction(transaction.id)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
project_transaction.0.insert(buffer, transaction);
|
project_transaction.0.insert(buffer.clone(), transaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4229,8 +4252,11 @@ impl Project {
|
||||||
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
||||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||||
if worktree.read(cx).is_local() {
|
if worktree.read(cx).is_local() {
|
||||||
cx.subscribe(worktree, |this, worktree, _, cx| {
|
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||||
this.update_local_worktree_buffers(worktree, cx);
|
worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
|
||||||
|
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||||
|
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
@ -4338,6 +4364,63 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_local_worktree_buffers_git_repos(
|
||||||
|
&mut self,
|
||||||
|
worktree: ModelHandle<Worktree>,
|
||||||
|
repos: &[GitRepositoryEntry],
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
for (_, buffer) in &self.opened_buffers {
|
||||||
|
if let Some(buffer) = buffer.upgrade(cx) {
|
||||||
|
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||||
|
Some(file) => file,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
if file.worktree != worktree {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = file.path().clone();
|
||||||
|
|
||||||
|
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
|
||||||
|
Some(repo) => repo.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let relative_repo = match path.strip_prefix(repo.content_path) {
|
||||||
|
Ok(relative_repo) => relative_repo.to_owned(),
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let remote_id = self.remote_id();
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let diff_base = cx
|
||||||
|
.background()
|
||||||
|
.spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.update_diff_base(diff_base.clone(), cx);
|
||||||
|
buffer.remote_id()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(project_id) = remote_id {
|
||||||
|
client
|
||||||
|
.send(proto::UpdateDiffBase {
|
||||||
|
project_id,
|
||||||
|
buffer_id: buffer_id as u64,
|
||||||
|
diff_base,
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
|
@ -4861,6 +4944,27 @@ impl Project {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_update_diff_base(
|
||||||
|
this: ModelHandle<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::UpdateDiffBase>,
|
||||||
|
_: Arc<Client>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let buffer_id = envelope.payload.buffer_id;
|
||||||
|
let diff_base = envelope.payload.diff_base;
|
||||||
|
let buffer = this
|
||||||
|
.opened_buffers
|
||||||
|
.get_mut(&buffer_id)
|
||||||
|
.and_then(|b| b.upgrade(cx))
|
||||||
|
.ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?;
|
||||||
|
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.update_diff_base(diff_base, cx));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_update_buffer_file(
|
async fn handle_update_buffer_file(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
||||||
|
@ -5427,7 +5531,7 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||||
let mut opened_buffer_rx = self.opened_buffer.1.clone();
|
let mut opened_buffer_rx = self.opened_buffer.1.clone();
|
||||||
cx.spawn(|this, cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let buffer = loop {
|
let buffer = loop {
|
||||||
let buffer = this.read_with(&cx, |this, cx| {
|
let buffer = this.read_with(&cx, |this, cx| {
|
||||||
this.opened_buffers
|
this.opened_buffers
|
||||||
|
@ -5445,6 +5549,7 @@ impl Project {
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||||
};
|
};
|
||||||
|
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
fs::{self, Fs},
|
fs::{self, Fs},
|
||||||
ignore::IgnoreStack,
|
ignore::IgnoreStack,
|
||||||
DiagnosticSummary,
|
DiagnosticSummary,
|
||||||
};
|
};
|
||||||
|
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
|
@ -18,6 +17,8 @@ use futures::{
|
||||||
Stream, StreamExt,
|
Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
|
use git::repository::GitRepository;
|
||||||
|
use git::{DOT_GIT, GITIGNORE};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||||
Task,
|
Task,
|
||||||
|
@ -26,12 +27,12 @@ use language::{
|
||||||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::{
|
use postage::{
|
||||||
prelude::{Sink as _, Stream as _},
|
prelude::{Sink as _, Stream as _},
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
|
|
||||||
use smol::channel::{self, Sender};
|
use smol::channel::{self, Sender};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
@ -40,6 +41,7 @@ use std::{
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -50,10 +52,6 @@ use std::{
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
pub struct WorktreeId(usize);
|
pub struct WorktreeId(usize);
|
||||||
|
|
||||||
|
@ -101,15 +99,51 @@ pub struct Snapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
pub struct GitRepositoryEntry {
|
||||||
|
pub(crate) repo: Arc<Mutex<dyn GitRepository>>,
|
||||||
|
|
||||||
|
pub(crate) scan_id: usize,
|
||||||
|
// Path to folder containing the .git file or directory
|
||||||
|
pub(crate) content_path: Arc<Path>,
|
||||||
|
// Path to the actual .git folder.
|
||||||
|
// Note: if .git is a file, this points to the folder indicated by the .git file
|
||||||
|
pub(crate) git_dir_path: Arc<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for GitRepositoryEntry {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("GitRepositoryEntry")
|
||||||
|
.field("content_path", &self.content_path)
|
||||||
|
.field("git_dir_path", &self.git_dir_path)
|
||||||
|
.field("libgit_repository", &"LibGitRepository")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LocalSnapshot {
|
pub struct LocalSnapshot {
|
||||||
abs_path: Arc<Path>,
|
abs_path: Arc<Path>,
|
||||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||||
|
git_repositories: Vec<GitRepositoryEntry>,
|
||||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||||
next_entry_id: Arc<AtomicUsize>,
|
next_entry_id: Arc<AtomicUsize>,
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
extension_counts: HashMap<OsString, usize>,
|
extension_counts: HashMap<OsString, usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for LocalSnapshot {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
abs_path: self.abs_path.clone(),
|
||||||
|
ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(),
|
||||||
|
git_repositories: self.git_repositories.iter().cloned().collect(),
|
||||||
|
removed_entry_ids: self.removed_entry_ids.clone(),
|
||||||
|
next_entry_id: self.next_entry_id.clone(),
|
||||||
|
snapshot: self.snapshot.clone(),
|
||||||
|
extension_counts: self.extension_counts.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for LocalSnapshot {
|
impl Deref for LocalSnapshot {
|
||||||
type Target = Snapshot;
|
type Target = Snapshot;
|
||||||
|
|
||||||
|
@ -142,6 +176,7 @@ struct ShareState {
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
UpdatedEntries,
|
UpdatedEntries,
|
||||||
|
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Worktree {
|
impl Entity for Worktree {
|
||||||
|
@ -372,6 +407,7 @@ impl LocalWorktree {
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
abs_path,
|
abs_path,
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
|
git_repositories: Default::default(),
|
||||||
removed_entry_ids: Default::default(),
|
removed_entry_ids: Default::default(),
|
||||||
next_entry_id,
|
next_entry_id,
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
|
@ -446,10 +482,14 @@ impl LocalWorktree {
|
||||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||||
let path = Arc::from(path);
|
let path = Arc::from(path);
|
||||||
cx.spawn(move |this, mut cx| async move {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
let (file, contents) = this
|
let (file, contents, diff_base) = this
|
||||||
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
|
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx)))
|
Ok(cx.add_model(|cx| {
|
||||||
|
let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx);
|
||||||
|
buffer.git_diff_recalc(cx);
|
||||||
|
buffer
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,17 +539,37 @@ impl LocalWorktree {
|
||||||
|
|
||||||
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
||||||
self.poll_task.take();
|
self.poll_task.take();
|
||||||
|
|
||||||
match self.scan_state() {
|
match self.scan_state() {
|
||||||
ScanState::Idle => {
|
ScanState::Idle => {
|
||||||
self.snapshot = self.background_snapshot.lock().clone();
|
let new_snapshot = self.background_snapshot.lock().clone();
|
||||||
|
let updated_repos = Self::changed_repos(
|
||||||
|
&self.snapshot.git_repositories,
|
||||||
|
&new_snapshot.git_repositories,
|
||||||
|
);
|
||||||
|
self.snapshot = new_snapshot;
|
||||||
|
|
||||||
if let Some(share) = self.share.as_mut() {
|
if let Some(share) = self.share.as_mut() {
|
||||||
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
|
||||||
|
if !updated_repos.is_empty() {
|
||||||
|
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanState::Initializing => {
|
ScanState::Initializing => {
|
||||||
let is_fake_fs = self.fs.is_fake();
|
let is_fake_fs = self.fs.is_fake();
|
||||||
self.snapshot = self.background_snapshot.lock().clone();
|
|
||||||
|
let new_snapshot = self.background_snapshot.lock().clone();
|
||||||
|
let updated_repos = Self::changed_repos(
|
||||||
|
&self.snapshot.git_repositories,
|
||||||
|
&new_snapshot.git_repositories,
|
||||||
|
);
|
||||||
|
self.snapshot = new_snapshot;
|
||||||
|
|
||||||
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||||
if is_fake_fs {
|
if is_fake_fs {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -521,17 +581,52 @@ impl LocalWorktree {
|
||||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
|
||||||
|
if !updated_repos.is_empty() {
|
||||||
|
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
if force {
|
if force {
|
||||||
self.snapshot = self.background_snapshot.lock().clone();
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn changed_repos(
|
||||||
|
old_repos: &[GitRepositoryEntry],
|
||||||
|
new_repos: &[GitRepositoryEntry],
|
||||||
|
) -> Vec<GitRepositoryEntry> {
|
||||||
|
fn diff<'a>(
|
||||||
|
a: &'a [GitRepositoryEntry],
|
||||||
|
b: &'a [GitRepositoryEntry],
|
||||||
|
updated: &mut HashMap<&'a Path, GitRepositoryEntry>,
|
||||||
|
) {
|
||||||
|
for a_repo in a {
|
||||||
|
let matched = b.iter().find(|b_repo| {
|
||||||
|
a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id
|
||||||
|
});
|
||||||
|
|
||||||
|
if matched.is_none() {
|
||||||
|
updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut updated = HashMap::<&Path, GitRepositoryEntry>::default();
|
||||||
|
|
||||||
|
diff(old_repos, new_repos, &mut updated);
|
||||||
|
diff(new_repos, old_repos, &mut updated);
|
||||||
|
|
||||||
|
updated.into_values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
||||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||||
async move {
|
async move {
|
||||||
|
@ -558,13 +653,33 @@ impl LocalWorktree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
|
fn load(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
cx: &mut ModelContext<Worktree>,
|
||||||
|
) -> Task<Result<(File, String, Option<String>)>> {
|
||||||
let handle = cx.handle();
|
let handle = cx.handle();
|
||||||
let path = Arc::from(path);
|
let path = Arc::from(path);
|
||||||
let abs_path = self.absolutize(&path);
|
let abs_path = self.absolutize(&path);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let text = fs.load(&abs_path).await?;
|
let text = fs.load(&abs_path).await?;
|
||||||
|
|
||||||
|
let diff_base = if let Some(repo) = snapshot.repo_for(&path) {
|
||||||
|
if let Ok(repo_relative) = path.strip_prefix(repo.content_path) {
|
||||||
|
let repo_relative = repo_relative.to_owned();
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move { repo.repo.lock().load_index_text(&repo_relative) })
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Eagerly populate the snapshot with an updated entry for the loaded file
|
// Eagerly populate the snapshot with an updated entry for the loaded file
|
||||||
let entry = this
|
let entry = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
|
@ -573,6 +688,7 @@ impl LocalWorktree {
|
||||||
.refresh_entry(path, abs_path, None, cx)
|
.refresh_entry(path, abs_path, None, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
File {
|
File {
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
|
@ -582,6 +698,7 @@ impl LocalWorktree {
|
||||||
is_local: true,
|
is_local: true,
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
|
diff_base,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1248,6 +1365,22 @@ impl LocalSnapshot {
|
||||||
&self.extension_counts
|
&self.extension_counts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gives the most specific git repository for a given path
|
||||||
|
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
|
||||||
|
self.git_repositories
|
||||||
|
.iter()
|
||||||
|
.rev() //git_repository is ordered lexicographically
|
||||||
|
.find(|repo| repo.manages(path))
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepositoryEntry> {
|
||||||
|
// Git repositories cannot be nested, so we don't need to reverse the order
|
||||||
|
self.git_repositories
|
||||||
|
.iter_mut()
|
||||||
|
.find(|repo| repo.in_dot_git(path))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||||
let root_name = self.root_name.clone();
|
let root_name = self.root_name.clone();
|
||||||
|
@ -1330,7 +1463,7 @@ impl LocalSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||||
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
|
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||||
let abs_path = self.abs_path.join(&entry.path);
|
let abs_path = self.abs_path.join(&entry.path);
|
||||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||||
Ok(ignore) => {
|
Ok(ignore) => {
|
||||||
|
@ -1384,6 +1517,7 @@ impl LocalSnapshot {
|
||||||
parent_path: Arc<Path>,
|
parent_path: Arc<Path>,
|
||||||
entries: impl IntoIterator<Item = Entry>,
|
entries: impl IntoIterator<Item = Entry>,
|
||||||
ignore: Option<Arc<Gitignore>>,
|
ignore: Option<Arc<Gitignore>>,
|
||||||
|
fs: &dyn Fs,
|
||||||
) {
|
) {
|
||||||
let mut parent_entry = if let Some(parent_entry) =
|
let mut parent_entry = if let Some(parent_entry) =
|
||||||
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
||||||
|
@ -1409,6 +1543,27 @@ impl LocalSnapshot {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if parent_path.file_name() == Some(&DOT_GIT) {
|
||||||
|
let abs_path = self.abs_path.join(&parent_path);
|
||||||
|
let content_path: Arc<Path> = parent_path.parent().unwrap().into();
|
||||||
|
if let Err(ix) = self
|
||||||
|
.git_repositories
|
||||||
|
.binary_search_by_key(&&content_path, |repo| &repo.content_path)
|
||||||
|
{
|
||||||
|
if let Some(repo) = fs.open_repo(abs_path.as_path()) {
|
||||||
|
self.git_repositories.insert(
|
||||||
|
ix,
|
||||||
|
GitRepositoryEntry {
|
||||||
|
repo,
|
||||||
|
scan_id: 0,
|
||||||
|
content_path,
|
||||||
|
git_dir_path: parent_path,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
|
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
|
||||||
let mut entries_by_id_edits = Vec::new();
|
let mut entries_by_id_edits = Vec::new();
|
||||||
|
|
||||||
|
@ -1493,6 +1648,14 @@ impl LocalSnapshot {
|
||||||
{
|
{
|
||||||
*scan_id = self.snapshot.scan_id;
|
*scan_id = self.snapshot.scan_id;
|
||||||
}
|
}
|
||||||
|
} else if path.file_name() == Some(&DOT_GIT) {
|
||||||
|
let parent_path = path.parent().unwrap();
|
||||||
|
if let Ok(ix) = self
|
||||||
|
.git_repositories
|
||||||
|
.binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref())
|
||||||
|
{
|
||||||
|
self.git_repositories[ix].scan_id = self.snapshot.scan_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1532,6 +1695,22 @@ impl LocalSnapshot {
|
||||||
|
|
||||||
ignore_stack
|
ignore_stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] {
|
||||||
|
&self.git_repositories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRepositoryEntry {
|
||||||
|
// Note that these paths should be relative to the worktree root.
|
||||||
|
pub(crate) fn manages(&self, path: &Path) -> bool {
|
||||||
|
path.starts_with(self.content_path.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that theis path should be relative to the worktree root.
|
||||||
|
pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
|
||||||
|
path.starts_with(self.git_dir_path.as_ref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
||||||
|
@ -2244,9 +2423,12 @@ impl BackgroundScanner {
|
||||||
new_entries.push(child_entry);
|
new_entries.push(child_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.snapshot
|
self.snapshot.lock().populate_dir(
|
||||||
.lock()
|
job.path.clone(),
|
||||||
.populate_dir(job.path.clone(), new_entries, new_ignore);
|
new_entries,
|
||||||
|
new_ignore,
|
||||||
|
self.fs.as_ref(),
|
||||||
|
);
|
||||||
for new_job in new_jobs {
|
for new_job in new_jobs {
|
||||||
job.scan_queue.send(new_job).await.unwrap();
|
job.scan_queue.send(new_job).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -2321,6 +2503,12 @@ impl BackgroundScanner {
|
||||||
fs_entry.is_ignored = ignore_stack.is_all();
|
fs_entry.is_ignored = ignore_stack.is_all();
|
||||||
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
||||||
|
|
||||||
|
let scan_id = snapshot.scan_id;
|
||||||
|
if let Some(repo) = snapshot.in_dot_git(&path) {
|
||||||
|
repo.repo.lock().reload_index();
|
||||||
|
repo.scan_id = scan_id;
|
||||||
|
}
|
||||||
|
|
||||||
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
||||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||||
ancestor_inodes.insert(metadata.inode);
|
ancestor_inodes.insert(metadata.inode);
|
||||||
|
@ -2367,6 +2555,7 @@ impl BackgroundScanner {
|
||||||
self.snapshot.lock().removed_entry_ids.clear();
|
self.snapshot.lock().removed_entry_ids.clear();
|
||||||
|
|
||||||
self.update_ignore_statuses().await;
|
self.update_ignore_statuses().await;
|
||||||
|
self.update_git_repositories();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2432,6 +2621,13 @@ impl BackgroundScanner {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_git_repositories(&self) {
|
||||||
|
let mut snapshot = self.snapshot.lock();
|
||||||
|
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
|
||||||
|
git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
|
||||||
|
snapshot.git_repositories = git_repositories;
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||||
let mut ignore_stack = job.ignore_stack;
|
let mut ignore_stack = job.ignore_stack;
|
||||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||||
|
@ -2778,6 +2974,7 @@ mod tests {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::test::FakeHttpClient;
|
use client::test::FakeHttpClient;
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
|
use git::repository::FakeGitRepository;
|
||||||
use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -2786,6 +2983,7 @@ mod tests {
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use util::test::temp_tree;
|
use util::test::temp_tree;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -2804,7 +3002,7 @@ mod tests {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client);
|
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -2866,8 +3064,7 @@ mod tests {
|
||||||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client);
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
Arc::from(Path::new("/root")),
|
Arc::from(Path::new("/root")),
|
||||||
|
@ -2945,8 +3142,7 @@ mod tests {
|
||||||
}));
|
}));
|
||||||
let dir = parent_dir.path().join("tree");
|
let dir = parent_dir.path().join("tree");
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -3007,6 +3203,135 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||||
|
let root = temp_tree(json!({
|
||||||
|
"dir1": {
|
||||||
|
".git": {},
|
||||||
|
"deps": {
|
||||||
|
"dep1": {
|
||||||
|
".git": {},
|
||||||
|
"src": {
|
||||||
|
"a.txt": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"src": {
|
||||||
|
"b.txt": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"c.txt": "",
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
let http_client = FakeHttpClient::with_404_response();
|
||||||
|
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||||
|
let tree = Worktree::local(
|
||||||
|
client,
|
||||||
|
root.path(),
|
||||||
|
true,
|
||||||
|
Arc::new(RealFs),
|
||||||
|
Default::default(),
|
||||||
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||||
|
.await;
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
|
||||||
|
tree.read_with(cx, |tree, _cx| {
|
||||||
|
let tree = tree.as_local().unwrap();
|
||||||
|
|
||||||
|
assert!(tree.repo_for("c.txt".as_ref()).is_none());
|
||||||
|
|
||||||
|
let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
|
||||||
|
assert_eq!(repo.content_path.as_ref(), Path::new("dir1"));
|
||||||
|
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git"));
|
||||||
|
|
||||||
|
let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
|
||||||
|
assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1"));
|
||||||
|
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),);
|
||||||
|
});
|
||||||
|
|
||||||
|
let original_scan_id = tree.read_with(cx, |tree, _cx| {
|
||||||
|
let tree = tree.as_local().unwrap();
|
||||||
|
tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id
|
||||||
|
});
|
||||||
|
|
||||||
|
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
|
||||||
|
tree.read_with(cx, |tree, _cx| {
|
||||||
|
let tree = tree.as_local().unwrap();
|
||||||
|
let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
|
||||||
|
assert_ne!(
|
||||||
|
original_scan_id, new_scan_id,
|
||||||
|
"original {original_scan_id}, new {new_scan_id}"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
|
||||||
|
tree.read_with(cx, |tree, _cx| {
|
||||||
|
let tree = tree.as_local().unwrap();
|
||||||
|
|
||||||
|
assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_changed_repos() {
|
||||||
|
fn fake_entry(git_dir_path: impl AsRef<Path>, scan_id: usize) -> GitRepositoryEntry {
|
||||||
|
GitRepositoryEntry {
|
||||||
|
repo: Arc::new(Mutex::new(FakeGitRepository::default())),
|
||||||
|
scan_id,
|
||||||
|
content_path: git_dir_path.as_ref().parent().unwrap().into(),
|
||||||
|
git_dir_path: git_dir_path.as_ref().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev_repos: Vec<GitRepositoryEntry> = vec![
|
||||||
|
fake_entry("/.git", 0),
|
||||||
|
fake_entry("/a/.git", 0),
|
||||||
|
fake_entry("/a/b/.git", 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let new_repos: Vec<GitRepositoryEntry> = vec![
|
||||||
|
fake_entry("/a/.git", 1),
|
||||||
|
fake_entry("/a/b/.git", 0),
|
||||||
|
fake_entry("/a/c/.git", 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
|
||||||
|
|
||||||
|
// Deletion retained
|
||||||
|
assert!(res
|
||||||
|
.iter()
|
||||||
|
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0)
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
// Update retained
|
||||||
|
assert!(res
|
||||||
|
.iter()
|
||||||
|
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1)
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
// Addition retained
|
||||||
|
assert!(res
|
||||||
|
.iter()
|
||||||
|
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0)
|
||||||
|
.is_some());
|
||||||
|
|
||||||
|
// Nochange, not retained
|
||||||
|
assert!(res
|
||||||
|
.iter()
|
||||||
|
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0)
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_write_file(cx: &mut TestAppContext) {
|
async fn test_write_file(cx: &mut TestAppContext) {
|
||||||
let dir = temp_tree(json!({
|
let dir = temp_tree(json!({
|
||||||
|
@ -3016,8 +3341,7 @@ mod tests {
|
||||||
"ignored-dir": {}
|
"ignored-dir": {}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let tree = Worktree::local(
|
let tree = Worktree::local(
|
||||||
client,
|
client,
|
||||||
|
@ -3064,8 +3388,7 @@ mod tests {
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_create_directory(cx: &mut TestAppContext) {
|
async fn test_create_directory(cx: &mut TestAppContext) {
|
||||||
let http_client = FakeHttpClient::with_404_response();
|
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
let client = Client::new(http_client.clone());
|
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
|
@ -3127,6 +3450,7 @@ mod tests {
|
||||||
abs_path: root_dir.path().into(),
|
abs_path: root_dir.path().into(),
|
||||||
removed_entry_ids: Default::default(),
|
removed_entry_ids: Default::default(),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
|
git_repositories: Default::default(),
|
||||||
next_entry_id: next_entry_id.clone(),
|
next_entry_id: next_entry_id.clone(),
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
id: WorktreeId::from_usize(0),
|
id: WorktreeId::from_usize(0),
|
||||||
|
|
|
@ -15,108 +15,111 @@ message Envelope {
|
||||||
CreateRoomResponse create_room_response = 9;
|
CreateRoomResponse create_room_response = 9;
|
||||||
JoinRoom join_room = 10;
|
JoinRoom join_room = 10;
|
||||||
JoinRoomResponse join_room_response = 11;
|
JoinRoomResponse join_room_response = 11;
|
||||||
LeaveRoom leave_room = 1002;
|
LeaveRoom leave_room = 12;
|
||||||
Call call = 12;
|
Call call = 13;
|
||||||
IncomingCall incoming_call = 1000;
|
IncomingCall incoming_call = 14;
|
||||||
CallCanceled call_canceled = 1001;
|
CallCanceled call_canceled = 15;
|
||||||
CancelCall cancel_call = 1004;
|
CancelCall cancel_call = 16;
|
||||||
DeclineCall decline_call = 13;
|
DeclineCall decline_call = 17;
|
||||||
UpdateParticipantLocation update_participant_location = 1003;
|
UpdateParticipantLocation update_participant_location = 18;
|
||||||
RoomUpdated room_updated = 14;
|
RoomUpdated room_updated = 19;
|
||||||
|
|
||||||
ShareProject share_project = 15;
|
ShareProject share_project = 20;
|
||||||
ShareProjectResponse share_project_response = 16;
|
ShareProjectResponse share_project_response = 21;
|
||||||
UnshareProject unshare_project = 17;
|
UnshareProject unshare_project = 22;
|
||||||
JoinProject join_project = 21;
|
JoinProject join_project = 23;
|
||||||
JoinProjectResponse join_project_response = 22;
|
JoinProjectResponse join_project_response = 24;
|
||||||
LeaveProject leave_project = 23;
|
LeaveProject leave_project = 25;
|
||||||
AddProjectCollaborator add_project_collaborator = 24;
|
AddProjectCollaborator add_project_collaborator = 26;
|
||||||
RemoveProjectCollaborator remove_project_collaborator = 25;
|
RemoveProjectCollaborator remove_project_collaborator = 27;
|
||||||
|
|
||||||
GetDefinition get_definition = 27;
|
GetDefinition get_definition = 28;
|
||||||
GetDefinitionResponse get_definition_response = 28;
|
GetDefinitionResponse get_definition_response = 29;
|
||||||
GetTypeDefinition get_type_definition = 29;
|
GetTypeDefinition get_type_definition = 30;
|
||||||
GetTypeDefinitionResponse get_type_definition_response = 30;
|
GetTypeDefinitionResponse get_type_definition_response = 31;
|
||||||
GetReferences get_references = 31;
|
GetReferences get_references = 32;
|
||||||
GetReferencesResponse get_references_response = 32;
|
GetReferencesResponse get_references_response = 33;
|
||||||
GetDocumentHighlights get_document_highlights = 33;
|
GetDocumentHighlights get_document_highlights = 34;
|
||||||
GetDocumentHighlightsResponse get_document_highlights_response = 34;
|
GetDocumentHighlightsResponse get_document_highlights_response = 35;
|
||||||
GetProjectSymbols get_project_symbols = 35;
|
GetProjectSymbols get_project_symbols = 36;
|
||||||
GetProjectSymbolsResponse get_project_symbols_response = 36;
|
GetProjectSymbolsResponse get_project_symbols_response = 37;
|
||||||
OpenBufferForSymbol open_buffer_for_symbol = 37;
|
OpenBufferForSymbol open_buffer_for_symbol = 38;
|
||||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 38;
|
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
|
||||||
|
|
||||||
UpdateProject update_project = 39;
|
UpdateProject update_project = 40;
|
||||||
RegisterProjectActivity register_project_activity = 40;
|
RegisterProjectActivity register_project_activity = 41;
|
||||||
UpdateWorktree update_worktree = 41;
|
UpdateWorktree update_worktree = 42;
|
||||||
UpdateWorktreeExtensions update_worktree_extensions = 42;
|
UpdateWorktreeExtensions update_worktree_extensions = 43;
|
||||||
|
|
||||||
CreateProjectEntry create_project_entry = 43;
|
CreateProjectEntry create_project_entry = 44;
|
||||||
RenameProjectEntry rename_project_entry = 44;
|
RenameProjectEntry rename_project_entry = 45;
|
||||||
CopyProjectEntry copy_project_entry = 45;
|
CopyProjectEntry copy_project_entry = 46;
|
||||||
DeleteProjectEntry delete_project_entry = 46;
|
DeleteProjectEntry delete_project_entry = 47;
|
||||||
ProjectEntryResponse project_entry_response = 47;
|
ProjectEntryResponse project_entry_response = 48;
|
||||||
|
|
||||||
UpdateDiagnosticSummary update_diagnostic_summary = 48;
|
UpdateDiagnosticSummary update_diagnostic_summary = 49;
|
||||||
StartLanguageServer start_language_server = 49;
|
StartLanguageServer start_language_server = 50;
|
||||||
UpdateLanguageServer update_language_server = 50;
|
UpdateLanguageServer update_language_server = 51;
|
||||||
|
|
||||||
OpenBufferById open_buffer_by_id = 51;
|
OpenBufferById open_buffer_by_id = 52;
|
||||||
OpenBufferByPath open_buffer_by_path = 52;
|
OpenBufferByPath open_buffer_by_path = 53;
|
||||||
OpenBufferResponse open_buffer_response = 53;
|
OpenBufferResponse open_buffer_response = 54;
|
||||||
CreateBufferForPeer create_buffer_for_peer = 54;
|
CreateBufferForPeer create_buffer_for_peer = 55;
|
||||||
UpdateBuffer update_buffer = 55;
|
UpdateBuffer update_buffer = 56;
|
||||||
UpdateBufferFile update_buffer_file = 56;
|
UpdateBufferFile update_buffer_file = 57;
|
||||||
SaveBuffer save_buffer = 57;
|
SaveBuffer save_buffer = 58;
|
||||||
BufferSaved buffer_saved = 58;
|
BufferSaved buffer_saved = 59;
|
||||||
BufferReloaded buffer_reloaded = 59;
|
BufferReloaded buffer_reloaded = 60;
|
||||||
ReloadBuffers reload_buffers = 60;
|
ReloadBuffers reload_buffers = 61;
|
||||||
ReloadBuffersResponse reload_buffers_response = 61;
|
ReloadBuffersResponse reload_buffers_response = 62;
|
||||||
FormatBuffers format_buffers = 62;
|
FormatBuffers format_buffers = 63;
|
||||||
FormatBuffersResponse format_buffers_response = 63;
|
FormatBuffersResponse format_buffers_response = 64;
|
||||||
GetCompletions get_completions = 64;
|
GetCompletions get_completions = 65;
|
||||||
GetCompletionsResponse get_completions_response = 65;
|
GetCompletionsResponse get_completions_response = 66;
|
||||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 66;
|
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
|
||||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 67;
|
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
|
||||||
GetCodeActions get_code_actions = 68;
|
GetCodeActions get_code_actions = 69;
|
||||||
GetCodeActionsResponse get_code_actions_response = 69;
|
GetCodeActionsResponse get_code_actions_response = 70;
|
||||||
GetHover get_hover = 70;
|
GetHover get_hover = 71;
|
||||||
GetHoverResponse get_hover_response = 71;
|
GetHoverResponse get_hover_response = 72;
|
||||||
ApplyCodeAction apply_code_action = 72;
|
ApplyCodeAction apply_code_action = 73;
|
||||||
ApplyCodeActionResponse apply_code_action_response = 73;
|
ApplyCodeActionResponse apply_code_action_response = 74;
|
||||||
PrepareRename prepare_rename = 74;
|
PrepareRename prepare_rename = 75;
|
||||||
PrepareRenameResponse prepare_rename_response = 75;
|
PrepareRenameResponse prepare_rename_response = 76;
|
||||||
PerformRename perform_rename = 76;
|
PerformRename perform_rename = 77;
|
||||||
PerformRenameResponse perform_rename_response = 77;
|
PerformRenameResponse perform_rename_response = 78;
|
||||||
SearchProject search_project = 78;
|
SearchProject search_project = 79;
|
||||||
SearchProjectResponse search_project_response = 79;
|
SearchProjectResponse search_project_response = 80;
|
||||||
|
|
||||||
GetChannels get_channels = 80;
|
GetChannels get_channels = 81;
|
||||||
GetChannelsResponse get_channels_response = 81;
|
GetChannelsResponse get_channels_response = 82;
|
||||||
JoinChannel join_channel = 82;
|
JoinChannel join_channel = 83;
|
||||||
JoinChannelResponse join_channel_response = 83;
|
JoinChannelResponse join_channel_response = 84;
|
||||||
LeaveChannel leave_channel = 84;
|
LeaveChannel leave_channel = 85;
|
||||||
SendChannelMessage send_channel_message = 85;
|
SendChannelMessage send_channel_message = 86;
|
||||||
SendChannelMessageResponse send_channel_message_response = 86;
|
SendChannelMessageResponse send_channel_message_response = 87;
|
||||||
ChannelMessageSent channel_message_sent = 87;
|
ChannelMessageSent channel_message_sent = 88;
|
||||||
GetChannelMessages get_channel_messages = 88;
|
GetChannelMessages get_channel_messages = 89;
|
||||||
GetChannelMessagesResponse get_channel_messages_response = 89;
|
GetChannelMessagesResponse get_channel_messages_response = 90;
|
||||||
|
|
||||||
UpdateContacts update_contacts = 90;
|
UpdateContacts update_contacts = 91;
|
||||||
UpdateInviteInfo update_invite_info = 91;
|
UpdateInviteInfo update_invite_info = 92;
|
||||||
ShowContacts show_contacts = 92;
|
ShowContacts show_contacts = 93;
|
||||||
|
|
||||||
GetUsers get_users = 93;
|
GetUsers get_users = 94;
|
||||||
FuzzySearchUsers fuzzy_search_users = 94;
|
FuzzySearchUsers fuzzy_search_users = 95;
|
||||||
UsersResponse users_response = 95;
|
UsersResponse users_response = 96;
|
||||||
RequestContact request_contact = 96;
|
RequestContact request_contact = 97;
|
||||||
RespondToContactRequest respond_to_contact_request = 97;
|
RespondToContactRequest respond_to_contact_request = 98;
|
||||||
RemoveContact remove_contact = 98;
|
RemoveContact remove_contact = 99;
|
||||||
|
|
||||||
Follow follow = 99;
|
Follow follow = 100;
|
||||||
FollowResponse follow_response = 100;
|
FollowResponse follow_response = 101;
|
||||||
UpdateFollowers update_followers = 101;
|
UpdateFollowers update_followers = 102;
|
||||||
Unfollow unfollow = 102;
|
Unfollow unfollow = 103;
|
||||||
|
GetPrivateUserInfo get_private_user_info = 104;
|
||||||
|
GetPrivateUserInfoResponse get_private_user_info_response = 105;
|
||||||
|
UpdateDiffBase update_diff_base = 106;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,6 +798,13 @@ message Unfollow {
|
||||||
uint32 leader_id = 2;
|
uint32 leader_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetPrivateUserInfo {}
|
||||||
|
|
||||||
|
message GetPrivateUserInfoResponse {
|
||||||
|
string metrics_id = 1;
|
||||||
|
bool staff = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
|
|
||||||
message UpdateActiveView {
|
message UpdateActiveView {
|
||||||
|
@ -868,7 +878,8 @@ message BufferState {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
optional File file = 2;
|
optional File file = 2;
|
||||||
string base_text = 3;
|
string base_text = 3;
|
||||||
LineEnding line_ending = 4;
|
optional string diff_base = 4;
|
||||||
|
LineEnding line_ending = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferChunk {
|
message BufferChunk {
|
||||||
|
@ -1032,3 +1043,9 @@ message WorktreeMetadata {
|
||||||
string root_name = 2;
|
string root_name = 2;
|
||||||
bool visible = 3;
|
bool visible = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message UpdateDiffBase {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 buffer_id = 2;
|
||||||
|
optional string diff_base = 3;
|
||||||
|
}
|
||||||
|
|
|
@ -175,6 +175,9 @@ messages!(
|
||||||
(UpdateProject, Foreground),
|
(UpdateProject, Foreground),
|
||||||
(UpdateWorktree, Foreground),
|
(UpdateWorktree, Foreground),
|
||||||
(UpdateWorktreeExtensions, Background),
|
(UpdateWorktreeExtensions, Background),
|
||||||
|
(UpdateDiffBase, Background),
|
||||||
|
(GetPrivateUserInfo, Foreground),
|
||||||
|
(GetPrivateUserInfoResponse, Foreground),
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
|
@ -201,6 +204,7 @@ request_messages!(
|
||||||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||||
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
||||||
(GetReferences, GetReferencesResponse),
|
(GetReferences, GetReferencesResponse),
|
||||||
|
(GetPrivateUserInfo, GetPrivateUserInfoResponse),
|
||||||
(GetProjectSymbols, GetProjectSymbolsResponse),
|
(GetProjectSymbols, GetProjectSymbolsResponse),
|
||||||
(FuzzySearchUsers, UsersResponse),
|
(FuzzySearchUsers, UsersResponse),
|
||||||
(GetUsers, UsersResponse),
|
(GetUsers, UsersResponse),
|
||||||
|
@ -274,6 +278,7 @@ entity_messages!(
|
||||||
UpdateProject,
|
UpdateProject,
|
||||||
UpdateWorktree,
|
UpdateWorktree,
|
||||||
UpdateWorktreeExtensions,
|
UpdateWorktreeExtensions,
|
||||||
|
UpdateDiffBase
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(channel_id, ChannelMessageSent);
|
entity_messages!(channel_id, ChannelMessageSent);
|
||||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub const PROTOCOL_VERSION: u32 = 32;
|
pub const PROTOCOL_VERSION: u32 = 35;
|
||||||
|
|
|
@ -32,6 +32,8 @@ pub struct Settings {
|
||||||
pub default_dock_anchor: DockAnchor,
|
pub default_dock_anchor: DockAnchor,
|
||||||
pub editor_defaults: EditorSettings,
|
pub editor_defaults: EditorSettings,
|
||||||
pub editor_overrides: EditorSettings,
|
pub editor_overrides: EditorSettings,
|
||||||
|
pub git: GitSettings,
|
||||||
|
pub git_overrides: GitSettings,
|
||||||
pub terminal_defaults: TerminalSettings,
|
pub terminal_defaults: TerminalSettings,
|
||||||
pub terminal_overrides: TerminalSettings,
|
pub terminal_overrides: TerminalSettings,
|
||||||
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
|
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
|
||||||
|
@ -52,6 +54,22 @@ impl FeatureFlags {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||||
|
pub struct GitSettings {
|
||||||
|
pub git_gutter: Option<GitGutter>,
|
||||||
|
pub gutter_debounce: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum GitGutter {
|
||||||
|
#[default]
|
||||||
|
TrackedFiles,
|
||||||
|
Hide,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GitGutterConfig {}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||||
pub struct EditorSettings {
|
pub struct EditorSettings {
|
||||||
pub tab_size: Option<NonZeroU32>,
|
pub tab_size: Option<NonZeroU32>,
|
||||||
|
@ -196,6 +214,8 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub terminal: TerminalSettings,
|
pub terminal: TerminalSettings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub git: Option<GitSettings>,
|
||||||
|
#[serde(default)]
|
||||||
#[serde(alias = "language_overrides")]
|
#[serde(alias = "language_overrides")]
|
||||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -252,6 +272,8 @@ impl Settings {
|
||||||
enable_language_server: required(defaults.editor.enable_language_server),
|
enable_language_server: required(defaults.editor.enable_language_server),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
|
git: defaults.git.unwrap(),
|
||||||
|
git_overrides: Default::default(),
|
||||||
terminal_defaults: Default::default(),
|
terminal_defaults: Default::default(),
|
||||||
terminal_overrides: Default::default(),
|
terminal_overrides: Default::default(),
|
||||||
language_defaults: defaults.languages,
|
language_defaults: defaults.languages,
|
||||||
|
@ -303,6 +325,7 @@ impl Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.editor_overrides = data.editor;
|
self.editor_overrides = data.editor;
|
||||||
|
self.git_overrides = data.git.unwrap_or_default();
|
||||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||||
self.terminal_overrides = data.terminal;
|
self.terminal_overrides = data.terminal;
|
||||||
self.language_overrides = data.languages;
|
self.language_overrides = data.languages;
|
||||||
|
@ -358,6 +381,14 @@ impl Settings {
|
||||||
.expect("missing default")
|
.expect("missing default")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_gutter(&self) -> GitGutter {
|
||||||
|
self.git_overrides.git_gutter.unwrap_or_else(|| {
|
||||||
|
self.git
|
||||||
|
.git_gutter
|
||||||
|
.expect("git_gutter should be some by setting setup")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &gpui::AppContext) -> Settings {
|
pub fn test(cx: &gpui::AppContext) -> Settings {
|
||||||
Settings {
|
Settings {
|
||||||
|
@ -382,6 +413,8 @@ impl Settings {
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
terminal_defaults: Default::default(),
|
terminal_defaults: Default::default(),
|
||||||
terminal_overrides: Default::default(),
|
terminal_overrides: Default::default(),
|
||||||
|
git: Default::default(),
|
||||||
|
git_overrides: Default::default(),
|
||||||
language_defaults: Default::default(),
|
language_defaults: Default::default(),
|
||||||
language_overrides: Default::default(),
|
language_overrides: Default::default(),
|
||||||
lsp: Default::default(),
|
lsp: Default::default(),
|
||||||
|
|
|
@ -101,6 +101,12 @@ pub enum Bias {
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Bias {
|
||||||
|
fn default() -> Self {
|
||||||
|
Bias::Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialOrd for Bias {
|
impl PartialOrd for Bias {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
|
|
|
@ -618,8 +618,34 @@ impl Terminal {
|
||||||
term.resize(new_size);
|
term.resize(new_size);
|
||||||
}
|
}
|
||||||
InternalEvent::Clear => {
|
InternalEvent::Clear => {
|
||||||
self.write_to_pty("\x0c".to_string());
|
// Clear back buffer
|
||||||
term.clear_screen(ClearMode::Saved);
|
term.clear_screen(ClearMode::Saved);
|
||||||
|
|
||||||
|
let cursor = term.grid().cursor.point;
|
||||||
|
|
||||||
|
// Clear the lines above
|
||||||
|
term.grid_mut().reset_region(..cursor.line);
|
||||||
|
|
||||||
|
// Copy the current line up
|
||||||
|
let line = term.grid()[cursor.line][..cursor.column]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.enumerate()
|
||||||
|
.collect::<Vec<(usize, Cell)>>();
|
||||||
|
|
||||||
|
for (i, cell) in line {
|
||||||
|
term.grid_mut()[Line(0)][Column(i)] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the cursor
|
||||||
|
term.grid_mut().cursor.point =
|
||||||
|
Point::new(Line(0), term.grid_mut().cursor.point.column);
|
||||||
|
let new_cursor = term.grid().cursor.point;
|
||||||
|
|
||||||
|
// Clear the lines below the new cursor
|
||||||
|
if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
|
||||||
|
term.grid_mut().reset_region((new_cursor.line + 1)..);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
InternalEvent::Scroll(scroll) => {
|
InternalEvent::Scroll(scroll) => {
|
||||||
term.scroll_display(*scroll);
|
term.scroll_display(*scroll);
|
||||||
|
|
|
@ -680,12 +680,12 @@ impl Element for TerminalElement {
|
||||||
let focused = self.focused;
|
let focused = self.focused;
|
||||||
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
||||||
move |(cursor_position, block_width)| {
|
move |(cursor_position, block_width)| {
|
||||||
let shape = match cursor.shape {
|
let (shape, text) = match cursor.shape {
|
||||||
AlacCursorShape::Block if !focused => CursorShape::Hollow,
|
AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
|
||||||
AlacCursorShape::Block => CursorShape::Block,
|
AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
|
||||||
AlacCursorShape::Underline => CursorShape::Underscore,
|
AlacCursorShape::Underline => (CursorShape::Underscore, None),
|
||||||
AlacCursorShape::Beam => CursorShape::Bar,
|
AlacCursorShape::Beam => (CursorShape::Bar, None),
|
||||||
AlacCursorShape::HollowBlock => CursorShape::Hollow,
|
AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
|
||||||
//This case is handled in the if wrapping the whole cursor layout
|
//This case is handled in the if wrapping the whole cursor layout
|
||||||
AlacCursorShape::Hidden => unreachable!(),
|
AlacCursorShape::Hidden => unreachable!(),
|
||||||
};
|
};
|
||||||
|
@ -696,7 +696,7 @@ impl Element for TerminalElement {
|
||||||
dimensions.line_height,
|
dimensions.line_height,
|
||||||
terminal_theme.colors.cursor,
|
terminal_theme.colors.cursor,
|
||||||
shape,
|
shape,
|
||||||
Some(cursor_text),
|
text,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||||
pub struct Anchor {
|
pub struct Anchor {
|
||||||
pub timestamp: clock::Local,
|
pub timestamp: clock::Local,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
|
|
|
@ -54,6 +54,13 @@ impl Rope {
|
||||||
cursor.slice(range.end)
|
cursor.slice(range.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn slice_rows(&self, range: Range<u32>) -> Rope {
|
||||||
|
//This would be more efficient with a forward advance after the first, but it's fine
|
||||||
|
let start = self.point_to_offset(Point::new(range.start, 0));
|
||||||
|
let end = self.point_to_offset(Point::new(range.end, 0));
|
||||||
|
self.slice(start..end)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, text: &str) {
|
pub fn push(&mut self, text: &str) {
|
||||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||||
let mut new_chunk = ArrayString::new();
|
let mut new_chunk = ArrayString::new();
|
||||||
|
|
|
@ -510,8 +510,7 @@ pub struct Editor {
|
||||||
pub rename_fade: f32,
|
pub rename_fade: f32,
|
||||||
pub document_highlight_read_background: Color,
|
pub document_highlight_read_background: Color,
|
||||||
pub document_highlight_write_background: Color,
|
pub document_highlight_write_background: Color,
|
||||||
pub diff_background_deleted: Color,
|
pub diff: DiffStyle,
|
||||||
pub diff_background_inserted: Color,
|
|
||||||
pub line_number: Color,
|
pub line_number: Color,
|
||||||
pub line_number_active: Color,
|
pub line_number_active: Color,
|
||||||
pub guest_selections: Vec<SelectionStyle>,
|
pub guest_selections: Vec<SelectionStyle>,
|
||||||
|
@ -595,6 +594,16 @@ pub struct CodeActions {
|
||||||
pub vertical_scale: f32,
|
pub vertical_scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
pub struct DiffStyle {
|
||||||
|
pub inserted: Color,
|
||||||
|
pub modified: Color,
|
||||||
|
pub deleted: Color,
|
||||||
|
pub removed_width_em: f32,
|
||||||
|
pub width_em: f32,
|
||||||
|
pub corner_radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct Interactive<T> {
|
pub struct Interactive<T> {
|
||||||
pub default: T,
|
pub default: T,
|
||||||
|
|
|
@ -7,17 +7,21 @@ edition = "2021"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["rand", "serde_json", "tempdir"]
|
test-support = ["rand", "serde_json", "tempdir", "git2"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.38"
|
anyhow = "1.0.38"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
tempdir = { version = "0.3.7", optional = true }
|
tempdir = { version = "0.3.7", optional = true }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
||||||
|
git2 = { version = "0.15", default-features = false, optional = true }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = { version = "0.8" }
|
rand = { version = "0.8" }
|
||||||
tempdir = { version = "0.3.7" }
|
tempdir = { version = "0.3.7" }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
git2 = { version = "0.15", default-features = false }
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
mod assertions;
|
mod assertions;
|
||||||
mod marked_text;
|
mod marked_text;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use git2;
|
||||||
|
use std::{
|
||||||
|
ffi::OsStr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
pub use assertions::*;
|
pub use assertions::*;
|
||||||
|
@ -24,6 +28,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
||||||
match contents {
|
match contents {
|
||||||
Value::Object(_) => {
|
Value::Object(_) => {
|
||||||
fs::create_dir(&path).unwrap();
|
fs::create_dir(&path).unwrap();
|
||||||
|
|
||||||
|
if path.file_name() == Some(&OsStr::new(".git")) {
|
||||||
|
git2::Repository::init(&path.parent().unwrap()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
write_tree(&path, contents);
|
write_tree(&path, contents);
|
||||||
}
|
}
|
||||||
Value::Null => {
|
Value::Null => {
|
||||||
|
|
|
@ -46,7 +46,6 @@ use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
mem,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
|
@ -295,7 +294,23 @@ pub trait Item: View {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
|
fn git_diff_recalc(
|
||||||
|
&mut self,
|
||||||
|
_project: ModelHandle<Project>,
|
||||||
|
_cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
|
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
|
||||||
|
fn should_close_item_on_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn is_edit_event(_: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn act_as_type(
|
fn act_as_type(
|
||||||
&self,
|
&self,
|
||||||
type_id: TypeId,
|
type_id: TypeId,
|
||||||
|
@ -412,6 +427,57 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DelayedDebouncedEditAction {
|
||||||
|
task: Option<Task<()>>,
|
||||||
|
cancel_channel: Option<oneshot::Sender<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DelayedDebouncedEditAction {
|
||||||
|
fn new() -> DelayedDebouncedEditAction {
|
||||||
|
DelayedDebouncedEditAction {
|
||||||
|
task: None,
|
||||||
|
cancel_channel: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fire_new<F, Fut>(
|
||||||
|
&mut self,
|
||||||
|
delay: Duration,
|
||||||
|
workspace: &Workspace,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
f: F,
|
||||||
|
) where
|
||||||
|
F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
|
||||||
|
Fut: 'static + Future<Output = ()>,
|
||||||
|
{
|
||||||
|
if let Some(channel) = self.cancel_channel.take() {
|
||||||
|
_ = channel.send(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let project = workspace.project().downgrade();
|
||||||
|
|
||||||
|
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||||
|
self.cancel_channel = Some(sender);
|
||||||
|
|
||||||
|
let previous_task = self.task.take();
|
||||||
|
self.task = Some(cx.spawn_weak(|_, cx| async move {
|
||||||
|
let mut timer = cx.background().timer(delay).fuse();
|
||||||
|
if let Some(previous_task) = previous_task {
|
||||||
|
previous_task.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
futures::select_biased! {
|
||||||
|
_ = receiver => return,
|
||||||
|
_ = timer => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(project) = project.upgrade(&cx) {
|
||||||
|
(f)(project, cx).await;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ItemHandle: 'static + fmt::Debug {
|
pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
fn subscribe_to_item_events(
|
fn subscribe_to_item_events(
|
||||||
&self,
|
&self,
|
||||||
|
@ -450,6 +516,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||||
-> Task<Result<()>>;
|
-> Task<Result<()>>;
|
||||||
|
fn git_diff_recalc(
|
||||||
|
&self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<()>>;
|
||||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||||
fn on_release(
|
fn on_release(
|
||||||
|
@ -555,8 +626,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
.insert(self.id(), pane.downgrade())
|
.insert(self.id(), pane.downgrade())
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
let mut pending_autosave = None;
|
let mut pending_autosave = DelayedDebouncedEditAction::new();
|
||||||
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
let mut pending_git_update = DelayedDebouncedEditAction::new();
|
||||||
let pending_update = Rc::new(RefCell::new(None));
|
let pending_update = Rc::new(RefCell::new(None));
|
||||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
@ -614,45 +685,66 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemEvent::UpdateTab => {
|
ItemEvent::UpdateTab => {
|
||||||
pane.update(cx, |_, cx| {
|
pane.update(cx, |_, cx| {
|
||||||
cx.emit(pane::Event::ChangeItemTitle);
|
cx.emit(pane::Event::ChangeItemTitle);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemEvent::Edit => {
|
ItemEvent::Edit => {
|
||||||
if let Autosave::AfterDelay { milliseconds } =
|
if let Autosave::AfterDelay { milliseconds } =
|
||||||
cx.global::<Settings>().autosave
|
cx.global::<Settings>().autosave
|
||||||
{
|
{
|
||||||
let prev_autosave = pending_autosave
|
let delay = Duration::from_millis(milliseconds);
|
||||||
.take()
|
|
||||||
.unwrap_or_else(|| Task::ready(Some(())));
|
|
||||||
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
|
|
||||||
let prev_cancel_tx =
|
|
||||||
mem::replace(&mut cancel_pending_autosave, cancel_tx);
|
|
||||||
let project = workspace.project.downgrade();
|
|
||||||
let _ = prev_cancel_tx.send(());
|
|
||||||
let item = item.clone();
|
let item = item.clone();
|
||||||
pending_autosave =
|
pending_autosave.fire_new(
|
||||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
delay,
|
||||||
let mut timer = cx
|
workspace,
|
||||||
.background()
|
cx,
|
||||||
.timer(Duration::from_millis(milliseconds))
|
|project, mut cx| async move {
|
||||||
.fuse();
|
|
||||||
prev_autosave.await;
|
|
||||||
futures::select_biased! {
|
|
||||||
_ = cancel_rx => return None,
|
|
||||||
_ = timer => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = project.upgrade(&cx)?;
|
|
||||||
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.log_err();
|
||||||
None
|
},
|
||||||
}));
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
let debounce_delay = settings.git_overrides.gutter_debounce;
|
||||||
|
|
||||||
|
let item = item.clone();
|
||||||
|
|
||||||
|
if let Some(delay) = debounce_delay {
|
||||||
|
const MIN_GIT_DELAY: u64 = 50;
|
||||||
|
|
||||||
|
let delay = delay.max(MIN_GIT_DELAY);
|
||||||
|
let duration = Duration::from_millis(delay);
|
||||||
|
|
||||||
|
pending_git_update.fire_new(
|
||||||
|
duration,
|
||||||
|
workspace,
|
||||||
|
cx,
|
||||||
|
|project, mut cx| async move {
|
||||||
|
cx.update(|cx| item.git_diff_recalc(project, cx))
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let project = workspace.project().downgrade();
|
||||||
|
cx.spawn_weak(|_, mut cx| async move {
|
||||||
|
if let Some(project) = project.upgrade(&cx) {
|
||||||
|
cx.update(|cx| item.git_diff_recalc(project, cx))
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -732,6 +824,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
self.update(cx, |item, cx| item.reload(project, cx))
|
self.update(cx, |item, cx| item.reload(project, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn git_diff_recalc(
|
||||||
|
&self,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
|
cx: &mut MutableAppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
|
||||||
|
}
|
||||||
|
|
||||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
|
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
|
||||||
self.read(cx).act_as_type(type_id, self, cx)
|
self.read(cx).act_as_type(type_id, self, cx)
|
||||||
}
|
}
|
||||||
|
@ -833,7 +933,7 @@ impl AppState {
|
||||||
let fs = project::FakeFs::new(cx.background().clone());
|
let fs = project::FakeFs::new(cx.background().clone());
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test());
|
||||||
let http_client = client::test::FakeHttpClient::with_404_response();
|
let http_client = client::test::FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(http_client.clone());
|
let client = Client::new(http_client.clone(), cx);
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||||
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.55.0"
|
version = "0.59.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
|
@ -92,6 +92,7 @@ toml = "0.5"
|
||||||
tree-sitter = "0.20"
|
tree-sitter = "0.20"
|
||||||
tree-sitter-c = "0.20.1"
|
tree-sitter-c = "0.20.1"
|
||||||
tree-sitter-cpp = "0.20.0"
|
tree-sitter-cpp = "0.20.0"
|
||||||
|
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
|
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
|
||||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
|
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
|
||||||
|
@ -100,6 +101,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
|
||||||
tree-sitter-python = "0.20.2"
|
tree-sitter-python = "0.20.2"
|
||||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||||
tree-sitter-typescript = "0.20.1"
|
tree-sitter-typescript = "0.20.1"
|
||||||
|
tree-sitter-html = "0.19.0"
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -3,6 +3,10 @@ use std::process::Command;
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
|
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
|
||||||
|
|
||||||
|
if let Ok(api_key) = std::env::var("ZED_AMPLITUDE_API_KEY") {
|
||||||
|
println!("cargo:rustc-env=ZED_AMPLITUDE_API_KEY={api_key}");
|
||||||
|
}
|
||||||
|
|
||||||
let output = Command::new("npm")
|
let output = Command::new("npm")
|
||||||
.current_dir("../../styles")
|
.current_dir("../../styles")
|
||||||
.args(["install", "--no-save"])
|
.args(["install", "--no-save"])
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{borrow::Cow, str, sync::Arc};
|
||||||
mod c;
|
mod c;
|
||||||
mod elixir;
|
mod elixir;
|
||||||
mod go;
|
mod go;
|
||||||
|
mod html;
|
||||||
mod installation;
|
mod installation;
|
||||||
mod json;
|
mod json;
|
||||||
mod language_plugin;
|
mod language_plugin;
|
||||||
|
@ -46,6 +47,11 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
||||||
tree_sitter_cpp::language(),
|
tree_sitter_cpp::language(),
|
||||||
Some(CachedLspAdapter::new(c::CLspAdapter).await),
|
Some(CachedLspAdapter::new(c::CLspAdapter).await),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"css",
|
||||||
|
tree_sitter_css::language(),
|
||||||
|
None, //
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"elixir",
|
"elixir",
|
||||||
tree_sitter_elixir::language(),
|
tree_sitter_elixir::language(),
|
||||||
|
@ -96,8 +102,13 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
||||||
tree_sitter_typescript::language_tsx(),
|
tree_sitter_typescript::language_tsx(),
|
||||||
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
|
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"html",
|
||||||
|
tree_sitter_html::language(),
|
||||||
|
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
languages.add(Arc::new(language(name, grammar, lsp_adapter)));
|
languages.add(language(name, grammar, lsp_adapter));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +116,7 @@ pub(crate) fn language(
|
||||||
name: &str,
|
name: &str,
|
||||||
grammar: tree_sitter::Language,
|
grammar: tree_sitter::Language,
|
||||||
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
||||||
) -> Language {
|
) -> Arc<Language> {
|
||||||
let config = toml::from_slice(
|
let config = toml::from_slice(
|
||||||
&LanguageDir::get(&format!("{}/config.toml", name))
|
&LanguageDir::get(&format!("{}/config.toml", name))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -142,7 +153,7 @@ pub(crate) fn language(
|
||||||
if let Some(lsp_adapter) = lsp_adapter {
|
if let Some(lsp_adapter) = lsp_adapter {
|
||||||
language = language.with_lsp_adapter(lsp_adapter)
|
language = language.with_lsp_adapter(lsp_adapter)
|
||||||
}
|
}
|
||||||
language
|
Arc::new(language)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
|
fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
|
||||||
|
|
|
@ -112,7 +112,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
completion: &lsp::CompletionItem,
|
completion: &lsp::CompletionItem,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let label = completion
|
let label = completion
|
||||||
.label
|
.label
|
||||||
|
@ -190,7 +190,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: lsp::SymbolKind,
|
kind: lsp::SymbolKind,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let (text, filter_range, display_range) = match kind {
|
let (text, filter_range, display_range) = match kind {
|
||||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||||
|
@ -251,7 +251,6 @@ mod tests {
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
use language::{AutoindentMode, Buffer};
|
use language::{AutoindentMode, Buffer};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_c_autoindent(cx: &mut MutableAppContext) {
|
fn test_c_autoindent(cx: &mut MutableAppContext) {
|
||||||
|
@ -262,7 +261,7 @@ mod tests {
|
||||||
let language = crate::languages::language("c", tree_sitter_c::language(), None);
|
let language = crate::languages::language("c", tree_sitter_c::language(), None);
|
||||||
|
|
||||||
cx.add_model(|cx| {
|
cx.add_model(|cx| {
|
||||||
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
|
let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
|
||||||
|
|
||||||
// empty function
|
// empty function
|
||||||
buffer.edit([(0..0, "int main() {}")], None, cx);
|
buffer.edit([(0..0, "int main() {}")], None, cx);
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
(identifier) @variable
|
(identifier) @variable
|
||||||
|
|
||||||
((identifier) @constant
|
((identifier) @constant
|
||||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
function: (identifier) @function)
|
function: (identifier) @function)
|
||||||
|
|
|
@ -37,11 +37,11 @@
|
||||||
(type_identifier) @type
|
(type_identifier) @type
|
||||||
|
|
||||||
((identifier) @constant
|
((identifier) @constant
|
||||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||||
|
|
||||||
(field_identifier) @property
|
(field_identifier) @property
|
||||||
(statement_identifier) @label
|
(statement_identifier) @label
|
||||||
(this) @variable.builtin
|
(this) @variable.special
|
||||||
|
|
||||||
[
|
[
|
||||||
"break"
|
"break"
|
||||||
|
|
3
crates/zed/src/languages/css/brackets.scm
Normal file
3
crates/zed/src/languages/css/brackets.scm
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
("(" @open ")" @close)
|
||||||
|
("[" @open "]" @close)
|
||||||
|
("{" @open "}" @close)
|
9
crates/zed/src/languages/css/config.toml
Normal file
9
crates/zed/src/languages/css/config.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
name = "CSS"
|
||||||
|
path_suffixes = ["css"]
|
||||||
|
autoclose_before = ";:.,=}])>"
|
||||||
|
brackets = [
|
||||||
|
{ start = "{", end = "}", close = true, newline = true },
|
||||||
|
{ start = "[", end = "]", close = true, newline = true },
|
||||||
|
{ start = "(", end = ")", close = true, newline = true },
|
||||||
|
{ start = "\"", end = "\"", close = true, newline = false }
|
||||||
|
]
|
78
crates/zed/src/languages/css/highlights.scm
Normal file
78
crates/zed/src/languages/css/highlights.scm
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
(comment) @comment
|
||||||
|
|
||||||
|
[
|
||||||
|
(tag_name)
|
||||||
|
(nesting_selector)
|
||||||
|
(universal_selector)
|
||||||
|
] @tag
|
||||||
|
|
||||||
|
[
|
||||||
|
"~"
|
||||||
|
">"
|
||||||
|
"+"
|
||||||
|
"-"
|
||||||
|
"*"
|
||||||
|
"/"
|
||||||
|
"="
|
||||||
|
"^="
|
||||||
|
"|="
|
||||||
|
"~="
|
||||||
|
"$="
|
||||||
|
"*="
|
||||||
|
"and"
|
||||||
|
"or"
|
||||||
|
"not"
|
||||||
|
"only"
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
(attribute_selector (plain_value) @string)
|
||||||
|
|
||||||
|
(attribute_name) @attribute
|
||||||
|
(pseudo_element_selector (tag_name) @attribute)
|
||||||
|
(pseudo_class_selector (class_name) @attribute)
|
||||||
|
|
||||||
|
[
|
||||||
|
(class_name)
|
||||||
|
(id_name)
|
||||||
|
(namespace_name)
|
||||||
|
(property_name)
|
||||||
|
(feature_name)
|
||||||
|
] @property
|
||||||
|
|
||||||
|
(function_name) @function
|
||||||
|
|
||||||
|
(
|
||||||
|
[
|
||||||
|
(property_name)
|
||||||
|
(plain_value)
|
||||||
|
] @variable.special
|
||||||
|
(#match? @variable.special "^--")
|
||||||
|
)
|
||||||
|
|
||||||
|
[
|
||||||
|
"@media"
|
||||||
|
"@import"
|
||||||
|
"@charset"
|
||||||
|
"@namespace"
|
||||||
|
"@supports"
|
||||||
|
"@keyframes"
|
||||||
|
(at_keyword)
|
||||||
|
(to)
|
||||||
|
(from)
|
||||||
|
(important)
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
(string_value) @string
|
||||||
|
(color_value) @string.special
|
||||||
|
|
||||||
|
[
|
||||||
|
(integer_value)
|
||||||
|
(float_value)
|
||||||
|
] @number
|
||||||
|
|
||||||
|
(unit) @type
|
||||||
|
|
||||||
|
[
|
||||||
|
","
|
||||||
|
":"
|
||||||
|
] @punctuation.delimiter
|
1
crates/zed/src/languages/css/indents.scm
Normal file
1
crates/zed/src/languages/css/indents.scm
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(_ "{" "}" @end) @indent
|
|
@ -113,7 +113,7 @@ impl LspAdapter for ElixirLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
completion: &lsp::CompletionItem,
|
completion: &lsp::CompletionItem,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
match completion.kind.zip(completion.detail.as_ref()) {
|
match completion.kind.zip(completion.detail.as_ref()) {
|
||||||
Some((_, detail)) if detail.starts_with("(function)") => {
|
Some((_, detail)) if detail.starts_with("(function)") => {
|
||||||
|
@ -168,7 +168,7 @@ impl LspAdapter for ElixirLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: SymbolKind,
|
kind: SymbolKind,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let (text, filter_range, display_range) = match kind {
|
let (text, filter_range, display_range) = match kind {
|
||||||
SymbolKind::METHOD | SymbolKind::FUNCTION => {
|
SymbolKind::METHOD | SymbolKind::FUNCTION => {
|
||||||
|
|
|
@ -134,7 +134,7 @@ impl super::LspAdapter for GoLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
completion: &lsp::CompletionItem,
|
completion: &lsp::CompletionItem,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let label = &completion.label;
|
let label = &completion.label;
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ impl super::LspAdapter for GoLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: lsp::SymbolKind,
|
kind: lsp::SymbolKind,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let (text, filter_range, display_range) = match kind {
|
let (text, filter_range, display_range) = match kind {
|
||||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||||
|
|
101
crates/zed/src/languages/html.rs
Normal file
101
crates/zed/src/languages/html.rs
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
use super::installation::{npm_install_packages, npm_package_latest_version};
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use client::http::HttpClient;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use language::{LanguageServerName, LspAdapter};
|
||||||
|
use serde_json::json;
|
||||||
|
use smol::fs;
|
||||||
|
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
pub struct HtmlLspAdapter;
|
||||||
|
|
||||||
|
impl HtmlLspAdapter {
|
||||||
|
const BIN_PATH: &'static str =
|
||||||
|
"node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LspAdapter for HtmlLspAdapter {
|
||||||
|
async fn name(&self) -> LanguageServerName {
|
||||||
|
LanguageServerName("vscode-html-language-server".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn server_args(&self) -> Vec<String> {
|
||||||
|
vec!["--stdio".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_latest_server_version(
|
||||||
|
&self,
|
||||||
|
_: Arc<dyn HttpClient>,
|
||||||
|
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||||
|
Ok(Box::new(npm_package_latest_version("vscode-langservers-extracted").await?) as Box<_>)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_server_binary(
|
||||||
|
&self,
|
||||||
|
version: Box<dyn 'static + Send + Any>,
|
||||||
|
_: Arc<dyn HttpClient>,
|
||||||
|
container_dir: PathBuf,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let version = version.downcast::<String>().unwrap();
|
||||||
|
let version_dir = container_dir.join(version.as_str());
|
||||||
|
fs::create_dir_all(&version_dir)
|
||||||
|
.await
|
||||||
|
.context("failed to create version directory")?;
|
||||||
|
let binary_path = version_dir.join(Self::BIN_PATH);
|
||||||
|
|
||||||
|
if fs::metadata(&binary_path).await.is_err() {
|
||||||
|
npm_install_packages(
|
||||||
|
[("vscode-langservers-extracted", version.as_str())],
|
||||||
|
&version_dir,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
|
||||||
|
while let Some(entry) = entries.next().await {
|
||||||
|
if let Some(entry) = entry.log_err() {
|
||||||
|
let entry_path = entry.path();
|
||||||
|
if entry_path.as_path() != version_dir {
|
||||||
|
fs::remove_dir_all(&entry_path).await.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(binary_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
|
||||||
|
(|| async move {
|
||||||
|
let mut last_version_dir = None;
|
||||||
|
let mut entries = fs::read_dir(&container_dir).await?;
|
||||||
|
while let Some(entry) = entries.next().await {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type().await?.is_dir() {
|
||||||
|
last_version_dir = Some(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||||
|
let bin_path = last_version_dir.join(Self::BIN_PATH);
|
||||||
|
if bin_path.exists() {
|
||||||
|
Ok(bin_path)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"missing executable in directory {:?}",
|
||||||
|
last_version_dir
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.await
|
||||||
|
.log_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||||
|
Some(json!({
|
||||||
|
"provideFormatter": true
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
2
crates/zed/src/languages/html/brackets.scm
Normal file
2
crates/zed/src/languages/html/brackets.scm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
("<" @open ">" @close)
|
||||||
|
("\"" @open "\"" @close)
|
12
crates/zed/src/languages/html/config.toml
Normal file
12
crates/zed/src/languages/html/config.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
name = "HTML"
|
||||||
|
path_suffixes = ["html"]
|
||||||
|
autoclose_before = ">})"
|
||||||
|
brackets = [
|
||||||
|
{ start = "<", end = ">", close = true, newline = true },
|
||||||
|
{ start = "{", end = "}", close = true, newline = true },
|
||||||
|
{ start = "(", end = ")", close = true, newline = true },
|
||||||
|
{ start = "\"", end = "\"", close = true, newline = false },
|
||||||
|
{ start = "!--", end = " --", close = true, newline = false },
|
||||||
|
]
|
||||||
|
|
||||||
|
block_comment = ["<!-- ", " -->"]
|
15
crates/zed/src/languages/html/highlights.scm
Normal file
15
crates/zed/src/languages/html/highlights.scm
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
(tag_name) @keyword
|
||||||
|
(erroneous_end_tag_name) @keyword
|
||||||
|
(doctype) @constant
|
||||||
|
(attribute_name) @property
|
||||||
|
(attribute_value) @string
|
||||||
|
(comment) @comment
|
||||||
|
|
||||||
|
"=" @operator
|
||||||
|
|
||||||
|
[
|
||||||
|
"<"
|
||||||
|
">"
|
||||||
|
"</"
|
||||||
|
"/>"
|
||||||
|
] @punctuation.bracket
|
6
crates/zed/src/languages/html/indents.scm
Normal file
6
crates/zed/src/languages/html/indents.scm
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
(start_tag ">" @end) @indent
|
||||||
|
(self_closing_tag "/>" @end) @indent
|
||||||
|
|
||||||
|
(element
|
||||||
|
(start_tag) @start
|
||||||
|
(end_tag)? @end) @indent
|
7
crates/zed/src/languages/html/injections.scm
Normal file
7
crates/zed/src/languages/html/injections.scm
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
(script_element
|
||||||
|
(raw_text) @content
|
||||||
|
(#set! "language" "javascript"))
|
||||||
|
|
||||||
|
(style_element
|
||||||
|
(raw_text) @content
|
||||||
|
(#set! "language" "css"))
|
0
crates/zed/src/languages/html/outline.scm
Normal file
0
crates/zed/src/languages/html/outline.scm
Normal file
|
@ -51,12 +51,12 @@
|
||||||
(shorthand_property_identifier)
|
(shorthand_property_identifier)
|
||||||
(shorthand_property_identifier_pattern)
|
(shorthand_property_identifier_pattern)
|
||||||
] @constant
|
] @constant
|
||||||
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
|
||||||
|
|
||||||
; Literals
|
; Literals
|
||||||
|
|
||||||
(this) @variable.builtin
|
(this) @variable.special
|
||||||
(super) @variable.builtin
|
(super) @variable.special
|
||||||
|
|
||||||
[
|
[
|
||||||
(true)
|
(true)
|
||||||
|
|
|
@ -90,7 +90,7 @@ impl LspAdapter for PythonLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
item: &lsp::CompletionItem,
|
item: &lsp::CompletionItem,
|
||||||
language: &language::Language,
|
language: &Arc<language::Language>,
|
||||||
) -> Option<language::CodeLabel> {
|
) -> Option<language::CodeLabel> {
|
||||||
let label = &item.label;
|
let label = &item.label;
|
||||||
let grammar = language.grammar()?;
|
let grammar = language.grammar()?;
|
||||||
|
@ -112,7 +112,7 @@ impl LspAdapter for PythonLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: lsp::SymbolKind,
|
kind: lsp::SymbolKind,
|
||||||
language: &language::Language,
|
language: &Arc<language::Language>,
|
||||||
) -> Option<language::CodeLabel> {
|
) -> Option<language::CodeLabel> {
|
||||||
let (text, filter_range, display_range) = match kind {
|
let (text, filter_range, display_range) = match kind {
|
||||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||||
|
@ -149,7 +149,6 @@ mod tests {
|
||||||
use gpui::{ModelContext, MutableAppContext};
|
use gpui::{ModelContext, MutableAppContext};
|
||||||
use language::{AutoindentMode, Buffer};
|
use language::{AutoindentMode, Buffer};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_python_autoindent(cx: &mut MutableAppContext) {
|
fn test_python_autoindent(cx: &mut MutableAppContext) {
|
||||||
|
@ -160,7 +159,7 @@ mod tests {
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
|
|
||||||
cx.add_model(|cx| {
|
cx.add_model(|cx| {
|
||||||
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
|
let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
|
||||||
let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
|
let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
|
||||||
let ix = buffer.len();
|
let ix = buffer.len();
|
||||||
buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
|
buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
(#match? @type "^[A-Z]"))
|
(#match? @type "^[A-Z]"))
|
||||||
|
|
||||||
((identifier) @constant
|
((identifier) @constant
|
||||||
(#match? @constant "^[A-Z][A-Z_]*$"))
|
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||||
|
|
||||||
; Builtin functions
|
; Builtin functions
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ impl LspAdapter for RustLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
completion: &lsp::CompletionItem,
|
completion: &lsp::CompletionItem,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
match completion.kind {
|
match completion.kind {
|
||||||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||||||
|
@ -196,7 +196,7 @@ impl LspAdapter for RustLspAdapter {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
kind: lsp::SymbolKind,
|
kind: lsp::SymbolKind,
|
||||||
language: &Language,
|
language: &Arc<Language>,
|
||||||
) -> Option<CodeLabel> {
|
) -> Option<CodeLabel> {
|
||||||
let (text, filter_range, display_range) = match kind {
|
let (text, filter_range, display_range) = match kind {
|
||||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||||
|
@ -439,7 +439,7 @@ mod tests {
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
|
|
||||||
cx.add_model(|cx| {
|
cx.add_model(|cx| {
|
||||||
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
|
let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
|
||||||
|
|
||||||
// indent between braces
|
// indent between braces
|
||||||
buffer.set_text("fn a() {}", cx);
|
buffer.set_text("fn a() {}", cx);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
(type_identifier) @type
|
(type_identifier) @type
|
||||||
(primitive_type) @type.builtin
|
(primitive_type) @type.builtin
|
||||||
(self) @variable.builtin
|
(self) @variable.special
|
||||||
(field_identifier) @property
|
(field_identifier) @property
|
||||||
|
|
||||||
(call_expression
|
(call_expression
|
||||||
|
@ -27,22 +27,13 @@
|
||||||
|
|
||||||
; Identifier conventions
|
; Identifier conventions
|
||||||
|
|
||||||
; Assume uppercase names are enum constructors
|
; Assume uppercase names are types/enum-constructors
|
||||||
((identifier) @variant
|
((identifier) @type
|
||||||
(#match? @variant "^[A-Z]"))
|
|
||||||
|
|
||||||
; Assume that uppercase names in paths are types
|
|
||||||
((scoped_identifier
|
|
||||||
path: (identifier) @type)
|
|
||||||
(#match? @type "^[A-Z]"))
|
|
||||||
((scoped_identifier
|
|
||||||
path: (scoped_identifier
|
|
||||||
name: (identifier) @type))
|
|
||||||
(#match? @type "^[A-Z]"))
|
(#match? @type "^[A-Z]"))
|
||||||
|
|
||||||
; Assume all-caps names are constants
|
; Assume all-caps names are constants
|
||||||
((identifier) @constant
|
((identifier) @constant
|
||||||
(#match? @constant "^[A-Z][A-Z\\d_]+$"))
|
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||||
|
|
||||||
[
|
[
|
||||||
"("
|
"("
|
||||||
|
|
|
@ -115,7 +115,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||||
async fn label_for_completion(
|
async fn label_for_completion(
|
||||||
&self,
|
&self,
|
||||||
item: &lsp::CompletionItem,
|
item: &lsp::CompletionItem,
|
||||||
language: &language::Language,
|
language: &Arc<language::Language>,
|
||||||
) -> Option<language::CodeLabel> {
|
) -> Option<language::CodeLabel> {
|
||||||
use lsp::CompletionItemKind as Kind;
|
use lsp::CompletionItemKind as Kind;
|
||||||
let len = item.label.len();
|
let len = item.label.len();
|
||||||
|
@ -144,7 +144,6 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use gpui::MutableAppContext;
|
use gpui::MutableAppContext;
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
@ -172,9 +171,8 @@ mod tests {
|
||||||
"#
|
"#
|
||||||
.unindent();
|
.unindent();
|
||||||
|
|
||||||
let buffer = cx.add_model(|cx| {
|
let buffer =
|
||||||
language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
|
cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
|
||||||
});
|
|
||||||
let outline = buffer.read(cx).snapshot().outline(None).unwrap();
|
let outline = buffer.read(cx).snapshot().outline(None).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outline
|
outline
|
||||||
|
|
|
@ -51,12 +51,12 @@
|
||||||
(shorthand_property_identifier)
|
(shorthand_property_identifier)
|
||||||
(shorthand_property_identifier_pattern)
|
(shorthand_property_identifier_pattern)
|
||||||
] @constant
|
] @constant
|
||||||
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
|
||||||
|
|
||||||
; Literals
|
; Literals
|
||||||
|
|
||||||
(this) @variable.builtin
|
(this) @variable.special
|
||||||
(super) @variable.builtin
|
(super) @variable.special
|
||||||
|
|
||||||
[
|
[
|
||||||
(true)
|
(true)
|
||||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
||||||
FutureExt, SinkExt, StreamExt,
|
FutureExt, SinkExt, StreamExt,
|
||||||
};
|
};
|
||||||
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
||||||
use isahc::{config::Configurable, AsyncBody, Request};
|
use isahc::{config::Configurable, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -88,7 +88,7 @@ fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
let client = client::Client::new(http.clone());
|
let client = client::Client::new(http.clone(), cx);
|
||||||
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
|
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
|
||||||
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
|
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
|
||||||
let languages = Arc::new(languages);
|
let languages = Arc::new(languages);
|
||||||
|
@ -120,7 +120,6 @@ fn main() {
|
||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
terminal::init(cx);
|
terminal::init(cx);
|
||||||
|
|
||||||
let db = cx.background().block(db);
|
|
||||||
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -139,6 +138,10 @@ fn main() {
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let project_store = cx.add_model(|_| ProjectStore::new());
|
let project_store = cx.add_model(|_| ProjectStore::new());
|
||||||
|
let db = cx.background().block(db);
|
||||||
|
client.start_telemetry(db.clone());
|
||||||
|
client.report_event("start app", Default::default());
|
||||||
|
|
||||||
let app_state = Arc::new(AppState {
|
let app_state = Arc::new(AppState {
|
||||||
languages,
|
languages,
|
||||||
themes,
|
themes,
|
||||||
|
@ -280,12 +283,10 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
|
||||||
"token": ZED_SECRET_CLIENT_TOKEN,
|
"token": ZED_SECRET_CLIENT_TOKEN,
|
||||||
}))
|
}))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let request = Request::builder()
|
let request = Request::post(&panic_report_url)
|
||||||
.uri(&panic_report_url)
|
|
||||||
.method(http::Method::POST)
|
|
||||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(AsyncBody::from(body))?;
|
.body(body.into())?;
|
||||||
let response = http.send(request).await.context("error sending panic")?;
|
let response = http.send(request).await.context("error sending panic")?;
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
fs::remove_file(child_path)
|
fs::remove_file(child_path)
|
||||||
|
|
|
@ -328,6 +328,11 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
action: Box::new(command_palette::Toggle),
|
action: Box::new(command_palette::Toggle),
|
||||||
},
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "View Telemetry Log",
|
||||||
|
action: Box::new(crate::OpenTelemetryLog),
|
||||||
|
},
|
||||||
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Documentation",
|
name: "Documentation",
|
||||||
action: Box::new(crate::OpenBrowser {
|
action: Box::new(crate::OpenBrowser {
|
||||||
|
|
|
@ -55,6 +55,7 @@ actions!(
|
||||||
DebugElements,
|
DebugElements,
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
OpenLog,
|
OpenLog,
|
||||||
|
OpenTelemetryLog,
|
||||||
OpenKeymap,
|
OpenKeymap,
|
||||||
OpenDefaultSettings,
|
OpenDefaultSettings,
|
||||||
OpenDefaultKeymap,
|
OpenDefaultKeymap,
|
||||||
|
@ -145,6 +146,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
open_log_file(workspace, app_state.clone(), cx);
|
open_log_file(workspace, app_state.clone(), cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
cx.add_action({
|
||||||
|
let app_state = app_state.clone();
|
||||||
|
move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
|
||||||
|
open_telemetry_log_file(workspace, app_state.clone(), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
cx.add_action({
|
cx.add_action({
|
||||||
let app_state = app_state.clone();
|
let app_state = app_state.clone();
|
||||||
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
|
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
|
||||||
|
@ -485,6 +492,62 @@ fn open_log_file(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_telemetry_log_file(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
|
||||||
|
cx.spawn_weak(|workspace, mut cx| async move {
|
||||||
|
let workspace = workspace.upgrade(&cx)?;
|
||||||
|
let path = app_state.client.telemetry_log_file_path()?;
|
||||||
|
let log = app_state.fs.load(&path).await.log_err()?;
|
||||||
|
|
||||||
|
const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
|
||||||
|
let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
|
||||||
|
if let Some(newline_offset) = log[start_offset..].find('\n') {
|
||||||
|
start_offset += newline_offset + 1;
|
||||||
|
}
|
||||||
|
let log_suffix = &log[start_offset..];
|
||||||
|
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let project = workspace.project().clone();
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| project.create_buffer("", None, cx))
|
||||||
|
.expect("creating buffers on a local workspace always succeeds");
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.set_language(app_state.languages.get_language("JSON"), cx);
|
||||||
|
buffer.edit(
|
||||||
|
[(
|
||||||
|
0..0,
|
||||||
|
concat!(
|
||||||
|
"// Zed collects anonymous usage data to help us understand how people are using the app.\n",
|
||||||
|
"// After the beta release, we'll provide the ability to opt out of this telemetry.\n",
|
||||||
|
"// Here is the data that has been reported for the current session:\n",
|
||||||
|
"\n"
|
||||||
|
),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let buffer = cx.add_model(|cx| {
|
||||||
|
MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
|
||||||
|
});
|
||||||
|
workspace.add_item(
|
||||||
|
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn open_bundled_config_file(
|
fn open_bundled_config_file(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
@ -1051,7 +1114,7 @@ mod tests {
|
||||||
assert!(!editor.is_dirty(cx));
|
assert!(!editor.is_dirty(cx));
|
||||||
assert_eq!(editor.title(cx), "untitled");
|
assert_eq!(editor.title(cx), "untitled");
|
||||||
assert!(Arc::ptr_eq(
|
assert!(Arc::ptr_eq(
|
||||||
editor.language_at(0, cx).unwrap(),
|
&editor.language_at(0, cx).unwrap(),
|
||||||
&languages::PLAIN_TEXT
|
&languages::PLAIN_TEXT
|
||||||
));
|
));
|
||||||
editor.handle_input("hi", cx);
|
editor.handle_input("hi", cx);
|
||||||
|
@ -1138,7 +1201,7 @@ mod tests {
|
||||||
|
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
assert!(Arc::ptr_eq(
|
assert!(Arc::ptr_eq(
|
||||||
editor.language_at(0, cx).unwrap(),
|
&editor.language_at(0, cx).unwrap(),
|
||||||
&languages::PLAIN_TEXT
|
&languages::PLAIN_TEXT
|
||||||
));
|
));
|
||||||
editor.handle_input("hi", cx);
|
editor.handle_input("hi", cx);
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
player,
|
player,
|
||||||
popoverShadow,
|
popoverShadow,
|
||||||
text,
|
text,
|
||||||
|
textColor,
|
||||||
TextColor,
|
TextColor,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import hoverPopover from "./hoverPopover";
|
import hoverPopover from "./hoverPopover";
|
||||||
|
@ -59,8 +60,14 @@ export default function editor(theme: Theme) {
|
||||||
indicator: iconColor(theme, "secondary"),
|
indicator: iconColor(theme, "secondary"),
|
||||||
verticalScale: 0.618
|
verticalScale: 0.618
|
||||||
},
|
},
|
||||||
diffBackgroundDeleted: backgroundColor(theme, "error"),
|
diff: {
|
||||||
diffBackgroundInserted: backgroundColor(theme, "ok"),
|
deleted: theme.iconColor.error,
|
||||||
|
inserted: theme.iconColor.ok,
|
||||||
|
modified: theme.iconColor.warning,
|
||||||
|
removedWidthEm: 0.275,
|
||||||
|
widthEm: 0.16,
|
||||||
|
cornerRadius: 0.05,
|
||||||
|
},
|
||||||
documentHighlightReadBackground: theme.editor.highlight.occurrence,
|
documentHighlightReadBackground: theme.editor.highlight.occurrence,
|
||||||
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
||||||
errorColor: theme.textColor.error,
|
errorColor: theme.textColor.error,
|
||||||
|
|
|
@ -113,6 +113,11 @@ export function createTheme(
|
||||||
hovered: sample(ramps.blue, 0.1),
|
hovered: sample(ramps.blue, 0.1),
|
||||||
active: sample(ramps.blue, 0.15),
|
active: sample(ramps.blue, 0.15),
|
||||||
},
|
},
|
||||||
|
on500Ok: {
|
||||||
|
base: sample(ramps.green, 0.05),
|
||||||
|
hovered: sample(ramps.green, 0.1),
|
||||||
|
active: sample(ramps.green, 0.15)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const borderColor = {
|
const borderColor = {
|
||||||
|
@ -180,6 +185,10 @@ export function createTheme(
|
||||||
color: sample(ramps.neutral, 7),
|
color: sample(ramps.neutral, 7),
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
},
|
},
|
||||||
|
"variable.special": {
|
||||||
|
color: sample(ramps.blue, 0.80),
|
||||||
|
weight: fontWeights.normal,
|
||||||
|
},
|
||||||
comment: {
|
comment: {
|
||||||
color: sample(ramps.neutral, 5),
|
color: sample(ramps.neutral, 5),
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
|
@ -205,15 +214,11 @@ export function createTheme(
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
},
|
},
|
||||||
constructor: {
|
constructor: {
|
||||||
color: sample(ramps.blue, 0.5),
|
color: sample(ramps.cyan, 0.5),
|
||||||
weight: fontWeights.normal,
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
color: sample(ramps.blue, 0.5),
|
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
},
|
},
|
||||||
property: {
|
property: {
|
||||||
color: sample(ramps.blue, 0.5),
|
color: sample(ramps.blue, 0.6),
|
||||||
weight: fontWeights.normal,
|
weight: fontWeights.normal,
|
||||||
},
|
},
|
||||||
enum: {
|
enum: {
|
||||||
|
|
|
@ -43,7 +43,7 @@ export interface Syntax {
|
||||||
keyword: SyntaxHighlightStyle;
|
keyword: SyntaxHighlightStyle;
|
||||||
function: SyntaxHighlightStyle;
|
function: SyntaxHighlightStyle;
|
||||||
type: SyntaxHighlightStyle;
|
type: SyntaxHighlightStyle;
|
||||||
variant: SyntaxHighlightStyle;
|
constructor: SyntaxHighlightStyle;
|
||||||
property: SyntaxHighlightStyle;
|
property: SyntaxHighlightStyle;
|
||||||
enum: SyntaxHighlightStyle;
|
enum: SyntaxHighlightStyle;
|
||||||
operator: SyntaxHighlightStyle;
|
operator: SyntaxHighlightStyle;
|
||||||
|
@ -78,6 +78,7 @@ export default interface Theme {
|
||||||
// Hacks for elements on top of the editor
|
// Hacks for elements on top of the editor
|
||||||
on500: BackgroundColorSet;
|
on500: BackgroundColorSet;
|
||||||
ok: BackgroundColorSet;
|
ok: BackgroundColorSet;
|
||||||
|
on500Ok: BackgroundColorSet;
|
||||||
error: BackgroundColorSet;
|
error: BackgroundColorSet;
|
||||||
on500Error: BackgroundColorSet;
|
on500Error: BackgroundColorSet;
|
||||||
warning: BackgroundColorSet;
|
warning: BackgroundColorSet;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue