Merge remote-tracking branch 'origin/main' into room

This commit is contained in:
Antonio Scandurra 2022-10-10 15:43:38 +02:00
commit afaacba41f
92 changed files with 10800 additions and 6586 deletions

View file

@ -56,6 +56,7 @@ jobs:
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
steps: steps:
- name: Install Rust - name: Install Rust
run: | run: |

22
.github/workflows/discord_webhook.yml vendored Normal file
View 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
View file

@ -959,6 +959,7 @@ dependencies = [
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
"collections", "collections",
"db",
"futures", "futures",
"gpui", "gpui",
"image", "image",
@ -969,13 +970,16 @@ dependencies = [
"postage", "postage",
"rand 0.8.5", "rand 0.8.5",
"rpc", "rpc",
"serde",
"smol", "smol",
"sum_tree", "sum_tree",
"tempfile",
"thiserror", "thiserror",
"time 0.3.11", "time 0.3.11",
"tiny_http", "tiny_http",
"url", "url",
"util", "util",
"uuid 1.1.2",
] ]
[[package]] [[package]]
@ -1042,6 +1046,7 @@ dependencies = [
"env_logger", "env_logger",
"envy", "envy",
"futures", "futures",
"git",
"gpui", "gpui",
"hyper", "hyper",
"language", "language",
@ -1072,6 +1077,7 @@ dependencies = [
"tracing", "tracing",
"tracing-log", "tracing-log",
"tracing-subscriber", "tracing-subscriber",
"unindent",
"util", "util",
"workspace", "workspace",
] ]
@ -1495,6 +1501,19 @@ dependencies = [
"matches", "matches",
] ]
[[package]]
name = "db"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"gpui",
"parking_lot 0.11.2",
"rocksdb",
"tempdir",
]
[[package]] [[package]]
name = "deflate" name = "deflate"
version = "0.8.6" version = "0.8.6"
@ -1672,6 +1691,7 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"fuzzy", "fuzzy",
"git",
"gpui", "gpui",
"indoc", "indoc",
"itertools", "itertools",
@ -1694,6 +1714,8 @@ dependencies = [
"text", "text",
"theme", "theme",
"tree-sitter", "tree-sitter",
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-rust", "tree-sitter-rust",
"unindent", "unindent",
"util", "util",
@ -2199,6 +2221,39 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "git"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"clock",
"collections",
"futures",
"git2",
"lazy_static",
"log",
"parking_lot 0.11.2",
"smol",
"sum_tree",
"text",
"unindent",
"util",
]
[[package]]
name = "git2"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]] [[package]]
name = "glob" name = "glob"
version = "0.3.0" version = "0.3.0"
@ -2815,6 +2870,7 @@ dependencies = [
"env_logger", "env_logger",
"futures", "futures",
"fuzzy", "fuzzy",
"git",
"gpui", "gpui",
"lazy_static", "lazy_static",
"log", "log",
@ -2834,6 +2890,8 @@ dependencies = [
"text", "text",
"theme", "theme",
"tree-sitter", "tree-sitter",
"tree-sitter-html",
"tree-sitter-javascript",
"tree-sitter-json 0.19.0", "tree-sitter-json 0.19.0",
"tree-sitter-python", "tree-sitter-python",
"tree-sitter-rust", "tree-sitter-rust",
@ -2869,6 +2927,18 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "libgit2-sys"
version = "0.14.0+1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.3" version = "0.7.3"
@ -3941,9 +4011,11 @@ dependencies = [
"client", "client",
"clock", "clock",
"collections", "collections",
"db",
"fsevent", "fsevent",
"futures", "futures",
"fuzzy", "fuzzy",
"git",
"gpui", "gpui",
"ignore", "ignore",
"language", "language",
@ -5999,6 +6071,15 @@ dependencies = [
"tree-sitter", "tree-sitter",
] ]
[[package]]
name = "tree-sitter-css"
version = "0.19.0"
source = "git+https://github.com/tree-sitter/tree-sitter-css?rev=769203d0f9abe1a9a691ac2b9fe4bb4397a73c51#769203d0f9abe1a9a691ac2b9fe4bb4397a73c51"
dependencies = [
"cc",
"tree-sitter",
]
[[package]] [[package]]
name = "tree-sitter-elixir" name = "tree-sitter-elixir"
version = "0.19.0" version = "0.19.0"
@ -6017,6 +6098,26 @@ dependencies = [
"tree-sitter", "tree-sitter",
] ]
[[package]]
name = "tree-sitter-html"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184e6b77953a354303dc87bf5fe36558c83569ce92606e7b382a0dc1b7443443"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-javascript"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2490fab08630b2c8943c320f7b63473cbf65511c8d83aec551beb9b4375906ed"
dependencies = [
"cc",
"tree-sitter",
]
[[package]] [[package]]
name = "tree-sitter-json" name = "tree-sitter-json"
version = "0.19.0" version = "0.19.0"
@ -6306,6 +6407,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures", "futures",
"git2",
"lazy_static",
"log", "log",
"rand 0.8.5", "rand 0.8.5",
"serde_json", "serde_json",
@ -6326,6 +6429,9 @@ name = "uuid"
version = "1.1.2" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
dependencies = [
"getrandom 0.2.7",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
@ -7122,7 +7228,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.55.0" version = "0.59.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"anyhow", "anyhow",
@ -7198,8 +7304,10 @@ dependencies = [
"tree-sitter", "tree-sitter",
"tree-sitter-c", "tree-sitter-c",
"tree-sitter-cpp", "tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-elixir", "tree-sitter-elixir",
"tree-sitter-go", "tree-sitter-go",
"tree-sitter-html",
"tree-sitter-json 0.20.0", "tree-sitter-json 0.20.0",
"tree-sitter-markdown", "tree-sitter-markdown",
"tree-sitter-python", "tree-sitter-python",

View file

@ -74,6 +74,15 @@
"hard_tabs": false, "hard_tabs": false,
// How many columns a tab should occupy. // How many columns a tab should occupy.
"tab_size": 4, "tab_size": 4,
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
},
// Settings specific to the terminal // Settings specific to the terminal
"terminal": { "terminal": {
// What shell to use when opening a terminal. May take 3 values: // What shell to use when opening a terminal. May take 3 values:

View file

@ -12,6 +12,7 @@ test-support = ["collections/test-support", "gpui/test-support", "rpc/test-suppo
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
db = { path = "../db" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
util = { path = "../util" } util = { path = "../util" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
@ -31,7 +32,10 @@ smol = "1.2.5"
thiserror = "1.0.29" thiserror = "1.0.29"
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }
tiny_http = "0.8" tiny_http = "0.8"
uuid = { version = "1.1.2", features = ["v4"] }
url = "2.2" url = "2.2"
serde = { version = "*", features = ["derive"] }
tempfile = "3"
[dev-dependencies] [dev-dependencies]
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }

View file

@ -601,7 +601,7 @@ mod tests {
let user_id = 5; let user_id = 5;
let http_client = FakeHttpClient::with_404_response(); let http_client = FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone()); let client = cx.update(|cx| Client::new(http_client.clone(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await; let server = FakeServer::for_client(user_id, &client, cx).await;
Channel::init(&client); Channel::init(&client);

View file

@ -3,6 +3,7 @@ pub mod test;
pub mod channel; pub mod channel;
pub mod http; pub mod http;
pub mod telemetry;
pub mod user; pub mod user;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
@ -11,10 +12,12 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError, error::Error as WebsocketError,
http::{Request, StatusCode}, http::{Request, StatusCode},
}; };
use db::Db;
use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt}; use futures::{future::LocalBoxFuture, FutureExt, SinkExt, StreamExt, TryStreamExt};
use gpui::{ use gpui::{
actions, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, actions, serde_json::Value, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle,
Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, View, ViewContext, ViewHandle,
}; };
use http::HttpClient; use http::HttpClient;
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -28,9 +31,11 @@ use std::{
convert::TryFrom, convert::TryFrom,
fmt::Write as _, fmt::Write as _,
future::Future, future::Future,
path::PathBuf,
sync::{Arc, Weak}, sync::{Arc, Weak},
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use telemetry::Telemetry;
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
@ -51,11 +56,16 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
actions!(client, [Authenticate]); actions!(client, [Authenticate]);
pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) { pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
cx.add_global_action(move |_: &Authenticate, cx| { cx.add_global_action({
let rpc = rpc.clone(); let client = client.clone();
cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await }) move |_: &Authenticate, cx| {
let client = client.clone();
cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
)
.detach(); .detach();
}
}); });
} }
@ -63,6 +73,7 @@ pub struct Client {
id: usize, id: usize,
peer: Arc<Peer>, peer: Arc<Peer>,
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
telemetry: Arc<Telemetry>,
state: RwLock<ClientState>, state: RwLock<ClientState>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
@ -232,10 +243,11 @@ impl Drop for Subscription {
} }
impl Client { impl Client {
pub fn new(http: Arc<dyn HttpClient>) -> Arc<Self> { pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
id: 0, id: 0,
peer: Peer::new(), peer: Peer::new(),
telemetry: Telemetry::new(http.clone(), cx),
http, http,
state: Default::default(), state: Default::default(),
@ -339,6 +351,7 @@ impl Client {
})); }));
} }
Status::SignedOut | Status::UpgradeRequired => { Status::SignedOut | Status::UpgradeRequired => {
self.telemetry.set_authenticated_user_info(None, false);
state._reconnect_task.take(); state._reconnect_task.take();
} }
_ => {} _ => {}
@ -618,6 +631,9 @@ impl Client {
if credentials.is_none() && try_keychain { if credentials.is_none() && try_keychain {
credentials = read_credentials_from_keychain(cx); credentials = read_credentials_from_keychain(cx);
read_from_keychain = credentials.is_some(); read_from_keychain = credentials.is_some();
if read_from_keychain {
self.report_event("read credentials from keychain", Default::default());
}
} }
if credentials.is_none() { if credentials.is_none() {
let mut status_rx = self.status(); let mut status_rx = self.status();
@ -901,6 +917,7 @@ impl Client {
) -> Task<Result<Credentials>> { ) -> Task<Result<Credentials>> {
let platform = cx.platform(); let platform = cx.platform();
let executor = cx.background(); let executor = cx.background();
let telemetry = self.telemetry.clone();
executor.clone().spawn(async move { executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the // Generate a pair of asymmetric encryption keys. The public key will be used by the
// zed server to encrypt the user's access token, so that it can'be intercepted by // zed server to encrypt the user's access token, so that it can'be intercepted by
@ -979,6 +996,8 @@ impl Client {
.context("failed to decrypt access token")?; .context("failed to decrypt access token")?;
platform.activate(true); platform.activate(true);
telemetry.report_event("authenticate with browser", Default::default());
Ok(Credentials { Ok(Credentials {
user_id: user_id.parse()?, user_id: user_id.parse()?,
access_token, access_token,
@ -1043,6 +1062,18 @@ impl Client {
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME); log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
self.peer.respond_with_error(receipt, error) self.peer.respond_with_error(receipt, error)
} }
pub fn start_telemetry(&self, db: Arc<Db>) {
self.telemetry.start(db);
}
pub fn report_event(&self, kind: &str, properties: Value) {
self.telemetry.report_event(kind, properties)
}
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
self.telemetry.log_file_path()
}
} }
impl AnyWeakEntityHandle { impl AnyWeakEntityHandle {
@ -1108,7 +1139,7 @@ mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let user_id = 5; let user_id = 5;
let client = Client::new(FakeHttpClient::with_404_response()); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await; let server = FakeServer::for_client(user_id, &client, cx).await;
let mut status = client.status(); let mut status = client.status();
assert!(matches!( assert!(matches!(
@ -1147,7 +1178,7 @@ mod tests {
let auth_count = Arc::new(Mutex::new(0)); let auth_count = Arc::new(Mutex::new(0));
let dropped_auth_count = Arc::new(Mutex::new(0)); let dropped_auth_count = Arc::new(Mutex::new(0));
let client = Client::new(FakeHttpClient::with_404_response()); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
client.override_authenticate({ client.override_authenticate({
let auth_count = auth_count.clone(); let auth_count = auth_count.clone();
let dropped_auth_count = dropped_auth_count.clone(); let dropped_auth_count = dropped_auth_count.clone();
@ -1196,7 +1227,7 @@ mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let user_id = 5; let user_id = 5;
let client = Client::new(FakeHttpClient::with_404_response()); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await; let server = FakeServer::for_client(user_id, &client, cx).await;
let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx1, mut done_rx1) = smol::channel::unbounded();
@ -1242,7 +1273,7 @@ mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let user_id = 5; let user_id = 5;
let client = Client::new(FakeHttpClient::with_404_response()); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await; let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.add_model(|_| Model::default()); let model = cx.add_model(|_| Model::default());
@ -1270,7 +1301,7 @@ mod tests {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let user_id = 5; let user_id = 5;
let client = Client::new(FakeHttpClient::with_404_response()); let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let server = FakeServer::for_client(user_id, &client, cx).await; let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.add_model(|_| Model::default()); let model = cx.add_model(|_| Model::default());

View 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();
}
}
}

View file

@ -6,7 +6,10 @@ use anyhow::{anyhow, Result};
use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt}; use futures::{future::BoxFuture, stream::BoxStream, Future, StreamExt};
use gpui::{executor, ModelHandle, TestAppContext}; use gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex; use parking_lot::Mutex;
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope}; use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{fmt, rc::Rc, sync::Arc}; use std::{fmt, rc::Rc, sync::Arc};
pub struct FakeServer { pub struct FakeServer {
@ -93,6 +96,7 @@ impl FakeServer {
.authenticate_and_connect(false, &cx.to_async()) .authenticate_and_connect(false, &cx.to_async())
.await .await
.unwrap(); .unwrap();
server server
} }
@ -126,6 +130,8 @@ impl FakeServer {
#[allow(clippy::await_holding_lock)] #[allow(clippy::await_holding_lock)]
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> { pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
self.executor.start_waiting(); self.executor.start_waiting();
loop {
let message = self let message = self
.state .state
.lock() .lock()
@ -137,15 +143,32 @@ impl FakeServer {
.ok_or_else(|| anyhow!("other half hung up"))?; .ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting(); self.executor.finish_waiting();
let type_name = message.payload_type_name(); let type_name = message.payload_type_name();
Ok(*message let message = message.into_any();
.into_any()
.downcast::<TypedEnvelope<M>>() if message.is::<TypedEnvelope<M>>() {
.unwrap_or_else(|_| { 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!( panic!(
"fake server received unexpected message type: {:?}", "fake server received unexpected message type: {:?}",
type_name type_name
); );
})) }
} }
pub async fn respond<T: proto::RequestMessage>( pub async fn respond<T: proto::RequestMessage>(

View file

@ -135,10 +135,21 @@ impl UserStore {
match status { match status {
Status::Connected { .. } => { Status::Connected { .. } => {
if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) { if let Some((this, user_id)) = this.upgrade(&cx).zip(client.user_id()) {
let user = this let fetch_user = this
.update(&mut cx, |this, cx| this.get_user(user_id, cx)) .update(&mut cx, |this, cx| this.get_user(user_id, cx))
.log_err() .log_err();
.await; let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
if let Some(info) = info {
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id),
info.staff,
);
} else {
client.telemetry.set_authenticated_user_info(None, false);
}
client.telemetry.report_event("sign in", Default::default());
current_user_tx.send(user).await.ok(); current_user_tx.send(user).await.ok();
} }
} }

View file

@ -1,5 +1,5 @@
[package] [package]
authors = ["Nathan Sobo <nathan@warp.dev>"] authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
@ -26,6 +26,7 @@ base64 = "0.13"
clap = { version = "3.1", features = ["derive"], optional = true } clap = { version = "3.1", features = ["derive"], optional = true }
envy = "0.4.2" envy = "0.4.2"
futures = "0.3" futures = "0.3"
git = { path = "../git" }
hyper = "0.14" hyper = "0.14"
lazy_static = "1.4" lazy_static = "1.4"
lipsum = { version = "0.8", optional = true } lipsum = { version = "0.8", optional = true }
@ -66,11 +67,13 @@ rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
ctor = "0.1" ctor = "0.1"
env_logger = "0.9" env_logger = "0.9"
util = { path = "../util" } util = { path = "../util" }
lazy_static = "1.4" lazy_static = "1.4"
serde_json = { version = "1.0", features = ["preserve_order"] } serde_json = { version = "1.0", features = ["preserve_order"] }
unindent = "0.1"
[features] [features]
seed-support = ["clap", "lipsum", "reqwest"] seed-support = ["clap", "lipsum", "reqwest"]

View 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");

View file

@ -0,0 +1,2 @@
ALTER TABLE "users"
ADD "metrics_id" uuid NOT NULL DEFAULT gen_random_uuid();

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
auth, auth,
db::{ProjectId, User, UserId}, db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
rpc::{self, ResultExt}, rpc::{self, ResultExt},
AppState, Error, Result, AppState, Error, Result,
}; };
@ -24,13 +24,10 @@ use tracing::instrument;
pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> { pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new() Router::new()
.route("/user", get(get_authenticated_user))
.route("/users", get(get_users).post(create_user)) .route("/users", get(get_users).post(create_user))
.route( .route("/users/:id", put(update_user).delete(destroy_user))
"/users/:id",
put(update_user).delete(destroy_user).get(get_user),
)
.route("/users/:id/access_tokens", post(create_access_token)) .route("/users/:id/access_tokens", post(create_access_token))
.route("/bulk_users", post(create_users))
.route("/users_with_no_invites", get(get_users_with_no_invites)) .route("/users_with_no_invites", get(get_users_with_no_invites))
.route("/invite_codes/:code", get(get_user_for_invite_code)) .route("/invite_codes/:code", get(get_user_for_invite_code))
.route("/panic", post(trace_panic)) .route("/panic", post(trace_panic))
@ -45,6 +42,11 @@ pub fn routes(rpc_server: &Arc<rpc::Server>, state: Arc<AppState>) -> Router<Bod
) )
.route("/user_activity/counts", get(get_active_user_counts)) .route("/user_activity/counts", get(get_active_user_counts))
.route("/project_metadata", get(get_project_metadata)) .route("/project_metadata", get(get_project_metadata))
.route("/signups", post(create_signup))
.route("/signups_summary", get(get_waitlist_summary))
.route("/user_invites", post(create_invite_from_code))
.route("/unsent_invites", get(get_unsent_invites))
.route("/sent_invites", post(record_sent_invites))
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()
.layer(Extension(state)) .layer(Extension(state))
@ -84,6 +86,31 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
Ok::<_, Error>(next.run(req).await) Ok::<_, Error>(next.run(req).await)
} }
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_login: String,
}
#[derive(Debug, Serialize)]
struct AuthenticatedUserResponse {
user: User,
metrics_id: String,
}
async fn get_authenticated_user(
Query(params): Query<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<AuthenticatedUserResponse>> {
let user = app
.db
.get_user_by_github_account(&params.github_login, Some(params.github_user_id))
.await?
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "user not found".into()))?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
return Ok(Json(AuthenticatedUserResponse { user, metrics_id }));
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct GetUsersQueryParams { struct GetUsersQueryParams {
query: Option<String>, query: Option<String>,
@ -108,48 +135,76 @@ async fn get_users(
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct CreateUserParams { struct CreateUserParams {
github_user_id: i32,
github_login: String, github_login: String,
invite_code: Option<String>, email_address: String,
email_address: Option<String>, email_confirmation_code: Option<String>,
#[serde(default)]
admin: bool, admin: bool,
#[serde(default)]
invite_count: i32,
}
#[derive(Serialize, Debug)]
struct CreateUserResponse {
user: User,
signup_device_id: Option<String>,
metrics_id: String,
} }
async fn create_user( async fn create_user(
Json(params): Json<CreateUserParams>, Json(params): Json<CreateUserParams>,
Extension(app): Extension<Arc<AppState>>, Extension(app): Extension<Arc<AppState>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>, Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<Json<User>> { ) -> Result<Json<CreateUserResponse>> {
let user_id = if let Some(invite_code) = params.invite_code { let user = NewUserParams {
let invitee_id = app github_login: params.github_login,
.db github_user_id: params.github_user_id,
.redeem_invite_code( invite_count: params.invite_count,
&invite_code, };
&params.github_login,
params.email_address.as_deref(), // Creating a user via the normal signup process
) let result = if let Some(email_confirmation_code) = params.email_confirmation_code {
.await?;
rpc_server
.invite_code_redeemed(&invite_code, invitee_id)
.await
.trace_err();
invitee_id
} else {
app.db app.db
.create_user( .create_user_from_invite(
&params.github_login, &Invite {
params.email_address.as_deref(), email_address: params.email_address,
params.admin, email_confirmation_code,
},
user,
) )
.await? .await?
}
// Creating a user as an admin
else if params.admin {
app.db
.create_user(&params.email_address, false, user)
.await?
} else {
Err(Error::Http(
StatusCode::UNPROCESSABLE_ENTITY,
"email confirmation code is required".into(),
))?
}; };
if let Some(inviter_id) = result.inviting_user_id {
rpc_server
.invite_code_redeemed(inviter_id, result.user_id)
.await
.trace_err();
}
let user = app let user = app
.db .db
.get_user_by_id(user_id) .get_user_by_id(result.user_id)
.await? .await?
.ok_or_else(|| anyhow!("couldn't find the user we just created"))?; .ok_or_else(|| anyhow!("couldn't find the user we just created"))?;
Ok(Json(user)) Ok(Json(CreateUserResponse {
user,
metrics_id: result.metrics_id,
signup_device_id: result.signup_device_id,
}))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -171,7 +226,9 @@ async fn update_user(
} }
if let Some(invite_count) = params.invite_count { if let Some(invite_count) = params.invite_count {
app.db.set_invite_count(user_id, invite_count).await?; app.db
.set_invite_count_for_user(user_id, invite_count)
.await?;
rpc_server.invite_count_updated(user_id).await.trace_err(); rpc_server.invite_count_updated(user_id).await.trace_err();
} }
@ -186,54 +243,6 @@ async fn destroy_user(
Ok(()) Ok(())
} }
async fn get_user(
Path(login): Path<String>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<User>> {
let user = app
.db
.get_user_by_github_login(&login)
.await?
.ok_or_else(|| Error::Http(StatusCode::NOT_FOUND, "User not found".to_string()))?;
Ok(Json(user))
}
#[derive(Deserialize)]
struct CreateUsersParams {
users: Vec<CreateUsersEntry>,
}
#[derive(Deserialize)]
struct CreateUsersEntry {
github_login: String,
email_address: String,
invite_count: usize,
}
async fn create_users(
Json(params): Json<CreateUsersParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Vec<User>>> {
let user_ids = app
.db
.create_users(
params
.users
.into_iter()
.map(|params| {
(
params.github_login,
params.email_address,
params.invite_count,
)
})
.collect(),
)
.await?;
let users = app.db.get_users_by_ids(user_ids).await?;
Ok(Json(users))
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct GetUsersWithNoInvites { struct GetUsersWithNoInvites {
invited_by_another_user: bool, invited_by_another_user: bool,
@ -368,22 +377,24 @@ struct CreateAccessTokenResponse {
} }
async fn create_access_token( async fn create_access_token(
Path(login): Path<String>, Path(user_id): Path<UserId>,
Query(params): Query<CreateAccessTokenQueryParams>, Query(params): Query<CreateAccessTokenQueryParams>,
Extension(app): Extension<Arc<AppState>>, Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<CreateAccessTokenResponse>> { ) -> Result<Json<CreateAccessTokenResponse>> {
// request.require_token().await?;
let user = app let user = app
.db .db
.get_user_by_github_login(&login) .get_user_by_id(user_id)
.await? .await?
.ok_or_else(|| anyhow!("user not found"))?; .ok_or_else(|| anyhow!("user not found"))?;
let mut user_id = user.id; let mut user_id = user.id;
if let Some(impersonate) = params.impersonate { if let Some(impersonate) = params.impersonate {
if user.admin { if user.admin {
if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { if let Some(impersonated_user) = app
.db
.get_user_by_github_account(&impersonate, None)
.await?
{
user_id = impersonated_user.id; user_id = impersonated_user.id;
} else { } else {
return Err(Error::Http( return Err(Error::Http(
@ -415,3 +426,59 @@ async fn get_user_for_invite_code(
) -> Result<Json<User>> { ) -> Result<Json<User>> {
Ok(Json(app.db.get_user_for_invite_code(&code).await?)) Ok(Json(app.db.get_user_for_invite_code(&code).await?))
} }
async fn create_signup(
Json(params): Json<Signup>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
app.db.create_signup(params).await?;
Ok(())
}
async fn get_waitlist_summary(
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<WaitlistSummary>> {
Ok(Json(app.db.get_waitlist_summary().await?))
}
#[derive(Deserialize)]
pub struct CreateInviteFromCodeParams {
invite_code: String,
email_address: String,
device_id: Option<String>,
}
async fn create_invite_from_code(
Json(params): Json<CreateInviteFromCodeParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Invite>> {
Ok(Json(
app.db
.create_invite_from_code(
&params.invite_code,
&params.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(&params).await?;
Ok(())
}

View file

@ -11,7 +11,7 @@ mod db;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct GitHubUser { struct GitHubUser {
id: usize, id: i32,
login: String, login: String,
email: Option<String>, email: Option<String>,
} }
@ -26,8 +26,11 @@ async fn main() {
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var"); let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let current_user = let mut current_user =
fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await; fetch_github::<GitHubUser>(&client, &github_token, "https://api.github.com/user").await;
current_user
.email
.get_or_insert_with(|| "placeholder@example.com".to_string());
let staff_users = fetch_github::<Vec<GitHubUser>>( let staff_users = fetch_github::<Vec<GitHubUser>>(
&client, &client,
&github_token, &github_token,
@ -64,14 +67,22 @@ async fn main() {
let mut zed_user_ids = Vec::<UserId>::new(); let mut zed_user_ids = Vec::<UserId>::new();
for (github_user, admin) in zed_users { for (github_user, admin) in zed_users {
if let Some(user) = db if let Some(user) = db
.get_user_by_github_login(&github_user.login) .get_user_by_github_account(&github_user.login, Some(github_user.id))
.await .await
.expect("failed to fetch user") .expect("failed to fetch user")
{ {
zed_user_ids.push(user.id); zed_user_ids.push(user.id);
} else { } else if let Some(email) = &github_user.email {
zed_user_ids.push( zed_user_ids.push(
db.create_user(&github_user.login, github_user.email.as_deref(), admin) db.create_user(
email,
admin,
db::NewUserParams {
github_login: github_user.login,
github_user_id: github_user.id,
invite_count: 5,
},
)
.await .await
.expect("failed to insert user"), .expect("failed to insert user"),
); );

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
db::{tests::TestDb, ProjectId, UserId}, db::{NewUserParams, ProjectId, TestDb, UserId},
rpc::{Executor, Server, Store}, rpc::{Executor, Server, Store},
AppState, AppState,
}; };
@ -52,6 +52,7 @@ use std::{
time::Duration, time::Duration,
}; };
use theme::ThemeRegistry; use theme::ThemeRegistry;
use unindent::Unindent as _;
use workspace::{Item, SplitDirection, ToggleFollow, Workspace}; use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
#[ctor::ctor] #[ctor::ctor]
@ -329,6 +330,7 @@ async fn test_room_uniqueness(
}) })
.await .await
.unwrap(); .unwrap();
deterministic.run_until_parked();
let call_b2 = incoming_call_b.next().await.unwrap().unwrap(); let call_b2 = incoming_call_b.next().await.unwrap().unwrap();
assert_eq!(call_b2.caller.github_login, "user_c"); assert_eq!(call_b2.caller.github_login, "user_c");
} }
@ -1174,6 +1176,258 @@ async fn test_propagate_saves_and_fs_changes(
.await; .await;
} }
#[gpui::test(iterations = 10)]
async fn test_git_diff_base_change(
executor: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
executor.forbid_parking();
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/dir",
json!({
".git": {},
"sub": {
".git": {},
"b.txt": "
one
two
three
".unindent(),
},
"a.txt": "
one
two
three
".unindent(),
}),
)
.await;
let (project_local, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
})
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
let diff_base = "
one
three
"
.unindent();
let new_diff_base = "
one
two
"
.unindent();
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), diff_base.clone())],
)
.await;
// Create the buffer
let buffer_local_a = project_local
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
// Wait for it to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
// Create remote buffer
let buffer_remote_a = project_remote
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
.unwrap();
// Wait remote buffer to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/.git"),
&[(Path::new("a.txt"), new_diff_base.clone())],
)
.await;
// Wait for buffer_local_a to receive it
executor.run_until_parked();
// Smoke test new diffing
buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
);
});
// Smoke test B
buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
);
});
//Nested git dir
let diff_base = "
one
three
"
.unindent();
let new_diff_base = "
one
two
"
.unindent();
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), diff_base.clone())],
)
.await;
// Create the buffer
let buffer_local_b = project_local
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
.await
.unwrap();
// Wait for it to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
// Create remote buffer
let buffer_remote_b = project_remote
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "sub/b.txt"), cx))
.await
.unwrap();
// Wait remote buffer to catch up to the new diff
executor.run_until_parked();
// Smoke test diffing
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(1..2, "", "two\n")],
);
});
client_a
.fs
.as_fake()
.set_index_for_repo(
Path::new("/dir/sub/.git"),
&[(Path::new("b.txt"), new_diff_base.clone())],
)
.await;
// Wait for buffer_local_b to receive it
executor.run_until_parked();
// Smoke test new diffing
buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
println!("{:?}", buffer.as_rope().to_string());
println!("{:?}", buffer.diff_base());
println!(
"{:?}",
buffer
.snapshot()
.git_diff_hunks_in_range(0..4)
.collect::<Vec<_>>()
);
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
);
});
// Smoke test B
buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_range(0..4),
&buffer,
&diff_base,
&[(2..3, "", "three\n")],
);
});
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_fs_operations( async fn test_fs_operations(
executor: Arc<Deterministic>, executor: Arc<Deterministic>,
@ -5092,7 +5346,19 @@ async fn test_random_collaboration(
let mut server = TestServer::start(cx.foreground(), cx.background()).await; let mut server = TestServer::start(cx.foreground(), cx.background()).await;
let db = server.app_state.db.clone(); let db = server.app_state.db.clone();
let room_creator_user_id = db.create_user("room-creator", None, false).await.unwrap(); let room_creator_user_id = db
.create_user(
"room-creator@example.com",
false,
NewUserParams {
github_login: "room-creator".into(),
github_user_id: 0,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
let mut available_guests = vec![ let mut available_guests = vec![
"guest-1".to_string(), "guest-1".to_string(),
"guest-2".to_string(), "guest-2".to_string(),
@ -5100,11 +5366,24 @@ async fn test_random_collaboration(
"guest-4".to_string(), "guest-4".to_string(),
]; ];
for username in Some(&"host".to_string()) for (ix, username) in Some(&"host".to_string())
.into_iter() .into_iter()
.chain(&available_guests) .chain(&available_guests)
.enumerate()
{ {
let user_id = db.create_user(username, None, false).await.unwrap(); let user_id = db
.create_user(
&format!("{username}@example.com"),
false,
NewUserParams {
github_login: username.into(),
github_user_id: (ix + 1) as i32,
invite_count: 0,
},
)
.await
.unwrap()
.user_id;
server server
.app_state .app_state
.db .db
@ -5632,18 +5911,31 @@ impl TestServer {
}); });
let http = FakeHttpClient::with_404_response(); let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await let user_id = if let Ok(Some(user)) = self
.app_state
.db
.get_user_by_github_account(name, None)
.await
{ {
user.id user.id
} else { } else {
self.app_state self.app_state
.db .db
.create_user(name, None, false) .create_user(
&format!("{name}@example.com"),
false,
NewUserParams {
github_login: name.into(),
github_user_id: 0,
invite_count: 0,
},
)
.await .await
.unwrap() .unwrap()
.user_id
}; };
let client_name = name.to_string(); let client_name = name.to_string();
let mut client = Client::new(http.clone()); let mut client = cx.read(|cx| Client::new(http.clone(), cx));
let server = self.server.clone(); let server = self.server.clone();
let db = self.app_state.db.clone(); let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone(); let connection_killers = self.connection_killers.clone();

View file

@ -4,6 +4,8 @@ mod db;
mod env; mod env;
mod rpc; mod rpc;
#[cfg(test)]
mod db_tests;
#[cfg(test)] #[cfg(test)]
mod integration_tests; mod integration_tests;

View file

@ -206,7 +206,9 @@ impl Server {
.add_request_handler(Server::follow) .add_request_handler(Server::follow)
.add_message_handler(Server::unfollow) .add_message_handler(Server::unfollow)
.add_message_handler(Server::update_followers) .add_message_handler(Server::update_followers)
.add_request_handler(Server::get_channel_messages); .add_request_handler(Server::get_channel_messages)
.add_message_handler(Server::update_diff_base)
.add_request_handler(Server::get_private_user_info);
Arc::new(server) Arc::new(server)
} }
@ -528,13 +530,14 @@ impl Server {
pub async fn invite_code_redeemed( pub async fn invite_code_redeemed(
self: &Arc<Self>, self: &Arc<Self>,
code: &str, inviter_id: UserId,
invitee_id: UserId, invitee_id: UserId,
) -> Result<()> { ) -> Result<()> {
let user = self.app_state.db.get_user_for_invite_code(code).await?; if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
if let Some(code) = &user.invite_code {
let store = self.store().await; let store = self.store().await;
let invitee_contact = store.contact_for_user(invitee_id, true); let invitee_contact = store.contact_for_user(invitee_id, true);
for connection_id in store.connection_ids_for_user(user.id) { for connection_id in store.connection_ids_for_user(inviter_id) {
self.peer.send( self.peer.send(
connection_id, connection_id,
proto::UpdateContacts { proto::UpdateContacts {
@ -545,11 +548,13 @@ impl Server {
self.peer.send( self.peer.send(
connection_id, connection_id,
proto::UpdateInviteInfo { proto::UpdateInviteInfo {
url: format!("{}{}", self.app_state.invite_link_prefix, code), url: format!("{}{}", self.app_state.invite_link_prefix, &code),
count: user.invite_count as u32, count: user.invite_count as u32,
}, },
)?; )?;
} }
}
}
Ok(()) Ok(())
} }
@ -1427,7 +1432,7 @@ impl Server {
let users = match query.len() { let users = match query.len() {
0 => vec![], 0 => vec![],
1 | 2 => db 1 | 2 => db
.get_user_by_github_login(&query) .get_user_by_github_account(&query, None)
.await? .await?
.into_iter() .into_iter()
.collect(), .collect(),
@ -1750,6 +1755,44 @@ impl Server {
Ok(()) Ok(())
} }
async fn update_diff_base(
self: Arc<Server>,
request: TypedEnvelope<proto::UpdateDiffBase>,
) -> Result<()> {
let receiver_ids = self.store().await.project_connection_ids(
ProjectId::from_proto(request.payload.project_id),
request.sender_id,
)?;
broadcast(request.sender_id, receiver_ids, |connection_id| {
self.peer
.forward_send(request.sender_id, connection_id, request.payload.clone())
});
Ok(())
}
async fn get_private_user_info(
self: Arc<Self>,
request: TypedEnvelope<proto::GetPrivateUserInfo>,
response: Response<proto::GetPrivateUserInfo>,
) -> Result<()> {
let user_id = self
.store()
.await
.user_id_for_connection(request.sender_id)?;
let metrics_id = self.app_state.db.get_user_metrics_id(user_id).await?;
let user = self
.app_state
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
response.send(proto::GetPrivateUserInfoResponse {
metrics_id,
staff: user.admin,
})?;
Ok(())
}
pub(crate) async fn store(&self) -> StoreGuard<'_> { pub(crate) async fn store(&self) -> StoreGuard<'_> {
#[cfg(test)] #[cfg(test)]
tokio::task::yield_now().await; tokio::task::yield_now().await;

22
crates/db/Cargo.toml Normal file
View 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" }

View file

@ -25,6 +25,7 @@ clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
@ -51,6 +52,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
smallvec = { version = "1.6", features = ["union"] } smallvec = { version = "1.6", features = ["union"] }
smol = "1.2" smol = "1.2"
tree-sitter-rust = { version = "*", optional = true } tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
[dev-dependencies] [dev-dependencies]
text = { path = "../text", features = ["test-support"] } text = { path = "../text", features = ["test-support"] }
@ -67,3 +70,5 @@ rand = "0.8"
unindent = "0.1.7" unindent = "0.1.7"
tree-sitter = "0.20" tree-sitter = "0.20"
tree-sitter-rust = "0.20" tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-javascript = "0.20"

View file

@ -274,6 +274,7 @@ impl FoldMap {
if buffer.edit_count() != new_buffer.edit_count() if buffer.edit_count() != new_buffer.edit_count()
|| buffer.parse_count() != new_buffer.parse_count() || buffer.parse_count() != new_buffer.parse_count()
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count() || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|| buffer.trailing_excerpt_update_count() || buffer.trailing_excerpt_update_count()
!= new_buffer.trailing_excerpt_update_count() != new_buffer.trailing_excerpt_update_count()
{ {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@ use crate::{
}; };
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use git::diff::{DiffHunk, DiffHunkStatus};
use gpui::{ use gpui::{
color::Color, color::Color,
elements::*, elements::*,
@ -36,7 +37,7 @@ use gpui::{
use json::json; use json::json;
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
use project::ProjectPath; use project::ProjectPath;
use settings::Settings; use settings::{GitGutter, Settings};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
cmp::{self, Ordering}, cmp::{self, Ordering},
@ -45,6 +46,7 @@ use std::{
ops::Range, ops::Range,
sync::Arc, sync::Arc,
}; };
use theme::DiffStyle;
struct SelectionLayout { struct SelectionLayout {
head: DisplayPoint, head: DisplayPoint,
@ -524,30 +526,141 @@ impl EditorElement {
layout: &mut LayoutState, layout: &mut LayoutState,
cx: &mut PaintContext, cx: &mut PaintContext,
) { ) {
let scroll_top = struct GutterLayout {
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height; line_height: f32,
// scroll_position: Vector2F,
scroll_top: f32,
bounds: RectF,
}
struct DiffLayout<'a> {
buffer_row: u32,
last_diff: Option<&'a DiffHunk<u32>>,
}
fn diff_quad(
hunk: &DiffHunk<u32>,
gutter_layout: &GutterLayout,
diff_style: &DiffStyle,
) -> Quad {
let color = match hunk.status() {
DiffHunkStatus::Added => diff_style.inserted,
DiffHunkStatus::Modified => diff_style.modified,
//TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => {
let row = hunk.buffer_range.start;
let offset = gutter_layout.line_height / 2.;
let start_y =
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
let end_y = start_y + gutter_layout.line_height;
let width = diff_style.removed_width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
return Quad {
bounds: highlight_bounds,
background: Some(diff_style.deleted),
border: Border::new(0., Color::transparent_black()),
corner_radius: 1. * gutter_layout.line_height,
};
}
};
let start_row = hunk.buffer_range.start;
let end_row = hunk.buffer_range.end;
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
let width = diff_style.width_em * gutter_layout.line_height;
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
let highlight_size = vec2f(width * 2., end_y - start_y);
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
Quad {
bounds: highlight_bounds,
background: Some(color),
border: Border::new(0., Color::transparent_black()),
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
}
}
let scroll_position = layout.position_map.snapshot.scroll_position();
let gutter_layout = {
let line_height = layout.position_map.line_height;
GutterLayout {
scroll_top: scroll_position.y() * line_height,
line_height,
bounds,
}
};
let mut diff_layout = DiffLayout {
buffer_row: scroll_position.y() as u32,
last_diff: None,
};
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
let show_gutter = matches!(
&cx.global::<Settings>()
.git_overrides
.git_gutter
.unwrap_or_default(),
GitGutter::TrackedFiles
);
// line is `None` when there's a line wrap
for (ix, line) in layout.line_number_layouts.iter().enumerate() { for (ix, line) in layout.line_number_layouts.iter().enumerate() {
if let Some(line) = line { if let Some(line) = line {
let line_origin = bounds.origin() let line_origin = bounds.origin()
+ vec2f( + vec2f(
bounds.width() - line.width() - layout.gutter_padding, bounds.width() - line.width() - layout.gutter_padding,
ix as f32 * layout.position_map.line_height ix as f32 * gutter_layout.line_height
- (scroll_top % layout.position_map.line_height), - (gutter_layout.scroll_top % gutter_layout.line_height),
);
line.paint(
line_origin,
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() { if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
let mut x = bounds.width() - layout.gutter_padding; let mut x = bounds.width() - layout.gutter_padding;
let mut y = *row as f32 * layout.position_map.line_height - scroll_top; let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.; x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
y += (layout.position_map.line_height - indicator.size().y()) / 2.; y += (gutter_layout.line_height - indicator.size().y()) / 2.;
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx); indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
} }
} }
@ -1252,6 +1365,27 @@ impl EditorElement {
} }
} }
/// Get the hunk that contains buffer_line, starting from start_idx
/// Returns none if there is none found, and
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
for i in 0..hunks.len() {
// Safety: Index out of bounds is handled by the check above
let hunk = hunks.get(i).unwrap();
if hunk.buffer_range.contains(&(buffer_line as u32)) {
return Some(hunk);
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
{
return Some(hunk);
} else if hunk.buffer_range.start > buffer_line as u32 {
// If we've passed the buffer_line, just stop
return None;
}
}
// We reached the end of the array without finding a hunk, just return none.
return None;
}
impl Element for EditorElement { impl Element for EditorElement {
type LayoutState = LayoutState; type LayoutState = LayoutState;
type PaintState = (); type PaintState = ();
@ -1425,6 +1559,11 @@ impl Element for EditorElement {
let line_number_layouts = let line_number_layouts =
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx); self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
let diff_hunks = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(start_row..end_row)
.collect();
let mut max_visible_line_width = 0.0; let mut max_visible_line_width = 0.0;
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
for line in &line_layouts { for line in &line_layouts {
@ -1573,6 +1712,7 @@ impl Element for EditorElement {
highlighted_rows, highlighted_rows,
highlighted_ranges, highlighted_ranges,
line_number_layouts, line_number_layouts,
diff_hunks,
blocks, blocks,
selections, selections,
context_menu, context_menu,
@ -1710,6 +1850,7 @@ pub struct LayoutState {
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>, highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>, selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
context_menu: Option<(DisplayPoint, ElementBox)>, context_menu: Option<(DisplayPoint, ElementBox)>,
diff_hunks: Vec<DiffHunk<u32>>,
code_actions_indicator: Option<(u32, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>,
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>, hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
} }

View file

@ -404,6 +404,8 @@ impl Item for Editor {
project: ModelHandle<Project>, project: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
self.report_event("save editor", cx);
let buffer = self.buffer().clone(); let buffer = self.buffer().clone();
let buffers = buffer.read(cx).all_buffers(); let buffers = buffer.read(cx).all_buffers();
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
@ -476,6 +478,17 @@ impl Item for Editor {
}) })
} }
fn git_diff_recalc(
&mut self,
_project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |multibuffer, cx| {
multibuffer.git_diff_recalc(cx);
});
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> { fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
let mut result = Vec::new(); let mut result = Vec::new();
match event { match event {

View file

@ -4,6 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
use anyhow::Result; use anyhow::Result;
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet}; use collections::{BTreeMap, Bound, HashMap, HashSet};
use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion; pub use language::Completion;
use language::{ use language::{
@ -90,6 +91,7 @@ struct BufferState {
last_selections_update_count: usize, last_selections_update_count: usize,
last_diagnostics_update_count: usize, last_diagnostics_update_count: usize,
last_file_update_count: usize, last_file_update_count: usize,
last_git_diff_update_count: usize,
excerpts: Vec<ExcerptId>, excerpts: Vec<ExcerptId>,
_subscriptions: [gpui::Subscription; 2], _subscriptions: [gpui::Subscription; 2],
} }
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
parse_count: usize, parse_count: usize,
diagnostics_update_count: usize, diagnostics_update_count: usize,
trailing_excerpt_update_count: usize, trailing_excerpt_update_count: usize,
git_diff_update_count: usize,
edit_count: usize, edit_count: usize,
is_dirty: bool, is_dirty: bool,
has_conflict: bool, has_conflict: bool,
@ -202,6 +205,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_state.last_selections_update_count, last_selections_update_count: buffer_state.last_selections_update_count,
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
last_file_update_count: buffer_state.last_file_update_count, last_file_update_count: buffer_state.last_file_update_count,
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
excerpts: buffer_state.excerpts.clone(), excerpts: buffer_state.excerpts.clone(),
_subscriptions: [ _subscriptions: [
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
@ -308,6 +312,17 @@ impl MultiBuffer {
self.read(cx).symbols_containing(offset, theme) self.read(cx).symbols_containing(offset, theme)
} }
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
let buffers = self.buffers.borrow();
for buffer_state in buffers.values() {
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
buffer_state
.buffer
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
}
}
}
pub fn edit<I, S, T>( pub fn edit<I, S, T>(
&mut self, &mut self,
edits: I, edits: I,
@ -827,6 +842,7 @@ impl MultiBuffer {
last_selections_update_count: buffer_snapshot.selections_update_count(), last_selections_update_count: buffer_snapshot.selections_update_count(),
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(), last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
last_file_update_count: buffer_snapshot.file_update_count(), last_file_update_count: buffer_snapshot.file_update_count(),
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
excerpts: Default::default(), excerpts: Default::default(),
_subscriptions: [ _subscriptions: [
cx.observe(&buffer, |_, _, cx| cx.notify()), cx.observe(&buffer, |_, _, cx| cx.notify()),
@ -1212,9 +1228,9 @@ impl MultiBuffer {
&self, &self,
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> Option<&'a Arc<Language>> { ) -> Option<Arc<Language>> {
self.point_to_buffer_offset(point, cx) self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, _)| buffer.read(cx).language()) .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
} }
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> { pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
@ -1249,6 +1265,7 @@ impl MultiBuffer {
let mut excerpts_to_edit = Vec::new(); let mut excerpts_to_edit = Vec::new();
let mut reparsed = false; let mut reparsed = false;
let mut diagnostics_updated = false; let mut diagnostics_updated = false;
let mut git_diff_updated = false;
let mut is_dirty = false; let mut is_dirty = false;
let mut has_conflict = false; let mut has_conflict = false;
let mut edited = false; let mut edited = false;
@ -1260,6 +1277,7 @@ impl MultiBuffer {
let selections_update_count = buffer.selections_update_count(); let selections_update_count = buffer.selections_update_count();
let diagnostics_update_count = buffer.diagnostics_update_count(); let diagnostics_update_count = buffer.diagnostics_update_count();
let file_update_count = buffer.file_update_count(); let file_update_count = buffer.file_update_count();
let git_diff_update_count = buffer.git_diff_update_count();
let buffer_edited = version.changed_since(&buffer_state.last_version); let buffer_edited = version.changed_since(&buffer_state.last_version);
let buffer_reparsed = parse_count > buffer_state.last_parse_count; let buffer_reparsed = parse_count > buffer_state.last_parse_count;
@ -1268,17 +1286,21 @@ impl MultiBuffer {
let buffer_diagnostics_updated = let buffer_diagnostics_updated =
diagnostics_update_count > buffer_state.last_diagnostics_update_count; diagnostics_update_count > buffer_state.last_diagnostics_update_count;
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count; let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
let buffer_git_diff_updated =
git_diff_update_count > buffer_state.last_git_diff_update_count;
if buffer_edited if buffer_edited
|| buffer_reparsed || buffer_reparsed
|| buffer_selections_updated || buffer_selections_updated
|| buffer_diagnostics_updated || buffer_diagnostics_updated
|| buffer_file_updated || buffer_file_updated
|| buffer_git_diff_updated
{ {
buffer_state.last_version = version; buffer_state.last_version = version;
buffer_state.last_parse_count = parse_count; buffer_state.last_parse_count = parse_count;
buffer_state.last_selections_update_count = selections_update_count; buffer_state.last_selections_update_count = selections_update_count;
buffer_state.last_diagnostics_update_count = diagnostics_update_count; buffer_state.last_diagnostics_update_count = diagnostics_update_count;
buffer_state.last_file_update_count = file_update_count; buffer_state.last_file_update_count = file_update_count;
buffer_state.last_git_diff_update_count = git_diff_update_count;
excerpts_to_edit.extend( excerpts_to_edit.extend(
buffer_state buffer_state
.excerpts .excerpts
@ -1290,6 +1312,7 @@ impl MultiBuffer {
edited |= buffer_edited; edited |= buffer_edited;
reparsed |= buffer_reparsed; reparsed |= buffer_reparsed;
diagnostics_updated |= buffer_diagnostics_updated; diagnostics_updated |= buffer_diagnostics_updated;
git_diff_updated |= buffer_git_diff_updated;
is_dirty |= buffer.is_dirty(); is_dirty |= buffer.is_dirty();
has_conflict |= buffer.has_conflict(); has_conflict |= buffer.has_conflict();
} }
@ -1302,6 +1325,9 @@ impl MultiBuffer {
if diagnostics_updated { if diagnostics_updated {
snapshot.diagnostics_update_count += 1; snapshot.diagnostics_update_count += 1;
} }
if git_diff_updated {
snapshot.git_diff_update_count += 1;
}
snapshot.is_dirty = is_dirty; snapshot.is_dirty = is_dirty;
snapshot.has_conflict = has_conflict; snapshot.has_conflict = has_conflict;
@ -1940,6 +1966,24 @@ impl MultiBufferSnapshot {
} }
} }
pub fn point_to_buffer_offset<T: ToOffset>(
&self,
point: T,
) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(&self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
cursor.item().map(|excerpt| {
let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
let buffer_point = excerpt_start + offset - *cursor.start();
(&excerpt.buffer, buffer_point)
})
}
pub fn suggested_indents( pub fn suggested_indents(
&self, &self,
rows: impl IntoIterator<Item = u32>, rows: impl IntoIterator<Item = u32>,
@ -1949,8 +1993,10 @@ impl MultiBufferSnapshot {
let mut rows_for_excerpt = Vec::new(); let mut rows_for_excerpt = Vec::new();
let mut cursor = self.excerpts.cursor::<Point>(); let mut cursor = self.excerpts.cursor::<Point>();
let mut rows = rows.into_iter().peekable(); let mut rows = rows.into_iter().peekable();
let mut prev_row = u32::MAX;
let mut prev_language_indent_size = IndentSize::default();
while let Some(row) = rows.next() { while let Some(row) = rows.next() {
cursor.seek(&Point::new(row, 0), Bias::Right, &()); cursor.seek(&Point::new(row, 0), Bias::Right, &());
let excerpt = match cursor.item() { let excerpt = match cursor.item() {
@ -1958,7 +2004,17 @@ impl MultiBufferSnapshot {
_ => continue, _ => continue,
}; };
let single_indent_size = excerpt.buffer.single_indent_size(cx); // Retrieve the language and indent size once for each disjoint region being indented.
let single_indent_size = if row.saturating_sub(1) == prev_row {
prev_language_indent_size
} else {
excerpt
.buffer
.language_indent_size_at(Point::new(row, 0), cx)
};
prev_language_indent_size = single_indent_size;
prev_row = row;
let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row; let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
let start_multibuffer_row = cursor.start().row; let start_multibuffer_row = cursor.start().row;
@ -2479,15 +2535,17 @@ impl MultiBufferSnapshot {
self.diagnostics_update_count self.diagnostics_update_count
} }
pub fn git_diff_update_count(&self) -> usize {
self.git_diff_update_count
}
pub fn trailing_excerpt_update_count(&self) -> usize { pub fn trailing_excerpt_update_count(&self) -> usize {
self.trailing_excerpt_update_count self.trailing_excerpt_update_count
} }
pub fn language(&self) -> Option<&Arc<Language>> { pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
self.excerpts self.point_to_buffer_offset(point)
.iter() .and_then(|(buffer, offset)| buffer.language_at(offset))
.next()
.and_then(|excerpt| excerpt.buffer.language())
} }
pub fn is_dirty(&self) -> bool { pub fn is_dirty(&self) -> bool {
@ -2529,6 +2587,15 @@ impl MultiBufferSnapshot {
}) })
} }
pub fn git_diff_hunks_in_range<'a>(
&'a self,
row_range: Range<u32>,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
self.as_singleton()
.into_iter()
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
}
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> { pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self)..range.end.to_offset(self);

28
crates/git/Cargo.toml Normal file
View 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
View 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
View 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");
}

View 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()
}
}

View file

@ -325,7 +325,12 @@ impl Deterministic {
let mut state = self.state.lock(); let mut state = self.state.lock();
let wakeup_at = state.now + duration; let wakeup_at = state.now + duration;
let id = util::post_inc(&mut state.next_timer_id); let id = util::post_inc(&mut state.next_timer_id);
state.pending_timers.push((id, wakeup_at, tx)); match state
.pending_timers
.binary_search_by_key(&wakeup_at, |e| e.1)
{
Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
}
let state = self.state.clone(); let state = self.state.clone();
Timer::Deterministic(DeterministicTimer { rx, id, state }) Timer::Deterministic(DeterministicTimer { rx, id, state })
} }

View file

@ -71,6 +71,8 @@ pub trait Platform: Send + Sync {
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>; fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf>;
fn app_path(&self) -> Result<PathBuf>; fn app_path(&self) -> Result<PathBuf>;
fn app_version(&self) -> Result<AppVersion>; fn app_version(&self) -> Result<AppVersion>;
fn os_name(&self) -> &'static str;
fn os_version(&self) -> Result<AppVersion>;
} }
pub(crate) trait ForegroundPlatform { pub(crate) trait ForegroundPlatform {

View file

@ -14,8 +14,10 @@ use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode}, event::{CGEvent, CGEventFlags, CGKeyCode},
event_source::{CGEventSource, CGEventSourceStateID}, event_source::{CGEventSource, CGEventSourceStateID},
}; };
use ctor::ctor;
use foreign_types::ForeignType;
use objc::{class, msg_send, sel, sel_impl}; use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::CStr, os::raw::c_char}; use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
const BACKSPACE_KEY: u16 = 0x7f; const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16; const SPACE_KEY: u16 = b' ' as u16;
@ -25,6 +27,15 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09; const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19; const SHIFT_TAB_KEY: u16 = 0x19;
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
#[ctor]
unsafe fn build_event_source() {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
EVENT_SOURCE = source.as_ptr();
mem::forget(source);
}
pub fn key_to_native(key: &str) -> Cow<str> { pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*; use cocoa::appkit::*;
let code = match key { let code = match key {
@ -228,7 +239,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let mut chars_ignoring_modifiers = let mut chars_ignoring_modifiers =
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char) CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
.to_str() .to_str()
.unwrap(); .unwrap()
.to_string();
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags(); let modifiers = native_event.modifierFlags();
@ -243,31 +255,31 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
let key = match first_char { let key = match first_char {
Some(SPACE_KEY) => "space", Some(SPACE_KEY) => "space".to_string(),
Some(BACKSPACE_KEY) => "backspace", Some(BACKSPACE_KEY) => "backspace".to_string(),
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter", Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
Some(ESCAPE_KEY) => "escape", Some(ESCAPE_KEY) => "escape".to_string(),
Some(TAB_KEY) => "tab", Some(TAB_KEY) => "tab".to_string(),
Some(SHIFT_TAB_KEY) => "tab", Some(SHIFT_TAB_KEY) => "tab".to_string(),
Some(NSUpArrowFunctionKey) => "up", Some(NSUpArrowFunctionKey) => "up".to_string(),
Some(NSDownArrowFunctionKey) => "down", Some(NSDownArrowFunctionKey) => "down".to_string(),
Some(NSLeftArrowFunctionKey) => "left", Some(NSLeftArrowFunctionKey) => "left".to_string(),
Some(NSRightArrowFunctionKey) => "right", Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup", Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown", Some(NSPageDownFunctionKey) => "pagedown".to_string(),
Some(NSDeleteFunctionKey) => "delete", Some(NSDeleteFunctionKey) => "delete".to_string(),
Some(NSF1FunctionKey) => "f1", Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2", Some(NSF2FunctionKey) => "f2".to_string(),
Some(NSF3FunctionKey) => "f3", Some(NSF3FunctionKey) => "f3".to_string(),
Some(NSF4FunctionKey) => "f4", Some(NSF4FunctionKey) => "f4".to_string(),
Some(NSF5FunctionKey) => "f5", Some(NSF5FunctionKey) => "f5".to_string(),
Some(NSF6FunctionKey) => "f6", Some(NSF6FunctionKey) => "f6".to_string(),
Some(NSF7FunctionKey) => "f7", Some(NSF7FunctionKey) => "f7".to_string(),
Some(NSF8FunctionKey) => "f8", Some(NSF8FunctionKey) => "f8".to_string(),
Some(NSF9FunctionKey) => "f9", Some(NSF9FunctionKey) => "f9".to_string(),
Some(NSF10FunctionKey) => "f10", Some(NSF10FunctionKey) => "f10".to_string(),
Some(NSF11FunctionKey) => "f11", Some(NSF11FunctionKey) => "f11".to_string(),
Some(NSF12FunctionKey) => "f12", Some(NSF12FunctionKey) => "f12".to_string(),
_ => { _ => {
let mut chars_ignoring_modifiers_and_shift = let mut chars_ignoring_modifiers_and_shift =
chars_for_modified_key(native_event.keyCode(), false, false); chars_for_modified_key(native_event.keyCode(), false, false);
@ -303,21 +315,19 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
shift, shift,
cmd, cmd,
function, function,
key: key.into(), key,
} }
} }
fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str { fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
// an event with the given flags instead lets us access `characters`, which always // an event with the given flags instead lets us access `characters`, which always
// returns a valid string. // returns a valid string.
let event = CGEvent::new_keyboard_event( let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
CGEventSource::new(CGEventSourceStateID::Private).unwrap(), let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
code, mem::forget(source);
true,
)
.unwrap();
let mut flags = CGEventFlags::empty(); let mut flags = CGEventFlags::empty();
if cmd { if cmd {
flags |= CGEventFlags::CGEventFlagCommand; flags |= CGEventFlags::CGEventFlagCommand;
@ -327,10 +337,11 @@ fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a st
} }
event.set_flags(flags); event.set_flags(flags);
let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
unsafe { unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
CStr::from_ptr(event.characters().UTF8String()) CStr::from_ptr(event.characters().UTF8String())
.to_str() .to_str()
.unwrap() .unwrap()
.to_string()
} }
} }

View file

@ -6,7 +6,7 @@ use crate::{
geometry::vector::{vec2f, Vector2F}, geometry::vector::{vec2f, Vector2F},
keymap, keymap,
platform::{self, CursorStyle}, platform::{self, CursorStyle},
Action, ClipboardItem, Event, Menu, MenuItem, Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use block::ConcreteBlock; use block::ConcreteBlock;
@ -18,7 +18,8 @@ use cocoa::{
}, },
base::{id, nil, selector, YES}, base::{id, nil, selector, YES},
foundation::{ foundation::{
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSString, NSUInteger, NSURL, NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString,
NSUInteger, NSURL,
}, },
}; };
use core_foundation::{ use core_foundation::{
@ -758,6 +759,22 @@ impl platform::Platform for MacPlatform {
} }
} }
} }
fn os_name(&self) -> &'static str {
"macOS"
}
fn os_version(&self) -> Result<crate::AppVersion> {
unsafe {
let process_info = NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
Ok(AppVersion {
major: version.majorVersion as usize,
minor: version.minorVersion as usize,
patch: version.patchVersion as usize,
})
}
}
} }
unsafe fn path_from_objc(path: id) -> PathBuf { unsafe fn path_from_objc(path: id) -> PathBuf {

View file

@ -200,6 +200,18 @@ impl super::Platform for Platform {
patch: 0, patch: 0,
}) })
} }
fn os_name(&self) -> &'static str {
"test"
}
fn os_version(&self) -> Result<AppVersion> {
Ok(AppVersion {
major: 1,
minor: 0,
patch: 0,
})
}
} }
impl Window { impl Window {

View file

@ -25,6 +25,7 @@ client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
@ -63,6 +64,8 @@ util = { path = "../util", features = ["test-support"] }
ctor = "0.1" ctor = "0.1"
env_logger = "0.9" env_logger = "0.9"
rand = "0.8.3" rand = "0.8.3"
tree-sitter-html = "*"
tree-sitter-javascript = "*"
tree-sitter-json = "*" tree-sitter-json = "*"
tree-sitter-rust = "*" tree-sitter-rust = "*"
tree-sitter-python = "*" tree-sitter-python = "*"

View file

@ -45,8 +45,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
pub use lsp::DiagnosticSeverity; pub use lsp::DiagnosticSeverity;
struct GitDiffStatus {
diff: git::diff::BufferDiff,
update_in_progress: bool,
update_requested: bool,
}
pub struct Buffer { pub struct Buffer {
text: TextBuffer, text: TextBuffer,
diff_base: Option<String>,
git_diff_status: GitDiffStatus,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
saved_version: clock::Global, saved_version: clock::Global,
saved_version_fingerprint: String, saved_version_fingerprint: String,
@ -66,6 +74,7 @@ pub struct Buffer {
diagnostics_update_count: usize, diagnostics_update_count: usize,
diagnostics_timestamp: clock::Lamport, diagnostics_timestamp: clock::Lamport,
file_update_count: usize, file_update_count: usize,
git_diff_update_count: usize,
completion_triggers: Vec<String>, completion_triggers: Vec<String>,
completion_triggers_timestamp: clock::Lamport, completion_triggers_timestamp: clock::Lamport,
deferred_ops: OperationQueue<Operation>, deferred_ops: OperationQueue<Operation>,
@ -73,25 +82,28 @@ pub struct Buffer {
pub struct BufferSnapshot { pub struct BufferSnapshot {
text: text::BufferSnapshot, text: text::BufferSnapshot,
pub git_diff: git::diff::BufferDiff,
pub(crate) syntax: SyntaxSnapshot, pub(crate) syntax: SyntaxSnapshot,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
diagnostics: DiagnosticSet, diagnostics: DiagnosticSet,
diagnostics_update_count: usize, diagnostics_update_count: usize,
file_update_count: usize, file_update_count: usize,
git_diff_update_count: usize,
remote_selections: TreeMap<ReplicaId, SelectionSet>, remote_selections: TreeMap<ReplicaId, SelectionSet>,
selections_update_count: usize, selections_update_count: usize,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
parse_count: usize, parse_count: usize,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct IndentSize { pub struct IndentSize {
pub len: u32, pub len: u32,
pub kind: IndentKind, pub kind: IndentKind,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum IndentKind { pub enum IndentKind {
#[default]
Space, Space,
Tab, Tab,
} }
@ -236,7 +248,6 @@ pub enum AutoindentMode {
struct AutoindentRequest { struct AutoindentRequest {
before_edit: BufferSnapshot, before_edit: BufferSnapshot,
entries: Vec<AutoindentRequestEntry>, entries: Vec<AutoindentRequestEntry>,
indent_size: IndentSize,
is_block_mode: bool, is_block_mode: bool,
} }
@ -249,6 +260,7 @@ struct AutoindentRequestEntry {
/// only be adjusted if the suggested indentation level has *changed* /// only be adjusted if the suggested indentation level has *changed*
/// since the edit was made. /// since the edit was made.
first_line_is_new: bool, first_line_is_new: bool,
indent_size: IndentSize,
original_indent_column: Option<u32>, original_indent_column: Option<u32>,
} }
@ -288,10 +300,8 @@ pub struct Chunk<'a> {
pub struct Diff { pub struct Diff {
base_version: clock::Global, base_version: clock::Global,
new_text: Arc<str>,
changes: Vec<(ChangeTag, usize)>,
line_ending: LineEnding, line_ending: LineEnding,
start_offset: usize, edits: Vec<(Range<usize>, Arc<str>)>,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -328,17 +338,20 @@ impl Buffer {
Self::build( Self::build(
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
None, None,
None,
) )
} }
pub fn from_file<T: Into<String>>( pub fn from_file<T: Into<String>>(
replica_id: ReplicaId, replica_id: ReplicaId,
base_text: T, base_text: T,
diff_base: Option<T>,
file: Arc<dyn File>, file: Arc<dyn File>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
Self::build( Self::build(
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
diff_base.map(|h| h.into().into_boxed_str().into()),
Some(file), Some(file),
) )
} }
@ -349,7 +362,11 @@ impl Buffer {
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
) -> Result<Self> { ) -> Result<Self> {
let buffer = TextBuffer::new(replica_id, message.id, message.base_text); let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
let mut this = Self::build(buffer, file); let mut this = Self::build(
buffer,
message.diff_base.map(|text| text.into_boxed_str().into()),
file,
);
this.text.set_line_ending(proto::deserialize_line_ending( this.text.set_line_ending(proto::deserialize_line_ending(
proto::LineEnding::from_i32(message.line_ending) proto::LineEnding::from_i32(message.line_ending)
.ok_or_else(|| anyhow!("missing line_ending"))?, .ok_or_else(|| anyhow!("missing line_ending"))?,
@ -362,6 +379,7 @@ impl Buffer {
id: self.remote_id(), id: self.remote_id(),
file: self.file.as_ref().map(|f| f.to_proto()), file: self.file.as_ref().map(|f| f.to_proto()),
base_text: self.base_text().to_string(), base_text: self.base_text().to_string(),
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32, line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
} }
} }
@ -404,7 +422,7 @@ impl Buffer {
self self
} }
fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self { fn build(buffer: TextBuffer, diff_base: Option<String>, file: Option<Arc<dyn File>>) -> Self {
let saved_mtime = if let Some(file) = file.as_ref() { let saved_mtime = if let Some(file) = file.as_ref() {
file.mtime() file.mtime()
} else { } else {
@ -418,6 +436,12 @@ impl Buffer {
transaction_depth: 0, transaction_depth: 0,
was_dirty_before_starting_transaction: None, was_dirty_before_starting_transaction: None,
text: buffer, text: buffer,
diff_base,
git_diff_status: GitDiffStatus {
diff: git::diff::BufferDiff::new(),
update_in_progress: false,
update_requested: false,
},
file, file,
syntax_map: Mutex::new(SyntaxMap::new()), syntax_map: Mutex::new(SyntaxMap::new()),
parsing_in_background: false, parsing_in_background: false,
@ -432,6 +456,7 @@ impl Buffer {
diagnostics_update_count: 0, diagnostics_update_count: 0,
diagnostics_timestamp: Default::default(), diagnostics_timestamp: Default::default(),
file_update_count: 0, file_update_count: 0,
git_diff_update_count: 0,
completion_triggers: Default::default(), completion_triggers: Default::default(),
completion_triggers_timestamp: Default::default(), completion_triggers_timestamp: Default::default(),
deferred_ops: OperationQueue::new(), deferred_ops: OperationQueue::new(),
@ -447,11 +472,13 @@ impl Buffer {
BufferSnapshot { BufferSnapshot {
text, text,
syntax, syntax,
git_diff: self.git_diff_status.diff.clone(),
file: self.file.clone(), file: self.file.clone(),
remote_selections: self.remote_selections.clone(), remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(), diagnostics: self.diagnostics.clone(),
diagnostics_update_count: self.diagnostics_update_count, diagnostics_update_count: self.diagnostics_update_count,
file_update_count: self.file_update_count, file_update_count: self.file_update_count,
git_diff_update_count: self.git_diff_update_count,
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
selections_update_count: self.selections_update_count, selections_update_count: self.selections_update_count,
@ -584,6 +611,7 @@ impl Buffer {
cx, cx,
); );
} }
self.git_diff_recalc(cx);
cx.emit(Event::Reloaded); cx.emit(Event::Reloaded);
cx.notify(); cx.notify();
} }
@ -633,6 +661,60 @@ impl Buffer {
task task
} }
#[cfg(any(test, feature = "test-support"))]
pub fn diff_base(&self) -> Option<&str> {
self.diff_base.as_deref()
}
pub fn update_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
self.diff_base = diff_base;
self.git_diff_recalc(cx);
}
pub fn needs_git_diff_recalc(&self) -> bool {
self.git_diff_status.diff.needs_update(self)
}
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
if self.git_diff_status.update_in_progress {
self.git_diff_status.update_requested = true;
return;
}
if let Some(diff_base) = &self.diff_base {
let snapshot = self.snapshot();
let diff_base = diff_base.clone();
let mut diff = self.git_diff_status.diff.clone();
let diff = cx.background().spawn(async move {
diff.update(&diff_base, &snapshot).await;
diff
});
cx.spawn_weak(|this, mut cx| async move {
let buffer_diff = diff.await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
this.git_diff_status.diff = buffer_diff;
this.git_diff_update_count += 1;
cx.notify();
this.git_diff_status.update_in_progress = false;
if this.git_diff_status.update_requested {
this.git_diff_recalc(cx);
}
})
}
})
.detach()
} else {
let snapshot = self.snapshot();
self.git_diff_status.diff.clear(&snapshot);
self.git_diff_update_count += 1;
cx.notify();
}
}
pub fn close(&mut self, cx: &mut ModelContext<Self>) { pub fn close(&mut self, cx: &mut ModelContext<Self>) {
cx.emit(Event::Closed); cx.emit(Event::Closed);
} }
@ -641,6 +723,16 @@ impl Buffer {
self.language.as_ref() self.language.as_ref()
} }
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<Arc<Language>> {
let offset = position.to_offset(self);
self.syntax_map
.lock()
.layers_for_range(offset..offset, &self.text)
.last()
.map(|info| info.language.clone())
.or_else(|| self.language.clone())
}
pub fn parse_count(&self) -> usize { pub fn parse_count(&self) -> usize {
self.parse_count self.parse_count
} }
@ -657,6 +749,10 @@ impl Buffer {
self.file_update_count self.file_update_count
} }
pub fn git_diff_update_count(&self) -> usize {
self.git_diff_update_count
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn is_parsing(&self) -> bool { pub fn is_parsing(&self) -> bool {
self.parsing_in_background self.parsing_in_background
@ -784,10 +880,13 @@ impl Buffer {
// buffer before this batch of edits. // buffer before this batch of edits.
let mut row_ranges = Vec::new(); let mut row_ranges = Vec::new();
let mut old_to_new_rows = BTreeMap::new(); let mut old_to_new_rows = BTreeMap::new();
let mut language_indent_sizes_by_new_row = Vec::new();
for entry in &request.entries { for entry in &request.entries {
let position = entry.range.start; let position = entry.range.start;
let new_row = position.to_point(&snapshot).row; let new_row = position.to_point(&snapshot).row;
let new_end_row = entry.range.end.to_point(&snapshot).row + 1; let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
language_indent_sizes_by_new_row.push((new_row, entry.indent_size));
if !entry.first_line_is_new { if !entry.first_line_is_new {
let old_row = position.to_point(&request.before_edit).row; let old_row = position.to_point(&request.before_edit).row;
old_to_new_rows.insert(old_row, new_row); old_to_new_rows.insert(old_row, new_row);
@ -801,6 +900,8 @@ impl Buffer {
let mut old_suggestions = BTreeMap::<u32, IndentSize>::default(); let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
let old_edited_ranges = let old_edited_ranges =
contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
let mut language_indent_size = IndentSize::default();
for old_edited_range in old_edited_ranges { for old_edited_range in old_edited_ranges {
let suggestions = request let suggestions = request
.before_edit .before_edit
@ -809,6 +910,17 @@ impl Buffer {
.flatten(); .flatten();
for (old_row, suggestion) in old_edited_range.zip(suggestions) { for (old_row, suggestion) in old_edited_range.zip(suggestions) {
if let Some(suggestion) = suggestion { if let Some(suggestion) = suggestion {
let new_row = *old_to_new_rows.get(&old_row).unwrap();
// Find the indent size based on the language for this row.
while let Some((row, size)) = language_indent_sizes.peek() {
if *row > new_row {
break;
}
language_indent_size = *size;
language_indent_sizes.next();
}
let suggested_indent = old_to_new_rows let suggested_indent = old_to_new_rows
.get(&suggestion.basis_row) .get(&suggestion.basis_row)
.and_then(|from_row| old_suggestions.get(from_row).copied()) .and_then(|from_row| old_suggestions.get(from_row).copied())
@ -817,9 +929,8 @@ impl Buffer {
.before_edit .before_edit
.indent_size_for_line(suggestion.basis_row) .indent_size_for_line(suggestion.basis_row)
}) })
.with_delta(suggestion.delta, request.indent_size); .with_delta(suggestion.delta, language_indent_size);
old_suggestions old_suggestions.insert(new_row, suggested_indent);
.insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
} }
} }
yield_now().await; yield_now().await;
@ -840,6 +951,8 @@ impl Buffer {
// Compute new suggestions for each line, but only include them in the result // Compute new suggestions for each line, but only include them in the result
// if they differ from the old suggestion for that line. // if they differ from the old suggestion for that line.
let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
let mut language_indent_size = IndentSize::default();
for new_edited_row_range in new_edited_row_ranges { for new_edited_row_range in new_edited_row_ranges {
let suggestions = snapshot let suggestions = snapshot
.suggest_autoindents(new_edited_row_range.clone()) .suggest_autoindents(new_edited_row_range.clone())
@ -847,13 +960,22 @@ impl Buffer {
.flatten(); .flatten();
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
if let Some(suggestion) = suggestion { if let Some(suggestion) = suggestion {
// Find the indent size based on the language for this row.
while let Some((row, size)) = language_indent_sizes.peek() {
if *row > new_row {
break;
}
language_indent_size = *size;
language_indent_sizes.next();
}
let suggested_indent = indent_sizes let suggested_indent = indent_sizes
.get(&suggestion.basis_row) .get(&suggestion.basis_row)
.copied() .copied()
.unwrap_or_else(|| { .unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row) snapshot.indent_size_for_line(suggestion.basis_row)
}) })
.with_delta(suggestion.delta, request.indent_size); .with_delta(suggestion.delta, language_indent_size);
if old_suggestions if old_suggestions
.get(&new_row) .get(&new_row)
.map_or(true, |old_indentation| { .map_or(true, |old_indentation| {
@ -965,16 +1087,30 @@ impl Buffer {
let old_text = old_text.to_string(); let old_text = old_text.to_string();
let line_ending = LineEnding::detect(&new_text); let line_ending = LineEnding::detect(&new_text);
LineEnding::normalize(&mut new_text); LineEnding::normalize(&mut new_text);
let changes = TextDiff::from_chars(old_text.as_str(), new_text.as_str()) let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
.iter_all_changes() let mut edits = Vec::new();
.map(|c| (c.tag(), c.value().len())) let mut offset = 0;
.collect::<Vec<_>>(); let empty: Arc<str> = "".into();
for change in diff.iter_all_changes() {
let value = change.value();
let end_offset = offset + value.len();
match change.tag() {
ChangeTag::Equal => {
offset = end_offset;
}
ChangeTag::Delete => {
edits.push((offset..end_offset, empty.clone()));
offset = end_offset;
}
ChangeTag::Insert => {
edits.push((offset..offset, value.into()));
}
}
}
Diff { Diff {
base_version, base_version,
new_text: new_text.into(),
changes,
line_ending, line_ending,
start_offset: 0, edits,
} }
}) })
} }
@ -984,28 +1120,7 @@ impl Buffer {
self.finalize_last_transaction(); self.finalize_last_transaction();
self.start_transaction(); self.start_transaction();
self.text.set_line_ending(diff.line_ending); self.text.set_line_ending(diff.line_ending);
let mut offset = diff.start_offset; self.edit(diff.edits, None, cx);
for (tag, len) in diff.changes {
let range = offset..(offset + len);
match tag {
ChangeTag::Equal => offset += len,
ChangeTag::Delete => {
self.edit([(range, "")], None, cx);
}
ChangeTag::Insert => {
self.edit(
[(
offset..offset,
&diff.new_text[range.start - diff.start_offset
..range.end - diff.start_offset],
)],
None,
cx,
);
offset += len;
}
}
}
if self.end_transaction(cx).is_some() { if self.end_transaction(cx).is_some() {
self.finalize_last_transaction() self.finalize_last_transaction()
} else { } else {
@ -1184,7 +1299,6 @@ impl Buffer {
let edit_id = edit_operation.local_timestamp(); let edit_id = edit_operation.local_timestamp();
if let Some((before_edit, mode)) = autoindent_request { if let Some((before_edit, mode)) = autoindent_request {
let indent_size = before_edit.single_indent_size(cx);
let (start_columns, is_block_mode) = match mode { let (start_columns, is_block_mode) = match mode {
AutoindentMode::Block { AutoindentMode::Block {
original_indent_columns: start_columns, original_indent_columns: start_columns,
@ -1233,6 +1347,7 @@ impl Buffer {
AutoindentRequestEntry { AutoindentRequestEntry {
first_line_is_new, first_line_is_new,
original_indent_column: start_column, original_indent_column: start_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: self.anchor_before(new_start + range_of_insertion_to_indent.start) range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
..self.anchor_after(new_start + range_of_insertion_to_indent.end), ..self.anchor_after(new_start + range_of_insertion_to_indent.end),
} }
@ -1242,7 +1357,6 @@ impl Buffer {
self.autoindent_requests.push(Arc::new(AutoindentRequest { self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit, before_edit,
entries, entries,
indent_size,
is_block_mode, is_block_mode,
})); }));
} }
@ -1560,8 +1674,8 @@ impl BufferSnapshot {
indent_size_for_line(self, row) indent_size_for_line(self, row)
} }
pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize { pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let language_name = self.language().map(|language| language.name()); let language_name = self.language_at(position).map(|language| language.name());
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
if settings.hard_tabs(language_name.as_deref()) { if settings.hard_tabs(language_name.as_deref()) {
IndentSize::tab() IndentSize::tab()
@ -1631,6 +1745,8 @@ impl BufferSnapshot {
if capture.index == config.indent_capture_ix { if capture.index == config.indent_capture_ix {
start.get_or_insert(Point::from_ts_point(capture.node.start_position())); start.get_or_insert(Point::from_ts_point(capture.node.start_position()));
end.get_or_insert(Point::from_ts_point(capture.node.end_position())); end.get_or_insert(Point::from_ts_point(capture.node.end_position()));
} else if Some(capture.index) == config.start_capture_ix {
start = Some(Point::from_ts_point(capture.node.end_position()));
} else if Some(capture.index) == config.end_capture_ix { } else if Some(capture.index) == config.end_capture_ix {
end = Some(Point::from_ts_point(capture.node.start_position())); end = Some(Point::from_ts_point(capture.node.start_position()));
} }
@ -1820,8 +1936,14 @@ impl BufferSnapshot {
} }
} }
pub fn language(&self) -> Option<&Arc<Language>> { pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
self.language.as_ref() let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.last()
.map(|info| info.language)
.or(self.language.as_ref())
} }
pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) { pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
@ -1856,8 +1978,8 @@ impl BufferSnapshot {
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> { pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
let range = range.start.to_offset(self)..range.end.to_offset(self); let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<Range<usize>> = None; let mut result: Option<Range<usize>> = None;
'outer: for (_, _, node) in self.syntax.layers_for_range(range.clone(), &self.text) { 'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
let mut cursor = node.walk(); let mut cursor = layer.node.walk();
// Descend to the first leaf that touches the start of the range, // Descend to the first leaf that touches the start of the range,
// and if the range is non-empty, extends beyond the start. // and if the range is non-empty, extends beyond the start.
@ -2139,6 +2261,13 @@ impl BufferSnapshot {
}) })
} }
pub fn git_diff_hunks_in_range<'a>(
&'a self,
query_row_range: Range<u32>,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff.hunks_in_range(query_row_range, self)
}
pub fn diagnostics_in_range<'a, T, O>( pub fn diagnostics_in_range<'a, T, O>(
&'a self, &'a self,
search_range: Range<T>, search_range: Range<T>,
@ -2186,6 +2315,10 @@ impl BufferSnapshot {
pub fn file_update_count(&self) -> usize { pub fn file_update_count(&self) -> usize {
self.file_update_count self.file_update_count
} }
pub fn git_diff_update_count(&self) -> usize {
self.git_diff_update_count
}
} }
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
@ -2212,6 +2345,7 @@ impl Clone for BufferSnapshot {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
text: self.text.clone(), text: self.text.clone(),
git_diff: self.git_diff.clone(),
syntax: self.syntax.clone(), syntax: self.syntax.clone(),
file: self.file.clone(), file: self.file.clone(),
remote_selections: self.remote_selections.clone(), remote_selections: self.remote_selections.clone(),
@ -2219,6 +2353,7 @@ impl Clone for BufferSnapshot {
selections_update_count: self.selections_update_count, selections_update_count: self.selections_update_count,
diagnostics_update_count: self.diagnostics_update_count, diagnostics_update_count: self.diagnostics_update_count,
file_update_count: self.file_update_count, file_update_count: self.file_update_count,
git_diff_update_count: self.git_diff_update_count,
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
} }

View file

@ -14,7 +14,7 @@ use std::{
}; };
use text::network::Network; use text::network::Network;
use unindent::Unindent as _; use unindent::Unindent as _;
use util::post_inc; use util::{post_inc, test::marked_text_ranges};
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]
@ -1035,6 +1035,120 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) {
}); });
} }
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) {
cx.set_global({
let mut settings = Settings::test(cx);
settings.language_overrides.extend([
(
"HTML".into(),
settings::EditorSettings {
tab_size: Some(2.try_into().unwrap()),
..Default::default()
},
),
(
"JavaScript".into(),
settings::EditorSettings {
tab_size: Some(8.try_into().unwrap()),
..Default::default()
},
),
]);
settings
});
let html_language = Arc::new(
Language::new(
LanguageConfig {
name: "HTML".into(),
..Default::default()
},
Some(tree_sitter_html::language()),
)
.with_indents_query(
"
(element
(start_tag) @start
(end_tag)? @end) @indent
",
)
.unwrap()
.with_injection_query(
r#"
(script_element
(raw_text) @content
(#set! "language" "javascript"))
"#,
)
.unwrap(),
);
let javascript_language = Arc::new(
Language::new(
LanguageConfig {
name: "JavaScript".into(),
..Default::default()
},
Some(tree_sitter_javascript::language()),
)
.with_indents_query(
r#"
(object "}" @end) @indent
"#,
)
.unwrap(),
);
let language_registry = Arc::new(LanguageRegistry::test());
language_registry.add(html_language.clone());
language_registry.add(javascript_language.clone());
cx.add_model(|cx| {
let (text, ranges) = marked_text_ranges(
&"
<div>ˇ
</div>
<script>
init({ˇ
})
</script>
<span>ˇ
</span>
"
.unindent(),
false,
);
let mut buffer = Buffer::new(0, text, cx);
buffer.set_language_registry(language_registry);
buffer.set_language(Some(html_language), cx);
buffer.edit(
ranges.into_iter().map(|range| (range, "\na")),
Some(AutoindentMode::EachLine),
cx,
);
assert_eq!(
buffer.text(),
"
<div>
a
</div>
<script>
init({
a
})
</script>
<span>
a
</span>
"
.unindent()
);
buffer
});
}
#[gpui::test] #[gpui::test]
fn test_serialization(cx: &mut gpui::MutableAppContext) { fn test_serialization(cx: &mut gpui::MutableAppContext) {
let mut now = Instant::now(); let mut now = Instant::now();
@ -1449,7 +1563,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
buffer.read_with(cx, |buffer, _| { buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot(); let snapshot = buffer.snapshot();
let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
layers[0].2.to_sexp() layers[0].node.to_sexp()
}) })
} }

View file

@ -4,8 +4,9 @@ mod highlight_map;
mod outline; mod outline;
pub mod proto; pub mod proto;
mod syntax_map; mod syntax_map;
#[cfg(test)] #[cfg(test)]
mod tests; mod buffer_tests;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
@ -26,6 +27,7 @@ use serde_json::Value;
use std::{ use std::{
any::Any, any::Any,
cell::RefCell, cell::RefCell,
fmt::Debug,
mem, mem,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -135,7 +137,7 @@ impl CachedLspAdapter {
pub async fn label_for_completion( pub async fn label_for_completion(
&self, &self,
completion_item: &lsp::CompletionItem, completion_item: &lsp::CompletionItem,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
self.adapter self.adapter
.label_for_completion(completion_item, language) .label_for_completion(completion_item, language)
@ -146,7 +148,7 @@ impl CachedLspAdapter {
&self, &self,
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await self.adapter.label_for_symbol(name, kind, language).await
} }
@ -175,7 +177,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
_: &lsp::CompletionItem, _: &lsp::CompletionItem,
_: &Language, _: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
None None
} }
@ -184,7 +186,7 @@ pub trait LspAdapter: 'static + Send + Sync {
&self, &self,
_: &str, _: &str,
_: lsp::SymbolKind, _: lsp::SymbolKind,
_: &Language, _: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
None None
} }
@ -230,7 +232,10 @@ pub struct LanguageConfig {
pub decrease_indent_pattern: Option<Regex>, pub decrease_indent_pattern: Option<Regex>,
#[serde(default)] #[serde(default)]
pub autoclose_before: String, pub autoclose_before: String,
pub line_comment: Option<String>, #[serde(default)]
pub line_comment: Option<Arc<str>>,
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
} }
impl Default for LanguageConfig { impl Default for LanguageConfig {
@ -244,6 +249,7 @@ impl Default for LanguageConfig {
decrease_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(),
autoclose_before: Default::default(), autoclose_before: Default::default(),
line_comment: Default::default(), line_comment: Default::default(),
block_comment: Default::default(),
} }
} }
} }
@ -270,7 +276,7 @@ pub struct FakeLspAdapter {
pub disk_based_diagnostics_sources: Vec<String>, pub disk_based_diagnostics_sources: Vec<String>,
} }
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
pub struct BracketPair { pub struct BracketPair {
pub start: String, pub start: String,
pub end: String, pub end: String,
@ -304,6 +310,7 @@ pub struct Grammar {
struct IndentConfig { struct IndentConfig {
query: Query, query: Query,
indent_capture_ix: u32, indent_capture_ix: u32,
start_capture_ix: Option<u32>,
end_capture_ix: Option<u32>, end_capture_ix: Option<u32>,
} }
@ -661,11 +668,13 @@ impl Language {
let grammar = self.grammar_mut(); let grammar = self.grammar_mut();
let query = Query::new(grammar.ts_language, source)?; let query = Query::new(grammar.ts_language, source)?;
let mut indent_capture_ix = None; let mut indent_capture_ix = None;
let mut start_capture_ix = None;
let mut end_capture_ix = None; let mut end_capture_ix = None;
get_capture_indices( get_capture_indices(
&query, &query,
&mut [ &mut [
("indent", &mut indent_capture_ix), ("indent", &mut indent_capture_ix),
("start", &mut start_capture_ix),
("end", &mut end_capture_ix), ("end", &mut end_capture_ix),
], ],
); );
@ -673,6 +682,7 @@ impl Language {
grammar.indents_config = Some(IndentConfig { grammar.indents_config = Some(IndentConfig {
query, query,
indent_capture_ix, indent_capture_ix,
start_capture_ix,
end_capture_ix, end_capture_ix,
}); });
} }
@ -763,8 +773,15 @@ impl Language {
self.config.name.clone() self.config.name.clone()
} }
pub fn line_comment_prefix(&self) -> Option<&str> { pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
self.config.line_comment.as_deref() self.config.line_comment.as_ref()
}
pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
self.config
.block_comment
.as_ref()
.map(|(start, end)| (start, end))
} }
pub async fn disk_based_diagnostic_sources(&self) -> &[String] { pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
@ -789,7 +806,7 @@ impl Language {
} }
pub async fn label_for_completion( pub async fn label_for_completion(
&self, self: &Arc<Self>,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
self.adapter self.adapter
@ -798,7 +815,11 @@ impl Language {
.await .await
} }
pub async fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> { pub async fn label_for_symbol(
self: &Arc<Self>,
name: &str,
kind: lsp::SymbolKind,
) -> Option<CodeLabel> {
self.adapter self.adapter
.as_ref()? .as_ref()?
.label_for_symbol(name, kind, self) .label_for_symbol(name, kind, self)
@ -806,20 +827,17 @@ impl Language {
} }
pub fn highlight_text<'a>( pub fn highlight_text<'a>(
&'a self, self: &'a Arc<Self>,
text: &'a Rope, text: &'a Rope,
range: Range<usize>, range: Range<usize>,
) -> Vec<(Range<usize>, HighlightId)> { ) -> Vec<(Range<usize>, HighlightId)> {
let mut result = Vec::new(); let mut result = Vec::new();
if let Some(grammar) = &self.grammar { if let Some(grammar) = &self.grammar {
let tree = grammar.parse_text(text, None); let tree = grammar.parse_text(text, None);
let captures = SyntaxSnapshot::single_tree_captures( let captures =
range.clone(), SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| {
text, grammar.highlights_query.as_ref()
&tree, });
grammar,
|grammar| grammar.highlights_query.as_ref(),
);
let highlight_maps = vec![grammar.highlight_map()]; let highlight_maps = vec![grammar.highlight_map()];
let mut offset = 0; let mut offset = 0;
for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) { for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
@ -861,6 +879,14 @@ impl Language {
} }
} }
impl Debug for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Language")
.field("name", &self.config.name)
.finish()
}
}
impl Grammar { impl Grammar {
pub fn id(&self) -> usize { pub fn id(&self) -> usize {
self.id self.id

View file

@ -92,6 +92,13 @@ struct SyntaxLayer {
language: Arc<Language>, language: Arc<Language>,
} }
#[derive(Debug)]
pub struct SyntaxLayerInfo<'a> {
pub depth: usize,
pub node: Node<'a>,
pub language: &'a Arc<Language>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SyntaxLayerSummary { struct SyntaxLayerSummary {
min_depth: usize, min_depth: usize,
@ -473,13 +480,18 @@ impl SyntaxSnapshot {
range: Range<usize>, range: Range<usize>,
text: &'a Rope, text: &'a Rope,
tree: &'a Tree, tree: &'a Tree,
grammar: &'a Grammar, language: &'a Arc<Language>,
query: fn(&Grammar) -> Option<&Query>, query: fn(&Grammar) -> Option<&Query>,
) -> SyntaxMapCaptures<'a> { ) -> SyntaxMapCaptures<'a> {
SyntaxMapCaptures::new( SyntaxMapCaptures::new(
range.clone(), range.clone(),
text, text,
[(grammar, 0, tree.root_node())].into_iter(), [SyntaxLayerInfo {
language,
depth: 0,
node: tree.root_node(),
}]
.into_iter(),
query, query,
) )
} }
@ -513,19 +525,19 @@ impl SyntaxSnapshot {
} }
#[cfg(test)] #[cfg(test)]
pub fn layers(&self, buffer: &BufferSnapshot) -> Vec<(&Grammar, usize, Node)> { pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayerInfo> {
self.layers_for_range(0..buffer.len(), buffer) self.layers_for_range(0..buffer.len(), buffer).collect()
} }
pub fn layers_for_range<'a, T: ToOffset>( pub fn layers_for_range<'a, T: ToOffset>(
&self, &'a self,
range: Range<T>, range: Range<T>,
buffer: &BufferSnapshot, buffer: &'a BufferSnapshot,
) -> Vec<(&Grammar, usize, Node)> { ) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
let start = buffer.anchor_before(range.start.to_offset(buffer)); let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = self.layers.filter::<_, ()>(|summary| { let mut cursor = self.layers.filter::<_, ()>(move |summary| {
if summary.max_depth > summary.min_depth { if summary.max_depth > summary.min_depth {
true true
} else { } else {
@ -535,23 +547,26 @@ impl SyntaxSnapshot {
} }
}); });
let mut result = Vec::new(); // let mut result = Vec::new();
cursor.next(buffer); cursor.next(buffer);
while let Some(layer) = cursor.item() { std::iter::from_fn(move || {
if let Some(grammar) = &layer.language.grammar { if let Some(layer) = cursor.item() {
result.push(( let info = SyntaxLayerInfo {
grammar.as_ref(), language: &layer.language,
layer.depth, depth: layer.depth,
layer.tree.root_node_with_offset( node: layer.tree.root_node_with_offset(
layer.range.start.to_offset(buffer), layer.range.start.to_offset(buffer),
layer.range.start.to_point(buffer).to_ts_point(), layer.range.start.to_point(buffer).to_ts_point(),
), ),
)); };
} cursor.next(buffer);
cursor.next(buffer) Some(info)
} else {
None
} }
})
result // result
} }
} }
@ -559,7 +574,7 @@ impl<'a> SyntaxMapCaptures<'a> {
fn new( fn new(
range: Range<usize>, range: Range<usize>,
text: &'a Rope, text: &'a Rope,
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>, layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
query: fn(&Grammar) -> Option<&Query>, query: fn(&Grammar) -> Option<&Query>,
) -> Self { ) -> Self {
let mut result = Self { let mut result = Self {
@ -567,11 +582,19 @@ impl<'a> SyntaxMapCaptures<'a> {
grammars: Vec::new(), grammars: Vec::new(),
active_layer_count: 0, active_layer_count: 0,
}; };
for (grammar, depth, node) in layers { for SyntaxLayerInfo {
let query = if let Some(query) = query(grammar) { language,
query depth,
} else { node,
continue; } in layers
{
let grammar = match &language.grammar {
Some(grammer) => grammer,
None => continue,
};
let query = match query(&grammar) {
Some(query) => query,
None => continue,
}; };
let mut query_cursor = QueryCursorHandle::new(); let mut query_cursor = QueryCursorHandle::new();
@ -678,15 +701,23 @@ impl<'a> SyntaxMapMatches<'a> {
fn new( fn new(
range: Range<usize>, range: Range<usize>,
text: &'a Rope, text: &'a Rope,
layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>, layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
query: fn(&Grammar) -> Option<&Query>, query: fn(&Grammar) -> Option<&Query>,
) -> Self { ) -> Self {
let mut result = Self::default(); let mut result = Self::default();
for (grammar, depth, node) in layers { for SyntaxLayerInfo {
let query = if let Some(query) = query(grammar) { language,
query depth,
} else { node,
continue; } in layers
{
let grammar = match &language.grammar {
Some(grammer) => grammer,
None => continue,
};
let query = match query(&grammar) {
Some(query) => query,
None => continue,
}; };
let mut query_cursor = QueryCursorHandle::new(); let mut query_cursor = QueryCursorHandle::new();
@ -1624,8 +1655,8 @@ mod tests {
let reference_layers = reference_syntax_map.layers(&buffer); let reference_layers = reference_syntax_map.layers(&buffer);
for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter())
{ {
assert_eq!(edited_layer.2.to_sexp(), reference_layer.2.to_sexp()); assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp());
assert_eq!(edited_layer.2.range(), reference_layer.2.range()); assert_eq!(edited_layer.node.range(), reference_layer.node.range());
} }
} }
@ -1770,13 +1801,13 @@ mod tests {
mutated_layers.into_iter().zip(reference_layers.into_iter()) mutated_layers.into_iter().zip(reference_layers.into_iter())
{ {
assert_eq!( assert_eq!(
edited_layer.2.to_sexp(), edited_layer.node.to_sexp(),
reference_layer.2.to_sexp(), reference_layer.node.to_sexp(),
"different layer at step {i}" "different layer at step {i}"
); );
assert_eq!( assert_eq!(
edited_layer.2.range(), edited_layer.node.range(),
reference_layer.2.range(), reference_layer.node.range(),
"different layer at step {i}" "different layer at step {i}"
); );
} }
@ -1822,13 +1853,15 @@ mod tests {
range: Range<Point>, range: Range<Point>,
expected_layers: &[&str], expected_layers: &[&str],
) { ) {
let layers = syntax_map.layers_for_range(range, &buffer); let layers = syntax_map
.layers_for_range(range, &buffer)
.collect::<Vec<_>>();
assert_eq!( assert_eq!(
layers.len(), layers.len(),
expected_layers.len(), expected_layers.len(),
"wrong number of layers" "wrong number of layers"
); );
for (i, ((_, _, node), expected_s_exp)) in for (i, (SyntaxLayerInfo { node, .. }, expected_s_exp)) in
layers.iter().zip(expected_layers.iter()).enumerate() layers.iter().zip(expected_layers.iter()).enumerate()
{ {
let actual_s_exp = node.to_sexp(); let actual_s_exp = node.to_sexp();

View file

@ -10,6 +10,7 @@ doctest = false
[features] [features]
test-support = [ test-support = [
"client/test-support", "client/test-support",
"db/test-support",
"language/test-support", "language/test-support",
"settings/test-support", "settings/test-support",
"text/test-support", "text/test-support",
@ -20,8 +21,10 @@ text = { path = "../text" }
client = { path = "../client" } client = { path = "../client" }
clock = { path = "../clock" } clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
db = { path = "../db" }
fsevent = { path = "../fsevent" } fsevent = { path = "../fsevent" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
@ -54,6 +57,7 @@ rocksdb = "0.18"
[dev-dependencies] [dev-dependencies]
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }

View file

@ -1,8 +1,11 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use fsevent::EventStream; use fsevent::EventStream;
use futures::{future::BoxFuture, Stream, StreamExt}; use futures::{future::BoxFuture, Stream, StreamExt};
use git::repository::{GitRepository, LibGitRepository};
use language::LineEnding; use language::LineEnding;
use parking_lot::Mutex as SyncMutex;
use smol::io::{AsyncReadExt, AsyncWriteExt}; use smol::io::{AsyncReadExt, AsyncWriteExt};
use std::sync::Arc;
use std::{ use std::{
io, io,
os::unix::fs::MetadataExt, os::unix::fs::MetadataExt,
@ -11,13 +14,16 @@ use std::{
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use text::Rope; use text::Rope;
use util::ResultExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap}; use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use futures::lock::Mutex; use futures::lock::Mutex;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use std::sync::{Arc, Weak}; use git::repository::FakeGitRepositoryState;
#[cfg(any(test, feature = "test-support"))]
use std::sync::Weak;
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait Fs: Send + Sync { pub trait Fs: Send + Sync {
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
path: &Path, path: &Path,
latency: Duration, latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>; ) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
fn is_fake(&self) -> bool; fn is_fake(&self) -> bool;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
fn as_fake(&self) -> &FakeFs; fn as_fake(&self) -> &FakeFs;
@ -235,6 +242,14 @@ impl Fs for RealFs {
}))) })))
} }
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
LibGitRepository::open(&dotgit_path)
.log_err()
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
Some(Arc::new(SyncMutex::new(libgit_repository)))
})
}
fn is_fake(&self) -> bool { fn is_fake(&self) -> bool {
false false
} }
@ -270,6 +285,7 @@ enum FakeFsEntry {
inode: u64, inode: u64,
mtime: SystemTime, mtime: SystemTime,
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>, entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
}, },
Symlink { Symlink {
target: PathBuf, target: PathBuf,
@ -384,6 +400,7 @@ impl FakeFs {
inode: 0, inode: 0,
mtime: SystemTime::now(), mtime: SystemTime::now(),
entries: Default::default(), entries: Default::default(),
git_repo_state: None,
})), })),
next_inode: 1, next_inode: 1,
event_txs: Default::default(), event_txs: Default::default(),
@ -473,6 +490,28 @@ impl FakeFs {
.boxed() .boxed()
} }
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
let mut state = self.state.lock().await;
let entry = state.read_path(dot_git).await.unwrap();
let mut entry = entry.lock().await;
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let mut repo_state = repo_state.lock();
repo_state.index_contents.clear();
repo_state.index_contents.extend(
head_state
.iter()
.map(|(path, content)| (path.to_path_buf(), content.clone())),
);
state.emit_event([dot_git]);
} else {
panic!("not a directory");
}
}
pub async fn files(&self) -> Vec<PathBuf> { pub async fn files(&self) -> Vec<PathBuf> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut queue = collections::VecDeque::new(); let mut queue = collections::VecDeque::new();
@ -562,6 +601,7 @@ impl Fs for FakeFs {
inode, inode,
mtime: SystemTime::now(), mtime: SystemTime::now(),
entries: Default::default(), entries: Default::default(),
git_repo_state: None,
})) }))
}); });
Ok(()) Ok(())
@ -846,6 +886,24 @@ impl Fs for FakeFs {
})) }))
} }
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
smol::block_on(async move {
let state = self.state.lock().await;
let entry = state.read_path(abs_dot_git).await.unwrap();
let mut entry = entry.lock().await;
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let state = git_repo_state
.get_or_insert_with(|| {
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
})
.clone();
Some(git::repository::FakeGitRepository::open(state))
} else {
None
}
})
}
fn is_fake(&self) -> bool { fn is_fake(&self) -> bool {
true true
} }

View file

@ -1,4 +1,3 @@
mod db;
pub mod fs; pub mod fs;
mod ignore; mod ignore;
mod lsp_command; mod lsp_command;
@ -13,6 +12,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
use gpui::{ use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
@ -62,7 +62,7 @@ use std::{
time::Instant, time::Instant,
}; };
use thiserror::Error; use thiserror::Error;
use util::{post_inc, ResultExt, TryFutureExt as _}; use util::{defer, post_inc, ResultExt, TryFutureExt as _};
pub use db::Db; pub use db::Db;
pub use fs::*; pub use fs::*;
@ -123,6 +123,7 @@ pub struct Project {
opened_buffers: HashMap<u64, OpenBuffer>, opened_buffers: HashMap<u64, OpenBuffer>,
incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>, incomplete_buffers: HashMap<u64, ModelHandle<Buffer>>,
buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>, buffer_snapshots: HashMap<u64, Vec<(i32, TextBufferSnapshot)>>,
buffers_being_formatted: HashSet<usize>,
nonce: u128, nonce: u128,
_maintain_buffer_languages: Task<()>, _maintain_buffer_languages: Task<()>,
} }
@ -407,6 +408,7 @@ impl Project {
client.add_model_request_handler(Self::handle_open_buffer_by_id); client.add_model_request_handler(Self::handle_open_buffer_by_id);
client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base);
} }
pub fn local( pub fn local(
@ -466,6 +468,7 @@ impl Project {
language_server_statuses: Default::default(), language_server_statuses: Default::default(),
last_workspace_edits_by_language_server: Default::default(), last_workspace_edits_by_language_server: Default::default(),
language_server_settings: Default::default(), language_server_settings: Default::default(),
buffers_being_formatted: Default::default(),
next_language_server_id: 0, next_language_server_id: 0,
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
} }
@ -562,6 +565,7 @@ impl Project {
last_workspace_edits_by_language_server: Default::default(), last_workspace_edits_by_language_server: Default::default(),
next_language_server_id: 0, next_language_server_id: 0,
opened_buffers: Default::default(), opened_buffers: Default::default(),
buffers_being_formatted: Default::default(),
buffer_snapshots: Default::default(), buffer_snapshots: Default::default(),
nonce: StdRng::from_entropy().gen(), nonce: StdRng::from_entropy().gen(),
}; };
@ -604,7 +608,7 @@ impl Project {
let languages = Arc::new(LanguageRegistry::test()); let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response(); let http_client = client::test::FakeHttpClient::with_404_response();
let client = client::Client::new(http_client.clone()); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project_store = cx.add_model(|_| ProjectStore::new()); let project_store = cx.add_model(|_| ProjectStore::new());
let project = let project =
@ -2804,7 +2808,26 @@ impl Project {
.await?; .await?;
} }
for (buffer, buffer_abs_path, language_server) in local_buffers { // Do not allow multiple concurrent formatting requests for the
// same buffer.
this.update(&mut cx, |this, _| {
local_buffers
.retain(|(buffer, _, _)| this.buffers_being_formatted.insert(buffer.id()));
});
let _cleanup = defer({
let this = this.clone();
let mut cx = cx.clone();
let local_buffers = &local_buffers;
move || {
this.update(&mut cx, |this, _| {
for (buffer, _, _) in local_buffers {
this.buffers_being_formatted.remove(&buffer.id());
}
});
}
});
for (buffer, buffer_abs_path, language_server) in &local_buffers {
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| { let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
let language_name = buffer.language().map(|language| language.name()); let language_name = buffer.language().map(|language| language.name());
@ -2856,7 +2879,7 @@ impl Project {
buffer.forget_transaction(transaction.id) buffer.forget_transaction(transaction.id)
}); });
} }
project_transaction.0.insert(buffer, transaction); project_transaction.0.insert(buffer.clone(), transaction);
} }
} }
@ -4229,8 +4252,11 @@ impl Project {
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) { fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
cx.observe(worktree, |_, _, cx| cx.notify()).detach(); cx.observe(worktree, |_, _, cx| cx.notify()).detach();
if worktree.read(cx).is_local() { if worktree.read(cx).is_local() {
cx.subscribe(worktree, |this, worktree, _, cx| { cx.subscribe(worktree, |this, worktree, event, cx| match event {
this.update_local_worktree_buffers(worktree, cx); worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
worktree::Event::UpdatedGitRepositories(updated_repos) => {
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
}
}) })
.detach(); .detach();
} }
@ -4338,6 +4364,63 @@ impl Project {
} }
} }
fn update_local_worktree_buffers_git_repos(
&mut self,
worktree: ModelHandle<Worktree>,
repos: &[GitRepositoryEntry],
cx: &mut ModelContext<Self>,
) {
for (_, buffer) in &self.opened_buffers {
if let Some(buffer) = buffer.upgrade(cx) {
let file = match File::from_dyn(buffer.read(cx).file()) {
Some(file) => file,
None => continue,
};
if file.worktree != worktree {
continue;
}
let path = file.path().clone();
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
Some(repo) => repo.clone(),
None => return,
};
let relative_repo = match path.strip_prefix(repo.content_path) {
Ok(relative_repo) => relative_repo.to_owned(),
Err(_) => return,
};
let remote_id = self.remote_id();
let client = self.client.clone();
cx.spawn(|_, mut cx| async move {
let diff_base = cx
.background()
.spawn(async move { repo.repo.lock().load_index_text(&relative_repo) })
.await;
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
buffer.update_diff_base(diff_base.clone(), cx);
buffer.remote_id()
});
if let Some(project_id) = remote_id {
client
.send(proto::UpdateDiffBase {
project_id,
buffer_id: buffer_id as u64,
diff_base,
})
.log_err();
}
})
.detach();
}
}
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) { pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| { let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@ -4861,6 +4944,27 @@ impl Project {
}) })
} }
async fn handle_update_diff_base(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateDiffBase>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let buffer_id = envelope.payload.buffer_id;
let diff_base = envelope.payload.diff_base;
let buffer = this
.opened_buffers
.get_mut(&buffer_id)
.and_then(|b| b.upgrade(cx))
.ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?;
buffer.update(cx, |buffer, cx| buffer.update_diff_base(diff_base, cx));
Ok(())
})
}
async fn handle_update_buffer_file( async fn handle_update_buffer_file(
this: ModelHandle<Self>, this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateBufferFile>, envelope: TypedEnvelope<proto::UpdateBufferFile>,
@ -5427,7 +5531,7 @@ impl Project {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Buffer>>> { ) -> Task<Result<ModelHandle<Buffer>>> {
let mut opened_buffer_rx = self.opened_buffer.1.clone(); let mut opened_buffer_rx = self.opened_buffer.1.clone();
cx.spawn(|this, cx| async move { cx.spawn(|this, mut cx| async move {
let buffer = loop { let buffer = loop {
let buffer = this.read_with(&cx, |this, cx| { let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers this.opened_buffers
@ -5445,6 +5549,7 @@ impl Project {
.await .await
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?; .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
}; };
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
Ok(buffer) Ok(buffer)
}) })
} }

View file

@ -1,10 +1,9 @@
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
use super::{ use super::{
fs::{self, Fs}, fs::{self, Fs},
ignore::IgnoreStack, ignore::IgnoreStack,
DiagnosticSummary, DiagnosticSummary,
}; };
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{proto, Client}; use client::{proto, Client};
@ -18,6 +17,8 @@ use futures::{
Stream, StreamExt, Stream, StreamExt,
}; };
use fuzzy::CharBag; use fuzzy::CharBag;
use git::repository::GitRepository;
use git::{DOT_GIT, GITIGNORE};
use gpui::{ use gpui::{
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task, Task,
@ -26,12 +27,12 @@ use language::{
proto::{deserialize_version, serialize_line_ending, serialize_version}, proto::{deserialize_version, serialize_line_ending, serialize_version},
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope, Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
}; };
use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use postage::{ use postage::{
prelude::{Sink as _, Stream as _}, prelude::{Sink as _, Stream as _},
watch, watch,
}; };
use smol::channel::{self, Sender}; use smol::channel::{self, Sender};
use std::{ use std::{
any::Any, any::Any,
@ -40,6 +41,7 @@ use std::{
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
fmt, fmt,
future::Future, future::Future,
mem,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
os::unix::prelude::{OsStrExt, OsStringExt}, os::unix::prelude::{OsStrExt, OsStringExt},
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -50,10 +52,6 @@ use std::{
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
lazy_static! {
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize); pub struct WorktreeId(usize);
@ -101,15 +99,51 @@ pub struct Snapshot {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct GitRepositoryEntry {
pub(crate) repo: Arc<Mutex<dyn GitRepository>>,
pub(crate) scan_id: usize,
// Path to folder containing the .git file or directory
pub(crate) content_path: Arc<Path>,
// Path to the actual .git folder.
// Note: if .git is a file, this points to the folder indicated by the .git file
pub(crate) git_dir_path: Arc<Path>,
}
impl std::fmt::Debug for GitRepositoryEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GitRepositoryEntry")
.field("content_path", &self.content_path)
.field("git_dir_path", &self.git_dir_path)
.field("libgit_repository", &"LibGitRepository")
.finish()
}
}
pub struct LocalSnapshot { pub struct LocalSnapshot {
abs_path: Arc<Path>, abs_path: Arc<Path>,
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>, ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
git_repositories: Vec<GitRepositoryEntry>,
removed_entry_ids: HashMap<u64, ProjectEntryId>, removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>, next_entry_id: Arc<AtomicUsize>,
snapshot: Snapshot, snapshot: Snapshot,
extension_counts: HashMap<OsString, usize>, extension_counts: HashMap<OsString, usize>,
} }
impl Clone for LocalSnapshot {
fn clone(&self) -> Self {
Self {
abs_path: self.abs_path.clone(),
ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(),
git_repositories: self.git_repositories.iter().cloned().collect(),
removed_entry_ids: self.removed_entry_ids.clone(),
next_entry_id: self.next_entry_id.clone(),
snapshot: self.snapshot.clone(),
extension_counts: self.extension_counts.clone(),
}
}
}
impl Deref for LocalSnapshot { impl Deref for LocalSnapshot {
type Target = Snapshot; type Target = Snapshot;
@ -142,6 +176,7 @@ struct ShareState {
pub enum Event { pub enum Event {
UpdatedEntries, UpdatedEntries,
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
} }
impl Entity for Worktree { impl Entity for Worktree {
@ -372,6 +407,7 @@ impl LocalWorktree {
let mut snapshot = LocalSnapshot { let mut snapshot = LocalSnapshot {
abs_path, abs_path,
ignores_by_parent_abs_path: Default::default(), ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
removed_entry_ids: Default::default(), removed_entry_ids: Default::default(),
next_entry_id, next_entry_id,
snapshot: Snapshot { snapshot: Snapshot {
@ -446,10 +482,14 @@ impl LocalWorktree {
) -> Task<Result<ModelHandle<Buffer>>> { ) -> Task<Result<ModelHandle<Buffer>>> {
let path = Arc::from(path); let path = Arc::from(path);
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let (file, contents) = this let (file, contents, diff_base) = this
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx)) .update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
.await?; .await?;
Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx))) Ok(cx.add_model(|cx| {
let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx);
buffer.git_diff_recalc(cx);
buffer
}))
}) })
} }
@ -499,17 +539,37 @@ impl LocalWorktree {
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) { fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
self.poll_task.take(); self.poll_task.take();
match self.scan_state() { match self.scan_state() {
ScanState::Idle => { ScanState::Idle => {
self.snapshot = self.background_snapshot.lock().clone(); let new_snapshot = self.background_snapshot.lock().clone();
let updated_repos = Self::changed_repos(
&self.snapshot.git_repositories,
&new_snapshot.git_repositories,
);
self.snapshot = new_snapshot;
if let Some(share) = self.share.as_mut() { if let Some(share) = self.share.as_mut() {
*share.snapshots_tx.borrow_mut() = self.snapshot.clone(); *share.snapshots_tx.borrow_mut() = self.snapshot.clone();
} }
cx.emit(Event::UpdatedEntries); cx.emit(Event::UpdatedEntries);
if !updated_repos.is_empty() {
cx.emit(Event::UpdatedGitRepositories(updated_repos));
} }
}
ScanState::Initializing => { ScanState::Initializing => {
let is_fake_fs = self.fs.is_fake(); let is_fake_fs = self.fs.is_fake();
self.snapshot = self.background_snapshot.lock().clone();
let new_snapshot = self.background_snapshot.lock().clone();
let updated_repos = Self::changed_repos(
&self.snapshot.git_repositories,
&new_snapshot.git_repositories,
);
self.snapshot = new_snapshot;
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move { self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
if is_fake_fs { if is_fake_fs {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -521,17 +581,52 @@ impl LocalWorktree {
this.update(&mut cx, |this, cx| this.poll_snapshot(cx)); this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
} }
})); }));
cx.emit(Event::UpdatedEntries); cx.emit(Event::UpdatedEntries);
if !updated_repos.is_empty() {
cx.emit(Event::UpdatedGitRepositories(updated_repos));
} }
}
_ => { _ => {
if force { if force {
self.snapshot = self.background_snapshot.lock().clone(); self.snapshot = self.background_snapshot.lock().clone();
} }
} }
} }
cx.notify(); cx.notify();
} }
fn changed_repos(
old_repos: &[GitRepositoryEntry],
new_repos: &[GitRepositoryEntry],
) -> Vec<GitRepositoryEntry> {
fn diff<'a>(
a: &'a [GitRepositoryEntry],
b: &'a [GitRepositoryEntry],
updated: &mut HashMap<&'a Path, GitRepositoryEntry>,
) {
for a_repo in a {
let matched = b.iter().find(|b_repo| {
a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id
});
if matched.is_none() {
updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone());
}
}
}
let mut updated = HashMap::<&Path, GitRepositoryEntry>::default();
diff(old_repos, new_repos, &mut updated);
diff(new_repos, old_repos, &mut updated);
updated.into_values().collect()
}
pub fn scan_complete(&self) -> impl Future<Output = ()> { pub fn scan_complete(&self) -> impl Future<Output = ()> {
let mut scan_state_rx = self.last_scan_state_rx.clone(); let mut scan_state_rx = self.last_scan_state_rx.clone();
async move { async move {
@ -558,13 +653,33 @@ impl LocalWorktree {
} }
} }
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> { fn load(
&self,
path: &Path,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<(File, String, Option<String>)>> {
let handle = cx.handle(); let handle = cx.handle();
let path = Arc::from(path); let path = Arc::from(path);
let abs_path = self.absolutize(&path); let abs_path = self.absolutize(&path);
let fs = self.fs.clone(); let fs = self.fs.clone();
let snapshot = self.snapshot();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?; let text = fs.load(&abs_path).await?;
let diff_base = if let Some(repo) = snapshot.repo_for(&path) {
if let Ok(repo_relative) = path.strip_prefix(repo.content_path) {
let repo_relative = repo_relative.to_owned();
cx.background()
.spawn(async move { repo.repo.lock().load_index_text(&repo_relative) })
.await
} else {
None
}
} else {
None
};
// Eagerly populate the snapshot with an updated entry for the loaded file // Eagerly populate the snapshot with an updated entry for the loaded file
let entry = this let entry = this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
@ -573,6 +688,7 @@ impl LocalWorktree {
.refresh_entry(path, abs_path, None, cx) .refresh_entry(path, abs_path, None, cx)
}) })
.await?; .await?;
Ok(( Ok((
File { File {
entry_id: Some(entry.id), entry_id: Some(entry.id),
@ -582,6 +698,7 @@ impl LocalWorktree {
is_local: true, is_local: true,
}, },
text, text,
diff_base,
)) ))
}) })
} }
@ -1248,6 +1365,22 @@ impl LocalSnapshot {
&self.extension_counts &self.extension_counts
} }
// Gives the most specific git repository for a given path
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
self.git_repositories
.iter()
.rev() //git_repository is ordered lexicographically
.find(|repo| repo.manages(path))
.cloned()
}
pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepositoryEntry> {
// Git repositories cannot be nested, so we don't need to reverse the order
self.git_repositories
.iter_mut()
.find(|repo| repo.in_dot_git(path))
}
#[cfg(test)] #[cfg(test)]
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree { pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
let root_name = self.root_name.clone(); let root_name = self.root_name.clone();
@ -1330,7 +1463,7 @@ impl LocalSnapshot {
} }
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
let abs_path = self.abs_path.join(&entry.path); let abs_path = self.abs_path.join(&entry.path);
match smol::block_on(build_gitignore(&abs_path, fs)) { match smol::block_on(build_gitignore(&abs_path, fs)) {
Ok(ignore) => { Ok(ignore) => {
@ -1384,6 +1517,7 @@ impl LocalSnapshot {
parent_path: Arc<Path>, parent_path: Arc<Path>,
entries: impl IntoIterator<Item = Entry>, entries: impl IntoIterator<Item = Entry>,
ignore: Option<Arc<Gitignore>>, ignore: Option<Arc<Gitignore>>,
fs: &dyn Fs,
) { ) {
let mut parent_entry = if let Some(parent_entry) = let mut parent_entry = if let Some(parent_entry) =
self.entries_by_path.get(&PathKey(parent_path.clone()), &()) self.entries_by_path.get(&PathKey(parent_path.clone()), &())
@ -1409,6 +1543,27 @@ impl LocalSnapshot {
unreachable!(); unreachable!();
} }
if parent_path.file_name() == Some(&DOT_GIT) {
let abs_path = self.abs_path.join(&parent_path);
let content_path: Arc<Path> = parent_path.parent().unwrap().into();
if let Err(ix) = self
.git_repositories
.binary_search_by_key(&&content_path, |repo| &repo.content_path)
{
if let Some(repo) = fs.open_repo(abs_path.as_path()) {
self.git_repositories.insert(
ix,
GitRepositoryEntry {
repo,
scan_id: 0,
content_path,
git_dir_path: parent_path,
},
);
}
}
}
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
let mut entries_by_id_edits = Vec::new(); let mut entries_by_id_edits = Vec::new();
@ -1493,6 +1648,14 @@ impl LocalSnapshot {
{ {
*scan_id = self.snapshot.scan_id; *scan_id = self.snapshot.scan_id;
} }
} else if path.file_name() == Some(&DOT_GIT) {
let parent_path = path.parent().unwrap();
if let Ok(ix) = self
.git_repositories
.binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref())
{
self.git_repositories[ix].scan_id = self.snapshot.scan_id;
}
} }
} }
@ -1532,6 +1695,22 @@ impl LocalSnapshot {
ignore_stack ignore_stack
} }
pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] {
&self.git_repositories
}
}
impl GitRepositoryEntry {
// Note that these paths should be relative to the worktree root.
pub(crate) fn manages(&self, path: &Path) -> bool {
path.starts_with(self.content_path.as_ref())
}
// Note that theis path should be relative to the worktree root.
pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
path.starts_with(self.git_dir_path.as_ref())
}
} }
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> { async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
@ -2244,9 +2423,12 @@ impl BackgroundScanner {
new_entries.push(child_entry); new_entries.push(child_entry);
} }
self.snapshot self.snapshot.lock().populate_dir(
.lock() job.path.clone(),
.populate_dir(job.path.clone(), new_entries, new_ignore); new_entries,
new_ignore,
self.fs.as_ref(),
);
for new_job in new_jobs { for new_job in new_jobs {
job.scan_queue.send(new_job).await.unwrap(); job.scan_queue.send(new_job).await.unwrap();
} }
@ -2321,6 +2503,12 @@ impl BackgroundScanner {
fs_entry.is_ignored = ignore_stack.is_all(); fs_entry.is_ignored = ignore_stack.is_all();
snapshot.insert_entry(fs_entry, self.fs.as_ref()); snapshot.insert_entry(fs_entry, self.fs.as_ref());
let scan_id = snapshot.scan_id;
if let Some(repo) = snapshot.in_dot_git(&path) {
repo.repo.lock().reload_index();
repo.scan_id = scan_id;
}
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path); let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) { if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
ancestor_inodes.insert(metadata.inode); ancestor_inodes.insert(metadata.inode);
@ -2367,6 +2555,7 @@ impl BackgroundScanner {
self.snapshot.lock().removed_entry_ids.clear(); self.snapshot.lock().removed_entry_ids.clear();
self.update_ignore_statuses().await; self.update_ignore_statuses().await;
self.update_git_repositories();
true true
} }
@ -2432,6 +2621,13 @@ impl BackgroundScanner {
.await; .await;
} }
fn update_git_repositories(&self) {
let mut snapshot = self.snapshot.lock();
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
snapshot.git_repositories = git_repositories;
}
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
let mut ignore_stack = job.ignore_stack; let mut ignore_stack = job.ignore_stack;
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
@ -2778,6 +2974,7 @@ mod tests {
use anyhow::Result; use anyhow::Result;
use client::test::FakeHttpClient; use client::test::FakeHttpClient;
use fs::RealFs; use fs::RealFs;
use git::repository::FakeGitRepository;
use gpui::{executor::Deterministic, TestAppContext}; use gpui::{executor::Deterministic, TestAppContext};
use rand::prelude::*; use rand::prelude::*;
use serde_json::json; use serde_json::json;
@ -2786,6 +2983,7 @@ mod tests {
fmt::Write, fmt::Write,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use util::test::temp_tree; use util::test::temp_tree;
#[gpui::test] #[gpui::test]
@ -2804,7 +3002,7 @@ mod tests {
.await; .await;
let http_client = FakeHttpClient::with_404_response(); let http_client = FakeHttpClient::with_404_response();
let client = Client::new(http_client); let client = cx.read(|cx| Client::new(http_client, cx));
let tree = Worktree::local( let tree = Worktree::local(
client, client,
@ -2866,8 +3064,7 @@ mod tests {
fs.insert_symlink("/root/lib/a/lib", "..".into()).await; fs.insert_symlink("/root/lib/a/lib", "..".into()).await;
fs.insert_symlink("/root/lib/b/lib", "..".into()).await; fs.insert_symlink("/root/lib/b/lib", "..".into()).await;
let http_client = FakeHttpClient::with_404_response(); let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = Client::new(http_client);
let tree = Worktree::local( let tree = Worktree::local(
client, client,
Arc::from(Path::new("/root")), Arc::from(Path::new("/root")),
@ -2945,8 +3142,7 @@ mod tests {
})); }));
let dir = parent_dir.path().join("tree"); let dir = parent_dir.path().join("tree");
let http_client = FakeHttpClient::with_404_response(); let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = Client::new(http_client.clone());
let tree = Worktree::local( let tree = Worktree::local(
client, client,
@ -3007,6 +3203,135 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
let root = temp_tree(json!({
"dir1": {
".git": {},
"deps": {
"dep1": {
".git": {},
"src": {
"a.txt": ""
}
}
},
"src": {
"b.txt": ""
}
},
"c.txt": "",
}));
let http_client = FakeHttpClient::with_404_response();
let client = cx.read(|cx| Client::new(http_client, cx));
let tree = Worktree::local(
client,
root.path(),
true,
Arc::new(RealFs),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
assert!(tree.repo_for("c.txt".as_ref()).is_none());
let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
assert_eq!(repo.content_path.as_ref(), Path::new("dir1"));
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git"));
let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1"));
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),);
});
let original_scan_id = tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id
});
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
assert_ne!(
original_scan_id, new_scan_id,
"original {original_scan_id}, new {new_scan_id}"
);
});
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
tree.flush_fs_events(cx).await;
tree.read_with(cx, |tree, _cx| {
let tree = tree.as_local().unwrap();
assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
});
}
#[test]
fn test_changed_repos() {
fn fake_entry(git_dir_path: impl AsRef<Path>, scan_id: usize) -> GitRepositoryEntry {
GitRepositoryEntry {
repo: Arc::new(Mutex::new(FakeGitRepository::default())),
scan_id,
content_path: git_dir_path.as_ref().parent().unwrap().into(),
git_dir_path: git_dir_path.as_ref().into(),
}
}
let prev_repos: Vec<GitRepositoryEntry> = vec![
fake_entry("/.git", 0),
fake_entry("/a/.git", 0),
fake_entry("/a/b/.git", 0),
];
let new_repos: Vec<GitRepositoryEntry> = vec![
fake_entry("/a/.git", 1),
fake_entry("/a/b/.git", 0),
fake_entry("/a/c/.git", 0),
];
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
// Deletion retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0)
.is_some());
// Update retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1)
.is_some());
// Addition retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0)
.is_some());
// Nochange, not retained
assert!(res
.iter()
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0)
.is_none());
}
#[gpui::test] #[gpui::test]
async fn test_write_file(cx: &mut TestAppContext) { async fn test_write_file(cx: &mut TestAppContext) {
let dir = temp_tree(json!({ let dir = temp_tree(json!({
@ -3016,8 +3341,7 @@ mod tests {
"ignored-dir": {} "ignored-dir": {}
})); }));
let http_client = FakeHttpClient::with_404_response(); let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = Client::new(http_client.clone());
let tree = Worktree::local( let tree = Worktree::local(
client, client,
@ -3064,8 +3388,7 @@ mod tests {
#[gpui::test(iterations = 30)] #[gpui::test(iterations = 30)]
async fn test_create_directory(cx: &mut TestAppContext) { async fn test_create_directory(cx: &mut TestAppContext) {
let http_client = FakeHttpClient::with_404_response(); let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = Client::new(http_client.clone());
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
@ -3127,6 +3450,7 @@ mod tests {
abs_path: root_dir.path().into(), abs_path: root_dir.path().into(),
removed_entry_ids: Default::default(), removed_entry_ids: Default::default(),
ignores_by_parent_abs_path: Default::default(), ignores_by_parent_abs_path: Default::default(),
git_repositories: Default::default(),
next_entry_id: next_entry_id.clone(), next_entry_id: next_entry_id.clone(),
snapshot: Snapshot { snapshot: Snapshot {
id: WorktreeId::from_usize(0), id: WorktreeId::from_usize(0),

View file

@ -15,108 +15,111 @@ message Envelope {
CreateRoomResponse create_room_response = 9; CreateRoomResponse create_room_response = 9;
JoinRoom join_room = 10; JoinRoom join_room = 10;
JoinRoomResponse join_room_response = 11; JoinRoomResponse join_room_response = 11;
LeaveRoom leave_room = 1002; LeaveRoom leave_room = 12;
Call call = 12; Call call = 13;
IncomingCall incoming_call = 1000; IncomingCall incoming_call = 14;
CallCanceled call_canceled = 1001; CallCanceled call_canceled = 15;
CancelCall cancel_call = 1004; CancelCall cancel_call = 16;
DeclineCall decline_call = 13; DeclineCall decline_call = 17;
UpdateParticipantLocation update_participant_location = 1003; UpdateParticipantLocation update_participant_location = 18;
RoomUpdated room_updated = 14; RoomUpdated room_updated = 19;
ShareProject share_project = 15; ShareProject share_project = 20;
ShareProjectResponse share_project_response = 16; ShareProjectResponse share_project_response = 21;
UnshareProject unshare_project = 17; UnshareProject unshare_project = 22;
JoinProject join_project = 21; JoinProject join_project = 23;
JoinProjectResponse join_project_response = 22; JoinProjectResponse join_project_response = 24;
LeaveProject leave_project = 23; LeaveProject leave_project = 25;
AddProjectCollaborator add_project_collaborator = 24; AddProjectCollaborator add_project_collaborator = 26;
RemoveProjectCollaborator remove_project_collaborator = 25; RemoveProjectCollaborator remove_project_collaborator = 27;
GetDefinition get_definition = 27; GetDefinition get_definition = 28;
GetDefinitionResponse get_definition_response = 28; GetDefinitionResponse get_definition_response = 29;
GetTypeDefinition get_type_definition = 29; GetTypeDefinition get_type_definition = 30;
GetTypeDefinitionResponse get_type_definition_response = 30; GetTypeDefinitionResponse get_type_definition_response = 31;
GetReferences get_references = 31; GetReferences get_references = 32;
GetReferencesResponse get_references_response = 32; GetReferencesResponse get_references_response = 33;
GetDocumentHighlights get_document_highlights = 33; GetDocumentHighlights get_document_highlights = 34;
GetDocumentHighlightsResponse get_document_highlights_response = 34; GetDocumentHighlightsResponse get_document_highlights_response = 35;
GetProjectSymbols get_project_symbols = 35; GetProjectSymbols get_project_symbols = 36;
GetProjectSymbolsResponse get_project_symbols_response = 36; GetProjectSymbolsResponse get_project_symbols_response = 37;
OpenBufferForSymbol open_buffer_for_symbol = 37; OpenBufferForSymbol open_buffer_for_symbol = 38;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 38; OpenBufferForSymbolResponse open_buffer_for_symbol_response = 39;
UpdateProject update_project = 39; UpdateProject update_project = 40;
RegisterProjectActivity register_project_activity = 40; RegisterProjectActivity register_project_activity = 41;
UpdateWorktree update_worktree = 41; UpdateWorktree update_worktree = 42;
UpdateWorktreeExtensions update_worktree_extensions = 42; UpdateWorktreeExtensions update_worktree_extensions = 43;
CreateProjectEntry create_project_entry = 43; CreateProjectEntry create_project_entry = 44;
RenameProjectEntry rename_project_entry = 44; RenameProjectEntry rename_project_entry = 45;
CopyProjectEntry copy_project_entry = 45; CopyProjectEntry copy_project_entry = 46;
DeleteProjectEntry delete_project_entry = 46; DeleteProjectEntry delete_project_entry = 47;
ProjectEntryResponse project_entry_response = 47; ProjectEntryResponse project_entry_response = 48;
UpdateDiagnosticSummary update_diagnostic_summary = 48; UpdateDiagnosticSummary update_diagnostic_summary = 49;
StartLanguageServer start_language_server = 49; StartLanguageServer start_language_server = 50;
UpdateLanguageServer update_language_server = 50; UpdateLanguageServer update_language_server = 51;
OpenBufferById open_buffer_by_id = 51; OpenBufferById open_buffer_by_id = 52;
OpenBufferByPath open_buffer_by_path = 52; OpenBufferByPath open_buffer_by_path = 53;
OpenBufferResponse open_buffer_response = 53; OpenBufferResponse open_buffer_response = 54;
CreateBufferForPeer create_buffer_for_peer = 54; CreateBufferForPeer create_buffer_for_peer = 55;
UpdateBuffer update_buffer = 55; UpdateBuffer update_buffer = 56;
UpdateBufferFile update_buffer_file = 56; UpdateBufferFile update_buffer_file = 57;
SaveBuffer save_buffer = 57; SaveBuffer save_buffer = 58;
BufferSaved buffer_saved = 58; BufferSaved buffer_saved = 59;
BufferReloaded buffer_reloaded = 59; BufferReloaded buffer_reloaded = 60;
ReloadBuffers reload_buffers = 60; ReloadBuffers reload_buffers = 61;
ReloadBuffersResponse reload_buffers_response = 61; ReloadBuffersResponse reload_buffers_response = 62;
FormatBuffers format_buffers = 62; FormatBuffers format_buffers = 63;
FormatBuffersResponse format_buffers_response = 63; FormatBuffersResponse format_buffers_response = 64;
GetCompletions get_completions = 64; GetCompletions get_completions = 65;
GetCompletionsResponse get_completions_response = 65; GetCompletionsResponse get_completions_response = 66;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 66; ApplyCompletionAdditionalEdits apply_completion_additional_edits = 67;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 67; ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 68;
GetCodeActions get_code_actions = 68; GetCodeActions get_code_actions = 69;
GetCodeActionsResponse get_code_actions_response = 69; GetCodeActionsResponse get_code_actions_response = 70;
GetHover get_hover = 70; GetHover get_hover = 71;
GetHoverResponse get_hover_response = 71; GetHoverResponse get_hover_response = 72;
ApplyCodeAction apply_code_action = 72; ApplyCodeAction apply_code_action = 73;
ApplyCodeActionResponse apply_code_action_response = 73; ApplyCodeActionResponse apply_code_action_response = 74;
PrepareRename prepare_rename = 74; PrepareRename prepare_rename = 75;
PrepareRenameResponse prepare_rename_response = 75; PrepareRenameResponse prepare_rename_response = 76;
PerformRename perform_rename = 76; PerformRename perform_rename = 77;
PerformRenameResponse perform_rename_response = 77; PerformRenameResponse perform_rename_response = 78;
SearchProject search_project = 78; SearchProject search_project = 79;
SearchProjectResponse search_project_response = 79; SearchProjectResponse search_project_response = 80;
GetChannels get_channels = 80; GetChannels get_channels = 81;
GetChannelsResponse get_channels_response = 81; GetChannelsResponse get_channels_response = 82;
JoinChannel join_channel = 82; JoinChannel join_channel = 83;
JoinChannelResponse join_channel_response = 83; JoinChannelResponse join_channel_response = 84;
LeaveChannel leave_channel = 84; LeaveChannel leave_channel = 85;
SendChannelMessage send_channel_message = 85; SendChannelMessage send_channel_message = 86;
SendChannelMessageResponse send_channel_message_response = 86; SendChannelMessageResponse send_channel_message_response = 87;
ChannelMessageSent channel_message_sent = 87; ChannelMessageSent channel_message_sent = 88;
GetChannelMessages get_channel_messages = 88; GetChannelMessages get_channel_messages = 89;
GetChannelMessagesResponse get_channel_messages_response = 89; GetChannelMessagesResponse get_channel_messages_response = 90;
UpdateContacts update_contacts = 90; UpdateContacts update_contacts = 91;
UpdateInviteInfo update_invite_info = 91; UpdateInviteInfo update_invite_info = 92;
ShowContacts show_contacts = 92; ShowContacts show_contacts = 93;
GetUsers get_users = 93; GetUsers get_users = 94;
FuzzySearchUsers fuzzy_search_users = 94; FuzzySearchUsers fuzzy_search_users = 95;
UsersResponse users_response = 95; UsersResponse users_response = 96;
RequestContact request_contact = 96; RequestContact request_contact = 97;
RespondToContactRequest respond_to_contact_request = 97; RespondToContactRequest respond_to_contact_request = 98;
RemoveContact remove_contact = 98; RemoveContact remove_contact = 99;
Follow follow = 99; Follow follow = 100;
FollowResponse follow_response = 100; FollowResponse follow_response = 101;
UpdateFollowers update_followers = 101; UpdateFollowers update_followers = 102;
Unfollow unfollow = 102; Unfollow unfollow = 103;
GetPrivateUserInfo get_private_user_info = 104;
GetPrivateUserInfoResponse get_private_user_info_response = 105;
UpdateDiffBase update_diff_base = 106;
} }
} }
@ -795,6 +798,13 @@ message Unfollow {
uint32 leader_id = 2; uint32 leader_id = 2;
} }
message GetPrivateUserInfo {}
message GetPrivateUserInfoResponse {
string metrics_id = 1;
bool staff = 2;
}
// Entities // Entities
message UpdateActiveView { message UpdateActiveView {
@ -868,7 +878,8 @@ message BufferState {
uint64 id = 1; uint64 id = 1;
optional File file = 2; optional File file = 2;
string base_text = 3; string base_text = 3;
LineEnding line_ending = 4; optional string diff_base = 4;
LineEnding line_ending = 5;
} }
message BufferChunk { message BufferChunk {
@ -1032,3 +1043,9 @@ message WorktreeMetadata {
string root_name = 2; string root_name = 2;
bool visible = 3; bool visible = 3;
} }
message UpdateDiffBase {
uint64 project_id = 1;
uint64 buffer_id = 2;
optional string diff_base = 3;
}

View file

@ -175,6 +175,9 @@ messages!(
(UpdateProject, Foreground), (UpdateProject, Foreground),
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeExtensions, Background), (UpdateWorktreeExtensions, Background),
(UpdateDiffBase, Background),
(GetPrivateUserInfo, Foreground),
(GetPrivateUserInfoResponse, Foreground),
); );
request_messages!( request_messages!(
@ -201,6 +204,7 @@ request_messages!(
(GetTypeDefinition, GetTypeDefinitionResponse), (GetTypeDefinition, GetTypeDefinitionResponse),
(GetDocumentHighlights, GetDocumentHighlightsResponse), (GetDocumentHighlights, GetDocumentHighlightsResponse),
(GetReferences, GetReferencesResponse), (GetReferences, GetReferencesResponse),
(GetPrivateUserInfo, GetPrivateUserInfoResponse),
(GetProjectSymbols, GetProjectSymbolsResponse), (GetProjectSymbols, GetProjectSymbolsResponse),
(FuzzySearchUsers, UsersResponse), (FuzzySearchUsers, UsersResponse),
(GetUsers, UsersResponse), (GetUsers, UsersResponse),
@ -274,6 +278,7 @@ entity_messages!(
UpdateProject, UpdateProject,
UpdateWorktree, UpdateWorktree,
UpdateWorktreeExtensions, UpdateWorktreeExtensions,
UpdateDiffBase
); );
entity_messages!(channel_id, ChannelMessageSent); entity_messages!(channel_id, ChannelMessageSent);

View file

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*; pub use peer::*;
mod macros; mod macros;
pub const PROTOCOL_VERSION: u32 = 32; pub const PROTOCOL_VERSION: u32 = 35;

View file

@ -32,6 +32,8 @@ pub struct Settings {
pub default_dock_anchor: DockAnchor, pub default_dock_anchor: DockAnchor,
pub editor_defaults: EditorSettings, pub editor_defaults: EditorSettings,
pub editor_overrides: EditorSettings, pub editor_overrides: EditorSettings,
pub git: GitSettings,
pub git_overrides: GitSettings,
pub terminal_defaults: TerminalSettings, pub terminal_defaults: TerminalSettings,
pub terminal_overrides: TerminalSettings, pub terminal_overrides: TerminalSettings,
pub language_defaults: HashMap<Arc<str>, EditorSettings>, pub language_defaults: HashMap<Arc<str>, EditorSettings>,
@ -52,6 +54,22 @@ impl FeatureFlags {
} }
} }
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct GitSettings {
pub git_gutter: Option<GitGutter>,
pub gutter_debounce: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitGutter {
#[default]
TrackedFiles,
Hide,
}
pub struct GitGutterConfig {}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct EditorSettings { pub struct EditorSettings {
pub tab_size: Option<NonZeroU32>, pub tab_size: Option<NonZeroU32>,
@ -196,6 +214,8 @@ pub struct SettingsFileContent {
#[serde(default)] #[serde(default)]
pub terminal: TerminalSettings, pub terminal: TerminalSettings,
#[serde(default)] #[serde(default)]
pub git: Option<GitSettings>,
#[serde(default)]
#[serde(alias = "language_overrides")] #[serde(alias = "language_overrides")]
pub languages: HashMap<Arc<str>, EditorSettings>, pub languages: HashMap<Arc<str>, EditorSettings>,
#[serde(default)] #[serde(default)]
@ -252,6 +272,8 @@ impl Settings {
enable_language_server: required(defaults.editor.enable_language_server), enable_language_server: required(defaults.editor.enable_language_server),
}, },
editor_overrides: Default::default(), editor_overrides: Default::default(),
git: defaults.git.unwrap(),
git_overrides: Default::default(),
terminal_defaults: Default::default(), terminal_defaults: Default::default(),
terminal_overrides: Default::default(), terminal_overrides: Default::default(),
language_defaults: defaults.languages, language_defaults: defaults.languages,
@ -303,6 +325,7 @@ impl Settings {
} }
self.editor_overrides = data.editor; self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default();
self.terminal_defaults.font_size = data.terminal.font_size; self.terminal_defaults.font_size = data.terminal.font_size;
self.terminal_overrides = data.terminal; self.terminal_overrides = data.terminal;
self.language_overrides = data.languages; self.language_overrides = data.languages;
@ -358,6 +381,14 @@ impl Settings {
.expect("missing default") .expect("missing default")
} }
pub fn git_gutter(&self) -> GitGutter {
self.git_overrides.git_gutter.unwrap_or_else(|| {
self.git
.git_gutter
.expect("git_gutter should be some by setting setup")
})
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings { pub fn test(cx: &gpui::AppContext) -> Settings {
Settings { Settings {
@ -382,6 +413,8 @@ impl Settings {
editor_overrides: Default::default(), editor_overrides: Default::default(),
terminal_defaults: Default::default(), terminal_defaults: Default::default(),
terminal_overrides: Default::default(), terminal_overrides: Default::default(),
git: Default::default(),
git_overrides: Default::default(),
language_defaults: Default::default(), language_defaults: Default::default(),
language_overrides: Default::default(), language_overrides: Default::default(),
lsp: Default::default(), lsp: Default::default(),

View file

@ -101,6 +101,12 @@ pub enum Bias {
Right, Right,
} }
impl Default for Bias {
fn default() -> Self {
Bias::Left
}
}
impl PartialOrd for Bias { impl PartialOrd for Bias {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) Some(self.cmp(other))

View file

@ -618,8 +618,34 @@ impl Terminal {
term.resize(new_size); term.resize(new_size);
} }
InternalEvent::Clear => { InternalEvent::Clear => {
self.write_to_pty("\x0c".to_string()); // Clear back buffer
term.clear_screen(ClearMode::Saved); term.clear_screen(ClearMode::Saved);
let cursor = term.grid().cursor.point;
// Clear the lines above
term.grid_mut().reset_region(..cursor.line);
// Copy the current line up
let line = term.grid()[cursor.line][..cursor.column]
.iter()
.cloned()
.enumerate()
.collect::<Vec<(usize, Cell)>>();
for (i, cell) in line {
term.grid_mut()[Line(0)][Column(i)] = cell;
}
// Reset the cursor
term.grid_mut().cursor.point =
Point::new(Line(0), term.grid_mut().cursor.point.column);
let new_cursor = term.grid().cursor.point;
// Clear the lines below the new cursor
if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
term.grid_mut().reset_region((new_cursor.line + 1)..);
}
} }
InternalEvent::Scroll(scroll) => { InternalEvent::Scroll(scroll) => {
term.scroll_display(*scroll); term.scroll_display(*scroll);

View file

@ -680,12 +680,12 @@ impl Element for TerminalElement {
let focused = self.focused; let focused = self.focused;
TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map(
move |(cursor_position, block_width)| { move |(cursor_position, block_width)| {
let shape = match cursor.shape { let (shape, text) = match cursor.shape {
AlacCursorShape::Block if !focused => CursorShape::Hollow, AlacCursorShape::Block if !focused => (CursorShape::Hollow, None),
AlacCursorShape::Block => CursorShape::Block, AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)),
AlacCursorShape::Underline => CursorShape::Underscore, AlacCursorShape::Underline => (CursorShape::Underscore, None),
AlacCursorShape::Beam => CursorShape::Bar, AlacCursorShape::Beam => (CursorShape::Bar, None),
AlacCursorShape::HollowBlock => CursorShape::Hollow, AlacCursorShape::HollowBlock => (CursorShape::Hollow, None),
//This case is handled in the if wrapping the whole cursor layout //This case is handled in the if wrapping the whole cursor layout
AlacCursorShape::Hidden => unreachable!(), AlacCursorShape::Hidden => unreachable!(),
}; };
@ -696,7 +696,7 @@ impl Element for TerminalElement {
dimensions.line_height, dimensions.line_height,
terminal_theme.colors.cursor, terminal_theme.colors.cursor,
shape, shape,
Some(cursor_text), text,
) )
}, },
) )

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use std::{cmp::Ordering, fmt::Debug, ops::Range}; use std::{cmp::Ordering, fmt::Debug, ops::Range};
use sum_tree::Bias; use sum_tree::Bias;
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
pub struct Anchor { pub struct Anchor {
pub timestamp: clock::Local, pub timestamp: clock::Local,
pub offset: usize, pub offset: usize,

View file

@ -54,6 +54,13 @@ impl Rope {
cursor.slice(range.end) cursor.slice(range.end)
} }
pub fn slice_rows(&self, range: Range<u32>) -> Rope {
//This would be more efficient with a forward advance after the first, but it's fine
let start = self.point_to_offset(Point::new(range.start, 0));
let end = self.point_to_offset(Point::new(range.end, 0));
self.slice(start..end)
}
pub fn push(&mut self, text: &str) { pub fn push(&mut self, text: &str) {
let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunks = SmallVec::<[_; 16]>::new();
let mut new_chunk = ArrayString::new(); let mut new_chunk = ArrayString::new();

View file

@ -510,8 +510,7 @@ pub struct Editor {
pub rename_fade: f32, pub rename_fade: f32,
pub document_highlight_read_background: Color, pub document_highlight_read_background: Color,
pub document_highlight_write_background: Color, pub document_highlight_write_background: Color,
pub diff_background_deleted: Color, pub diff: DiffStyle,
pub diff_background_inserted: Color,
pub line_number: Color, pub line_number: Color,
pub line_number_active: Color, pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>, pub guest_selections: Vec<SelectionStyle>,
@ -595,6 +594,16 @@ pub struct CodeActions {
pub vertical_scale: f32, pub vertical_scale: f32,
} }
#[derive(Clone, Deserialize, Default)]
pub struct DiffStyle {
pub inserted: Color,
pub modified: Color,
pub deleted: Color,
pub removed_width_em: f32,
pub width_em: f32,
pub corner_radius: f32,
}
#[derive(Debug, Default, Clone, Copy)] #[derive(Debug, Default, Clone, Copy)]
pub struct Interactive<T> { pub struct Interactive<T> {
pub default: T, pub default: T,

View file

@ -7,17 +7,21 @@ edition = "2021"
doctest = false doctest = false
[features] [features]
test-support = ["rand", "serde_json", "tempdir"] test-support = ["rand", "serde_json", "tempdir", "git2"]
[dependencies] [dependencies]
anyhow = "1.0.38" anyhow = "1.0.38"
futures = "0.3" futures = "0.3"
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }
lazy_static = "1.4.0"
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
tempdir = { version = "0.3.7", optional = true } tempdir = { version = "0.3.7", optional = true }
serde_json = { version = "1.0", features = ["preserve_order"], optional = true } serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
git2 = { version = "0.15", default-features = false, optional = true }
[dev-dependencies] [dev-dependencies]
rand = { version = "0.8" } rand = { version = "0.8" }
tempdir = { version = "0.3.7" } tempdir = { version = "0.3.7" }
serde_json = { version = "1.0", features = ["preserve_order"] } serde_json = { version = "1.0", features = ["preserve_order"] }
git2 = { version = "0.15", default-features = false }

View file

@ -1,7 +1,11 @@
mod assertions; mod assertions;
mod marked_text; mod marked_text;
use std::path::{Path, PathBuf}; use git2;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use tempdir::TempDir; use tempdir::TempDir;
pub use assertions::*; pub use assertions::*;
@ -24,6 +28,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
match contents { match contents {
Value::Object(_) => { Value::Object(_) => {
fs::create_dir(&path).unwrap(); fs::create_dir(&path).unwrap();
if path.file_name() == Some(&OsStr::new(".git")) {
git2::Repository::init(&path.parent().unwrap()).unwrap();
}
write_tree(&path, contents); write_tree(&path, contents);
} }
Value::Null => { Value::Null => {

View file

@ -46,7 +46,6 @@ use std::{
cell::RefCell, cell::RefCell,
fmt, fmt,
future::Future, future::Future,
mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
@ -295,7 +294,23 @@ pub trait Item: View {
project: ModelHandle<Project>, project: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>>; ) -> Task<Result<()>>;
fn git_diff_recalc(
&mut self,
_project: ModelHandle<Project>,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>; fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
fn should_close_item_on_event(_: &Self::Event) -> bool {
false
}
fn should_update_tab_on_event(_: &Self::Event) -> bool {
false
}
fn is_edit_event(_: &Self::Event) -> bool {
false
}
fn act_as_type( fn act_as_type(
&self, &self,
type_id: TypeId, type_id: TypeId,
@ -412,6 +427,57 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
} }
} }
struct DelayedDebouncedEditAction {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,
}
impl DelayedDebouncedEditAction {
fn new() -> DelayedDebouncedEditAction {
DelayedDebouncedEditAction {
task: None,
cancel_channel: None,
}
}
fn fire_new<F, Fut>(
&mut self,
delay: Duration,
workspace: &Workspace,
cx: &mut ViewContext<Workspace>,
f: F,
) where
F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
Fut: 'static + Future<Output = ()>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
}
let project = workspace.project().downgrade();
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
let previous_task = self.task.take();
self.task = Some(cx.spawn_weak(|_, cx| async move {
let mut timer = cx.background().timer(delay).fuse();
if let Some(previous_task) = previous_task {
previous_task.await;
}
futures::select_biased! {
_ = receiver => return,
_ = timer => {}
}
if let Some(project) = project.upgrade(&cx) {
(f)(project, cx).await;
}
}));
}
}
pub trait ItemHandle: 'static + fmt::Debug { pub trait ItemHandle: 'static + fmt::Debug {
fn subscribe_to_item_events( fn subscribe_to_item_events(
&self, &self,
@ -450,6 +516,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
) -> Task<Result<()>>; ) -> Task<Result<()>>;
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
-> Task<Result<()>>; -> Task<Result<()>>;
fn git_diff_recalc(
&self,
project: ModelHandle<Project>,
cx: &mut MutableAppContext,
) -> Task<Result<()>>;
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>; fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
fn on_release( fn on_release(
@ -555,8 +626,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
.insert(self.id(), pane.downgrade()) .insert(self.id(), pane.downgrade())
.is_none() .is_none()
{ {
let mut pending_autosave = None; let mut pending_autosave = DelayedDebouncedEditAction::new();
let mut cancel_pending_autosave = oneshot::channel::<()>().0; let mut pending_git_update = DelayedDebouncedEditAction::new();
let pending_update = Rc::new(RefCell::new(None)); let pending_update = Rc::new(RefCell::new(None));
let pending_update_scheduled = Rc::new(AtomicBool::new(false)); let pending_update_scheduled = Rc::new(AtomicBool::new(false));
@ -614,45 +685,66 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
.detach_and_log_err(cx); .detach_and_log_err(cx);
return; return;
} }
ItemEvent::UpdateTab => { ItemEvent::UpdateTab => {
pane.update(cx, |_, cx| { pane.update(cx, |_, cx| {
cx.emit(pane::Event::ChangeItemTitle); cx.emit(pane::Event::ChangeItemTitle);
cx.notify(); cx.notify();
}); });
} }
ItemEvent::Edit => { ItemEvent::Edit => {
if let Autosave::AfterDelay { milliseconds } = if let Autosave::AfterDelay { milliseconds } =
cx.global::<Settings>().autosave cx.global::<Settings>().autosave
{ {
let prev_autosave = pending_autosave let delay = Duration::from_millis(milliseconds);
.take()
.unwrap_or_else(|| Task::ready(Some(())));
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
let prev_cancel_tx =
mem::replace(&mut cancel_pending_autosave, cancel_tx);
let project = workspace.project.downgrade();
let _ = prev_cancel_tx.send(());
let item = item.clone(); let item = item.clone();
pending_autosave = pending_autosave.fire_new(
Some(cx.spawn_weak(|_, mut cx| async move { delay,
let mut timer = cx workspace,
.background() cx,
.timer(Duration::from_millis(milliseconds)) |project, mut cx| async move {
.fuse();
prev_autosave.await;
futures::select_biased! {
_ = cancel_rx => return None,
_ = timer => {}
}
let project = project.upgrade(&cx)?;
cx.update(|cx| Pane::autosave_item(&item, project, cx)) cx.update(|cx| Pane::autosave_item(&item, project, cx))
.await .await
.log_err(); .log_err();
None },
})); );
}
let settings = cx.global::<Settings>();
let debounce_delay = settings.git_overrides.gutter_debounce;
let item = item.clone();
if let Some(delay) = debounce_delay {
const MIN_GIT_DELAY: u64 = 50;
let delay = delay.max(MIN_GIT_DELAY);
let duration = Duration::from_millis(delay);
pending_git_update.fire_new(
duration,
workspace,
cx,
|project, mut cx| async move {
cx.update(|cx| item.git_diff_recalc(project, cx))
.await
.log_err();
},
);
} else {
let project = workspace.project().downgrade();
cx.spawn_weak(|_, mut cx| async move {
if let Some(project) = project.upgrade(&cx) {
cx.update(|cx| item.git_diff_recalc(project, cx))
.await
.log_err();
}
})
.detach();
} }
} }
_ => {} _ => {}
} }
} }
@ -732,6 +824,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
self.update(cx, |item, cx| item.reload(project, cx)) self.update(cx, |item, cx| item.reload(project, cx))
} }
fn git_diff_recalc(
&self,
project: ModelHandle<Project>,
cx: &mut MutableAppContext,
) -> Task<Result<()>> {
self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
}
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
self.read(cx).act_as_type(type_id, self, cx) self.read(cx).act_as_type(type_id, self, cx)
} }
@ -833,7 +933,7 @@ impl AppState {
let fs = project::FakeFs::new(cx.background().clone()); let fs = project::FakeFs::new(cx.background().clone());
let languages = Arc::new(LanguageRegistry::test()); let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response(); let http_client = client::test::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone()); let client = Client::new(http_client.clone(), cx);
let project_store = cx.add_model(|_| ProjectStore::new()); let project_store = cx.add_model(|_| ProjectStore::new());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let themes = ThemeRegistry::new((), cx.font_cache().clone()); let themes = ThemeRegistry::new((), cx.font_cache().clone());

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition = "2021" edition = "2021"
name = "zed" name = "zed"
version = "0.55.0" version = "0.59.0"
[lib] [lib]
name = "zed" name = "zed"
@ -92,6 +92,7 @@ toml = "0.5"
tree-sitter = "0.20" tree-sitter = "0.20"
tree-sitter-c = "0.20.1" tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0" tree-sitter-cpp = "0.20.0"
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
@ -100,6 +101,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
tree-sitter-python = "0.20.2" tree-sitter-python = "0.20.2"
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" } tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
tree-sitter-typescript = "0.20.1" tree-sitter-typescript = "0.20.1"
tree-sitter-html = "0.19.0"
url = "2.2" url = "2.2"
[dev-dependencies] [dev-dependencies]

View file

@ -3,6 +3,10 @@ use std::process::Command;
fn main() { fn main() {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14"); println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
if let Ok(api_key) = std::env::var("ZED_AMPLITUDE_API_KEY") {
println!("cargo:rustc-env=ZED_AMPLITUDE_API_KEY={api_key}");
}
let output = Command::new("npm") let output = Command::new("npm")
.current_dir("../../styles") .current_dir("../../styles")
.args(["install", "--no-save"]) .args(["install", "--no-save"])

View file

@ -7,6 +7,7 @@ use std::{borrow::Cow, str, sync::Arc};
mod c; mod c;
mod elixir; mod elixir;
mod go; mod go;
mod html;
mod installation; mod installation;
mod json; mod json;
mod language_plugin; mod language_plugin;
@ -46,6 +47,11 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
tree_sitter_cpp::language(), tree_sitter_cpp::language(),
Some(CachedLspAdapter::new(c::CLspAdapter).await), Some(CachedLspAdapter::new(c::CLspAdapter).await),
), ),
(
"css",
tree_sitter_css::language(),
None, //
),
( (
"elixir", "elixir",
tree_sitter_elixir::language(), tree_sitter_elixir::language(),
@ -96,8 +102,13 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
tree_sitter_typescript::language_tsx(), tree_sitter_typescript::language_tsx(),
Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await), Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
), ),
(
"html",
tree_sitter_html::language(),
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
),
] { ] {
languages.add(Arc::new(language(name, grammar, lsp_adapter))); languages.add(language(name, grammar, lsp_adapter));
} }
} }
@ -105,7 +116,7 @@ pub(crate) fn language(
name: &str, name: &str,
grammar: tree_sitter::Language, grammar: tree_sitter::Language,
lsp_adapter: Option<Arc<CachedLspAdapter>>, lsp_adapter: Option<Arc<CachedLspAdapter>>,
) -> Language { ) -> Arc<Language> {
let config = toml::from_slice( let config = toml::from_slice(
&LanguageDir::get(&format!("{}/config.toml", name)) &LanguageDir::get(&format!("{}/config.toml", name))
.unwrap() .unwrap()
@ -142,7 +153,7 @@ pub(crate) fn language(
if let Some(lsp_adapter) = lsp_adapter { if let Some(lsp_adapter) = lsp_adapter {
language = language.with_lsp_adapter(lsp_adapter) language = language.with_lsp_adapter(lsp_adapter)
} }
language Arc::new(language)
} }
fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> { fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {

View file

@ -112,7 +112,7 @@ impl super::LspAdapter for CLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let label = completion let label = completion
.label .label
@ -190,7 +190,7 @@ impl super::LspAdapter for CLspAdapter {
&self, &self,
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind { let (text, filter_range, display_range) = match kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@ -251,7 +251,6 @@ mod tests {
use gpui::MutableAppContext; use gpui::MutableAppContext;
use language::{AutoindentMode, Buffer}; use language::{AutoindentMode, Buffer};
use settings::Settings; use settings::Settings;
use std::sync::Arc;
#[gpui::test] #[gpui::test]
fn test_c_autoindent(cx: &mut MutableAppContext) { fn test_c_autoindent(cx: &mut MutableAppContext) {
@ -262,7 +261,7 @@ mod tests {
let language = crate::languages::language("c", tree_sitter_c::language(), None); let language = crate::languages::language("c", tree_sitter_c::language(), None);
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
// empty function // empty function
buffer.edit([(0..0, "int main() {}")], None, cx); buffer.edit([(0..0, "int main() {}")], None, cx);

View file

@ -86,7 +86,7 @@
(identifier) @variable (identifier) @variable
((identifier) @constant ((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$")) (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)

View file

@ -37,11 +37,11 @@
(type_identifier) @type (type_identifier) @type
((identifier) @constant ((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]*$")) (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
(field_identifier) @property (field_identifier) @property
(statement_identifier) @label (statement_identifier) @label
(this) @variable.builtin (this) @variable.special
[ [
"break" "break"

View file

@ -0,0 +1,3 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View 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 }
]

View 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

View file

@ -0,0 +1 @@
(_ "{" "}" @end) @indent

View file

@ -113,7 +113,7 @@ impl LspAdapter for ElixirLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
match completion.kind.zip(completion.detail.as_ref()) { match completion.kind.zip(completion.detail.as_ref()) {
Some((_, detail)) if detail.starts_with("(function)") => { Some((_, detail)) if detail.starts_with("(function)") => {
@ -168,7 +168,7 @@ impl LspAdapter for ElixirLspAdapter {
&self, &self,
name: &str, name: &str,
kind: SymbolKind, kind: SymbolKind,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind { let (text, filter_range, display_range) = match kind {
SymbolKind::METHOD | SymbolKind::FUNCTION => { SymbolKind::METHOD | SymbolKind::FUNCTION => {

View file

@ -134,7 +134,7 @@ impl super::LspAdapter for GoLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let label = &completion.label; let label = &completion.label;
@ -235,7 +235,7 @@ impl super::LspAdapter for GoLspAdapter {
&self, &self,
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind { let (text, filter_range, display_range) = match kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {

View 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
}))
}
}

View file

@ -0,0 +1,2 @@
("<" @open ">" @close)
("\"" @open "\"" @close)

View 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 = ["<!-- ", " -->"]

View 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

View file

@ -0,0 +1,6 @@
(start_tag ">" @end) @indent
(self_closing_tag "/>" @end) @indent
(element
(start_tag) @start
(end_tag)? @end) @indent

View file

@ -0,0 +1,7 @@
(script_element
(raw_text) @content
(#set! "language" "javascript"))
(style_element
(raw_text) @content
(#set! "language" "css"))

View file

@ -51,12 +51,12 @@
(shorthand_property_identifier) (shorthand_property_identifier)
(shorthand_property_identifier_pattern) (shorthand_property_identifier_pattern)
] @constant ] @constant
(#match? @constant "^[A-Z_][A-Z\\d_]+$")) (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
; Literals ; Literals
(this) @variable.builtin (this) @variable.special
(super) @variable.builtin (super) @variable.special
[ [
(true) (true)

View file

@ -90,7 +90,7 @@ impl LspAdapter for PythonLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
item: &lsp::CompletionItem, item: &lsp::CompletionItem,
language: &language::Language, language: &Arc<language::Language>,
) -> Option<language::CodeLabel> { ) -> Option<language::CodeLabel> {
let label = &item.label; let label = &item.label;
let grammar = language.grammar()?; let grammar = language.grammar()?;
@ -112,7 +112,7 @@ impl LspAdapter for PythonLspAdapter {
&self, &self,
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
language: &language::Language, language: &Arc<language::Language>,
) -> Option<language::CodeLabel> { ) -> Option<language::CodeLabel> {
let (text, filter_range, display_range) = match kind { let (text, filter_range, display_range) = match kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@ -149,7 +149,6 @@ mod tests {
use gpui::{ModelContext, MutableAppContext}; use gpui::{ModelContext, MutableAppContext};
use language::{AutoindentMode, Buffer}; use language::{AutoindentMode, Buffer};
use settings::Settings; use settings::Settings;
use std::sync::Arc;
#[gpui::test] #[gpui::test]
fn test_python_autoindent(cx: &mut MutableAppContext) { fn test_python_autoindent(cx: &mut MutableAppContext) {
@ -160,7 +159,7 @@ mod tests {
cx.set_global(settings); cx.set_global(settings);
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| { let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
let ix = buffer.len(); let ix = buffer.len();
buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);

View file

@ -21,7 +21,7 @@
(#match? @type "^[A-Z]")) (#match? @type "^[A-Z]"))
((identifier) @constant ((identifier) @constant
(#match? @constant "^[A-Z][A-Z_]*$")) (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
; Builtin functions ; Builtin functions

View file

@ -119,7 +119,7 @@ impl LspAdapter for RustLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
completion: &lsp::CompletionItem, completion: &lsp::CompletionItem,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
match completion.kind { match completion.kind {
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => { Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
@ -196,7 +196,7 @@ impl LspAdapter for RustLspAdapter {
&self, &self,
name: &str, name: &str,
kind: lsp::SymbolKind, kind: lsp::SymbolKind,
language: &Language, language: &Arc<Language>,
) -> Option<CodeLabel> { ) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind { let (text, filter_range, display_range) = match kind {
lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@ -439,7 +439,7 @@ mod tests {
cx.set_global(settings); cx.set_global(settings);
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx); let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
// indent between braces // indent between braces
buffer.set_text("fn a() {}", cx); buffer.set_text("fn a() {}", cx);

View file

@ -1,6 +1,6 @@
(type_identifier) @type (type_identifier) @type
(primitive_type) @type.builtin (primitive_type) @type.builtin
(self) @variable.builtin (self) @variable.special
(field_identifier) @property (field_identifier) @property
(call_expression (call_expression
@ -27,22 +27,13 @@
; Identifier conventions ; Identifier conventions
; Assume uppercase names are enum constructors ; Assume uppercase names are types/enum-constructors
((identifier) @variant ((identifier) @type
(#match? @variant "^[A-Z]"))
; Assume that uppercase names in paths are types
((scoped_identifier
path: (identifier) @type)
(#match? @type "^[A-Z]"))
((scoped_identifier
path: (scoped_identifier
name: (identifier) @type))
(#match? @type "^[A-Z]")) (#match? @type "^[A-Z]"))
; Assume all-caps names are constants ; Assume all-caps names are constants
((identifier) @constant ((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]+$")) (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
[ [
"(" "("

View file

@ -115,7 +115,7 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn label_for_completion( async fn label_for_completion(
&self, &self,
item: &lsp::CompletionItem, item: &lsp::CompletionItem,
language: &language::Language, language: &Arc<language::Language>,
) -> Option<language::CodeLabel> { ) -> Option<language::CodeLabel> {
use lsp::CompletionItemKind as Kind; use lsp::CompletionItemKind as Kind;
let len = item.label.len(); let len = item.label.len();
@ -144,7 +144,6 @@ impl LspAdapter for TypeScriptLspAdapter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::sync::Arc;
use gpui::MutableAppContext; use gpui::MutableAppContext;
use unindent::Unindent; use unindent::Unindent;
@ -172,9 +171,8 @@ mod tests {
"# "#
.unindent(); .unindent();
let buffer = cx.add_model(|cx| { let buffer =
language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx) cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
});
let outline = buffer.read(cx).snapshot().outline(None).unwrap(); let outline = buffer.read(cx).snapshot().outline(None).unwrap();
assert_eq!( assert_eq!(
outline outline

View file

@ -51,12 +51,12 @@
(shorthand_property_identifier) (shorthand_property_identifier)
(shorthand_property_identifier_pattern) (shorthand_property_identifier_pattern)
] @constant ] @constant
(#match? @constant "^[A-Z_][A-Z\\d_]+$")) (#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
; Literals ; Literals
(this) @variable.builtin (this) @variable.special
(super) @variable.builtin (super) @variable.special
[ [
(true) (true)

View file

@ -20,7 +20,7 @@ use futures::{
FutureExt, SinkExt, StreamExt, FutureExt, SinkExt, StreamExt,
}; };
use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext}; use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task, ViewContext};
use isahc::{config::Configurable, AsyncBody, Request}; use isahc::{config::Configurable, Request};
use language::LanguageRegistry; use language::LanguageRegistry;
use log::LevelFilter; use log::LevelFilter;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -88,7 +88,7 @@ fn main() {
}); });
app.run(move |cx| { app.run(move |cx| {
let client = client::Client::new(http.clone()); let client = client::Client::new(http.clone(), cx);
let mut languages = LanguageRegistry::new(login_shell_env_loaded); let mut languages = LanguageRegistry::new(login_shell_env_loaded);
languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone()); languages.set_language_server_download_dir(zed::paths::LANGUAGES_DIR.clone());
let languages = Arc::new(languages); let languages = Arc::new(languages);
@ -120,7 +120,6 @@ fn main() {
vim::init(cx); vim::init(cx);
terminal::init(cx); terminal::init(cx);
let db = cx.background().block(db);
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx)) cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
.detach(); .detach();
@ -139,6 +138,10 @@ fn main() {
.detach(); .detach();
let project_store = cx.add_model(|_| ProjectStore::new()); let project_store = cx.add_model(|_| ProjectStore::new());
let db = cx.background().block(db);
client.start_telemetry(db.clone());
client.report_event("start app", Default::default());
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
languages, languages,
themes, themes,
@ -280,12 +283,10 @@ fn init_panic_hook(app_version: String, http: Arc<dyn HttpClient>, background: A
"token": ZED_SECRET_CLIENT_TOKEN, "token": ZED_SECRET_CLIENT_TOKEN,
})) }))
.unwrap(); .unwrap();
let request = Request::builder() let request = Request::post(&panic_report_url)
.uri(&panic_report_url)
.method(http::Method::POST)
.redirect_policy(isahc::config::RedirectPolicy::Follow) .redirect_policy(isahc::config::RedirectPolicy::Follow)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(AsyncBody::from(body))?; .body(body.into())?;
let response = http.send(request).await.context("error sending panic")?; let response = http.send(request).await.context("error sending panic")?;
if response.status().is_success() { if response.status().is_success() {
fs::remove_file(child_path) fs::remove_file(child_path)

View file

@ -328,6 +328,11 @@ pub fn menus() -> Vec<Menu<'static>> {
action: Box::new(command_palette::Toggle), action: Box::new(command_palette::Toggle),
}, },
MenuItem::Separator, MenuItem::Separator,
MenuItem::Action {
name: "View Telemetry Log",
action: Box::new(crate::OpenTelemetryLog),
},
MenuItem::Separator,
MenuItem::Action { MenuItem::Action {
name: "Documentation", name: "Documentation",
action: Box::new(crate::OpenBrowser { action: Box::new(crate::OpenBrowser {

View file

@ -55,6 +55,7 @@ actions!(
DebugElements, DebugElements,
OpenSettings, OpenSettings,
OpenLog, OpenLog,
OpenTelemetryLog,
OpenKeymap, OpenKeymap,
OpenDefaultSettings, OpenDefaultSettings,
OpenDefaultKeymap, OpenDefaultKeymap,
@ -145,6 +146,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
open_log_file(workspace, app_state.clone(), cx); open_log_file(workspace, app_state.clone(), cx);
} }
}); });
cx.add_action({
let app_state = app_state.clone();
move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
open_telemetry_log_file(workspace, app_state.clone(), cx);
}
});
cx.add_action({ cx.add_action({
let app_state = app_state.clone(); let app_state = app_state.clone();
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| { move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
@ -485,6 +492,62 @@ fn open_log_file(
}); });
} }
fn open_telemetry_log_file(
workspace: &mut Workspace,
app_state: Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) {
workspace.with_local_workspace(cx, app_state.clone(), |_, cx| {
cx.spawn_weak(|workspace, mut cx| async move {
let workspace = workspace.upgrade(&cx)?;
let path = app_state.client.telemetry_log_file_path()?;
let log = app_state.fs.load(&path).await.log_err()?;
const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
if let Some(newline_offset) = log[start_offset..].find('\n') {
start_offset += newline_offset + 1;
}
let log_suffix = &log[start_offset..];
workspace.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project
.update(cx, |project, cx| project.create_buffer("", None, cx))
.expect("creating buffers on a local workspace always succeeds");
buffer.update(cx, |buffer, cx| {
buffer.set_language(app_state.languages.get_language("JSON"), cx);
buffer.edit(
[(
0..0,
concat!(
"// Zed collects anonymous usage data to help us understand how people are using the app.\n",
"// After the beta release, we'll provide the ability to opt out of this telemetry.\n",
"// Here is the data that has been reported for the current session:\n",
"\n"
),
)],
None,
cx,
);
buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
});
let buffer = cx.add_model(|cx| {
MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
});
workspace.add_item(
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
cx,
);
});
Some(())
})
.detach();
});
}
fn open_bundled_config_file( fn open_bundled_config_file(
workspace: &mut Workspace, workspace: &mut Workspace,
app_state: Arc<AppState>, app_state: Arc<AppState>,
@ -1051,7 +1114,7 @@ mod tests {
assert!(!editor.is_dirty(cx)); assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "untitled"); assert_eq!(editor.title(cx), "untitled");
assert!(Arc::ptr_eq( assert!(Arc::ptr_eq(
editor.language_at(0, cx).unwrap(), &editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT &languages::PLAIN_TEXT
)); ));
editor.handle_input("hi", cx); editor.handle_input("hi", cx);
@ -1138,7 +1201,7 @@ mod tests {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
assert!(Arc::ptr_eq( assert!(Arc::ptr_eq(
editor.language_at(0, cx).unwrap(), &editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT &languages::PLAIN_TEXT
)); ));
editor.handle_input("hi", cx); editor.handle_input("hi", cx);

View file

@ -7,6 +7,7 @@ import {
player, player,
popoverShadow, popoverShadow,
text, text,
textColor,
TextColor, TextColor,
} from "./components"; } from "./components";
import hoverPopover from "./hoverPopover"; import hoverPopover from "./hoverPopover";
@ -59,8 +60,14 @@ export default function editor(theme: Theme) {
indicator: iconColor(theme, "secondary"), indicator: iconColor(theme, "secondary"),
verticalScale: 0.618 verticalScale: 0.618
}, },
diffBackgroundDeleted: backgroundColor(theme, "error"), diff: {
diffBackgroundInserted: backgroundColor(theme, "ok"), deleted: theme.iconColor.error,
inserted: theme.iconColor.ok,
modified: theme.iconColor.warning,
removedWidthEm: 0.275,
widthEm: 0.16,
cornerRadius: 0.05,
},
documentHighlightReadBackground: theme.editor.highlight.occurrence, documentHighlightReadBackground: theme.editor.highlight.occurrence,
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence, documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
errorColor: theme.textColor.error, errorColor: theme.textColor.error,

View file

@ -113,6 +113,11 @@ export function createTheme(
hovered: sample(ramps.blue, 0.1), hovered: sample(ramps.blue, 0.1),
active: sample(ramps.blue, 0.15), active: sample(ramps.blue, 0.15),
}, },
on500Ok: {
base: sample(ramps.green, 0.05),
hovered: sample(ramps.green, 0.1),
active: sample(ramps.green, 0.15)
}
}; };
const borderColor = { const borderColor = {
@ -180,6 +185,10 @@ export function createTheme(
color: sample(ramps.neutral, 7), color: sample(ramps.neutral, 7),
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
"variable.special": {
color: sample(ramps.blue, 0.80),
weight: fontWeights.normal,
},
comment: { comment: {
color: sample(ramps.neutral, 5), color: sample(ramps.neutral, 5),
weight: fontWeights.normal, weight: fontWeights.normal,
@ -205,15 +214,11 @@ export function createTheme(
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
constructor: { constructor: {
color: sample(ramps.blue, 0.5), color: sample(ramps.cyan, 0.5),
weight: fontWeights.normal,
},
variant: {
color: sample(ramps.blue, 0.5),
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
property: { property: {
color: sample(ramps.blue, 0.5), color: sample(ramps.blue, 0.6),
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
enum: { enum: {

View file

@ -43,7 +43,7 @@ export interface Syntax {
keyword: SyntaxHighlightStyle; keyword: SyntaxHighlightStyle;
function: SyntaxHighlightStyle; function: SyntaxHighlightStyle;
type: SyntaxHighlightStyle; type: SyntaxHighlightStyle;
variant: SyntaxHighlightStyle; constructor: SyntaxHighlightStyle;
property: SyntaxHighlightStyle; property: SyntaxHighlightStyle;
enum: SyntaxHighlightStyle; enum: SyntaxHighlightStyle;
operator: SyntaxHighlightStyle; operator: SyntaxHighlightStyle;
@ -78,6 +78,7 @@ export default interface Theme {
// Hacks for elements on top of the editor // Hacks for elements on top of the editor
on500: BackgroundColorSet; on500: BackgroundColorSet;
ok: BackgroundColorSet; ok: BackgroundColorSet;
on500Ok: BackgroundColorSet;
error: BackgroundColorSet; error: BackgroundColorSet;
on500Error: BackgroundColorSet; on500Error: BackgroundColorSet;
warning: BackgroundColorSet; warning: BackgroundColorSet;