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 }}
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
ZED_AMPLITUDE_API_KEY: ${{ secrets.ZED_AMPLITUDE_API_KEY }}
steps:
- name: Install Rust
run: |

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

View file

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

View file

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

View file

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

View file

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

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 gpui::{executor, ModelHandle, TestAppContext};
use parking_lot::Mutex;
use rpc::{proto, ConnectionId, Peer, Receipt, TypedEnvelope};
use rpc::{
proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
ConnectionId, Peer, Receipt, TypedEnvelope,
};
use std::{fmt, rc::Rc, sync::Arc};
pub struct FakeServer {
@ -93,6 +96,7 @@ impl FakeServer {
.authenticate_and_connect(false, &cx.to_async())
.await
.unwrap();
server
}
@ -126,26 +130,45 @@ impl FakeServer {
#[allow(clippy::await_holding_lock)]
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
self.executor.start_waiting();
let message = self
.state
.lock()
.incoming
.as_mut()
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
Ok(*message
.into_any()
.downcast::<TypedEnvelope<M>>()
.unwrap_or_else(|_| {
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}))
loop {
let message = self
.state
.lock()
.incoming
.as_mut()
.expect("not connected")
.next()
.await
.ok_or_else(|| anyhow!("other half hung up"))?;
self.executor.finish_waiting();
let type_name = message.payload_type_name();
let message = message.into_any();
if message.is::<TypedEnvelope<M>>() {
return Ok(*message.downcast().unwrap());
}
if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
self.respond(
message
.downcast::<TypedEnvelope<GetPrivateUserInfo>>()
.unwrap()
.receipt(),
GetPrivateUserInfoResponse {
metrics_id: "the-metrics-id".into(),
staff: false,
},
)
.await;
continue;
}
panic!(
"fake server received unexpected message type: {:?}",
type_name
);
}
}
pub async fn respond<T: proto::RequestMessage>(

View file

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

View file

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

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

22
crates/db/Cargo.toml Normal file
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" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
@ -51,6 +52,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-html = { version = "*", optional = true }
tree-sitter-javascript = { version = "*", optional = true }
[dev-dependencies]
text = { path = "../text", features = ["test-support"] }
@ -67,3 +70,5 @@ rand = "0.8"
unindent = "0.1.7"
tree-sitter = "0.20"
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-javascript = "0.20"

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

28
crates/git/Cargo.toml Normal file
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 wakeup_at = state.now + duration;
let id = util::post_inc(&mut state.next_timer_id);
state.pending_timers.push((id, wakeup_at, tx));
match state
.pending_timers
.binary_search_by_key(&wakeup_at, |e| e.1)
{
Ok(ix) | Err(ix) => state.pending_timers.insert(ix, (id, wakeup_at, tx)),
}
let state = self.state.clone();
Timer::Deterministic(DeterministicTimer { rx, id, state })
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

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_pattern)
] @constant
(#match? @constant "^[A-Z_][A-Z\\d_]+$"))
(#match? @constant "^_*[A-Z_][A-Z\\d_]*$"))
; Literals
(this) @variable.builtin
(super) @variable.builtin
(this) @variable.special
(super) @variable.special
[
(true)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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