mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 10:40:54 +00:00
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 }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
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-tungstenite",
|
||||
"collections",
|
||||
"db",
|
||||
"futures",
|
||||
"gpui",
|
||||
"image",
|
||||
|
@ -969,13 +970,16 @@ dependencies = [
|
|||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rpc",
|
||||
"serde",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time 0.3.11",
|
||||
"tiny_http",
|
||||
"url",
|
||||
"util",
|
||||
"uuid 1.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1042,6 +1046,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"envy",
|
||||
"futures",
|
||||
"git",
|
||||
"gpui",
|
||||
"hyper",
|
||||
"language",
|
||||
|
@ -1072,6 +1077,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
@ -1495,6 +1501,19 @@ dependencies = [
|
|||
"matches",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "db"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"gpui",
|
||||
"parking_lot 0.11.2",
|
||||
"rocksdb",
|
||||
"tempdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate"
|
||||
version = "0.8.6"
|
||||
|
@ -1672,6 +1691,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools",
|
||||
|
@ -1694,6 +1714,8 @@ dependencies = [
|
|||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-rust",
|
||||
"unindent",
|
||||
"util",
|
||||
|
@ -2199,6 +2221,39 @@ dependencies = [
|
|||
"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]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
|
@ -2815,6 +2870,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -2834,6 +2890,8 @@ dependencies = [
|
|||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json 0.19.0",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
|
@ -2869,6 +2927,18 @@ version = "0.2.126"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "libloading"
|
||||
version = "0.7.3"
|
||||
|
@ -3941,9 +4011,11 @@ dependencies = [
|
|||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"db",
|
||||
"fsevent",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"language",
|
||||
|
@ -5999,6 +6071,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tree-sitter-elixir"
|
||||
version = "0.19.0"
|
||||
|
@ -6017,6 +6098,26 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tree-sitter-json"
|
||||
version = "0.19.0"
|
||||
|
@ -6306,6 +6407,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
"git2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"serde_json",
|
||||
|
@ -6326,6 +6429,9 @@ name = "uuid"
|
|||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||
dependencies = [
|
||||
"getrandom 0.2.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
|
@ -7122,7 +7228,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.55.0"
|
||||
version = "0.59.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
@ -7198,8 +7304,10 @@ dependencies = [
|
|||
"tree-sitter",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-python",
|
||||
|
|
|
@ -74,6 +74,15 @@
|
|||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"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
|
||||
"terminal": {
|
||||
// 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]
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
@ -31,7 +32,10 @@ smol = "1.2.5"
|
|||
thiserror = "1.0.29"
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
url = "2.2"
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
|
|
|
@ -601,7 +601,7 @@ mod tests {
|
|||
|
||||
let user_id = 5;
|
||||
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;
|
||||
|
||||
Channel::init(&client);
|
||||
|
|
|
@ -3,6 +3,7 @@ pub mod test;
|
|||
|
||||
pub mod channel;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
@ -11,10 +12,12 @@ use async_tungstenite::tungstenite::{
|
|||
error::Error as WebsocketError,
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use db::Db;
|
||||
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use gpui::{
|
||||
actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext,
|
||||
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
|
||||
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use http::HttpClient;
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -28,9 +31,11 @@ use std::{
|
|||
convert::TryFrom,
|
||||
fmt::Write as _,
|
||||
future::Future,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::Telemetry;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
@ -51,11 +56,16 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
|||
|
||||
actions!(client, [Authenticate]);
|
||||
|
||||
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action(move |_: &Authenticate, cx| {
|
||||
let rpc = rpc.clone();
|
||||
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await })
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &Authenticate, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(
|
||||
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -63,6 +73,7 @@ pub struct Client {
|
|||
id: usize,
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
state: RwLock<ClientState>,
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
|
@ -232,10 +243,11 @@ impl Drop for Subscription {
|
|||
}
|
||||
|
||||
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 {
|
||||
id: 0,
|
||||
peer: Peer::new(),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
http,
|
||||
state: Default::default(),
|
||||
|
||||
|
@ -339,6 +351,7 @@ impl Client {
|
|||
}));
|
||||
}
|
||||
Status::SignedOut | Status::UpgradeRequired => {
|
||||
self.telemetry.set_authenticated_user_info(None, false);
|
||||
state._reconnect_task.take();
|
||||
}
|
||||
_ => {}
|
||||
|
@ -618,6 +631,9 @@ impl Client {
|
|||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
self.report_event("read credentials from keychain", Default::default());
|
||||
}
|
||||
}
|
||||
if credentials.is_none() {
|
||||
let mut status_rx = self.status();
|
||||
|
@ -901,6 +917,7 @@ impl Client {
|
|||
) -> Task<Result<Credentials>> {
|
||||
let platform = cx.platform();
|
||||
let executor = cx.background();
|
||||
let telemetry = self.telemetry.clone();
|
||||
executor.clone().spawn(async move {
|
||||
// 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
|
||||
|
@ -979,6 +996,8 @@ impl Client {
|
|||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_event("authenticate with browser", Default::default());
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
|
@ -1043,6 +1062,18 @@ impl Client {
|
|||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||
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 {
|
||||
|
@ -1108,7 +1139,7 @@ mod tests {
|
|||
cx.foreground().forbid_parking();
|
||||
|
||||
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 mut status = client.status();
|
||||
assert!(matches!(
|
||||
|
@ -1147,7 +1178,7 @@ mod tests {
|
|||
|
||||
let 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({
|
||||
let auth_count = auth_count.clone();
|
||||
let dropped_auth_count = dropped_auth_count.clone();
|
||||
|
@ -1196,7 +1227,7 @@ mod tests {
|
|||
cx.foreground().forbid_parking();
|
||||
|
||||
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 (done_tx1, mut done_rx1) = smol::channel::unbounded();
|
||||
|
@ -1242,7 +1273,7 @@ mod tests {
|
|||
cx.foreground().forbid_parking();
|
||||
|
||||
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 model = cx.add_model(|_| Model::default());
|
||||
|
@ -1270,7 +1301,7 @@ mod tests {
|
|||
cx.foreground().forbid_parking();
|
||||
|
||||
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 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 gpui::{executor, ModelHandle, TestAppContext};
|
||||
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};
|
||||
|
||||
pub struct FakeServer {
|
||||
|
@ -93,6 +96,7 @@ impl FakeServer {
|
|||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
server
|
||||
}
|
||||
|
||||
|
@ -126,26 +130,45 @@ impl FakeServer {
|
|||
#[allow(clippy::await_holding_lock)]
|
||||
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
||||
self.executor.start_waiting();
|
||||
let message = self
|
||||
.state
|
||||
.lock()
|
||||
.incoming
|
||||
.as_mut()
|
||||
.expect("not connected")
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("other half hung up"))?;
|
||||
self.executor.finish_waiting();
|
||||
let type_name = message.payload_type_name();
|
||||
Ok(*message
|
||||
.into_any()
|
||||
.downcast::<TypedEnvelope<M>>()
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"fake server received unexpected message type: {:?}",
|
||||
type_name
|
||||
);
|
||||
}))
|
||||
|
||||
loop {
|
||||
let message = self
|
||||
.state
|
||||
.lock()
|
||||
.incoming
|
||||
.as_mut()
|
||||
.expect("not connected")
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("other half hung up"))?;
|
||||
self.executor.finish_waiting();
|
||||
let type_name = message.payload_type_name();
|
||||
let message = message.into_any();
|
||||
|
||||
if message.is::<TypedEnvelope<M>>() {
|
||||
return Ok(*message.downcast().unwrap());
|
||||
}
|
||||
|
||||
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>(
|
||||
|
|
|
@ -135,10 +135,21 @@ impl UserStore {
|
|||
match status {
|
||||
Status::Connected { .. } => {
|
||||
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))
|
||||
.log_err()
|
||||
.await;
|
||||
.log_err();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
authors = ["Nathan Sobo <nathan@warp.dev>"]
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
|
@ -26,6 +26,7 @@ base64 = "0.13"
|
|||
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||
envy = "0.4.2"
|
||||
futures = "0.3"
|
||||
git = { path = "../git" }
|
||||
hyper = "0.14"
|
||||
lazy_static = "1.4"
|
||||
lipsum = { version = "0.8", optional = true }
|
||||
|
@ -66,11 +67,13 @@ rpc = { path = "../rpc", features = ["test-support"] }
|
|||
settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
util = { path = "../util" }
|
||||
lazy_static = "1.4"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
unindent = "0.1"
|
||||
|
||||
[features]
|
||||
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::{
|
||||
auth,
|
||||
db::{ProjectId, User, UserId},
|
||||
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||
rpc::{self, ResultExt},
|
||||
AppState, Error, Result,
|
||||
};
|
||||
|
@ -24,13 +24,10 @@ use tracing::instrument;
|
|||
|
||||
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
|
||||
Router::new()
|
||||
.route("/user", get(get_authenticated_user))
|
||||
.route("/users", get(get_users).post(create_user))
|
||||
.route(
|
||||
"/users/:id",
|
||||
put(update_user).delete(destroy_user).get(get_user),
|
||||
)
|
||||
.route("/users/:id", put(update_user).delete(destroy_user))
|
||||
.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("/invite_codes/:code", get(get_user_for_invite_code))
|
||||
.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("/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(
|
||||
ServiceBuilder::new()
|
||||
.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)
|
||||
}
|
||||
|
||||
#[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)]
|
||||
struct GetUsersQueryParams {
|
||||
query: Option<String>,
|
||||
|
@ -108,48 +135,76 @@ async fn get_users(
|
|||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct CreateUserParams {
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
invite_code: Option<String>,
|
||||
email_address: Option<String>,
|
||||
email_address: String,
|
||||
email_confirmation_code: Option<String>,
|
||||
#[serde(default)]
|
||||
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(
|
||||
Json(params): Json<CreateUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Extension(rpc_server): Extension<Arc<rpc::Server>>,
|
||||
) -> Result<Json<User>> {
|
||||
let user_id = if let Some(invite_code) = params.invite_code {
|
||||
let invitee_id = app
|
||||
.db
|
||||
.redeem_invite_code(
|
||||
&invite_code,
|
||||
¶ms.github_login,
|
||||
params.email_address.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
rpc_server
|
||||
.invite_code_redeemed(&invite_code, invitee_id)
|
||||
.await
|
||||
.trace_err();
|
||||
invitee_id
|
||||
} else {
|
||||
) -> Result<Json<CreateUserResponse>> {
|
||||
let user = NewUserParams {
|
||||
github_login: params.github_login,
|
||||
github_user_id: params.github_user_id,
|
||||
invite_count: params.invite_count,
|
||||
};
|
||||
|
||||
// Creating a user via the normal signup process
|
||||
let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
|
||||
app.db
|
||||
.create_user(
|
||||
¶ms.github_login,
|
||||
params.email_address.as_deref(),
|
||||
params.admin,
|
||||
.create_user_from_invite(
|
||||
&Invite {
|
||||
email_address: params.email_address,
|
||||
email_confirmation_code,
|
||||
},
|
||||
user,
|
||||
)
|
||||
.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
|
||||
.db
|
||||
.get_user_by_id(user_id)
|
||||
.get_user_by_id(result.user_id)
|
||||
.await?
|
||||
.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)]
|
||||
|
@ -171,7 +226,9 @@ async fn update_user(
|
|||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -186,54 +243,6 @@ async fn destroy_user(
|
|||
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)]
|
||||
struct GetUsersWithNoInvites {
|
||||
invited_by_another_user: bool,
|
||||
|
@ -368,22 +377,24 @@ struct CreateAccessTokenResponse {
|
|||
}
|
||||
|
||||
async fn create_access_token(
|
||||
Path(login): Path<String>,
|
||||
Path(user_id): Path<UserId>,
|
||||
Query(params): Query<CreateAccessTokenQueryParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<CreateAccessTokenResponse>> {
|
||||
// request.require_token().await?;
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_login(&login)
|
||||
.get_user_by_id(user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let mut user_id = user.id;
|
||||
if let Some(impersonate) = params.impersonate {
|
||||
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;
|
||||
} else {
|
||||
return Err(Error::Http(
|
||||
|
@ -415,3 +426,59 @@ async fn get_user_for_invite_code(
|
|||
) -> Result<Json<User>> {
|
||||
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)]
|
||||
struct GitHubUser {
|
||||
id: usize,
|
||||
id: i32,
|
||||
login: 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 client = reqwest::Client::new();
|
||||
|
||||
let current_user =
|
||||
let mut current_user =
|
||||
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>>(
|
||||
&client,
|
||||
&github_token,
|
||||
|
@ -64,16 +67,24 @@ async fn main() {
|
|||
let mut zed_user_ids = Vec::<UserId>::new();
|
||||
for (github_user, admin) in zed_users {
|
||||
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
|
||||
.expect("failed to fetch user")
|
||||
{
|
||||
zed_user_ids.push(user.id);
|
||||
} else {
|
||||
} else if let Some(email) = &github_user.email {
|
||||
zed_user_ids.push(
|
||||
db.create_user(&github_user.login, github_user.email.as_deref(), admin)
|
||||
.await
|
||||
.expect("failed to insert user"),
|
||||
db.create_user(
|
||||
email,
|
||||
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::{
|
||||
db::{tests::TestDb, ProjectId, UserId},
|
||||
db::{NewUserParams, ProjectId, TestDb, UserId},
|
||||
rpc::{Executor, Server, Store},
|
||||
AppState,
|
||||
};
|
||||
|
@ -52,6 +52,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||
|
||||
#[ctor::ctor]
|
||||
|
@ -329,6 +330,7 @@ async fn test_room_uniqueness(
|
|||
})
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
|
||||
assert_eq!(call_b2.caller.github_login, "user_c");
|
||||
}
|
||||
|
@ -1174,6 +1176,258 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
.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)]
|
||||
async fn test_fs_operations(
|
||||
executor: Arc<Deterministic>,
|
||||
|
@ -5092,7 +5346,19 @@ async fn test_random_collaboration(
|
|||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||
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![
|
||||
"guest-1".to_string(),
|
||||
"guest-2".to_string(),
|
||||
|
@ -5100,11 +5366,24 @@ async fn test_random_collaboration(
|
|||
"guest-4".to_string(),
|
||||
];
|
||||
|
||||
for username in Some(&"host".to_string())
|
||||
for (ix, username) in Some(&"host".to_string())
|
||||
.into_iter()
|
||||
.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
|
||||
.app_state
|
||||
.db
|
||||
|
@ -5632,18 +5911,31 @@ impl TestServer {
|
|||
});
|
||||
|
||||
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
|
||||
} else {
|
||||
self.app_state
|
||||
.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
|
||||
.unwrap()
|
||||
.user_id
|
||||
};
|
||||
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 db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
|
|
|
@ -4,6 +4,8 @@ mod db;
|
|||
mod env;
|
||||
mod rpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod db_tests;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
|
||||
|
|
|
@ -206,7 +206,9 @@ impl Server {
|
|||
.add_request_handler(Server::follow)
|
||||
.add_message_handler(Server::unfollow)
|
||||
.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)
|
||||
}
|
||||
|
@ -528,27 +530,30 @@ impl Server {
|
|||
|
||||
pub async fn invite_code_redeemed(
|
||||
self: &Arc<Self>,
|
||||
code: &str,
|
||||
inviter_id: UserId,
|
||||
invitee_id: UserId,
|
||||
) -> Result<()> {
|
||||
let user = self.app_state.db.get_user_for_invite_code(code).await?;
|
||||
let store = self.store().await;
|
||||
let invitee_contact = store.contact_for_user(invitee_id, true);
|
||||
for connection_id in store.connection_ids_for_user(user.id) {
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateContacts {
|
||||
contacts: vec![invitee_contact.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", self.app_state.invite_link_prefix, code),
|
||||
count: user.invite_count as u32,
|
||||
},
|
||||
)?;
|
||||
if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
|
||||
if let Some(code) = &user.invite_code {
|
||||
let store = self.store().await;
|
||||
let invitee_contact = store.contact_for_user(invitee_id, true);
|
||||
for connection_id in store.connection_ids_for_user(inviter_id) {
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateContacts {
|
||||
contacts: vec![invitee_contact.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
self.peer.send(
|
||||
connection_id,
|
||||
proto::UpdateInviteInfo {
|
||||
url: format!("{}{}", self.app_state.invite_link_prefix, &code),
|
||||
count: user.invite_count as u32,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1427,7 +1432,7 @@ impl Server {
|
|||
let users = match query.len() {
|
||||
0 => vec![],
|
||||
1 | 2 => db
|
||||
.get_user_by_github_login(&query)
|
||||
.get_user_by_github_account(&query, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
|
@ -1750,6 +1755,44 @@ impl Server {
|
|||
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<'_> {
|
||||
#[cfg(test)]
|
||||
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" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
|
@ -51,6 +52,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
|
|||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-html = { version = "*", optional = true }
|
||||
tree-sitter-javascript = { version = "*", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
|
@ -67,3 +70,5 @@ rand = "0.8"
|
|||
unindent = "0.1.7"
|
||||
tree-sitter = "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()
|
||||
|| buffer.parse_count() != new_buffer.parse_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()
|
||||
!= 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 collections::{BTreeMap, HashMap};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
|
@ -36,7 +37,7 @@ use gpui::{
|
|||
use json::json;
|
||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||
use project::ProjectPath;
|
||||
use settings::Settings;
|
||||
use settings::{GitGutter, Settings};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
|
@ -45,6 +46,7 @@ use std::{
|
|||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::DiffStyle;
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
|
@ -524,30 +526,141 @@ impl EditorElement {
|
|||
layout: &mut LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let scroll_top =
|
||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
||||
struct GutterLayout {
|
||||
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() {
|
||||
if let Some(line) = line {
|
||||
let line_origin = bounds.origin()
|
||||
+ vec2f(
|
||||
bounds.width() - line.width() - layout.gutter_padding,
|
||||
ix as f32 * layout.position_map.line_height
|
||||
- (scroll_top % layout.position_map.line_height),
|
||||
ix as f32 * gutter_layout.line_height
|
||||
- (gutter_layout.scroll_top % gutter_layout.line_height),
|
||||
);
|
||||
line.paint(
|
||||
line_origin,
|
||||
visible_bounds,
|
||||
layout.position_map.line_height,
|
||||
cx,
|
||||
);
|
||||
|
||||
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
|
||||
|
||||
if show_gutter {
|
||||
//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() {
|
||||
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.;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
|
@ -1425,6 +1559,11 @@ impl Element for EditorElement {
|
|||
let line_number_layouts =
|
||||
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 line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||
for line in &line_layouts {
|
||||
|
@ -1573,6 +1712,7 @@ impl Element for EditorElement {
|
|||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
line_number_layouts,
|
||||
diff_hunks,
|
||||
blocks,
|
||||
selections,
|
||||
context_menu,
|
||||
|
@ -1710,6 +1850,7 @@ pub struct LayoutState {
|
|||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
diff_hunks: Vec<DiffHunk<u32>>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||
}
|
||||
|
|
|
@ -404,6 +404,8 @@ impl Item for Editor {
|
|||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
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> {
|
||||
let mut result = Vec::new();
|
||||
match event {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
|||
use anyhow::Result;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
pub use language::Completion;
|
||||
use language::{
|
||||
|
@ -90,6 +91,7 @@ struct BufferState {
|
|||
last_selections_update_count: usize,
|
||||
last_diagnostics_update_count: usize,
|
||||
last_file_update_count: usize,
|
||||
last_git_diff_update_count: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
|
|||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
trailing_excerpt_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
edit_count: usize,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
|
@ -202,6 +205,7 @@ impl MultiBuffer {
|
|||
last_selections_update_count: buffer_state.last_selections_update_count,
|
||||
last_diagnostics_update_count: buffer_state.last_diagnostics_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(),
|
||||
_subscriptions: [
|
||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||
|
@ -308,6 +312,17 @@ impl MultiBuffer {
|
|||
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>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
|
@ -827,6 +842,7 @@ impl MultiBuffer {
|
|||
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
||||
last_diagnostics_update_count: buffer_snapshot.diagnostics_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(),
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
|
@ -1212,9 +1228,9 @@ impl MultiBuffer {
|
|||
&self,
|
||||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<&'a Arc<Language>> {
|
||||
) -> Option<Arc<Language>> {
|
||||
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]> {
|
||||
|
@ -1249,6 +1265,7 @@ impl MultiBuffer {
|
|||
let mut excerpts_to_edit = Vec::new();
|
||||
let mut reparsed = false;
|
||||
let mut diagnostics_updated = false;
|
||||
let mut git_diff_updated = false;
|
||||
let mut is_dirty = false;
|
||||
let mut has_conflict = false;
|
||||
let mut edited = false;
|
||||
|
@ -1260,6 +1277,7 @@ impl MultiBuffer {
|
|||
let selections_update_count = buffer.selections_update_count();
|
||||
let diagnostics_update_count = buffer.diagnostics_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_reparsed = parse_count > buffer_state.last_parse_count;
|
||||
|
@ -1268,17 +1286,21 @@ impl MultiBuffer {
|
|||
let buffer_diagnostics_updated =
|
||||
diagnostics_update_count > buffer_state.last_diagnostics_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
|
||||
|| buffer_reparsed
|
||||
|| buffer_selections_updated
|
||||
|| buffer_diagnostics_updated
|
||||
|| buffer_file_updated
|
||||
|| buffer_git_diff_updated
|
||||
{
|
||||
buffer_state.last_version = version;
|
||||
buffer_state.last_parse_count = parse_count;
|
||||
buffer_state.last_selections_update_count = selections_update_count;
|
||||
buffer_state.last_diagnostics_update_count = diagnostics_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(
|
||||
buffer_state
|
||||
.excerpts
|
||||
|
@ -1290,6 +1312,7 @@ impl MultiBuffer {
|
|||
edited |= buffer_edited;
|
||||
reparsed |= buffer_reparsed;
|
||||
diagnostics_updated |= buffer_diagnostics_updated;
|
||||
git_diff_updated |= buffer_git_diff_updated;
|
||||
is_dirty |= buffer.is_dirty();
|
||||
has_conflict |= buffer.has_conflict();
|
||||
}
|
||||
|
@ -1302,6 +1325,9 @@ impl MultiBuffer {
|
|||
if diagnostics_updated {
|
||||
snapshot.diagnostics_update_count += 1;
|
||||
}
|
||||
if git_diff_updated {
|
||||
snapshot.git_diff_update_count += 1;
|
||||
}
|
||||
snapshot.is_dirty = is_dirty;
|
||||
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(
|
||||
&self,
|
||||
rows: impl IntoIterator<Item = u32>,
|
||||
|
@ -1949,8 +1993,10 @@ impl MultiBufferSnapshot {
|
|||
|
||||
let mut rows_for_excerpt = Vec::new();
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
|
||||
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() {
|
||||
cursor.seek(&Point::new(row, 0), Bias::Right, &());
|
||||
let excerpt = match cursor.item() {
|
||||
|
@ -1958,7 +2004,17 @@ impl MultiBufferSnapshot {
|
|||
_ => 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_multibuffer_row = cursor.start().row;
|
||||
|
||||
|
@ -2479,15 +2535,17 @@ impl MultiBufferSnapshot {
|
|||
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 {
|
||||
self.trailing_excerpt_update_count
|
||||
}
|
||||
|
||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||
self.excerpts
|
||||
.iter()
|
||||
.next()
|
||||
.and_then(|excerpt| excerpt.buffer.language())
|
||||
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
|
||||
self.point_to_buffer_offset(point)
|
||||
.and_then(|(buffer, offset)| buffer.language_at(offset))
|
||||
}
|
||||
|
||||
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>> {
|
||||
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 wakeup_at = state.now + duration;
|
||||
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();
|
||||
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 app_path(&self) -> Result<PathBuf>;
|
||||
fn app_version(&self) -> Result<AppVersion>;
|
||||
fn os_name(&self) -> &'static str;
|
||||
fn os_version(&self) -> Result<AppVersion>;
|
||||
}
|
||||
|
||||
pub(crate) trait ForegroundPlatform {
|
||||
|
|
|
@ -14,8 +14,10 @@ use core_graphics::{
|
|||
event::{CGEvent, CGEventFlags, CGKeyCode},
|
||||
event_source::{CGEventSource, CGEventSourceStateID},
|
||||
};
|
||||
use ctor::ctor;
|
||||
use foreign_types::ForeignType;
|
||||
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 SPACE_KEY: u16 = b' ' as u16;
|
||||
|
@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b;
|
|||
const TAB_KEY: u16 = 0x09;
|
||||
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> {
|
||||
use cocoa::appkit::*;
|
||||
let code = match key {
|
||||
|
@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
let mut chars_ignoring_modifiers =
|
||||
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
|
@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
|
||||
#[allow(non_upper_case_globals)]
|
||||
let key = match first_char {
|
||||
Some(SPACE_KEY) => "space",
|
||||
Some(BACKSPACE_KEY) => "backspace",
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
|
||||
Some(ESCAPE_KEY) => "escape",
|
||||
Some(TAB_KEY) => "tab",
|
||||
Some(SHIFT_TAB_KEY) => "tab",
|
||||
Some(NSUpArrowFunctionKey) => "up",
|
||||
Some(NSDownArrowFunctionKey) => "down",
|
||||
Some(NSLeftArrowFunctionKey) => "left",
|
||||
Some(NSRightArrowFunctionKey) => "right",
|
||||
Some(NSPageUpFunctionKey) => "pageup",
|
||||
Some(NSPageDownFunctionKey) => "pagedown",
|
||||
Some(NSDeleteFunctionKey) => "delete",
|
||||
Some(NSF1FunctionKey) => "f1",
|
||||
Some(NSF2FunctionKey) => "f2",
|
||||
Some(NSF3FunctionKey) => "f3",
|
||||
Some(NSF4FunctionKey) => "f4",
|
||||
Some(NSF5FunctionKey) => "f5",
|
||||
Some(NSF6FunctionKey) => "f6",
|
||||
Some(NSF7FunctionKey) => "f7",
|
||||
Some(NSF8FunctionKey) => "f8",
|
||||
Some(NSF9FunctionKey) => "f9",
|
||||
Some(NSF10FunctionKey) => "f10",
|
||||
Some(NSF11FunctionKey) => "f11",
|
||||
Some(NSF12FunctionKey) => "f12",
|
||||
Some(SPACE_KEY) => "space".to_string(),
|
||||
Some(BACKSPACE_KEY) => "backspace".to_string(),
|
||||
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
|
||||
Some(ESCAPE_KEY) => "escape".to_string(),
|
||||
Some(TAB_KEY) => "tab".to_string(),
|
||||
Some(SHIFT_TAB_KEY) => "tab".to_string(),
|
||||
Some(NSUpArrowFunctionKey) => "up".to_string(),
|
||||
Some(NSDownArrowFunctionKey) => "down".to_string(),
|
||||
Some(NSLeftArrowFunctionKey) => "left".to_string(),
|
||||
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
||||
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
||||
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
||||
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
||||
Some(NSF1FunctionKey) => "f1".to_string(),
|
||||
Some(NSF2FunctionKey) => "f2".to_string(),
|
||||
Some(NSF3FunctionKey) => "f3".to_string(),
|
||||
Some(NSF4FunctionKey) => "f4".to_string(),
|
||||
Some(NSF5FunctionKey) => "f5".to_string(),
|
||||
Some(NSF6FunctionKey) => "f6".to_string(),
|
||||
Some(NSF7FunctionKey) => "f7".to_string(),
|
||||
Some(NSF8FunctionKey) => "f8".to_string(),
|
||||
Some(NSF9FunctionKey) => "f9".to_string(),
|
||||
Some(NSF10FunctionKey) => "f10".to_string(),
|
||||
Some(NSF11FunctionKey) => "f11".to_string(),
|
||||
Some(NSF12FunctionKey) => "f12".to_string(),
|
||||
_ => {
|
||||
let mut chars_ignoring_modifiers_and_shift =
|
||||
chars_for_modified_key(native_event.keyCode(), false, false);
|
||||
|
@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
|||
shift,
|
||||
cmd,
|
||||
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
|
||||
// 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
|
||||
// returns a valid string.
|
||||
let event = CGEvent::new_keyboard_event(
|
||||
CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
|
||||
code,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
|
||||
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
|
||||
mem::forget(source);
|
||||
|
||||
let mut flags = CGEventFlags::empty();
|
||||
if cmd {
|
||||
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);
|
||||
|
||||
let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
|
||||
unsafe {
|
||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||
CStr::from_ptr(event.characters().UTF8String())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
geometry::vector::{vec2f, Vector2F},
|
||||
keymap,
|
||||
platform::{self, CursorStyle},
|
||||
Action, ClipboardItem, Event, Menu, MenuItem,
|
||||
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use block::ConcreteBlock;
|
||||
|
@ -18,7 +18,8 @@ use cocoa::{
|
|||
},
|
||||
base::{id, nil, selector, YES},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL,
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
|
||||
NSUInteger, NSURL,
|
||||
},
|
||||
};
|
||||
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 {
|
||||
|
|
|
@ -200,6 +200,18 @@ impl super::Platform for Platform {
|
|||
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 {
|
||||
|
|
|
@ -25,6 +25,7 @@ client = { path = "../client" }
|
|||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
@ -63,6 +64,8 @@ util = { path = "../util", features = ["test-support"] }
|
|||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
rand = "0.8.3"
|
||||
tree-sitter-html = "*"
|
||||
tree-sitter-javascript = "*"
|
||||
tree-sitter-json = "*"
|
||||
tree-sitter-rust = "*"
|
||||
tree-sitter-python = "*"
|
||||
|
|
|
@ -45,8 +45,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
|
|||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
struct GitDiffStatus {
|
||||
diff: git::diff::BufferDiff,
|
||||
update_in_progress: bool,
|
||||
update_requested: bool,
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
git_diff_status: GitDiffStatus,
|
||||
file: Option<Arc<dyn File>>,
|
||||
saved_version: clock::Global,
|
||||
saved_version_fingerprint: String,
|
||||
|
@ -66,6 +74,7 @@ pub struct Buffer {
|
|||
diagnostics_update_count: usize,
|
||||
diagnostics_timestamp: clock::Lamport,
|
||||
file_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
completion_triggers: Vec<String>,
|
||||
completion_triggers_timestamp: clock::Lamport,
|
||||
deferred_ops: OperationQueue<Operation>,
|
||||
|
@ -73,25 +82,28 @@ pub struct Buffer {
|
|||
|
||||
pub struct BufferSnapshot {
|
||||
text: text::BufferSnapshot,
|
||||
pub git_diff: git::diff::BufferDiff,
|
||||
pub(crate) syntax: SyntaxSnapshot,
|
||||
file: Option<Arc<dyn File>>,
|
||||
diagnostics: DiagnosticSet,
|
||||
diagnostics_update_count: usize,
|
||||
file_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
||||
selections_update_count: usize,
|
||||
language: Option<Arc<Language>>,
|
||||
parse_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub struct IndentSize {
|
||||
pub len: u32,
|
||||
pub kind: IndentKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum IndentKind {
|
||||
#[default]
|
||||
Space,
|
||||
Tab,
|
||||
}
|
||||
|
@ -236,7 +248,6 @@ pub enum AutoindentMode {
|
|||
struct AutoindentRequest {
|
||||
before_edit: BufferSnapshot,
|
||||
entries: Vec<AutoindentRequestEntry>,
|
||||
indent_size: IndentSize,
|
||||
is_block_mode: bool,
|
||||
}
|
||||
|
||||
|
@ -249,6 +260,7 @@ struct AutoindentRequestEntry {
|
|||
/// only be adjusted if the suggested indentation level has *changed*
|
||||
/// since the edit was made.
|
||||
first_line_is_new: bool,
|
||||
indent_size: IndentSize,
|
||||
original_indent_column: Option<u32>,
|
||||
}
|
||||
|
||||
|
@ -288,10 +300,8 @@ pub struct Chunk<'a> {
|
|||
|
||||
pub struct Diff {
|
||||
base_version: clock::Global,
|
||||
new_text: Arc<str>,
|
||||
changes: Vec<(ChangeTag, usize)>,
|
||||
line_ending: LineEnding,
|
||||
start_offset: usize,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -328,17 +338,20 @@ impl Buffer {
|
|||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_file<T: Into<String>>(
|
||||
replica_id: ReplicaId,
|
||||
base_text: T,
|
||||
diff_base: Option<T>,
|
||||
file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||
diff_base.map(|h| h.into().into_boxed_str().into()),
|
||||
Some(file),
|
||||
)
|
||||
}
|
||||
|
@ -349,7 +362,11 @@ impl Buffer {
|
|||
file: Option<Arc<dyn File>>,
|
||||
) -> Result<Self> {
|
||||
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(
|
||||
proto::LineEnding::from_i32(message.line_ending)
|
||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||
|
@ -362,6 +379,7 @@ impl Buffer {
|
|||
id: self.remote_id(),
|
||||
file: self.file.as_ref().map(|f| f.to_proto()),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
@ -404,7 +422,7 @@ impl Buffer {
|
|||
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() {
|
||||
file.mtime()
|
||||
} else {
|
||||
|
@ -418,6 +436,12 @@ impl Buffer {
|
|||
transaction_depth: 0,
|
||||
was_dirty_before_starting_transaction: None,
|
||||
text: buffer,
|
||||
diff_base,
|
||||
git_diff_status: GitDiffStatus {
|
||||
diff: git::diff::BufferDiff::new(),
|
||||
update_in_progress: false,
|
||||
update_requested: false,
|
||||
},
|
||||
file,
|
||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||
parsing_in_background: false,
|
||||
|
@ -432,6 +456,7 @@ impl Buffer {
|
|||
diagnostics_update_count: 0,
|
||||
diagnostics_timestamp: Default::default(),
|
||||
file_update_count: 0,
|
||||
git_diff_update_count: 0,
|
||||
completion_triggers: Default::default(),
|
||||
completion_triggers_timestamp: Default::default(),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
|
@ -447,11 +472,13 @@ impl Buffer {
|
|||
BufferSnapshot {
|
||||
text,
|
||||
syntax,
|
||||
git_diff: self.git_diff_status.diff.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
diagnostics_update_count: self.diagnostics_update_count,
|
||||
file_update_count: self.file_update_count,
|
||||
git_diff_update_count: self.git_diff_update_count,
|
||||
language: self.language.clone(),
|
||||
parse_count: self.parse_count,
|
||||
selections_update_count: self.selections_update_count,
|
||||
|
@ -584,6 +611,7 @@ impl Buffer {
|
|||
cx,
|
||||
);
|
||||
}
|
||||
self.git_diff_recalc(cx);
|
||||
cx.emit(Event::Reloaded);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -633,6 +661,60 @@ impl Buffer {
|
|||
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>) {
|
||||
cx.emit(Event::Closed);
|
||||
}
|
||||
|
@ -641,6 +723,16 @@ impl Buffer {
|
|||
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 {
|
||||
self.parse_count
|
||||
}
|
||||
|
@ -657,6 +749,10 @@ impl Buffer {
|
|||
self.file_update_count
|
||||
}
|
||||
|
||||
pub fn git_diff_update_count(&self) -> usize {
|
||||
self.git_diff_update_count
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_parsing(&self) -> bool {
|
||||
self.parsing_in_background
|
||||
|
@ -784,10 +880,13 @@ impl Buffer {
|
|||
// buffer before this batch of edits.
|
||||
let mut row_ranges = Vec::new();
|
||||
let mut old_to_new_rows = BTreeMap::new();
|
||||
let mut language_indent_sizes_by_new_row = Vec::new();
|
||||
for entry in &request.entries {
|
||||
let position = entry.range.start;
|
||||
let new_row = position.to_point(&snapshot).row;
|
||||
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 {
|
||||
let old_row = position.to_point(&request.before_edit).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 old_edited_ranges =
|
||||
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 {
|
||||
let suggestions = request
|
||||
.before_edit
|
||||
|
@ -809,6 +910,17 @@ impl Buffer {
|
|||
.flatten();
|
||||
for (old_row, suggestion) in old_edited_range.zip(suggestions) {
|
||||
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
|
||||
.get(&suggestion.basis_row)
|
||||
.and_then(|from_row| old_suggestions.get(from_row).copied())
|
||||
|
@ -817,9 +929,8 @@ impl Buffer {
|
|||
.before_edit
|
||||
.indent_size_for_line(suggestion.basis_row)
|
||||
})
|
||||
.with_delta(suggestion.delta, request.indent_size);
|
||||
old_suggestions
|
||||
.insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
|
||||
.with_delta(suggestion.delta, language_indent_size);
|
||||
old_suggestions.insert(new_row, suggested_indent);
|
||||
}
|
||||
}
|
||||
yield_now().await;
|
||||
|
@ -840,6 +951,8 @@ impl Buffer {
|
|||
|
||||
// Compute new suggestions for each line, but only include them in the result
|
||||
// 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 {
|
||||
let suggestions = snapshot
|
||||
.suggest_autoindents(new_edited_row_range.clone())
|
||||
|
@ -847,13 +960,22 @@ impl Buffer {
|
|||
.flatten();
|
||||
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
|
||||
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
|
||||
.get(&suggestion.basis_row)
|
||||
.copied()
|
||||
.unwrap_or_else(|| {
|
||||
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
|
||||
.get(&new_row)
|
||||
.map_or(true, |old_indentation| {
|
||||
|
@ -965,16 +1087,30 @@ impl Buffer {
|
|||
let old_text = old_text.to_string();
|
||||
let line_ending = LineEnding::detect(&new_text);
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let changes = TextDiff::from_chars(old_text.as_str(), new_text.as_str())
|
||||
.iter_all_changes()
|
||||
.map(|c| (c.tag(), c.value().len()))
|
||||
.collect::<Vec<_>>();
|
||||
let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
|
||||
let mut edits = Vec::new();
|
||||
let mut offset = 0;
|
||||
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 {
|
||||
base_version,
|
||||
new_text: new_text.into(),
|
||||
changes,
|
||||
line_ending,
|
||||
start_offset: 0,
|
||||
edits,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -984,28 +1120,7 @@ impl Buffer {
|
|||
self.finalize_last_transaction();
|
||||
self.start_transaction();
|
||||
self.text.set_line_ending(diff.line_ending);
|
||||
let mut offset = diff.start_offset;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.edit(diff.edits, None, cx);
|
||||
if self.end_transaction(cx).is_some() {
|
||||
self.finalize_last_transaction()
|
||||
} else {
|
||||
|
@ -1184,7 +1299,6 @@ impl Buffer {
|
|||
let edit_id = edit_operation.local_timestamp();
|
||||
|
||||
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 {
|
||||
AutoindentMode::Block {
|
||||
original_indent_columns: start_columns,
|
||||
|
@ -1233,6 +1347,7 @@ impl Buffer {
|
|||
AutoindentRequestEntry {
|
||||
first_line_is_new,
|
||||
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)
|
||||
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
|
||||
}
|
||||
|
@ -1242,7 +1357,6 @@ impl Buffer {
|
|||
self.autoindent_requests.push(Arc::new(AutoindentRequest {
|
||||
before_edit,
|
||||
entries,
|
||||
indent_size,
|
||||
is_block_mode,
|
||||
}));
|
||||
}
|
||||
|
@ -1560,8 +1674,8 @@ impl BufferSnapshot {
|
|||
indent_size_for_line(self, row)
|
||||
}
|
||||
|
||||
pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize {
|
||||
let language_name = self.language().map(|language| language.name());
|
||||
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
|
||||
let language_name = self.language_at(position).map(|language| language.name());
|
||||
let settings = cx.global::<Settings>();
|
||||
if settings.hard_tabs(language_name.as_deref()) {
|
||||
IndentSize::tab()
|
||||
|
@ -1631,6 +1745,8 @@ impl BufferSnapshot {
|
|||
if capture.index == config.indent_capture_ix {
|
||||
start.get_or_insert(Point::from_ts_point(capture.node.start_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 {
|
||||
end = Some(Point::from_ts_point(capture.node.start_position()));
|
||||
}
|
||||
|
@ -1820,8 +1936,14 @@ impl BufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||
self.language.as_ref()
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
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>) {
|
||||
|
@ -1856,8 +1978,8 @@ impl BufferSnapshot {
|
|||
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 mut result: Option<Range<usize>> = None;
|
||||
'outer: for (_, _, node) in self.syntax.layers_for_range(range.clone(), &self.text) {
|
||||
let mut cursor = node.walk();
|
||||
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
|
||||
let mut cursor = layer.node.walk();
|
||||
|
||||
// Descend to the first leaf that touches the start of the range,
|
||||
// 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>(
|
||||
&'a self,
|
||||
search_range: Range<T>,
|
||||
|
@ -2186,6 +2315,10 @@ impl BufferSnapshot {
|
|||
pub fn file_update_count(&self) -> usize {
|
||||
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 {
|
||||
|
@ -2212,6 +2345,7 @@ impl Clone for BufferSnapshot {
|
|||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
text: self.text.clone(),
|
||||
git_diff: self.git_diff.clone(),
|
||||
syntax: self.syntax.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
|
@ -2219,6 +2353,7 @@ impl Clone for BufferSnapshot {
|
|||
selections_update_count: self.selections_update_count,
|
||||
diagnostics_update_count: self.diagnostics_update_count,
|
||||
file_update_count: self.file_update_count,
|
||||
git_diff_update_count: self.git_diff_update_count,
|
||||
language: self.language.clone(),
|
||||
parse_count: self.parse_count,
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ use std::{
|
|||
};
|
||||
use text::network::Network;
|
||||
use unindent::Unindent as _;
|
||||
use util::post_inc;
|
||||
use util::{post_inc, test::marked_text_ranges};
|
||||
|
||||
#[cfg(test)]
|
||||
#[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]
|
||||
fn test_serialization(cx: &mut gpui::MutableAppContext) {
|
||||
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, _| {
|
||||
let snapshot = buffer.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;
|
||||
pub mod proto;
|
||||
mod syntax_map;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod buffer_tests;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
|
@ -26,6 +27,7 @@ use serde_json::Value;
|
|||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -135,7 +137,7 @@ impl CachedLspAdapter {
|
|||
pub async fn label_for_completion(
|
||||
&self,
|
||||
completion_item: &lsp::CompletionItem,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter
|
||||
.label_for_completion(completion_item, language)
|
||||
|
@ -146,7 +148,7 @@ impl CachedLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter.label_for_symbol(name, kind, language).await
|
||||
}
|
||||
|
@ -175,7 +177,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
async fn label_for_completion(
|
||||
&self,
|
||||
_: &lsp::CompletionItem,
|
||||
_: &Language,
|
||||
_: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
None
|
||||
}
|
||||
|
@ -184,7 +186,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
&self,
|
||||
_: &str,
|
||||
_: lsp::SymbolKind,
|
||||
_: &Language,
|
||||
_: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
None
|
||||
}
|
||||
|
@ -230,7 +232,10 @@ pub struct LanguageConfig {
|
|||
pub decrease_indent_pattern: Option<Regex>,
|
||||
#[serde(default)]
|
||||
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 {
|
||||
|
@ -244,6 +249,7 @@ impl Default for LanguageConfig {
|
|||
decrease_indent_pattern: Default::default(),
|
||||
autoclose_before: Default::default(),
|
||||
line_comment: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -270,7 +276,7 @@ pub struct FakeLspAdapter {
|
|||
pub disk_based_diagnostics_sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct BracketPair {
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
|
@ -304,6 +310,7 @@ pub struct Grammar {
|
|||
struct IndentConfig {
|
||||
query: Query,
|
||||
indent_capture_ix: u32,
|
||||
start_capture_ix: Option<u32>,
|
||||
end_capture_ix: Option<u32>,
|
||||
}
|
||||
|
||||
|
@ -661,11 +668,13 @@ impl Language {
|
|||
let grammar = self.grammar_mut();
|
||||
let query = Query::new(grammar.ts_language, source)?;
|
||||
let mut indent_capture_ix = None;
|
||||
let mut start_capture_ix = None;
|
||||
let mut end_capture_ix = None;
|
||||
get_capture_indices(
|
||||
&query,
|
||||
&mut [
|
||||
("indent", &mut indent_capture_ix),
|
||||
("start", &mut start_capture_ix),
|
||||
("end", &mut end_capture_ix),
|
||||
],
|
||||
);
|
||||
|
@ -673,6 +682,7 @@ impl Language {
|
|||
grammar.indents_config = Some(IndentConfig {
|
||||
query,
|
||||
indent_capture_ix,
|
||||
start_capture_ix,
|
||||
end_capture_ix,
|
||||
});
|
||||
}
|
||||
|
@ -763,8 +773,15 @@ impl Language {
|
|||
self.config.name.clone()
|
||||
}
|
||||
|
||||
pub fn line_comment_prefix(&self) -> Option<&str> {
|
||||
self.config.line_comment.as_deref()
|
||||
pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
|
||||
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] {
|
||||
|
@ -789,7 +806,7 @@ impl Language {
|
|||
}
|
||||
|
||||
pub async fn label_for_completion(
|
||||
&self,
|
||||
self: &Arc<Self>,
|
||||
completion: &lsp::CompletionItem,
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter
|
||||
|
@ -798,7 +815,11 @@ impl Language {
|
|||
.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
|
||||
.as_ref()?
|
||||
.label_for_symbol(name, kind, self)
|
||||
|
@ -806,20 +827,17 @@ impl Language {
|
|||
}
|
||||
|
||||
pub fn highlight_text<'a>(
|
||||
&'a self,
|
||||
self: &'a Arc<Self>,
|
||||
text: &'a Rope,
|
||||
range: Range<usize>,
|
||||
) -> Vec<(Range<usize>, HighlightId)> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(grammar) = &self.grammar {
|
||||
let tree = grammar.parse_text(text, None);
|
||||
let captures = SyntaxSnapshot::single_tree_captures(
|
||||
range.clone(),
|
||||
text,
|
||||
&tree,
|
||||
grammar,
|
||||
|grammar| grammar.highlights_query.as_ref(),
|
||||
);
|
||||
let captures =
|
||||
SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| {
|
||||
grammar.highlights_query.as_ref()
|
||||
});
|
||||
let highlight_maps = vec![grammar.highlight_map()];
|
||||
let mut offset = 0;
|
||||
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 {
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
|
|
|
@ -92,6 +92,13 @@ struct SyntaxLayer {
|
|||
language: Arc<Language>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxLayerInfo<'a> {
|
||||
pub depth: usize,
|
||||
pub node: Node<'a>,
|
||||
pub language: &'a Arc<Language>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SyntaxLayerSummary {
|
||||
min_depth: usize,
|
||||
|
@ -473,13 +480,18 @@ impl SyntaxSnapshot {
|
|||
range: Range<usize>,
|
||||
text: &'a Rope,
|
||||
tree: &'a Tree,
|
||||
grammar: &'a Grammar,
|
||||
language: &'a Arc<Language>,
|
||||
query: fn(&Grammar) -> Option<&Query>,
|
||||
) -> SyntaxMapCaptures<'a> {
|
||||
SyntaxMapCaptures::new(
|
||||
range.clone(),
|
||||
text,
|
||||
[(grammar, 0, tree.root_node())].into_iter(),
|
||||
[SyntaxLayerInfo {
|
||||
language,
|
||||
depth: 0,
|
||||
node: tree.root_node(),
|
||||
}]
|
||||
.into_iter(),
|
||||
query,
|
||||
)
|
||||
}
|
||||
|
@ -513,19 +525,19 @@ impl SyntaxSnapshot {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn layers(&self, buffer: &BufferSnapshot) -> Vec<(&Grammar, usize, Node)> {
|
||||
self.layers_for_range(0..buffer.len(), buffer)
|
||||
pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayerInfo> {
|
||||
self.layers_for_range(0..buffer.len(), buffer).collect()
|
||||
}
|
||||
|
||||
pub fn layers_for_range<'a, T: ToOffset>(
|
||||
&self,
|
||||
&'a self,
|
||||
range: Range<T>,
|
||||
buffer: &BufferSnapshot,
|
||||
) -> Vec<(&Grammar, usize, Node)> {
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
|
||||
let start = buffer.anchor_before(range.start.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 {
|
||||
true
|
||||
} else {
|
||||
|
@ -535,23 +547,26 @@ impl SyntaxSnapshot {
|
|||
}
|
||||
});
|
||||
|
||||
let mut result = Vec::new();
|
||||
// let mut result = Vec::new();
|
||||
cursor.next(buffer);
|
||||
while let Some(layer) = cursor.item() {
|
||||
if let Some(grammar) = &layer.language.grammar {
|
||||
result.push((
|
||||
grammar.as_ref(),
|
||||
layer.depth,
|
||||
layer.tree.root_node_with_offset(
|
||||
std::iter::from_fn(move || {
|
||||
if let Some(layer) = cursor.item() {
|
||||
let info = SyntaxLayerInfo {
|
||||
language: &layer.language,
|
||||
depth: layer.depth,
|
||||
node: layer.tree.root_node_with_offset(
|
||||
layer.range.start.to_offset(buffer),
|
||||
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(
|
||||
range: Range<usize>,
|
||||
text: &'a Rope,
|
||||
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
|
||||
layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
|
||||
query: fn(&Grammar) -> Option<&Query>,
|
||||
) -> Self {
|
||||
let mut result = Self {
|
||||
|
@ -567,11 +582,19 @@ impl<'a> SyntaxMapCaptures<'a> {
|
|||
grammars: Vec::new(),
|
||||
active_layer_count: 0,
|
||||
};
|
||||
for (grammar, depth, node) in layers {
|
||||
let query = if let Some(query) = query(grammar) {
|
||||
query
|
||||
} else {
|
||||
continue;
|
||||
for SyntaxLayerInfo {
|
||||
language,
|
||||
depth,
|
||||
node,
|
||||
} 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();
|
||||
|
@ -678,15 +701,23 @@ impl<'a> SyntaxMapMatches<'a> {
|
|||
fn new(
|
||||
range: Range<usize>,
|
||||
text: &'a Rope,
|
||||
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
|
||||
layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
|
||||
query: fn(&Grammar) -> Option<&Query>,
|
||||
) -> Self {
|
||||
let mut result = Self::default();
|
||||
for (grammar, depth, node) in layers {
|
||||
let query = if let Some(query) = query(grammar) {
|
||||
query
|
||||
} else {
|
||||
continue;
|
||||
for SyntaxLayerInfo {
|
||||
language,
|
||||
depth,
|
||||
node,
|
||||
} 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();
|
||||
|
@ -1624,8 +1655,8 @@ mod tests {
|
|||
let reference_layers = reference_syntax_map.layers(&buffer);
|
||||
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.2.range(), reference_layer.2.range());
|
||||
assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp());
|
||||
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())
|
||||
{
|
||||
assert_eq!(
|
||||
edited_layer.2.to_sexp(),
|
||||
reference_layer.2.to_sexp(),
|
||||
edited_layer.node.to_sexp(),
|
||||
reference_layer.node.to_sexp(),
|
||||
"different layer at step {i}"
|
||||
);
|
||||
assert_eq!(
|
||||
edited_layer.2.range(),
|
||||
reference_layer.2.range(),
|
||||
edited_layer.node.range(),
|
||||
reference_layer.node.range(),
|
||||
"different layer at step {i}"
|
||||
);
|
||||
}
|
||||
|
@ -1822,13 +1853,15 @@ mod tests {
|
|||
range: Range<Point>,
|
||||
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!(
|
||||
layers.len(),
|
||||
expected_layers.len(),
|
||||
"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()
|
||||
{
|
||||
let actual_s_exp = node.to_sexp();
|
||||
|
|
|
@ -10,6 +10,7 @@ doctest = false
|
|||
[features]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"db/test-support",
|
||||
"language/test-support",
|
||||
"settings/test-support",
|
||||
"text/test-support",
|
||||
|
@ -20,8 +21,10 @@ text = { path = "../text" }
|
|||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
|
@ -54,6 +57,7 @@ rocksdb = "0.18"
|
|||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
db = { path = "../db", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use fsevent::EventStream;
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use git::repository::{GitRepository, LibGitRepository};
|
||||
use language::LineEnding;
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io,
|
||||
os::unix::fs::MetadataExt,
|
||||
|
@ -11,13 +14,16 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
use text::Rope;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::lock::Mutex;
|
||||
#[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]
|
||||
pub trait Fs: Send + Sync {
|
||||
|
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
|
|||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> 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;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
@ -270,6 +285,7 @@ enum FakeFsEntry {
|
|||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
|
||||
},
|
||||
Symlink {
|
||||
target: PathBuf,
|
||||
|
@ -384,6 +400,7 @@ impl FakeFs {
|
|||
inode: 0,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
})),
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
|
@ -473,6 +490,28 @@ impl FakeFs {
|
|||
.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> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
|
@ -562,6 +601,7 @@ impl Fs for FakeFs {
|
|||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
}))
|
||||
});
|
||||
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 {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
mod db;
|
||||
pub mod fs;
|
||||
mod ignore;
|
||||
mod lsp_command;
|
||||
|
@ -13,6 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
|
|||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||
|
@ -62,7 +62,7 @@ use std::{
|
|||
time::Instant,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
||||
use util::{defer, post_inc, ResultExt, TryFutureExt as _};
|
||||
|
||||
pub use db::Db;
|
||||
pub use fs::*;
|
||||
|
@ -123,6 +123,7 @@ pub struct Project {
|
|||
opened_buffers: HashMap<u64, OpenBuffer>,
|
||||
incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>,
|
||||
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
|
||||
buffers_being_formatted: HashSet<usize>,
|
||||
nonce: u128,
|
||||
_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_path);
|
||||
client.add_model_request_handler(Self::handle_save_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diff_base);
|
||||
}
|
||||
|
||||
pub fn local(
|
||||
|
@ -466,6 +468,7 @@ impl Project {
|
|||
language_server_statuses: Default::default(),
|
||||
last_workspace_edits_by_language_server: Default::default(),
|
||||
language_server_settings: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
next_language_server_id: 0,
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
}
|
||||
|
@ -562,6 +565,7 @@ impl Project {
|
|||
last_workspace_edits_by_language_server: Default::default(),
|
||||
next_language_server_id: 0,
|
||||
opened_buffers: Default::default(),
|
||||
buffers_being_formatted: Default::default(),
|
||||
buffer_snapshots: Default::default(),
|
||||
nonce: StdRng::from_entropy().gen(),
|
||||
};
|
||||
|
@ -604,7 +608,7 @@ impl Project {
|
|||
|
||||
let languages = Arc::new(LanguageRegistry::test());
|
||||
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 project_store = cx.add_model(|_| ProjectStore::new());
|
||||
let project =
|
||||
|
@ -2804,7 +2808,26 @@ impl Project {
|
|||
.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 settings = cx.global::<Settings>();
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
|
@ -2856,7 +2879,7 @@ impl Project {
|
|||
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>) {
|
||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, _, cx| {
|
||||
this.update_local_worktree_buffers(worktree, cx);
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
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();
|
||||
}
|
||||
|
@ -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>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
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(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
||||
|
@ -5427,7 +5531,7 @@ impl Project {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
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 = this.read_with(&cx, |this, cx| {
|
||||
this.opened_buffers
|
||||
|
@ -5445,6 +5549,7 @@ impl Project {
|
|||
.await
|
||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||
};
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||
Ok(buffer)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
|
||||
use super::{
|
||||
fs::{self, Fs},
|
||||
ignore::IgnoreStack,
|
||||
DiagnosticSummary,
|
||||
};
|
||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client};
|
||||
|
@ -18,6 +17,8 @@ use futures::{
|
|||
Stream, StreamExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::repository::GitRepository;
|
||||
use git::{DOT_GIT, GITIGNORE};
|
||||
use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task,
|
||||
|
@ -26,12 +27,12 @@ use language::{
|
|||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
any::Any,
|
||||
|
@ -40,6 +41,7 @@ use std::{
|
|||
ffi::{OsStr, OsString},
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||
path::{Path, PathBuf},
|
||||
|
@ -50,10 +52,6 @@ use std::{
|
|||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
lazy_static! {
|
||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
|
||||
|
@ -101,15 +99,51 @@ pub struct Snapshot {
|
|||
}
|
||||
|
||||
#[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 {
|
||||
abs_path: Arc<Path>,
|
||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
git_repositories: Vec<GitRepositoryEntry>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
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 {
|
||||
type Target = Snapshot;
|
||||
|
||||
|
@ -142,6 +176,7 @@ struct ShareState {
|
|||
|
||||
pub enum Event {
|
||||
UpdatedEntries,
|
||||
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
|
||||
}
|
||||
|
||||
impl Entity for Worktree {
|
||||
|
@ -372,6 +407,7 @@ impl LocalWorktree {
|
|||
let mut snapshot = LocalSnapshot {
|
||||
abs_path,
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
removed_entry_ids: Default::default(),
|
||||
next_entry_id,
|
||||
snapshot: Snapshot {
|
||||
|
@ -446,10 +482,14 @@ impl LocalWorktree {
|
|||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let path = Arc::from(path);
|
||||
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))
|
||||
.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>) {
|
||||
self.poll_task.take();
|
||||
|
||||
match self.scan_state() {
|
||||
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() {
|
||||
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||
}
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
ScanState::Initializing => {
|
||||
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 {
|
||||
if is_fake_fs {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -521,17 +581,52 @@ impl LocalWorktree {
|
|||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||
}
|
||||
}));
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if force {
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = ()> {
|
||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||
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 path = Arc::from(path);
|
||||
let abs_path = self.absolutize(&path);
|
||||
let fs = self.fs.clone();
|
||||
let snapshot = self.snapshot();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
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
|
||||
let entry = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
|
@ -573,6 +688,7 @@ impl LocalWorktree {
|
|||
.refresh_entry(path, abs_path, None, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
File {
|
||||
entry_id: Some(entry.id),
|
||||
|
@ -582,6 +698,7 @@ impl LocalWorktree {
|
|||
is_local: true,
|
||||
},
|
||||
text,
|
||||
diff_base,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
@ -1248,6 +1365,22 @@ impl LocalSnapshot {
|
|||
&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)]
|
||||
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||
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 {
|
||||
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);
|
||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||
Ok(ignore) => {
|
||||
|
@ -1384,6 +1517,7 @@ impl LocalSnapshot {
|
|||
parent_path: Arc<Path>,
|
||||
entries: impl IntoIterator<Item = Entry>,
|
||||
ignore: Option<Arc<Gitignore>>,
|
||||
fs: &dyn Fs,
|
||||
) {
|
||||
let mut parent_entry = if let Some(parent_entry) =
|
||||
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
||||
|
@ -1409,6 +1543,27 @@ impl LocalSnapshot {
|
|||
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_id_edits = Vec::new();
|
||||
|
||||
|
@ -1493,6 +1648,14 @@ impl LocalSnapshot {
|
|||
{
|
||||
*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
|
||||
}
|
||||
|
||||
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> {
|
||||
|
@ -2244,9 +2423,12 @@ impl BackgroundScanner {
|
|||
new_entries.push(child_entry);
|
||||
}
|
||||
|
||||
self.snapshot
|
||||
.lock()
|
||||
.populate_dir(job.path.clone(), new_entries, new_ignore);
|
||||
self.snapshot.lock().populate_dir(
|
||||
job.path.clone(),
|
||||
new_entries,
|
||||
new_ignore,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
for new_job in new_jobs {
|
||||
job.scan_queue.send(new_job).await.unwrap();
|
||||
}
|
||||
|
@ -2321,6 +2503,12 @@ impl BackgroundScanner {
|
|||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
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);
|
||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||
ancestor_inodes.insert(metadata.inode);
|
||||
|
@ -2367,6 +2555,7 @@ impl BackgroundScanner {
|
|||
self.snapshot.lock().removed_entry_ids.clear();
|
||||
|
||||
self.update_ignore_statuses().await;
|
||||
self.update_git_repositories();
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -2432,6 +2621,13 @@ impl BackgroundScanner {
|
|||
.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) {
|
||||
let mut ignore_stack = job.ignore_stack;
|
||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||
|
@ -2778,6 +2974,7 @@ mod tests {
|
|||
use anyhow::Result;
|
||||
use client::test::FakeHttpClient;
|
||||
use fs::RealFs;
|
||||
use git::repository::FakeGitRepository;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
|
@ -2786,6 +2983,7 @@ mod tests {
|
|||
fmt::Write,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -2804,7 +3002,7 @@ mod tests {
|
|||
.await;
|
||||
|
||||
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(
|
||||
client,
|
||||
|
@ -2866,8 +3064,7 @@ mod tests {
|
|||
fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
|
||||
fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client);
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
Arc::from(Path::new("/root")),
|
||||
|
@ -2945,8 +3142,7 @@ mod tests {
|
|||
}));
|
||||
let dir = parent_dir.path().join("tree");
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
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]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
|
@ -3016,8 +3341,7 @@ mod tests {
|
|||
"ignored-dir": {}
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
|
@ -3064,8 +3388,7 @@ mod tests {
|
|||
|
||||
#[gpui::test(iterations = 30)]
|
||||
async fn test_create_directory(cx: &mut TestAppContext) {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http_client.clone());
|
||||
let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
|
@ -3127,6 +3450,7 @@ mod tests {
|
|||
abs_path: root_dir.path().into(),
|
||||
removed_entry_ids: Default::default(),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
next_entry_id: next_entry_id.clone(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(0),
|
||||
|
|
|
@ -15,108 +15,111 @@ message Envelope {
|
|||
CreateRoomResponse create_room_response = 9;
|
||||
JoinRoom join_room = 10;
|
||||
JoinRoomResponse join_room_response = 11;
|
||||
LeaveRoom leave_room = 1002;
|
||||
Call call = 12;
|
||||
IncomingCall incoming_call = 1000;
|
||||
CallCanceled call_canceled = 1001;
|
||||
CancelCall cancel_call = 1004;
|
||||
DeclineCall decline_call = 13;
|
||||
UpdateParticipantLocation update_participant_location = 1003;
|
||||
RoomUpdated room_updated = 14;
|
||||
LeaveRoom leave_room = 12;
|
||||
Call call = 13;
|
||||
IncomingCall incoming_call = 14;
|
||||
CallCanceled call_canceled = 15;
|
||||
CancelCall cancel_call = 16;
|
||||
DeclineCall decline_call = 17;
|
||||
UpdateParticipantLocation update_participant_location = 18;
|
||||
RoomUpdated room_updated = 19;
|
||||
|
||||
ShareProject share_project = 15;
|
||||
ShareProjectResponse share_project_response = 16;
|
||||
UnshareProject unshare_project = 17;
|
||||
JoinProject join_project = 21;
|
||||
JoinProjectResponse join_project_response = 22;
|
||||
LeaveProject leave_project = 23;
|
||||
AddProjectCollaborator add_project_collaborator = 24;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 25;
|
||||
ShareProject share_project = 20;
|
||||
ShareProjectResponse share_project_response = 21;
|
||||
UnshareProject unshare_project = 22;
|
||||
JoinProject join_project = 23;
|
||||
JoinProjectResponse join_project_response = 24;
|
||||
LeaveProject leave_project = 25;
|
||||
AddProjectCollaborator add_project_collaborator = 26;
|
||||
RemoveProjectCollaborator remove_project_collaborator = 27;
|
||||
|
||||
GetDefinition get_definition = 27;
|
||||
GetDefinitionResponse get_definition_response = 28;
|
||||
GetTypeDefinition get_type_definition = 29;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 30;
|
||||
GetReferences get_references = 31;
|
||||
GetReferencesResponse get_references_response = 32;
|
||||
GetDocumentHighlights get_document_highlights = 33;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 34;
|
||||
GetProjectSymbols get_project_symbols = 35;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 36;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 37;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 38;
|
||||
GetDefinition get_definition = 28;
|
||||
GetDefinitionResponse get_definition_response = 29;
|
||||
GetTypeDefinition get_type_definition = 30;
|
||||
GetTypeDefinitionResponse get_type_definition_response = 31;
|
||||
GetReferences get_references = 32;
|
||||
GetReferencesResponse get_references_response = 33;
|
||||
GetDocumentHighlights get_document_highlights = 34;
|
||||
GetDocumentHighlightsResponse get_document_highlights_response = 35;
|
||||
GetProjectSymbols get_project_symbols = 36;
|
||||
GetProjectSymbolsResponse get_project_symbols_response = 37;
|
||||
OpenBufferForSymbol open_buffer_for_symbol = 38;
|
||||
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
|
||||
|
||||
UpdateProject update_project = 39;
|
||||
RegisterProjectActivity register_project_activity = 40;
|
||||
UpdateWorktree update_worktree = 41;
|
||||
UpdateWorktreeExtensions update_worktree_extensions = 42;
|
||||
UpdateProject update_project = 40;
|
||||
RegisterProjectActivity register_project_activity = 41;
|
||||
UpdateWorktree update_worktree = 42;
|
||||
UpdateWorktreeExtensions update_worktree_extensions = 43;
|
||||
|
||||
CreateProjectEntry create_project_entry = 43;
|
||||
RenameProjectEntry rename_project_entry = 44;
|
||||
CopyProjectEntry copy_project_entry = 45;
|
||||
DeleteProjectEntry delete_project_entry = 46;
|
||||
ProjectEntryResponse project_entry_response = 47;
|
||||
CreateProjectEntry create_project_entry = 44;
|
||||
RenameProjectEntry rename_project_entry = 45;
|
||||
CopyProjectEntry copy_project_entry = 46;
|
||||
DeleteProjectEntry delete_project_entry = 47;
|
||||
ProjectEntryResponse project_entry_response = 48;
|
||||
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 48;
|
||||
StartLanguageServer start_language_server = 49;
|
||||
UpdateLanguageServer update_language_server = 50;
|
||||
UpdateDiagnosticSummary update_diagnostic_summary = 49;
|
||||
StartLanguageServer start_language_server = 50;
|
||||
UpdateLanguageServer update_language_server = 51;
|
||||
|
||||
OpenBufferById open_buffer_by_id = 51;
|
||||
OpenBufferByPath open_buffer_by_path = 52;
|
||||
OpenBufferResponse open_buffer_response = 53;
|
||||
CreateBufferForPeer create_buffer_for_peer = 54;
|
||||
UpdateBuffer update_buffer = 55;
|
||||
UpdateBufferFile update_buffer_file = 56;
|
||||
SaveBuffer save_buffer = 57;
|
||||
BufferSaved buffer_saved = 58;
|
||||
BufferReloaded buffer_reloaded = 59;
|
||||
ReloadBuffers reload_buffers = 60;
|
||||
ReloadBuffersResponse reload_buffers_response = 61;
|
||||
FormatBuffers format_buffers = 62;
|
||||
FormatBuffersResponse format_buffers_response = 63;
|
||||
GetCompletions get_completions = 64;
|
||||
GetCompletionsResponse get_completions_response = 65;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 66;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 67;
|
||||
GetCodeActions get_code_actions = 68;
|
||||
GetCodeActionsResponse get_code_actions_response = 69;
|
||||
GetHover get_hover = 70;
|
||||
GetHoverResponse get_hover_response = 71;
|
||||
ApplyCodeAction apply_code_action = 72;
|
||||
ApplyCodeActionResponse apply_code_action_response = 73;
|
||||
PrepareRename prepare_rename = 74;
|
||||
PrepareRenameResponse prepare_rename_response = 75;
|
||||
PerformRename perform_rename = 76;
|
||||
PerformRenameResponse perform_rename_response = 77;
|
||||
SearchProject search_project = 78;
|
||||
SearchProjectResponse search_project_response = 79;
|
||||
OpenBufferById open_buffer_by_id = 52;
|
||||
OpenBufferByPath open_buffer_by_path = 53;
|
||||
OpenBufferResponse open_buffer_response = 54;
|
||||
CreateBufferForPeer create_buffer_for_peer = 55;
|
||||
UpdateBuffer update_buffer = 56;
|
||||
UpdateBufferFile update_buffer_file = 57;
|
||||
SaveBuffer save_buffer = 58;
|
||||
BufferSaved buffer_saved = 59;
|
||||
BufferReloaded buffer_reloaded = 60;
|
||||
ReloadBuffers reload_buffers = 61;
|
||||
ReloadBuffersResponse reload_buffers_response = 62;
|
||||
FormatBuffers format_buffers = 63;
|
||||
FormatBuffersResponse format_buffers_response = 64;
|
||||
GetCompletions get_completions = 65;
|
||||
GetCompletionsResponse get_completions_response = 66;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
|
||||
GetCodeActions get_code_actions = 69;
|
||||
GetCodeActionsResponse get_code_actions_response = 70;
|
||||
GetHover get_hover = 71;
|
||||
GetHoverResponse get_hover_response = 72;
|
||||
ApplyCodeAction apply_code_action = 73;
|
||||
ApplyCodeActionResponse apply_code_action_response = 74;
|
||||
PrepareRename prepare_rename = 75;
|
||||
PrepareRenameResponse prepare_rename_response = 76;
|
||||
PerformRename perform_rename = 77;
|
||||
PerformRenameResponse perform_rename_response = 78;
|
||||
SearchProject search_project = 79;
|
||||
SearchProjectResponse search_project_response = 80;
|
||||
|
||||
GetChannels get_channels = 80;
|
||||
GetChannelsResponse get_channels_response = 81;
|
||||
JoinChannel join_channel = 82;
|
||||
JoinChannelResponse join_channel_response = 83;
|
||||
LeaveChannel leave_channel = 84;
|
||||
SendChannelMessage send_channel_message = 85;
|
||||
SendChannelMessageResponse send_channel_message_response = 86;
|
||||
ChannelMessageSent channel_message_sent = 87;
|
||||
GetChannelMessages get_channel_messages = 88;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 89;
|
||||
GetChannels get_channels = 81;
|
||||
GetChannelsResponse get_channels_response = 82;
|
||||
JoinChannel join_channel = 83;
|
||||
JoinChannelResponse join_channel_response = 84;
|
||||
LeaveChannel leave_channel = 85;
|
||||
SendChannelMessage send_channel_message = 86;
|
||||
SendChannelMessageResponse send_channel_message_response = 87;
|
||||
ChannelMessageSent channel_message_sent = 88;
|
||||
GetChannelMessages get_channel_messages = 89;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 90;
|
||||
|
||||
UpdateContacts update_contacts = 90;
|
||||
UpdateInviteInfo update_invite_info = 91;
|
||||
ShowContacts show_contacts = 92;
|
||||
UpdateContacts update_contacts = 91;
|
||||
UpdateInviteInfo update_invite_info = 92;
|
||||
ShowContacts show_contacts = 93;
|
||||
|
||||
GetUsers get_users = 93;
|
||||
FuzzySearchUsers fuzzy_search_users = 94;
|
||||
UsersResponse users_response = 95;
|
||||
RequestContact request_contact = 96;
|
||||
RespondToContactRequest respond_to_contact_request = 97;
|
||||
RemoveContact remove_contact = 98;
|
||||
GetUsers get_users = 94;
|
||||
FuzzySearchUsers fuzzy_search_users = 95;
|
||||
UsersResponse users_response = 96;
|
||||
RequestContact request_contact = 97;
|
||||
RespondToContactRequest respond_to_contact_request = 98;
|
||||
RemoveContact remove_contact = 99;
|
||||
|
||||
Follow follow = 99;
|
||||
FollowResponse follow_response = 100;
|
||||
UpdateFollowers update_followers = 101;
|
||||
Unfollow unfollow = 102;
|
||||
Follow follow = 100;
|
||||
FollowResponse follow_response = 101;
|
||||
UpdateFollowers update_followers = 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;
|
||||
}
|
||||
|
||||
message GetPrivateUserInfo {}
|
||||
|
||||
message GetPrivateUserInfoResponse {
|
||||
string metrics_id = 1;
|
||||
bool staff = 2;
|
||||
}
|
||||
|
||||
// Entities
|
||||
|
||||
message UpdateActiveView {
|
||||
|
@ -868,7 +878,8 @@ message BufferState {
|
|||
uint64 id = 1;
|
||||
optional File file = 2;
|
||||
string base_text = 3;
|
||||
LineEnding line_ending = 4;
|
||||
optional string diff_base = 4;
|
||||
LineEnding line_ending = 5;
|
||||
}
|
||||
|
||||
message BufferChunk {
|
||||
|
@ -1032,3 +1043,9 @@ message WorktreeMetadata {
|
|||
string root_name = 2;
|
||||
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),
|
||||
(UpdateWorktree, Foreground),
|
||||
(UpdateWorktreeExtensions, Background),
|
||||
(UpdateDiffBase, Background),
|
||||
(GetPrivateUserInfo, Foreground),
|
||||
(GetPrivateUserInfoResponse, Foreground),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
|
@ -201,6 +204,7 @@ request_messages!(
|
|||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
||||
(GetReferences, GetReferencesResponse),
|
||||
(GetPrivateUserInfo, GetPrivateUserInfoResponse),
|
||||
(GetProjectSymbols, GetProjectSymbolsResponse),
|
||||
(FuzzySearchUsers, UsersResponse),
|
||||
(GetUsers, UsersResponse),
|
||||
|
@ -274,6 +278,7 @@ entity_messages!(
|
|||
UpdateProject,
|
||||
UpdateWorktree,
|
||||
UpdateWorktreeExtensions,
|
||||
UpdateDiffBase
|
||||
);
|
||||
|
||||
entity_messages!(channel_id, ChannelMessageSent);
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
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 editor_defaults: EditorSettings,
|
||||
pub editor_overrides: EditorSettings,
|
||||
pub git: GitSettings,
|
||||
pub git_overrides: GitSettings,
|
||||
pub terminal_defaults: TerminalSettings,
|
||||
pub terminal_overrides: TerminalSettings,
|
||||
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)]
|
||||
pub struct EditorSettings {
|
||||
pub tab_size: Option<NonZeroU32>,
|
||||
|
@ -196,6 +214,8 @@ pub struct SettingsFileContent {
|
|||
#[serde(default)]
|
||||
pub terminal: TerminalSettings,
|
||||
#[serde(default)]
|
||||
pub git: Option<GitSettings>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "language_overrides")]
|
||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||
#[serde(default)]
|
||||
|
@ -252,6 +272,8 @@ impl Settings {
|
|||
enable_language_server: required(defaults.editor.enable_language_server),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
git: defaults.git.unwrap(),
|
||||
git_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
language_defaults: defaults.languages,
|
||||
|
@ -303,6 +325,7 @@ impl Settings {
|
|||
}
|
||||
|
||||
self.editor_overrides = data.editor;
|
||||
self.git_overrides = data.git.unwrap_or_default();
|
||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||
self.terminal_overrides = data.terminal;
|
||||
self.language_overrides = data.languages;
|
||||
|
@ -358,6 +381,14 @@ impl Settings {
|
|||
.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"))]
|
||||
pub fn test(cx: &gpui::AppContext) -> Settings {
|
||||
Settings {
|
||||
|
@ -382,6 +413,8 @@ impl Settings {
|
|||
editor_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
git: Default::default(),
|
||||
git_overrides: Default::default(),
|
||||
language_defaults: Default::default(),
|
||||
language_overrides: Default::default(),
|
||||
lsp: Default::default(),
|
||||
|
|
|
@ -101,6 +101,12 @@ pub enum Bias {
|
|||
Right,
|
||||
}
|
||||
|
||||
impl Default for Bias {
|
||||
fn default() -> Self {
|
||||
Bias::Left
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Bias {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
|
|
|
@ -618,8 +618,34 @@ impl Terminal {
|
|||
term.resize(new_size);
|
||||
}
|
||||
InternalEvent::Clear => {
|
||||
self.write_to_pty("\x0c".to_string());
|
||||
// Clear back buffer
|
||||
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) => {
|
||||
term.scroll_display(*scroll);
|
||||
|
|
|
@ -680,12 +680,12 @@ impl Element for TerminalElement {
|
|||
let focused = self.focused;
|
||||
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
|
||||
move |(cursor_position, block_width)| {
|
||||
let shape = match cursor.shape {
|
||||
AlacCursorShape::Block if !focused => CursorShape::Hollow,
|
||||
AlacCursorShape::Block => CursorShape::Block,
|
||||
AlacCursorShape::Underline => CursorShape::Underscore,
|
||||
AlacCursorShape::Beam => CursorShape::Bar,
|
||||
AlacCursorShape::HollowBlock => CursorShape::Hollow,
|
||||
let (shape, text) = match cursor.shape {
|
||||
AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
|
||||
AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
|
||||
AlacCursorShape::Underline => (CursorShape::Underscore, None),
|
||||
AlacCursorShape::Beam => (CursorShape::Bar, None),
|
||||
AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
|
||||
//This case is handled in the if wrapping the whole cursor layout
|
||||
AlacCursorShape::Hidden => unreachable!(),
|
||||
};
|
||||
|
@ -696,7 +696,7 @@ impl Element for TerminalElement {
|
|||
dimensions.line_height,
|
||||
terminal_theme.colors.cursor,
|
||||
shape,
|
||||
Some(cursor_text),
|
||||
text,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
|||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||
pub struct Anchor {
|
||||
pub timestamp: clock::Local,
|
||||
pub offset: usize,
|
||||
|
|
|
@ -54,6 +54,13 @@ impl Rope {
|
|||
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) {
|
||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||
let mut new_chunk = ArrayString::new();
|
||||
|
|
|
@ -510,8 +510,7 @@ pub struct Editor {
|
|||
pub rename_fade: f32,
|
||||
pub document_highlight_read_background: Color,
|
||||
pub document_highlight_write_background: Color,
|
||||
pub diff_background_deleted: Color,
|
||||
pub diff_background_inserted: Color,
|
||||
pub diff: DiffStyle,
|
||||
pub line_number: Color,
|
||||
pub line_number_active: Color,
|
||||
pub guest_selections: Vec<SelectionStyle>,
|
||||
|
@ -595,6 +594,16 @@ pub struct CodeActions {
|
|||
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)]
|
||||
pub struct Interactive<T> {
|
||||
pub default: T,
|
||||
|
|
|
@ -7,17 +7,21 @@ edition = "2021"
|
|||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["rand", "serde_json", "tempdir"]
|
||||
test-support = ["rand", "serde_json", "tempdir", "git2"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
futures = "0.3"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
lazy_static = "1.4.0"
|
||||
rand = { version = "0.8", optional = true }
|
||||
tempdir = { version = "0.3.7", optional = true }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
||||
git2 = { version = "0.15", default-features = false, optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { version = "0.8" }
|
||||
tempdir = { version = "0.3.7" }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
mod assertions;
|
||||
mod marked_text;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use git2;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub use assertions::*;
|
||||
|
@ -24,6 +28,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
|||
match contents {
|
||||
Value::Object(_) => {
|
||||
fs::create_dir(&path).unwrap();
|
||||
|
||||
if path.file_name() == Some(&OsStr::new(".git")) {
|
||||
git2::Repository::init(&path.parent().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
write_tree(&path, contents);
|
||||
}
|
||||
Value::Null => {
|
||||
|
|
|
@ -46,7 +46,6 @@ use std::{
|
|||
cell::RefCell,
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
|
@ -295,7 +294,23 @@ pub trait Item: View {
|
|||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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 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(
|
||||
&self,
|
||||
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 {
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
|
@ -450,6 +516,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
|||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||
-> 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 to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
fn on_release(
|
||||
|
@ -555,8 +626,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
.insert(self.id(), pane.downgrade())
|
||||
.is_none()
|
||||
{
|
||||
let mut pending_autosave = None;
|
||||
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
||||
let mut pending_autosave = DelayedDebouncedEditAction::new();
|
||||
let mut pending_git_update = DelayedDebouncedEditAction::new();
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemEvent::UpdateTab => {
|
||||
pane.update(cx, |_, cx| {
|
||||
cx.emit(pane::Event::ChangeItemTitle);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
ItemEvent::Edit => {
|
||||
if let Autosave::AfterDelay { milliseconds } =
|
||||
cx.global::<Settings>().autosave
|
||||
{
|
||||
let prev_autosave = pending_autosave
|
||||
.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 delay = Duration::from_millis(milliseconds);
|
||||
let item = item.clone();
|
||||
pending_autosave =
|
||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let mut timer = cx
|
||||
.background()
|
||||
.timer(Duration::from_millis(milliseconds))
|
||||
.fuse();
|
||||
prev_autosave.await;
|
||||
futures::select_biased! {
|
||||
_ = cancel_rx => return None,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
let project = project.upgrade(&cx)?;
|
||||
pending_autosave.fire_new(
|
||||
delay,
|
||||
workspace,
|
||||
cx,
|
||||
|project, mut cx| async move {
|
||||
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||
.await
|
||||
.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))
|
||||
}
|
||||
|
||||
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> {
|
||||
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 languages = Arc::new(LanguageRegistry::test());
|
||||
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 user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
let themes = ThemeRegistry::new((), cx.font_cache().clone());
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.55.0"
|
||||
version = "0.59.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
|
@ -92,6 +92,7 @@ toml = "0.5"
|
|||
tree-sitter = "0.20"
|
||||
tree-sitter-c = "0.20.1"
|
||||
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-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" }
|
||||
|
@ -100,6 +101,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
|
|||
tree-sitter-python = "0.20.2"
|
||||
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
|
||||
tree-sitter-typescript = "0.20.1"
|
||||
tree-sitter-html = "0.19.0"
|
||||
url = "2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -3,6 +3,10 @@ use std::process::Command;
|
|||
fn main() {
|
||||
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")
|
||||
.current_dir("../../styles")
|
||||
.args(["install", "--no-save"])
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{borrow::Cow, str, sync::Arc};
|
|||
mod c;
|
||||
mod elixir;
|
||||
mod go;
|
||||
mod html;
|
||||
mod installation;
|
||||
mod json;
|
||||
mod language_plugin;
|
||||
|
@ -46,6 +47,11 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
|||
tree_sitter_cpp::language(),
|
||||
Some(CachedLspAdapter::new(c::CLspAdapter).await),
|
||||
),
|
||||
(
|
||||
"css",
|
||||
tree_sitter_css::language(),
|
||||
None, //
|
||||
),
|
||||
(
|
||||
"elixir",
|
||||
tree_sitter_elixir::language(),
|
||||
|
@ -96,8 +102,13 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
|||
tree_sitter_typescript::language_tsx(),
|
||||
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,
|
||||
grammar: tree_sitter::Language,
|
||||
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
||||
) -> Language {
|
||||
) -> Arc<Language> {
|
||||
let config = toml::from_slice(
|
||||
&LanguageDir::get(&format!("{}/config.toml", name))
|
||||
.unwrap()
|
||||
|
@ -142,7 +153,7 @@ pub(crate) fn language(
|
|||
if let Some(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>> {
|
||||
|
|
|
@ -112,7 +112,7 @@ impl super::LspAdapter for CLspAdapter {
|
|||
async fn label_for_completion(
|
||||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let label = completion
|
||||
.label
|
||||
|
@ -190,7 +190,7 @@ impl super::LspAdapter for CLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
|
@ -251,7 +251,6 @@ mod tests {
|
|||
use gpui::MutableAppContext;
|
||||
use language::{AutoindentMode, Buffer};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_c_autoindent(cx: &mut MutableAppContext) {
|
||||
|
@ -262,7 +261,7 @@ mod tests {
|
|||
let language = crate::languages::language("c", tree_sitter_c::language(), None);
|
||||
|
||||
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
|
||||
buffer.edit([(0..0, "int main() {}")], None, cx);
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
(identifier) @variable
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function)
|
||||
|
|
|
@ -37,11 +37,11 @@
|
|||
(type_identifier) @type
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
(field_identifier) @property
|
||||
(statement_identifier) @label
|
||||
(this) @variable.builtin
|
||||
(this) @variable.special
|
||||
|
||||
[
|
||||
"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(
|
||||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
match completion.kind.zip(completion.detail.as_ref()) {
|
||||
Some((_, detail)) if detail.starts_with("(function)") => {
|
||||
|
@ -168,7 +168,7 @@ impl LspAdapter for ElixirLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: SymbolKind,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
SymbolKind::METHOD | SymbolKind::FUNCTION => {
|
||||
|
|
|
@ -134,7 +134,7 @@ impl super::LspAdapter for GoLspAdapter {
|
|||
async fn label_for_completion(
|
||||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let label = &completion.label;
|
||||
|
||||
|
@ -235,7 +235,7 @@ impl super::LspAdapter for GoLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
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_pattern)
|
||||
] @constant
|
||||
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
||||
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
|
||||
|
||||
; Literals
|
||||
|
||||
(this) @variable.builtin
|
||||
(super) @variable.builtin
|
||||
(this) @variable.special
|
||||
(super) @variable.special
|
||||
|
||||
[
|
||||
(true)
|
||||
|
|
|
@ -90,7 +90,7 @@ impl LspAdapter for PythonLspAdapter {
|
|||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &language::Language,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let grammar = language.grammar()?;
|
||||
|
@ -112,7 +112,7 @@ impl LspAdapter for PythonLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &language::Language,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
|
@ -149,7 +149,6 @@ mod tests {
|
|||
use gpui::{ModelContext, MutableAppContext};
|
||||
use language::{AutoindentMode, Buffer};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_python_autoindent(cx: &mut MutableAppContext) {
|
||||
|
@ -160,7 +159,7 @@ mod tests {
|
|||
cx.set_global(settings);
|
||||
|
||||
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 ix = buffer.len();
|
||||
buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
(#match? @type "^[A-Z]"))
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z_]*$"))
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
; Builtin functions
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ impl LspAdapter for RustLspAdapter {
|
|||
async fn label_for_completion(
|
||||
&self,
|
||||
completion: &lsp::CompletionItem,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
match completion.kind {
|
||||
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
|
||||
|
@ -196,7 +196,7 @@ impl LspAdapter for RustLspAdapter {
|
|||
&self,
|
||||
name: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Language,
|
||||
language: &Arc<Language>,
|
||||
) -> Option<CodeLabel> {
|
||||
let (text, filter_range, display_range) = match kind {
|
||||
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
|
||||
|
@ -439,7 +439,7 @@ mod tests {
|
|||
cx.set_global(settings);
|
||||
|
||||
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
|
||||
buffer.set_text("fn a() {}", cx);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
(type_identifier) @type
|
||||
(primitive_type) @type.builtin
|
||||
(self) @variable.builtin
|
||||
(self) @variable.special
|
||||
(field_identifier) @property
|
||||
|
||||
(call_expression
|
||||
|
@ -27,22 +27,13 @@
|
|||
|
||||
; Identifier conventions
|
||||
|
||||
; Assume uppercase names are enum constructors
|
||||
((identifier) @variant
|
||||
(#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))
|
||||
; Assume uppercase names are types/enum-constructors
|
||||
((identifier) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
; Assume all-caps names are constants
|
||||
((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(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &language::Language,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
|
@ -144,7 +144,6 @@ impl LspAdapter for TypeScriptLspAdapter {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::MutableAppContext;
|
||||
use unindent::Unindent;
|
||||
|
@ -172,9 +171,8 @@ mod tests {
|
|||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
|
||||
});
|
||||
let buffer =
|
||||
cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
|
||||
let outline = buffer.read(cx).snapshot().outline(None).unwrap();
|
||||
assert_eq!(
|
||||
outline
|
||||
|
|
|
@ -51,12 +51,12 @@
|
|||
(shorthand_property_identifier)
|
||||
(shorthand_property_identifier_pattern)
|
||||
] @constant
|
||||
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
|
||||
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
|
||||
|
||||
; Literals
|
||||
|
||||
(this) @variable.builtin
|
||||
(super) @variable.builtin
|
||||
(this) @variable.special
|
||||
(super) @variable.special
|
||||
|
||||
[
|
||||
(true)
|
||||
|
|
|
@ -20,7 +20,7 @@ use futures::{
|
|||
FutureExt, SinkExt, StreamExt,
|
||||
};
|
||||
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
|
||||
use isahc::{config::Configurable, AsyncBody, Request};
|
||||
use isahc::{config::Configurable, Request};
|
||||
use language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -88,7 +88,7 @@ fn main() {
|
|||
});
|
||||
|
||||
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);
|
||||
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
|
||||
let languages = Arc::new(languages);
|
||||
|
@ -120,7 +120,6 @@ fn main() {
|
|||
vim::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
let db = cx.background().block(db);
|
||||
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
||||
.detach();
|
||||
|
||||
|
@ -139,6 +138,10 @@ fn main() {
|
|||
.detach();
|
||||
|
||||
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 {
|
||||
languages,
|
||||
themes,
|
||||
|
@ -280,12 +283,10 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
|
|||
"token": ZED_SECRET_CLIENT_TOKEN,
|
||||
}))
|
||||
.unwrap();
|
||||
let request = Request::builder()
|
||||
.uri(&panic_report_url)
|
||||
.method(http::Method::POST)
|
||||
let request = Request::post(&panic_report_url)
|
||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(AsyncBody::from(body))?;
|
||||
.body(body.into())?;
|
||||
let response = http.send(request).await.context("error sending panic")?;
|
||||
if response.status().is_success() {
|
||||
fs::remove_file(child_path)
|
||||
|
|
|
@ -328,6 +328,11 @@ pub fn menus() -> Vec<Menu<'static>> {
|
|||
action: Box::new(command_palette::Toggle),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "View Telemetry Log",
|
||||
action: Box::new(crate::OpenTelemetryLog),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Documentation",
|
||||
action: Box::new(crate::OpenBrowser {
|
||||
|
|
|
@ -55,6 +55,7 @@ actions!(
|
|||
DebugElements,
|
||||
OpenSettings,
|
||||
OpenLog,
|
||||
OpenTelemetryLog,
|
||||
OpenKeymap,
|
||||
OpenDefaultSettings,
|
||||
OpenDefaultKeymap,
|
||||
|
@ -145,6 +146,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||
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({
|
||||
let app_state = app_state.clone();
|
||||
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(
|
||||
workspace: &mut Workspace,
|
||||
app_state: Arc<AppState>,
|
||||
|
@ -1051,7 +1114,7 @@ mod tests {
|
|||
assert!(!editor.is_dirty(cx));
|
||||
assert_eq!(editor.title(cx), "untitled");
|
||||
assert!(Arc::ptr_eq(
|
||||
editor.language_at(0, cx).unwrap(),
|
||||
&editor.language_at(0, cx).unwrap(),
|
||||
&languages::PLAIN_TEXT
|
||||
));
|
||||
editor.handle_input("hi", cx);
|
||||
|
@ -1138,7 +1201,7 @@ mod tests {
|
|||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(Arc::ptr_eq(
|
||||
editor.language_at(0, cx).unwrap(),
|
||||
&editor.language_at(0, cx).unwrap(),
|
||||
&languages::PLAIN_TEXT
|
||||
));
|
||||
editor.handle_input("hi", cx);
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
player,
|
||||
popoverShadow,
|
||||
text,
|
||||
textColor,
|
||||
TextColor,
|
||||
} from "./components";
|
||||
import hoverPopover from "./hoverPopover";
|
||||
|
@ -59,8 +60,14 @@ export default function editor(theme: Theme) {
|
|||
indicator: iconColor(theme, "secondary"),
|
||||
verticalScale: 0.618
|
||||
},
|
||||
diffBackgroundDeleted: backgroundColor(theme, "error"),
|
||||
diffBackgroundInserted: backgroundColor(theme, "ok"),
|
||||
diff: {
|
||||
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,
|
||||
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
||||
errorColor: theme.textColor.error,
|
||||
|
|
|
@ -113,6 +113,11 @@ export function createTheme(
|
|||
hovered: sample(ramps.blue, 0.1),
|
||||
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 = {
|
||||
|
@ -180,6 +185,10 @@ export function createTheme(
|
|||
color: sample(ramps.neutral, 7),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
"variable.special": {
|
||||
color: sample(ramps.blue, 0.80),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
comment: {
|
||||
color: sample(ramps.neutral, 5),
|
||||
weight: fontWeights.normal,
|
||||
|
@ -205,15 +214,11 @@ export function createTheme(
|
|||
weight: fontWeights.normal,
|
||||
},
|
||||
constructor: {
|
||||
color: sample(ramps.blue, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
variant: {
|
||||
color: sample(ramps.blue, 0.5),
|
||||
color: sample(ramps.cyan, 0.5),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
property: {
|
||||
color: sample(ramps.blue, 0.5),
|
||||
color: sample(ramps.blue, 0.6),
|
||||
weight: fontWeights.normal,
|
||||
},
|
||||
enum: {
|
||||
|
|
|
@ -43,7 +43,7 @@ export interface Syntax {
|
|||
keyword: SyntaxHighlightStyle;
|
||||
function: SyntaxHighlightStyle;
|
||||
type: SyntaxHighlightStyle;
|
||||
variant: SyntaxHighlightStyle;
|
||||
constructor: SyntaxHighlightStyle;
|
||||
property: SyntaxHighlightStyle;
|
||||
enum: SyntaxHighlightStyle;
|
||||
operator: SyntaxHighlightStyle;
|
||||
|
@ -78,6 +78,7 @@ export default interface Theme {
|
|||
// Hacks for elements on top of the editor
|
||||
on500: BackgroundColorSet;
|
||||
ok: BackgroundColorSet;
|
||||
on500Ok: BackgroundColorSet;
|
||||
error: BackgroundColorSet;
|
||||
on500Error: BackgroundColorSet;
|
||||
warning: BackgroundColorSet;
|
||||
|
|
Loading…
Reference in a new issue