Merge branch 'main' into stream-git-statuses
This commit is contained in:
commit
28ba27c9c5
206 changed files with 5946 additions and 3086 deletions
|
@ -9,17 +9,26 @@ path = "src/ai.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets"}
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = "0.4"
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tiktoken-rs = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
|
|
@ -1,22 +1,10 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::AsyncBufReadExt;
|
||||
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
|
||||
use gpui::executor::Background;
|
||||
use gpui::{actions, AppContext, Task, ViewContext};
|
||||
use isahc::prelude::*;
|
||||
use isahc::{http::StatusCode, Request};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::rc::Rc;
|
||||
use std::{io, sync::Arc};
|
||||
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
|
||||
actions!(ai, [Assist]);
|
||||
pub use assistant::AssistantPanel;
|
||||
use gpui::AppContext;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Serialize)]
|
||||
|
@ -38,7 +26,7 @@ struct ResponseMessage {
|
|||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Role {
|
||||
User,
|
||||
|
@ -46,6 +34,26 @@ enum Role {
|
|||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "User"),
|
||||
Role::Assistant => write!(f, "Assistant"),
|
||||
Role::System => write!(f, "System"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
|
@ -86,228 +94,5 @@ struct OpenAIChoice {
|
|||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
|
||||
return;
|
||||
}
|
||||
|
||||
let assistant = Rc::new(Assistant::default());
|
||||
cx.add_action({
|
||||
let assistant = assistant.clone();
|
||||
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
|
||||
assistant.assist(editor, cx).log_err();
|
||||
}
|
||||
});
|
||||
cx.capture_action({
|
||||
let assistant = assistant.clone();
|
||||
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
|
||||
if !assistant.cancel_last_assist(cx.view_id()) {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Assistant(RefCell<AssistantState>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct AssistantState {
|
||||
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
|
||||
next_completion_id: CompletionId,
|
||||
}
|
||||
|
||||
impl Assistant {
|
||||
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
|
||||
let api_key = std::env::var("OPENAI_API_KEY")?;
|
||||
|
||||
let selections = editor.selections.all(cx);
|
||||
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
|
||||
// Insert markers around selected text as described in the system prompt above.
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let mut user_message = String::new();
|
||||
let mut user_message_suffix = String::new();
|
||||
let mut buffer_offset = 0;
|
||||
for selection in selections {
|
||||
if !selection.is_empty() {
|
||||
if user_message_suffix.is_empty() {
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
user_message_suffix.push_str("[Selected excerpt from above]\n");
|
||||
user_message_suffix
|
||||
.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
|
||||
user_message.push_str("[SELECTION_START]");
|
||||
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
buffer_offset = selection.end;
|
||||
user_message.push_str("[SELECTION_END]");
|
||||
}
|
||||
if buffer_offset < snapshot.len() {
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
|
||||
}
|
||||
user_message.push_str(&user_message_suffix);
|
||||
|
||||
// Ensure the document ends with 4 trailing newlines.
|
||||
let trailing_newline_count = snapshot
|
||||
.reversed_chars_at(snapshot.len())
|
||||
.take_while(|c| *c == '\n')
|
||||
.take(4);
|
||||
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
|
||||
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
|
||||
|
||||
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
|
||||
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
|
||||
|
||||
(user_message, insertion_site)
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
let buffer = editor.buffer().clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let editor_id = cx.view_id();
|
||||
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
|
||||
let assist_task = cx.spawn(|_, mut cx| {
|
||||
async move {
|
||||
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
|
||||
let content = Assets::get("contexts/system.zmd").unwrap();
|
||||
let mut system_message = std::str::from_utf8(content.data.as_ref())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if let Ok(custom_system_message_path) =
|
||||
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
|
||||
{
|
||||
system_message.push_str(
|
||||
"\n\nAlso consider the following user-defined system prompt:\n\n",
|
||||
);
|
||||
// TODO: Replace this with our file system trait object.
|
||||
system_message.push_str(
|
||||
&cx.background()
|
||||
.spawn(async move { fs::read_to_string(custom_system_message_path) })
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let stream = stream_completion(
|
||||
api_key,
|
||||
executor,
|
||||
OpenAIRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![
|
||||
RequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message.to_string(),
|
||||
},
|
||||
RequestMessage {
|
||||
role: Role::User,
|
||||
content: user_message,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
},
|
||||
);
|
||||
|
||||
let mut messages = stream.await?;
|
||||
while let Some(message) = messages.next().await {
|
||||
let mut message = message?;
|
||||
if let Some(choice) = message.choices.pop() {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let text: Arc<str> = choice.delta.content?.into();
|
||||
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.unwrap()
|
||||
.retain(|(id, _)| *id != assist_id);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.entry(cx.view_id())
|
||||
.or_default()
|
||||
.push((assist_id, assist_task));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.and_then(|assists| assists.pop())
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
tx.unbounded_send(event).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
))
|
||||
}
|
||||
assistant::init(cx);
|
||||
}
|
||||
|
|
1383
crates/ai/src/assistant.rs
Normal file
1383
crates/ai/src/assistant.rs
Normal file
File diff suppressed because it is too large
Load diff
40
crates/ai/src/assistant_settings.rs
Normal file
40
crates/ai/src/assistant_settings.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assets.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow.workspace = true
|
||||
rust-embed = { version = "6.3", features = ["include-exclude"] }
|
|
@ -1,29 +0,0 @@
|
|||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["install", "--no-save"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"failed to install theme dependencies {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["run", "build"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"build script failed {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=../../styles/src");
|
||||
}
|
|
@ -159,10 +159,7 @@ impl Bundle {
|
|||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath {
|
||||
executable: excutable,
|
||||
..
|
||||
} => excutable,
|
||||
Self::LocalPath { executable, .. } => executable,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -776,15 +776,6 @@ impl Client {
|
|||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
cx.read(|cx| {
|
||||
self.telemetry().report_mixpanel_event(
|
||||
"read credentials from keychain",
|
||||
Default::default(),
|
||||
*settings::get::<TelemetrySettings>(cx),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if credentials.is_none() {
|
||||
let mut status_rx = self.status();
|
||||
|
@ -1072,11 +1063,8 @@ impl Client {
|
|||
) -> Task<Result<Credentials>> {
|
||||
let platform = cx.platform();
|
||||
let executor = cx.background();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let http = self.http.clone();
|
||||
|
||||
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
|
||||
|
||||
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
|
||||
|
@ -1159,12 +1147,6 @@ impl Client {
|
|||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_mixpanel_event(
|
||||
"authenticate with browser",
|
||||
Default::default(),
|
||||
telemetry_settings,
|
||||
);
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
serde_json::{self, value::Map, Value},
|
||||
AppContext, Task,
|
||||
};
|
||||
use gpui::{executor::Background, serde_json, AppContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
|
@ -19,7 +14,7 @@ use std::{
|
|||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt};
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Telemetry {
|
||||
|
@ -37,23 +32,15 @@ struct TelemetryState {
|
|||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
mixpanel_events_queue: Vec<MixpanelEvent>,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
next_mixpanel_event_id: usize,
|
||||
flush_mixpanel_events_task: Option<Task<()>>,
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
|
||||
lazy_static! {
|
||||
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
|
||||
static ref CLICKHOUSE_EVENTS_URL: String =
|
||||
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
|
||||
}
|
||||
|
@ -95,47 +82,6 @@ pub enum ClickhouseEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEvent {
|
||||
event: String,
|
||||
properties: MixpanelEventProperties,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct MixpanelEventProperties {
|
||||
// Mixpanel required fields
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
token: &'static str,
|
||||
time: u128,
|
||||
#[serde(rename = "distinct_id")]
|
||||
installation_id: Option<Arc<str>>,
|
||||
#[serde(rename = "$insert_id")]
|
||||
insert_id: usize,
|
||||
// Custom fields
|
||||
#[serde(skip_serializing_if = "Option::is_none", flatten)]
|
||||
event_properties: Option<Map<String, Value>>,
|
||||
#[serde(rename = "OS Name")]
|
||||
os_name: &'static str,
|
||||
#[serde(rename = "OS Version")]
|
||||
os_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Release Channel")]
|
||||
release_channel: Option<&'static str>,
|
||||
#[serde(rename = "App Version")]
|
||||
app_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Signed In")]
|
||||
signed_in: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MixpanelEngageRequest {
|
||||
#[serde(rename = "$token")]
|
||||
token: &'static str,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
installation_id: Arc<str>,
|
||||
#[serde(rename = "$set")]
|
||||
set: Value,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_QUEUE_LEN: usize = 1;
|
||||
|
||||
|
@ -168,29 +114,13 @@ impl Telemetry {
|
|||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
mixpanel_events_queue: Default::default(),
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_mixpanel_events_task: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
next_mixpanel_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
});
|
||||
|
||||
if MIXPANEL_TOKEN.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
|
||||
}
|
||||
|
||||
|
@ -218,20 +148,9 @@ impl Telemetry {
|
|||
let mut state = this.state.lock();
|
||||
state.installation_id = Some(installation_id.clone());
|
||||
|
||||
for event in &mut state.mixpanel_events_queue {
|
||||
event
|
||||
.properties
|
||||
.installation_id
|
||||
.get_or_insert_with(|| installation_id.clone());
|
||||
}
|
||||
|
||||
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_mixpanel_events {
|
||||
this.flush_mixpanel_events();
|
||||
}
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
this.flush_clickhouse_events();
|
||||
|
@ -256,37 +175,11 @@ impl Telemetry {
|
|||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let installation_id = state.installation_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
|
||||
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
installation_id,
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
"App": true
|
||||
}),
|
||||
}])?;
|
||||
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_clickhouse_event(
|
||||
|
@ -310,7 +203,7 @@ impl Telemetry {
|
|||
});
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
|
@ -324,55 +217,6 @@ impl Telemetry {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn report_mixpanel_event(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = MixpanelEvent {
|
||||
event: kind.into(),
|
||||
properties: MixpanelEventProperties {
|
||||
token: "",
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
installation_id: state.installation_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_mixpanel_event_id),
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
release_channel: state.release_channel,
|
||||
app_version: state.app_version.clone(),
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
},
|
||||
};
|
||||
state.mixpanel_events_queue.push(event);
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_mixpanel_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush_mixpanel_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
@ -385,44 +229,6 @@ impl Telemetry {
|
|||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush_mixpanel_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.mixpanel_events_queue);
|
||||
state.flush_mixpanel_events_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(token) = MIXPANEL_TOKEN.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 &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
|
||||
event.properties.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &events)?;
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
|||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.12.4"
|
||||
version = "0.13.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -97,6 +97,17 @@ CREATE TABLE "worktree_repositories" (
|
|||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"content" TEXT,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
|
||||
CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
CREATE TABLE "worktree_settings_files" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
|
||||
CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");
|
|
@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
|
|||
mod worktree_entry;
|
||||
mod worktree_repository;
|
||||
mod worktree_repository_statuses;
|
||||
mod worktree_settings_file;
|
||||
|
||||
use crate::executor::Executor;
|
||||
use crate::{Error, Result};
|
||||
|
@ -1494,6 +1495,7 @@ impl Database {
|
|||
updated_repositories: Default::default(),
|
||||
removed_repositories: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
};
|
||||
|
@ -1591,6 +1593,25 @@ impl Database {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) = worktrees
|
||||
.iter_mut()
|
||||
.find(|w| w.id == db_settings_file.worktree_id as u64)
|
||||
{
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut collaborators = project
|
||||
.find_related(project_collaborator::Entity)
|
||||
.all(&*tx)
|
||||
|
@ -2530,6 +2551,58 @@ impl Database {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn update_worktree_settings(
|
||||
&self,
|
||||
update: &proto::UpdateWorktreeSettings,
|
||||
connection: ConnectionId,
|
||||
) -> Result<RoomGuard<Vec<ConnectionId>>> {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
if project.host_connection()? != connection {
|
||||
return Err(anyhow!("can't update a project hosted by someone else"))?;
|
||||
}
|
||||
|
||||
if let Some(content) = &update.content {
|
||||
worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
|
||||
project_id: ActiveValue::Set(project_id),
|
||||
worktree_id: ActiveValue::Set(update.worktree_id as i64),
|
||||
path: ActiveValue::Set(update.path.clone()),
|
||||
content: ActiveValue::Set(content.clone()),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_settings_file::Column::ProjectId,
|
||||
worktree_settings_file::Column::WorktreeId,
|
||||
worktree_settings_file::Column::Path,
|
||||
])
|
||||
.update_column(worktree_settings_file::Column::Content)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
|
||||
project_id: ActiveValue::Set(project_id),
|
||||
worktree_id: ActiveValue::Set(update.worktree_id as i64),
|
||||
path: ActiveValue::Set(update.path.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn join_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
@ -2600,6 +2673,7 @@ impl Database {
|
|||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
settings_files: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
},
|
||||
|
@ -2684,6 +2758,25 @@ impl Database {
|
|||
}
|
||||
}
|
||||
|
||||
// Populate worktree settings files
|
||||
{
|
||||
let mut db_settings_files = worktree_settings_file::Entity::find()
|
||||
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_settings_file) = db_settings_files.next().await {
|
||||
let db_settings_file = db_settings_file?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_settings_file.worktree_id as u64))
|
||||
{
|
||||
worktree.settings_files.push(WorktreeSettingsFile {
|
||||
path: db_settings_file.path,
|
||||
content: db_settings_file.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
|
@ -3347,6 +3440,7 @@ pub struct RejoinedWorktree {
|
|||
pub updated_repositories: Vec<proto::RepositoryEntry>,
|
||||
pub removed_repositories: Vec<u64>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub settings_files: Vec<WorktreeSettingsFile>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
}
|
||||
|
@ -3402,10 +3496,17 @@ pub struct Worktree {
|
|||
pub entries: Vec<proto::Entry>,
|
||||
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub settings_files: Vec<WorktreeSettingsFile>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorktreeSettingsFile {
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub use test::*;
|
||||
|
||||
|
|
19
crates/collab/src/db/worktree_settings_file.rs
Normal file
19
crates/collab/src/db/worktree_settings_file.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_settings_files")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub path: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -200,6 +200,7 @@ impl Server {
|
|||
.add_message_handler(start_language_server)
|
||||
.add_message_handler(update_language_server)
|
||||
.add_message_handler(update_diagnostic_summary)
|
||||
.add_message_handler(update_worktree_settings)
|
||||
.add_request_handler(forward_project_request::<proto::GetHover>)
|
||||
.add_request_handler(forward_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
|
||||
|
@ -1088,6 +1089,18 @@ async fn rejoin_room(
|
|||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
for settings_file in worktree.settings_files {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateWorktreeSettings {
|
||||
project_id: project.id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for language_server in &project.language_servers {
|
||||
|
@ -1410,6 +1423,18 @@ async fn join_project(
|
|||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
for settings_file in worktree.settings_files {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateWorktreeSettings {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
path: settings_file.path,
|
||||
content: Some(settings_file.content),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for language_server in &project.language_servers {
|
||||
|
@ -1525,6 +1550,29 @@ async fn update_diagnostic_summary(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree_settings(
|
||||
message: proto::UpdateWorktreeSettings,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let guest_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.update_worktree_settings(&message, session.connection_id)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, message.clone())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
|
|
|
@ -3090,6 +3090,135 @@ async fn test_fs_operations(
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_local_settings(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).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);
|
||||
|
||||
// As client A, open a project that contains some local settings files
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 2 }"#
|
||||
},
|
||||
"a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 8 }"#
|
||||
},
|
||||
"a.txt": "a-contents",
|
||||
},
|
||||
"b": {
|
||||
"b.txt": "b-contents",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// As client B, join that project and observe the local settings.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client A, update a settings file. As Client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/.zed/settings.json", r#"{}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client A, create and remove some settings files. As client B, see the changed settings.
|
||||
client_a
|
||||
.fs
|
||||
.remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.create_dir("/dir/b/.zed".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
(Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
|
||||
]
|
||||
)
|
||||
});
|
||||
|
||||
// As client B, disconnect.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b.peer_id().unwrap());
|
||||
|
||||
// As client A, change and remove settings files while client B is disconnected.
|
||||
client_a
|
||||
.fs
|
||||
.insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
|
||||
.await;
|
||||
client_a
|
||||
.fs
|
||||
.remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// As client B, reconnect and see the changed settings.
|
||||
server.allow_connections();
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT);
|
||||
cx_b.read(|cx| {
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_conflict_after_save(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
|
|
@ -472,7 +472,7 @@ impl CollabTitlebarItem {
|
|||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
//TODO: Ensure this button has consistent width for both text variations
|
||||
let style = titlebar.share_button.style_for(state, false);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
|
|
|
@ -4,7 +4,7 @@ mod sign_in;
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
|
||||
|
@ -127,7 +127,7 @@ impl CopilotServer {
|
|||
struct RunningCopilotServer {
|
||||
lsp: Arc<LanguageServer>,
|
||||
sign_in_status: SignInStatus,
|
||||
registered_buffers: HashMap<u64, RegisteredBuffer>,
|
||||
registered_buffers: HashMap<usize, RegisteredBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -163,7 +163,6 @@ impl Status {
|
|||
}
|
||||
|
||||
struct RegisteredBuffer {
|
||||
id: u64,
|
||||
uri: lsp::Url,
|
||||
language_id: String,
|
||||
snapshot: BufferSnapshot,
|
||||
|
@ -178,13 +177,13 @@ impl RegisteredBuffer {
|
|||
buffer: &ModelHandle<Buffer>,
|
||||
cx: &mut ModelContext<Copilot>,
|
||||
) -> oneshot::Receiver<(i32, BufferSnapshot)> {
|
||||
let id = self.id;
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
|
||||
if buffer.read(cx).version() == self.snapshot.version {
|
||||
let _ = done_tx.send((self.snapshot_version, self.snapshot.clone()));
|
||||
} else {
|
||||
let buffer = buffer.downgrade();
|
||||
let id = buffer.id();
|
||||
let prev_pending_change =
|
||||
mem::replace(&mut self.pending_buffer_change, Task::ready(None));
|
||||
self.pending_buffer_change = cx.spawn_weak(|copilot, mut cx| async move {
|
||||
|
@ -268,7 +267,7 @@ pub struct Copilot {
|
|||
http: Arc<dyn HttpClient>,
|
||||
node_runtime: Arc<NodeRuntime>,
|
||||
server: CopilotServer,
|
||||
buffers: HashMap<u64, WeakModelHandle<Buffer>>,
|
||||
buffers: HashSet<WeakModelHandle<Buffer>>,
|
||||
}
|
||||
|
||||
impl Entity for Copilot {
|
||||
|
@ -318,7 +317,7 @@ impl Copilot {
|
|||
fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
|
||||
let http = self.http.clone();
|
||||
let node_runtime = self.node_runtime.clone();
|
||||
if all_language_settings(cx).copilot_enabled(None, None) {
|
||||
if all_language_settings(None, cx).copilot_enabled(None, None) {
|
||||
if matches!(self.server, CopilotServer::Disabled) {
|
||||
let start_task = cx
|
||||
.spawn({
|
||||
|
@ -375,7 +374,7 @@ impl Copilot {
|
|||
server
|
||||
.on_notification::<LogMessage, _>(|params, _cx| {
|
||||
match params.level {
|
||||
// Copilot is pretty agressive about logging
|
||||
// Copilot is pretty aggressive about logging
|
||||
0 => debug!("copilot: {}", params.message),
|
||||
1 => debug!("copilot: {}", params.message),
|
||||
_ => error!("copilot: {}", params.message),
|
||||
|
@ -559,8 +558,8 @@ impl Copilot {
|
|||
}
|
||||
|
||||
pub fn register_buffer(&mut self, buffer: &ModelHandle<Buffer>, cx: &mut ModelContext<Self>) {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
self.buffers.insert(buffer_id, buffer.downgrade());
|
||||
let weak_buffer = buffer.downgrade();
|
||||
self.buffers.insert(weak_buffer.clone());
|
||||
|
||||
if let CopilotServer::Running(RunningCopilotServer {
|
||||
lsp: server,
|
||||
|
@ -573,8 +572,7 @@ impl Copilot {
|
|||
return;
|
||||
}
|
||||
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
registered_buffers.entry(buffer_id).or_insert_with(|| {
|
||||
registered_buffers.entry(buffer.id()).or_insert_with(|| {
|
||||
let uri: lsp::Url = uri_for_buffer(buffer, cx);
|
||||
let language_id = id_for_language(buffer.read(cx).language());
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
@ -592,7 +590,6 @@ impl Copilot {
|
|||
.log_err();
|
||||
|
||||
RegisteredBuffer {
|
||||
id: buffer_id,
|
||||
uri,
|
||||
language_id,
|
||||
snapshot,
|
||||
|
@ -603,8 +600,8 @@ impl Copilot {
|
|||
this.handle_buffer_event(buffer, event, cx).log_err();
|
||||
}),
|
||||
cx.observe_release(buffer, move |this, _buffer, _cx| {
|
||||
this.buffers.remove(&buffer_id);
|
||||
this.unregister_buffer(buffer_id);
|
||||
this.buffers.remove(&weak_buffer);
|
||||
this.unregister_buffer(&weak_buffer);
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
@ -619,8 +616,7 @@ impl Copilot {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer_id) {
|
||||
if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.id()) {
|
||||
match event {
|
||||
language::Event::Edited => {
|
||||
let _ = registered_buffer.report_changes(&buffer, cx);
|
||||
|
@ -674,9 +670,9 @@ impl Copilot {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister_buffer(&mut self, buffer_id: u64) {
|
||||
fn unregister_buffer(&mut self, buffer: &WeakModelHandle<Buffer>) {
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
if let Some(buffer) = server.registered_buffers.remove(&buffer_id) {
|
||||
if let Some(buffer) = server.registered_buffers.remove(&buffer.id()) {
|
||||
server
|
||||
.lsp
|
||||
.notify::<lsp::notification::DidCloseTextDocument>(
|
||||
|
@ -779,16 +775,12 @@ impl Copilot {
|
|||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let lsp = server.lsp.clone();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer_id).unwrap();
|
||||
let registered_buffer = server.registered_buffers.get_mut(&buffer.id()).unwrap();
|
||||
let snapshot = registered_buffer.report_changes(buffer, cx);
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()).as_deref(),
|
||||
cx,
|
||||
);
|
||||
let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
|
@ -853,7 +845,7 @@ impl Copilot {
|
|||
lsp_status: request::SignInStatus,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.buffers.retain(|_, buffer| buffer.is_upgradable(cx));
|
||||
self.buffers.retain(|buffer| buffer.is_upgradable(cx));
|
||||
|
||||
if let Ok(server) = self.server.as_running() {
|
||||
match lsp_status {
|
||||
|
@ -861,7 +853,7 @@ impl Copilot {
|
|||
| request::SignInStatus::MaybeOk { .. }
|
||||
| request::SignInStatus::AlreadySignedIn { .. } => {
|
||||
server.sign_in_status = SignInStatus::Authorized;
|
||||
for buffer in self.buffers.values().cloned().collect::<Vec<_>>() {
|
||||
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
self.register_buffer(&buffer, cx);
|
||||
}
|
||||
|
@ -869,14 +861,14 @@ impl Copilot {
|
|||
}
|
||||
request::SignInStatus::NotAuthorized { .. } => {
|
||||
server.sign_in_status = SignInStatus::Unauthorized;
|
||||
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(buffer_id);
|
||||
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
}
|
||||
request::SignInStatus::NotSignedIn => {
|
||||
server.sign_in_status = SignInStatus::SignedOut;
|
||||
for buffer_id in self.buffers.keys().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(buffer_id);
|
||||
for buffer in self.buffers.iter().copied().collect::<Vec<_>>() {
|
||||
self.unregister_buffer(&buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -899,9 +891,7 @@ fn uri_for_buffer(buffer: &ModelHandle<Buffer>, cx: &AppContext) -> lsp::Url {
|
|||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
|
||||
} else {
|
||||
format!("buffer://{}", buffer.read(cx).remote_id())
|
||||
.parse()
|
||||
.unwrap()
|
||||
format!("buffer://{}", buffer.id()).parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1175,6 +1165,10 @@ mod tests {
|
|||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl language::LocalFile for File {
|
||||
|
|
|
@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
|
|||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
copilot = { path = "../copilot" }
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
|
|
|
@ -9,7 +9,10 @@ use gpui::{
|
|||
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::language_settings::{self, all_language_settings, AllLanguageSettings};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
|
@ -26,8 +29,8 @@ pub struct CopilotButton {
|
|||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<str>>,
|
||||
path: Option<Arc<Path>>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
|
@ -41,7 +44,7 @@ impl View for CopilotButton {
|
|||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let all_language_settings = &all_language_settings(cx);
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
|
@ -165,7 +168,7 @@ impl CopilotButton {
|
|||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
path: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
@ -197,14 +200,13 @@ impl CopilotButton {
|
|||
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(language.as_ref()), cx)
|
||||
.show_copilot_suggestions;
|
||||
let language_enabled = language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language
|
||||
language.name()
|
||||
),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
));
|
||||
|
@ -212,9 +214,9 @@ impl CopilotButton {
|
|||
|
||||
let settings = settings::get::<AllLanguageSettings>(cx);
|
||||
|
||||
if let Some(path) = self.path.as_ref() {
|
||||
let path_enabled = settings.copilot_enabled_for_path(path);
|
||||
let path = path.clone();
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
|
@ -276,17 +278,15 @@ impl CopilotButton {
|
|||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language_name = snapshot
|
||||
.language_at(suggestion_anchor)
|
||||
.map(|language| language.name());
|
||||
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(cx)
|
||||
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
|
||||
all_language_settings(self.file.as_ref(), cx)
|
||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||
);
|
||||
self.language = language_name;
|
||||
self.path = path.cloned();
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
|
@ -315,9 +315,7 @@ async fn configure_disabled_globs(
|
|||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content(&assets::Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
|
@ -363,17 +361,18 @@ async fn configure_disabled_globs(
|
|||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language)
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
|
|
|
@ -272,12 +272,11 @@ impl DisplayMap {
|
|||
}
|
||||
|
||||
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
|
||||
let language_name = buffer
|
||||
let language = buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).language())
|
||||
.map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
.and_then(|buffer| buffer.read(cx).language());
|
||||
language_settings(language.as_deref(), None, cx).tab_size
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -476,7 +475,7 @@ impl DisplaySnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
|
||||
/// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn find_while<'a>(
|
||||
&'a self,
|
||||
|
@ -487,7 +486,7 @@ impl DisplaySnapshot {
|
|||
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
|
||||
}
|
||||
|
||||
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
|
||||
/// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn reverse_find_while<'a>(
|
||||
&'a self,
|
||||
|
|
|
@ -10,7 +10,7 @@ pub mod items;
|
|||
mod link_go_to_definition;
|
||||
mod mouse_context_menu;
|
||||
pub mod movement;
|
||||
mod multi_buffer;
|
||||
pub mod multi_buffer;
|
||||
mod persistence;
|
||||
pub mod scroll;
|
||||
pub mod selections_collection;
|
||||
|
@ -31,9 +31,13 @@ use copilot::Copilot;
|
|||
pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use editor_settings::EditorSettings;
|
||||
pub use element::*;
|
||||
pub use element::RenderExcerptHeaderParams;
|
||||
pub use element::{
|
||||
Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::LayoutContext;
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
|
@ -44,9 +48,9 @@ use gpui::{
|
|||
impl_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json::{self, json},
|
||||
AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, Entity,
|
||||
ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element,
|
||||
Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
|
@ -109,6 +113,12 @@ pub struct SelectNext {
|
|||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq, Default)]
|
||||
pub struct SelectPrevious {
|
||||
#[serde(default)]
|
||||
pub replace_newest: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct SelectToBeginningOfLine {
|
||||
#[serde(default)]
|
||||
|
@ -270,6 +280,7 @@ impl_actions!(
|
|||
editor,
|
||||
[
|
||||
SelectNext,
|
||||
SelectPrevious,
|
||||
SelectToBeginningOfLine,
|
||||
SelectToEndOfLine,
|
||||
ToggleCodeActions,
|
||||
|
@ -365,6 +376,7 @@ pub fn init(cx: &mut AppContext) {
|
|||
cx.add_action(Editor::add_selection_above);
|
||||
cx.add_action(Editor::add_selection_below);
|
||||
cx.add_action(Editor::select_next);
|
||||
cx.add_action(Editor::select_previous);
|
||||
cx.add_action(Editor::toggle_comments);
|
||||
cx.add_action(Editor::select_larger_syntax_node);
|
||||
cx.add_action(Editor::select_smaller_syntax_node);
|
||||
|
@ -482,6 +494,7 @@ pub struct Editor {
|
|||
columnar_selection_tail: Option<Anchor>,
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
select_next_state: Option<SelectNextState>,
|
||||
select_prev_state: Option<SelectNextState>,
|
||||
selection_history: SelectionHistory,
|
||||
autoclose_regions: Vec<AutocloseRegion>,
|
||||
snippet_stack: InvalidationStack<SnippetState>,
|
||||
|
@ -496,7 +509,9 @@ pub struct Editor {
|
|||
blink_manager: ModelHandle<BlinkManager>,
|
||||
show_local_selections: bool,
|
||||
mode: EditorMode,
|
||||
show_gutter: bool,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
render_excerpt_header: Option<element::RenderExcerptHeader>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
background_highlights: BTreeMap<TypeId, (fn(&Theme) -> Color, Vec<Range<Anchor>>)>,
|
||||
|
@ -526,6 +541,7 @@ pub struct Editor {
|
|||
|
||||
pub struct EditorSnapshot {
|
||||
pub mode: EditorMode,
|
||||
pub show_gutter: bool,
|
||||
pub display_snapshot: DisplaySnapshot,
|
||||
pub placeholder_text: Option<Arc<str>>,
|
||||
is_focused: bool,
|
||||
|
@ -537,6 +553,7 @@ pub struct EditorSnapshot {
|
|||
struct SelectionHistoryEntry {
|
||||
selections: Arc<[Selection<Anchor>]>,
|
||||
select_next_state: Option<SelectNextState>,
|
||||
select_prev_state: Option<SelectNextState>,
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
|
@ -1284,6 +1301,7 @@ impl Editor {
|
|||
columnar_selection_tail: None,
|
||||
add_selections_state: None,
|
||||
select_next_state: None,
|
||||
select_prev_state: None,
|
||||
selection_history: Default::default(),
|
||||
autoclose_regions: Default::default(),
|
||||
snippet_stack: Default::default(),
|
||||
|
@ -1297,7 +1315,9 @@ impl Editor {
|
|||
blink_manager: blink_manager.clone(),
|
||||
show_local_selections: true,
|
||||
mode,
|
||||
show_gutter: mode == EditorMode::Full,
|
||||
placeholder_text: None,
|
||||
render_excerpt_header: None,
|
||||
highlighted_rows: None,
|
||||
background_highlights: Default::default(),
|
||||
nav_history: None,
|
||||
|
@ -1393,6 +1413,7 @@ impl Editor {
|
|||
pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
|
||||
EditorSnapshot {
|
||||
mode: self.mode,
|
||||
show_gutter: self.show_gutter,
|
||||
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
|
||||
scroll_anchor: self.scroll_manager.anchor(),
|
||||
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
|
||||
|
@ -1505,6 +1526,7 @@ impl Editor {
|
|||
let buffer = &display_map.buffer_snapshot;
|
||||
self.add_selections_state = None;
|
||||
self.select_next_state = None;
|
||||
self.select_prev_state = None;
|
||||
self.select_larger_syntax_node_stack.clear();
|
||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
|
||||
self.snippet_stack
|
||||
|
@ -2157,40 +2179,72 @@ impl Editor {
|
|||
indent.len = cmp::min(indent.len, start_point.column);
|
||||
let start = selection.start;
|
||||
let end = selection.end;
|
||||
let is_cursor = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, insert_extra_newline) =
|
||||
if let Some(language) = &language_scope {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let mut insert_extra_newline = false;
|
||||
if let Some(language) = buffer.language_scope_at(start) {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let insert_extra_newline =
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
insert_extra_newline = language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(
|
||||
end + trailing_whitespace_len,
|
||||
pair_end,
|
||||
)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter =
|
||||
language.line_comment_prefix().filter(|_| is_cursor);
|
||||
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
|
||||
buffer
|
||||
.buffer_line_for_row(start_point.row)
|
||||
.is_some_and(|(snapshot, range)| {
|
||||
snapshot
|
||||
.chars_for_range(range)
|
||||
.skip_while(|c| c.is_whitespace())
|
||||
.take(delimiter.len())
|
||||
.eq(delimiter.chars())
|
||||
})
|
||||
.then(|| delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(comment_delimiter, insert_extra_newline)
|
||||
} else {
|
||||
(None, false)
|
||||
};
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer
|
||||
.contains_str_at(end + trailing_whitespace_len, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
(start - leading_whitespace_len)
|
||||
.saturating_sub(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let mut new_text = String::with_capacity(1 + indent.len as usize);
|
||||
new_text.push('\n');
|
||||
let capacity_for_delimiter = comment_delimiter
|
||||
.as_deref()
|
||||
.map(str::len)
|
||||
.unwrap_or_default();
|
||||
let mut new_text =
|
||||
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
|
||||
new_text.push_str("\n");
|
||||
new_text.extend(indent.chars());
|
||||
if let Some(delimiter) = &comment_delimiter {
|
||||
new_text.push_str(&delimiter);
|
||||
}
|
||||
if insert_extra_newline {
|
||||
new_text = new_text.repeat(2);
|
||||
}
|
||||
|
@ -2372,7 +2426,7 @@ impl Editor {
|
|||
old_selections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let anchor = snapshot.anchor_after(s.end);
|
||||
let anchor = snapshot.anchor_after(s.head());
|
||||
s.map(|_| anchor)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -2525,7 +2579,7 @@ impl Editor {
|
|||
.read(cx)
|
||||
.text_anchor_for_position(position.clone(), cx)?;
|
||||
|
||||
// OnTypeFormatting retuns a list of edits, no need to pass them between Zed instances,
|
||||
// OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
|
||||
// hence we do LSP request & edit on host side only — add formats to host's history.
|
||||
let push_to_lsp_host_history = true;
|
||||
// If this is not the host, append its history with new edits.
|
||||
|
@ -3207,12 +3261,10 @@ impl Editor {
|
|||
snapshot: &MultiBufferSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let path = snapshot.file_at(location).map(|file| file.path().as_ref());
|
||||
let language_name = snapshot
|
||||
.language_at(location)
|
||||
.map(|language| language.name());
|
||||
let settings = all_language_settings(cx);
|
||||
settings.copilot_enabled(language_name.as_deref(), path)
|
||||
let file = snapshot.file_at(location);
|
||||
let language = snapshot.language_at(location);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
|
||||
}
|
||||
|
||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
||||
|
@ -3549,7 +3601,9 @@ impl Editor {
|
|||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
let cursor = movement::right(map, selection.head());
|
||||
selection.set_head(cursor, SelectionGoal::None);
|
||||
selection.end = cursor;
|
||||
selection.reversed = true;
|
||||
selection.goal = SelectionGoal::None;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -5213,6 +5267,101 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(&mut self, action: &SelectPrevious, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_selection_history();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let mut selections = self.selections.all::<usize>(cx);
|
||||
if let Some(mut select_prev_state) = self.select_prev_state.take() {
|
||||
let query = &select_prev_state.query;
|
||||
if !select_prev_state.done {
|
||||
let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
|
||||
let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
|
||||
let mut next_selected_range = None;
|
||||
// When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
|
||||
let bytes_before_last_selection =
|
||||
buffer.reversed_bytes_in_range(0..last_selection.start);
|
||||
let bytes_after_first_selection =
|
||||
buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
|
||||
let query_matches = query
|
||||
.stream_find_iter(bytes_before_last_selection)
|
||||
.map(|result| (last_selection.start, result))
|
||||
.chain(
|
||||
query
|
||||
.stream_find_iter(bytes_after_first_selection)
|
||||
.map(|result| (buffer.len(), result)),
|
||||
);
|
||||
for (end_offset, query_match) in query_matches {
|
||||
let query_match = query_match.unwrap(); // can only fail due to I/O
|
||||
let offset_range =
|
||||
end_offset - query_match.end()..end_offset - query_match.start();
|
||||
let display_range = offset_range.start.to_display_point(&display_map)
|
||||
..offset_range.end.to_display_point(&display_map);
|
||||
|
||||
if !select_prev_state.wordwise
|
||||
|| (!movement::is_inside_word(&display_map, display_range.start)
|
||||
&& !movement::is_inside_word(&display_map, display_range.end))
|
||||
{
|
||||
next_selected_range = Some(offset_range);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
s.insert_range(next_selected_range);
|
||||
});
|
||||
} else {
|
||||
select_prev_state.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.select_prev_state = Some(select_prev_state);
|
||||
} else if selections.len() == 1 {
|
||||
let selection = selections.last_mut().unwrap();
|
||||
if selection.start == selection.end {
|
||||
let word_range = movement::surrounding_word(
|
||||
&display_map,
|
||||
selection.start.to_display_point(&display_map),
|
||||
);
|
||||
selection.start = word_range.start.to_offset(&display_map, Bias::Left);
|
||||
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
|
||||
selection.goal = SelectionGoal::None;
|
||||
selection.reversed = false;
|
||||
|
||||
let query = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
let select_state = SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
wordwise: true,
|
||||
done: false,
|
||||
};
|
||||
self.unfold_ranges([selection.start..selection.end], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
self.select_prev_state = Some(select_state);
|
||||
} else {
|
||||
let query = buffer
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.collect::<String>();
|
||||
let query = query.chars().rev().collect::<String>();
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: AhoCorasick::new_auto_configured(&[query]),
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
self.select_previous(action, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let mut selections = this.selections.all::<Point>(cx);
|
||||
|
@ -5586,6 +5735,7 @@ impl Editor {
|
|||
if let Some(entry) = self.selection_history.undo_stack.pop_back() {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
|
@ -5598,6 +5748,7 @@ impl Editor {
|
|||
if let Some(entry) = self.selection_history.redo_stack.pop_back() {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.select_prev_state = entry.select_prev_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
|
@ -6375,6 +6526,7 @@ impl Editor {
|
|||
self.selection_history.push(SelectionHistoryEntry {
|
||||
selections: self.selections.disjoint_anchors(),
|
||||
select_next_state: self.select_next_state.clone(),
|
||||
select_prev_state: self.select_prev_state.clone(),
|
||||
add_selections_state: self.add_selections_state.clone(),
|
||||
});
|
||||
}
|
||||
|
@ -6654,6 +6806,25 @@ impl Editor {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
|
||||
self.show_gutter = show_gutter;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_render_excerpt_header(
|
||||
&mut self,
|
||||
render_excerpt_header: impl 'static
|
||||
+ Fn(
|
||||
&mut Editor,
|
||||
RenderExcerptHeaderParams,
|
||||
&mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.render_excerpt_header = Some(Arc::new(render_excerpt_header));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
|
||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
|
||||
|
@ -6878,7 +7049,7 @@ impl Editor {
|
|||
multi_buffer::Event::DiagnosticsUpdated => {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
}
|
||||
multi_buffer::Event::LanguageChanged => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7076,11 +7247,13 @@ impl Editor {
|
|||
};
|
||||
|
||||
// If None, we are in a file without an extension
|
||||
let file_extension = file_extension.or(self
|
||||
let file = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|b| b.read(cx).file())
|
||||
.and_then(|b| b.read(cx).file());
|
||||
let file_extension = file_extension.or(file
|
||||
.as_ref()
|
||||
.and_then(|file| Path::new(file.file_name(cx)).extension())
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
@ -7091,7 +7264,7 @@ impl Editor {
|
|||
.get("vim_mode")
|
||||
== Some(&serde_json::Value::Bool(true));
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
|
||||
let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
|
||||
let copilot_enabled_for_language = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
|
@ -7099,15 +7272,6 @@ impl Editor {
|
|||
.show_copilot_suggestions;
|
||||
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_mixpanel_event(
|
||||
match name {
|
||||
"open" => "open editor",
|
||||
"save" => "save editor",
|
||||
_ => name,
|
||||
},
|
||||
json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true }),
|
||||
telemetry_settings,
|
||||
);
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
|
@ -7299,8 +7463,12 @@ impl View for Editor {
|
|||
});
|
||||
}
|
||||
|
||||
let mut editor = EditorElement::new(style.clone());
|
||||
if let Some(render_excerpt_header) = self.render_excerpt_header.clone() {
|
||||
editor = editor.with_render_excerpt_header(render_excerpt_header);
|
||||
}
|
||||
Stack::new()
|
||||
.with_child(EditorElement::new(style.clone()))
|
||||
.with_child(editor)
|
||||
.with_child(ChildView::new(&self.mouse_context_menu, cx))
|
||||
.into_any()
|
||||
}
|
||||
|
@ -7801,13 +7969,13 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
|||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
inital_highlights: Vec<usize>,
|
||||
initial_highlights: Vec<usize>,
|
||||
message: &str,
|
||||
) -> (String, Vec<usize>) {
|
||||
let mut message_without_backticks = String::new();
|
||||
let mut prev_offset = 0;
|
||||
let mut inside_block = false;
|
||||
let mut highlights = inital_highlights;
|
||||
let mut highlights = initial_highlights;
|
||||
for (match_ix, (offset, _)) in message
|
||||
.match_indices('`')
|
||||
.chain([(message.len(), "")])
|
||||
|
|
|
@ -9,7 +9,8 @@ use gpui::{
|
|||
executor::Deterministic,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
serde_json, TestAppContext,
|
||||
serde_json::{self, json},
|
||||
TestAppContext,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
|
@ -578,7 +579,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
|||
assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
|
||||
|
||||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor;
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
|
||||
invalid_anchor.text_anchor.buffer_id = Some(999);
|
||||
let invalid_point = Point::new(9999, 0);
|
||||
editor.navigate(
|
||||
|
@ -586,7 +587,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
|||
cursor_anchor: invalid_anchor,
|
||||
cursor_position: invalid_point,
|
||||
scroll_anchor: ScrollAnchor {
|
||||
top_anchor: invalid_anchor,
|
||||
anchor: invalid_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
scroll_top_row: invalid_point.row,
|
||||
|
@ -1718,6 +1719,33 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
|
|||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comment: Some("//".into()),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
// Fooˇ
|
||||
"});
|
||||
|
||||
cx.update_editor(|e, cx| e.newline(&Newline, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
// Foo
|
||||
//ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -3107,6 +3135,57 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
|
|||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_previous(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
{
|
||||
// `Select previous` without a selection (selects wordwise)
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
{
|
||||
// `Select previous` with a selection
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx));
|
||||
cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
@ -4270,7 +4349,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
|||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overriden tabsize is sent to language server
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
|
@ -4384,7 +4463,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
|||
);
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overriden tabsize is sent to language server
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
|
@ -4725,7 +4804,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
|||
two
|
||||
threeˇ
|
||||
"},
|
||||
"overlapping aditional edit",
|
||||
"overlapping additional edit",
|
||||
),
|
||||
(
|
||||
indoc! {"
|
||||
|
@ -5225,7 +5304,28 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
|||
Point::new(0, 1)..Point::new(0, 1),
|
||||
Point::new(1, 1)..Point::new(1, 1),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure the cursor's head is respected when deleting across an excerpt boundary.
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
|
||||
});
|
||||
view.backspace(&Default::default(), cx);
|
||||
assert_eq!(view.text(cx), "Xa\nbbb");
|
||||
assert_eq!(
|
||||
view.selections.ranges(cx),
|
||||
[Point::new(1, 0)..Point::new(1, 0)]
|
||||
);
|
||||
|
||||
view.change_selections(None, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
|
||||
});
|
||||
view.backspace(&Default::default(), cx);
|
||||
assert_eq!(view.text(cx), "X\nbb");
|
||||
assert_eq!(
|
||||
view.selections.ranges(cx),
|
||||
[Point::new(0, 1)..Point::new(0, 1)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5742,7 +5842,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
|
|||
let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
|
||||
follower.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
offset: vec2f(0.0, 0.5),
|
||||
},
|
||||
cx,
|
||||
|
|
|
@ -91,18 +91,41 @@ impl SelectionLayout {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderExcerptHeaderParams<'a> {
|
||||
pub id: crate::ExcerptId,
|
||||
pub buffer: &'a language::BufferSnapshot,
|
||||
pub range: &'a crate::ExcerptRange<text::Anchor>,
|
||||
pub starts_new_buffer: bool,
|
||||
pub gutter_padding: f32,
|
||||
pub editor_style: &'a EditorStyle,
|
||||
}
|
||||
|
||||
pub type RenderExcerptHeader = Arc<
|
||||
dyn Fn(
|
||||
&mut Editor,
|
||||
RenderExcerptHeaderParams,
|
||||
&mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor>,
|
||||
>;
|
||||
|
||||
pub struct EditorElement {
|
||||
style: Arc<EditorStyle>,
|
||||
render_excerpt_header: RenderExcerptHeader,
|
||||
}
|
||||
|
||||
impl EditorElement {
|
||||
pub fn new(style: EditorStyle) -> Self {
|
||||
Self {
|
||||
style: Arc::new(style),
|
||||
render_excerpt_header: Arc::new(render_excerpt_header),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_render_excerpt_header(mut self, render: RenderExcerptHeader) -> Self {
|
||||
self.render_excerpt_header = render;
|
||||
self
|
||||
}
|
||||
|
||||
fn attach_mouse_handlers(
|
||||
scene: &mut SceneBuilder,
|
||||
position_map: &Arc<PositionMap>,
|
||||
|
@ -1465,11 +1488,9 @@ impl EditorElement {
|
|||
line_height: f32,
|
||||
style: &EditorStyle,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
include_root: bool,
|
||||
editor: &mut Editor,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
) -> (f32, Vec<BlockLayout>) {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let scroll_x = snapshot.scroll_anchor.offset.x();
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
.blocks_in_range(rows.clone())
|
||||
|
@ -1510,112 +1531,18 @@ impl EditorElement {
|
|||
range,
|
||||
starts_new_buffer,
|
||||
..
|
||||
} => {
|
||||
let id = *id;
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
|
||||
let style = style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, editor, cx| {
|
||||
if let Some(workspace) = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Editor::jump(
|
||||
workspace,
|
||||
jump_path.clone(),
|
||||
jump_position,
|
||||
jump_anchor,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JumpIcon>(
|
||||
id.into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
});
|
||||
|
||||
if *starts_new_buffer {
|
||||
let style = &self.style.diagnostic_path_header;
|
||||
let font_size =
|
||||
(style.text_scale_factor * self.style.text.font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path =
|
||||
path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(parent_path.map(|path| {
|
||||
Label::new(path, style.path.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("path header block")
|
||||
} else {
|
||||
let text_style = self.style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("⋯", text_style))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("collapsed context")
|
||||
}
|
||||
}
|
||||
} => (self.render_excerpt_header)(
|
||||
editor,
|
||||
RenderExcerptHeaderParams {
|
||||
id: *id,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer: *starts_new_buffer,
|
||||
gutter_padding,
|
||||
editor_style: style,
|
||||
},
|
||||
cx,
|
||||
),
|
||||
};
|
||||
|
||||
element.layout(
|
||||
|
@ -1899,7 +1826,7 @@ impl Element<Editor> for EditorElement {
|
|||
let gutter_padding;
|
||||
let gutter_width;
|
||||
let gutter_margin;
|
||||
if snapshot.mode == EditorMode::Full {
|
||||
if snapshot.show_gutter {
|
||||
let em_width = style.text.em_width(cx.font_cache());
|
||||
gutter_padding = (em_width * style.gutter_padding_factor).round();
|
||||
gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
|
||||
|
@ -2080,12 +2007,6 @@ impl Element<Editor> for EditorElement {
|
|||
ShowScrollbar::Never => false,
|
||||
};
|
||||
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
|
||||
.into_iter()
|
||||
.map(|(id, fold)| {
|
||||
|
@ -2144,7 +2065,6 @@ impl Element<Editor> for EditorElement {
|
|||
line_height,
|
||||
&style,
|
||||
&line_layouts,
|
||||
include_root,
|
||||
editor,
|
||||
cx,
|
||||
);
|
||||
|
@ -2759,6 +2679,121 @@ impl HighlightedRange {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_excerpt_header(
|
||||
editor: &mut Editor,
|
||||
RenderExcerptHeaderParams {
|
||||
id,
|
||||
buffer,
|
||||
range,
|
||||
starts_new_buffer,
|
||||
gutter_padding,
|
||||
editor_style,
|
||||
}: RenderExcerptHeaderParams,
|
||||
cx: &mut LayoutContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let include_root = editor
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||
.unwrap_or_default();
|
||||
let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
|
||||
let jump_path = ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
};
|
||||
let jump_anchor = range
|
||||
.primary
|
||||
.as_ref()
|
||||
.map_or(range.context.start, |primary| primary.start);
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
|
||||
enum JumpIcon {}
|
||||
MouseEventHandler::<JumpIcon, _>::new(id.into(), cx, |state, _| {
|
||||
let style = editor_style.jump_icon.style_for(state, false);
|
||||
Svg::new("icons/arrow_up_right_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, editor, cx| {
|
||||
if let Some(workspace) = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Editor::jump(workspace, jump_path.clone(), jump_position, jump_anchor, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JumpIcon>(
|
||||
id.into(),
|
||||
"Jump to Buffer".to_string(),
|
||||
Some(Box::new(crate::OpenExcerpts)),
|
||||
tooltip_style.clone(),
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
});
|
||||
|
||||
if starts_new_buffer {
|
||||
let style = &editor_style.diagnostic_path_header;
|
||||
let font_size = (style.text_scale_factor * editor_style.text.font_size).round();
|
||||
|
||||
let path = buffer.resolve_file_path(cx, include_root);
|
||||
let mut filename = None;
|
||||
let mut parent_path = None;
|
||||
// Can't use .and_then() because `.file_name()` and `.parent()` return references :(
|
||||
if let Some(path) = path {
|
||||
filename = path.file_name().map(|f| f.to_string_lossy().to_string());
|
||||
parent_path = path.parent().map(|p| p.to_string_lossy().to_string() + "/");
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
filename.unwrap_or_else(|| "untitled".to_string()),
|
||||
style.filename.text.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.filename.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(parent_path.map(|path| {
|
||||
Label::new(path, style.path.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.path.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("path header block")
|
||||
} else {
|
||||
let text_style = editor_style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("⋯", text_style))
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
.with_padding_right(gutter_padding)
|
||||
.expanded()
|
||||
.into_any_named("collapsed context")
|
||||
}
|
||||
}
|
||||
|
||||
fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
|
@ -3080,7 +3115,7 @@ mod tests {
|
|||
editor_width: f32,
|
||||
) -> Vec<Invisible> {
|
||||
info!(
|
||||
"Creating editor with mode {editor_mode:?}, witdh {editor_width} and text '{input_text}'"
|
||||
"Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
|
||||
);
|
||||
let (_, editor) = cx.add_window(|cx| {
|
||||
let buffer = MultiBuffer::build_simple(&input_text, cx);
|
||||
|
|
|
@ -221,6 +221,7 @@ fn show_hover(
|
|||
project: project.clone(),
|
||||
symbol_range: range,
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
})
|
||||
});
|
||||
|
@ -253,6 +254,7 @@ fn render_blocks(
|
|||
theme_id: usize,
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
style: &EditorStyle,
|
||||
) -> RenderedInfo {
|
||||
let mut text = String::new();
|
||||
|
@ -351,11 +353,13 @@ fn render_blocks(
|
|||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
if let CodeBlockKind::Fenced(language) = kind {
|
||||
current_language = language_registry
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.now_or_never()
|
||||
.and_then(Result::ok);
|
||||
.and_then(Result::ok)
|
||||
} else {
|
||||
language.cloned()
|
||||
}
|
||||
}
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
|
@ -414,10 +418,6 @@ fn render_blocks(
|
|||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
RenderedInfo {
|
||||
theme_id,
|
||||
text,
|
||||
|
@ -524,6 +524,7 @@ pub struct InfoPopover {
|
|||
pub project: ModelHandle<Project>,
|
||||
pub symbol_range: Range<Anchor>,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
}
|
||||
|
||||
|
@ -559,6 +560,7 @@ impl InfoPopover {
|
|||
style.theme_id,
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
style,
|
||||
)
|
||||
});
|
||||
|
@ -588,10 +590,7 @@ impl InfoPopover {
|
|||
MouseRegion::new::<Self>(view_id, region_id, bounds)
|
||||
.on_click::<Editor, _>(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
println!("clicked link {url}");
|
||||
cx.platform().open_url(&url);
|
||||
},
|
||||
move |_, _, cx| cx.platform().open_url(&url),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -906,7 +905,7 @@ mod tests {
|
|||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec
|
||||
- d
|
||||
"
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
|
@ -949,8 +947,7 @@ mod tests {
|
|||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d
|
||||
"
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
|
@ -973,9 +970,8 @@ mod tests {
|
|||
|
||||
nine
|
||||
* ten
|
||||
* six
|
||||
"
|
||||
.unindent(),
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
|
@ -985,9 +981,8 @@ mod tests {
|
|||
|
||||
nine
|
||||
- ten
|
||||
- six
|
||||
"
|
||||
.unindent(),
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
|
@ -1004,7 +999,7 @@ mod tests {
|
|||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), &style);
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
|
|
|
@ -196,7 +196,7 @@ impl FollowableItem for Editor {
|
|||
singleton: buffer.is_singleton(),
|
||||
title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
|
||||
excerpts,
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.top_anchor)),
|
||||
scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
|
||||
scroll_x: scroll_anchor.offset.x(),
|
||||
scroll_y: scroll_anchor.offset.y(),
|
||||
selections: self
|
||||
|
@ -253,7 +253,7 @@ impl FollowableItem for Editor {
|
|||
}
|
||||
Event::ScrollPositionChanged { .. } => {
|
||||
let scroll_anchor = self.scroll_manager.anchor();
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.top_anchor));
|
||||
update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
|
||||
update.scroll_x = scroll_anchor.offset.x();
|
||||
update.scroll_y = scroll_anchor.offset.y();
|
||||
true
|
||||
|
@ -412,7 +412,7 @@ async fn update_editor_from_message(
|
|||
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
anchor: scroll_top_anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
cx,
|
||||
|
@ -510,8 +510,8 @@ impl Item for Editor {
|
|||
};
|
||||
|
||||
let mut scroll_anchor = data.scroll_anchor;
|
||||
if !buffer.can_resolve(&scroll_anchor.top_anchor) {
|
||||
scroll_anchor.top_anchor = buffer.anchor_before(
|
||||
if !buffer.can_resolve(&scroll_anchor.anchor) {
|
||||
scroll_anchor.anchor = buffer.anchor_before(
|
||||
buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
|
||||
);
|
||||
}
|
||||
|
@ -1231,6 +1231,10 @@ mod tests {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
|
|
@ -64,6 +64,9 @@ pub enum Event {
|
|||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
ExcerptsEdited {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
Edited,
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
|
@ -196,6 +199,13 @@ pub struct MultiBufferBytes<'a> {
|
|||
chunk: &'a [u8],
|
||||
}
|
||||
|
||||
pub struct ReversedMultiBufferBytes<'a> {
|
||||
range: Range<usize>,
|
||||
excerpts: Cursor<'a, Excerpt, usize>,
|
||||
excerpt_bytes: Option<ExcerptBytes<'a>>,
|
||||
chunk: &'a [u8],
|
||||
}
|
||||
|
||||
struct ExcerptChunks<'a> {
|
||||
content_chunks: BufferChunks<'a>,
|
||||
footer_height: usize,
|
||||
|
@ -387,6 +397,7 @@ impl MultiBuffer {
|
|||
original_indent_column: u32,
|
||||
}
|
||||
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
|
||||
let mut edited_excerpt_ids = Vec::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
for (ix, (range, new_text)) in edits.enumerate() {
|
||||
let new_text: Arc<str> = new_text.into();
|
||||
|
@ -403,6 +414,7 @@ impl MultiBuffer {
|
|||
.start
|
||||
.to_offset(&start_excerpt.buffer)
|
||||
+ start_overshoot;
|
||||
edited_excerpt_ids.push(start_excerpt.id);
|
||||
|
||||
cursor.seek(&range.end, Bias::Right, &());
|
||||
if cursor.item().is_none() && range.end == *cursor.start() {
|
||||
|
@ -428,6 +440,7 @@ impl MultiBuffer {
|
|||
original_indent_column,
|
||||
});
|
||||
} else {
|
||||
edited_excerpt_ids.push(end_excerpt.id);
|
||||
let start_excerpt_range = buffer_start
|
||||
..start_excerpt
|
||||
.range
|
||||
|
@ -474,6 +487,7 @@ impl MultiBuffer {
|
|||
is_insertion: false,
|
||||
original_indent_column,
|
||||
});
|
||||
edited_excerpt_ids.push(excerpt.id);
|
||||
cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
@ -546,6 +560,10 @@ impl MultiBuffer {
|
|||
buffer.edit(insertions, insertion_autoindent_mode, cx);
|
||||
})
|
||||
}
|
||||
|
||||
cx.emit(Event::ExcerptsEdited {
|
||||
ids: edited_excerpt_ids,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||
|
@ -1377,8 +1395,14 @@ impl MultiBuffer {
|
|||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language = self.language_at(point, cx);
|
||||
language_settings(language.map(|l| l.name()).as_deref(), cx)
|
||||
let mut language = None;
|
||||
let mut file = None;
|
||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
language = buffer.language_at(offset);
|
||||
file = buffer.file();
|
||||
}
|
||||
language_settings(language.as_ref(), file, cx)
|
||||
}
|
||||
|
||||
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
|
||||
|
@ -1961,7 +1985,6 @@ impl MultiBufferSnapshot {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
MultiBufferBytes {
|
||||
range,
|
||||
excerpts,
|
||||
|
@ -1970,6 +1993,33 @@ impl MultiBufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn reversed_bytes_in_range<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
) -> ReversedMultiBufferBytes {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut excerpts = self.excerpts.cursor::<usize>();
|
||||
excerpts.seek(&range.end, Bias::Left, &());
|
||||
|
||||
let mut chunk = &[][..];
|
||||
let excerpt_bytes = if let Some(excerpt) = excerpts.item() {
|
||||
let mut excerpt_bytes = excerpt.reversed_bytes_in_range(
|
||||
range.start - excerpts.start()..range.end - excerpts.start(),
|
||||
);
|
||||
chunk = excerpt_bytes.next().unwrap_or(&[][..]);
|
||||
Some(excerpt_bytes)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ReversedMultiBufferBytes {
|
||||
range,
|
||||
excerpts,
|
||||
excerpt_bytes,
|
||||
chunk,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, start_row: u32) -> MultiBufferRows {
|
||||
let mut result = MultiBufferRows {
|
||||
buffer_row_range: 0..0,
|
||||
|
@ -2785,9 +2835,13 @@ impl MultiBufferSnapshot {
|
|||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
self.point_to_buffer_offset(point)
|
||||
.map(|(buffer, offset)| buffer.settings_at(offset, cx))
|
||||
.unwrap_or_else(|| language_settings(None, cx))
|
||||
let mut language = None;
|
||||
let mut file = None;
|
||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
|
||||
language = buffer.language_at(offset);
|
||||
file = buffer.file();
|
||||
}
|
||||
language_settings(language, file, cx)
|
||||
}
|
||||
|
||||
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
|
||||
|
@ -3399,6 +3453,26 @@ impl Excerpt {
|
|||
}
|
||||
}
|
||||
|
||||
fn reversed_bytes_in_range(&self, range: Range<usize>) -> ExcerptBytes {
|
||||
let content_start = self.range.context.start.to_offset(&self.buffer);
|
||||
let bytes_start = content_start + range.start;
|
||||
let bytes_end = content_start + cmp::min(range.end, self.text_summary.len);
|
||||
let footer_height = if self.has_trailing_newline
|
||||
&& range.start <= self.text_summary.len
|
||||
&& range.end > self.text_summary.len
|
||||
{
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let content_bytes = self.buffer.reversed_bytes_in_range(bytes_start..bytes_end);
|
||||
|
||||
ExcerptBytes {
|
||||
content_bytes,
|
||||
footer_height,
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor {
|
||||
if text_anchor
|
||||
.cmp(&self.range.context.start, &self.buffer)
|
||||
|
@ -3717,6 +3791,38 @@ impl<'a> io::Read for MultiBufferBytes<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> ReversedMultiBufferBytes<'a> {
|
||||
fn consume(&mut self, len: usize) {
|
||||
self.range.end -= len;
|
||||
self.chunk = &self.chunk[..self.chunk.len() - len];
|
||||
|
||||
if !self.range.is_empty() && self.chunk.is_empty() {
|
||||
if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) {
|
||||
self.chunk = chunk;
|
||||
} else {
|
||||
self.excerpts.next(&());
|
||||
if let Some(excerpt) = self.excerpts.item() {
|
||||
let mut excerpt_bytes =
|
||||
excerpt.bytes_in_range(0..self.range.end - self.excerpts.start());
|
||||
self.chunk = excerpt_bytes.next().unwrap();
|
||||
self.excerpt_bytes = Some(excerpt_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> io::Read for ReversedMultiBufferBytes<'a> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let len = cmp::min(buf.len(), self.chunk.len());
|
||||
buf[..len].copy_from_slice(&self.chunk[..len]);
|
||||
buf[..len].reverse();
|
||||
if len > 0 {
|
||||
self.consume(len);
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
impl<'a> Iterator for ExcerptBytes<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
|
@ -5237,7 +5343,7 @@ mod tests {
|
|||
assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678");
|
||||
|
||||
// An undo in the multibuffer undoes the multibuffer transaction
|
||||
// and also any individual buffer edits that have occured since
|
||||
// and also any individual buffer edits that have occurred since
|
||||
// that transaction.
|
||||
multibuffer.undo(cx);
|
||||
assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678");
|
||||
|
|
|
@ -36,21 +36,21 @@ pub struct ScrollbarAutoHide(pub bool);
|
|||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ScrollAnchor {
|
||||
pub offset: Vector2F,
|
||||
pub top_anchor: Anchor,
|
||||
pub anchor: Anchor,
|
||||
}
|
||||
|
||||
impl ScrollAnchor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
offset: Vector2F::zero(),
|
||||
top_anchor: Anchor::min(),
|
||||
anchor: Anchor::min(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
|
||||
let mut scroll_position = self.offset;
|
||||
if self.top_anchor != Anchor::min() {
|
||||
let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32;
|
||||
if self.anchor != Anchor::min() {
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
|
||||
scroll_position.set_y(scroll_top + scroll_position.y());
|
||||
} else {
|
||||
scroll_position.set_y(0.);
|
||||
|
@ -59,7 +59,7 @@ impl ScrollAnchor {
|
|||
}
|
||||
|
||||
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
|
||||
self.top_anchor.to_point(buffer).row
|
||||
self.anchor.to_point(buffer).row
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ impl ScrollManager {
|
|||
let (new_anchor, top_row) = if scroll_position.y() <= 0. {
|
||||
(
|
||||
ScrollAnchor {
|
||||
top_anchor: Anchor::min(),
|
||||
anchor: Anchor::min(),
|
||||
offset: scroll_position.max(vec2f(0., 0.)),
|
||||
},
|
||||
0,
|
||||
|
@ -193,7 +193,7 @@ impl ScrollManager {
|
|||
|
||||
(
|
||||
ScrollAnchor {
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
offset: vec2f(
|
||||
scroll_position.x(),
|
||||
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
|
||||
|
@ -322,7 +322,7 @@ impl Editor {
|
|||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
|
@ -337,7 +337,7 @@ impl Editor {
|
|||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
|
@ -377,7 +377,7 @@ impl Editor {
|
|||
let screen_top = self
|
||||
.scroll_manager
|
||||
.anchor
|
||||
.top_anchor
|
||||
.anchor
|
||||
.to_display_point(&snapshot);
|
||||
|
||||
if screen_top > newest_head {
|
||||
|
@ -408,7 +408,7 @@ impl Editor {
|
|||
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
|
||||
let scroll_anchor = ScrollAnchor {
|
||||
offset: Vector2F::new(x, y),
|
||||
top_anchor,
|
||||
anchor: top_anchor,
|
||||
};
|
||||
self.set_scroll_anchor(scroll_anchor, cx);
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ impl Editor {
|
|||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
|
@ -113,7 +113,7 @@ impl Editor {
|
|||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
|
@ -143,7 +143,7 @@ impl Editor {
|
|||
|
||||
editor.set_scroll_anchor(
|
||||
ScrollAnchor {
|
||||
top_anchor: new_anchor,
|
||||
anchor: new_anchor,
|
||||
offset: Default::default(),
|
||||
},
|
||||
cx,
|
||||
|
|
|
@ -76,6 +76,9 @@ impl SelectionsCollection {
|
|||
count
|
||||
}
|
||||
|
||||
/// The non-pending, non-overlapping selections. There could still be a pending
|
||||
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
|
||||
/// selections over Anchors.
|
||||
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
|
||||
self.disjoint.clone()
|
||||
}
|
||||
|
|
|
@ -48,8 +48,8 @@ pub fn marked_display_snapshot(
|
|||
}
|
||||
|
||||
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
|
||||
let (umarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), umarked_text);
|
||||
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ use std::{env, fmt::Display};
|
|||
use sysinfo::{System, SystemExt};
|
||||
use util::channel::ReleaseChannel;
|
||||
|
||||
// TODO: Move this file out of feedback and into a more general place
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SystemSpecs {
|
||||
#[serde(serialize_with = "serialize_app_version")]
|
||||
|
|
|
@ -33,7 +33,7 @@ use repository::{FakeGitRepositoryState, GitFileStatus};
|
|||
use std::sync::Weak;
|
||||
|
||||
lazy_static! {
|
||||
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||
static ref LINE_SEPARATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
@ -78,13 +78,13 @@ impl LineEnding {
|
|||
}
|
||||
|
||||
pub fn normalize(text: &mut String) {
|
||||
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
|
||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") {
|
||||
*text = replaced;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
|
||||
if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") {
|
||||
replaced.into()
|
||||
} else {
|
||||
text
|
||||
|
|
|
@ -53,7 +53,7 @@ uuid = { version = "1.1.2", features = ["v4"] }
|
|||
waker-fn = "1.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
||||
bindgen = "0.65.1"
|
||||
cc = "1.0.67"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -6335,9 +6335,9 @@ mod tests {
|
|||
#[crate::test(self)]
|
||||
async fn test_labeled_tasks(cx: &mut TestAppContext) {
|
||||
assert_eq!(None, cx.update(|cx| cx.active_labeled_tasks().next()));
|
||||
let (mut sender, mut reciever) = postage::oneshot::channel::<()>();
|
||||
let (mut sender, mut receiver) = postage::oneshot::channel::<()>();
|
||||
let task = cx
|
||||
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { reciever.recv().await }));
|
||||
.update(|cx| cx.spawn_labeled("Test Label", |_| async move { receiver.recv().await }));
|
||||
|
||||
assert_eq!(
|
||||
Some("Test Label"),
|
||||
|
|
|
@ -965,10 +965,10 @@ impl<'a> WindowContext<'a> {
|
|||
}
|
||||
|
||||
pub fn rect_for_text_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||
let root_view_id = self.window.root_view().id();
|
||||
let focused_view_id = self.window.focused_view_id?;
|
||||
self.window
|
||||
.rendered_views
|
||||
.get(&root_view_id)?
|
||||
.get(&focused_view_id)?
|
||||
.rect_for_text_range(range_utf16, self)
|
||||
.log_err()
|
||||
.flatten()
|
||||
|
|
|
@ -84,8 +84,8 @@ impl InputHandler for WindowInputHandler {
|
|||
|
||||
fn rect_for_range(&self, range_utf16: Range<usize>) -> Option<RectF> {
|
||||
self.app
|
||||
.borrow_mut()
|
||||
.update_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
|
||||
.borrow()
|
||||
.read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ struct StateInner {
|
|||
scroll_to: Option<ScrollTarget>,
|
||||
}
|
||||
|
||||
pub struct LayoutState<V: View> {
|
||||
pub struct UniformListLayoutState<V: View> {
|
||||
scroll_max: f32,
|
||||
item_height: f32,
|
||||
items: Vec<AnyElement<V>>,
|
||||
|
@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
|
|||
}
|
||||
|
||||
impl<V: View> Element<V> for UniformList<V> {
|
||||
type LayoutState = LayoutState<V>;
|
||||
type LayoutState = UniformListLayoutState<V>;
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
|
@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
|
|||
|
||||
let no_items = (
|
||||
constraint.min,
|
||||
LayoutState {
|
||||
UniformListLayoutState {
|
||||
item_height: 0.,
|
||||
scroll_max: 0.,
|
||||
items: Default::default(),
|
||||
|
@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
|
|||
|
||||
(
|
||||
size,
|
||||
LayoutState {
|
||||
UniformListLayoutState {
|
||||
item_height,
|
||||
scroll_max,
|
||||
items,
|
||||
|
|
|
@ -67,7 +67,7 @@ impl KeymapMatcher {
|
|||
/// MatchResult::Pending =>
|
||||
/// There exist bindings which are still waiting for more keys.
|
||||
/// MatchResult::Complete(matches) =>
|
||||
/// 1 or more bindings have recieved the necessary key presses.
|
||||
/// 1 or more bindings have received the necessary key presses.
|
||||
/// The order of the matched actions is by position of the matching first,
|
||||
// and order in the keymap second.
|
||||
pub fn push_keystroke(
|
||||
|
|
|
@ -216,6 +216,11 @@ pub trait File: Send + Sync {
|
|||
/// of its worktree, then this method will return the name of the worktree itself.
|
||||
fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
|
||||
|
||||
/// Returns the id of the worktree to which this file belongs.
|
||||
///
|
||||
/// This is needed for looking up project-specific settings.
|
||||
fn worktree_id(&self) -> usize;
|
||||
|
||||
fn is_deleted(&self) -> bool;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
@ -1802,8 +1807,7 @@ impl BufferSnapshot {
|
|||
}
|
||||
|
||||
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 = language_settings(language_name.as_deref(), cx);
|
||||
let settings = language_settings(self.language_at(position), self.file(), cx);
|
||||
if settings.hard_tabs {
|
||||
IndentSize::tab()
|
||||
} else {
|
||||
|
@ -2127,8 +2131,7 @@ impl BufferSnapshot {
|
|||
position: D,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language = self.language_at(position);
|
||||
language_settings(language.map(|l| l.name()).as_deref(), cx)
|
||||
language_settings(self.language_at(position), self.file.as_ref(), cx)
|
||||
}
|
||||
|
||||
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
|
||||
|
@ -2250,7 +2253,7 @@ impl BufferSnapshot {
|
|||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
self.outline_items_containing(0..self.len(), theme)
|
||||
self.outline_items_containing(0..self.len(), true, theme)
|
||||
.map(Outline::new)
|
||||
}
|
||||
|
||||
|
@ -2262,6 +2265,7 @@ impl BufferSnapshot {
|
|||
let position = position.to_offset(self);
|
||||
let mut items = self.outline_items_containing(
|
||||
position.saturating_sub(1)..self.len().min(position + 1),
|
||||
false,
|
||||
theme,
|
||||
)?;
|
||||
let mut prev_depth = None;
|
||||
|
@ -2276,6 +2280,7 @@ impl BufferSnapshot {
|
|||
fn outline_items_containing(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
include_extra_context: bool,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<Vec<OutlineItem<Anchor>>> {
|
||||
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
|
||||
|
@ -2310,7 +2315,10 @@ impl BufferSnapshot {
|
|||
let node_is_name;
|
||||
if capture.index == config.name_capture_ix {
|
||||
node_is_name = true;
|
||||
} else if Some(capture.index) == config.context_capture_ix {
|
||||
} else if Some(capture.index) == config.context_capture_ix
|
||||
|| (Some(capture.index) == config.extra_context_capture_ix
|
||||
&& include_extra_context)
|
||||
{
|
||||
node_is_name = false;
|
||||
} else {
|
||||
continue;
|
||||
|
@ -2337,10 +2345,12 @@ impl BufferSnapshot {
|
|||
buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
|
||||
true,
|
||||
);
|
||||
let mut last_buffer_range_end = 0;
|
||||
for (buffer_range, is_name) in buffer_ranges {
|
||||
if !text.is_empty() {
|
||||
if !text.is_empty() && buffer_range.start > last_buffer_range_end {
|
||||
text.push(' ');
|
||||
}
|
||||
last_buffer_range_end = buffer_range.end;
|
||||
if is_name {
|
||||
let mut start = text.len();
|
||||
let end = start + buffer_range.len();
|
||||
|
|
|
@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
|
||||
let language = javascript_lang()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(function_declaration
|
||||
"function" @context
|
||||
name: (_) @name
|
||||
parameters: (formal_parameters
|
||||
"(" @context.extra
|
||||
")" @context.extra)) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let text = r#"
|
||||
function a() {}
|
||||
function b(c) {}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
// extra context nodes are included in the outline.
|
||||
let outline = snapshot.outline(None).unwrap();
|
||||
assert_eq!(
|
||||
outline
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("function a()", 0), ("function b( )", 0),]
|
||||
);
|
||||
|
||||
// extra context nodes do not appear in breadcrumbs.
|
||||
let symbols = snapshot.symbols_containing(3, None).unwrap();
|
||||
assert_eq!(
|
||||
symbols
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("function a", 0)]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
|
||||
let text = r#"
|
||||
|
|
|
@ -34,7 +34,7 @@ use std::{
|
|||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Not, Range},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::{
|
||||
|
@ -455,6 +455,7 @@ struct OutlineConfig {
|
|||
item_capture_ix: u32,
|
||||
name_capture_ix: u32,
|
||||
context_capture_ix: Option<u32>,
|
||||
extra_context_capture_ix: Option<u32>,
|
||||
}
|
||||
|
||||
struct InjectionConfig {
|
||||
|
@ -500,6 +501,7 @@ struct AvailableLanguage {
|
|||
grammar: tree_sitter::Language,
|
||||
lsp_adapters: Vec<Arc<dyn LspAdapter>>,
|
||||
get_queries: fn(&str) -> LanguageQueries,
|
||||
loaded: bool,
|
||||
}
|
||||
|
||||
pub struct LanguageRegistry {
|
||||
|
@ -527,6 +529,7 @@ struct LanguageRegistryState {
|
|||
subscription: (watch::Sender<()>, watch::Receiver<()>),
|
||||
theme: Option<Arc<Theme>>,
|
||||
version: usize,
|
||||
reload_count: usize,
|
||||
}
|
||||
|
||||
pub struct PendingLanguageServer {
|
||||
|
@ -547,6 +550,7 @@ impl LanguageRegistry {
|
|||
subscription: watch::channel(),
|
||||
theme: Default::default(),
|
||||
version: 0,
|
||||
reload_count: 0,
|
||||
}),
|
||||
language_server_download_dir: None,
|
||||
lsp_binary_statuses_tx,
|
||||
|
@ -566,6 +570,14 @@ impl LanguageRegistry {
|
|||
self.executor = Some(executor);
|
||||
}
|
||||
|
||||
/// Clear out all of the loaded languages and reload them from scratch.
|
||||
///
|
||||
/// This is useful in development, when queries have changed.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn reload(&self) {
|
||||
self.state.write().reload();
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&self,
|
||||
path: &'static str,
|
||||
|
@ -582,6 +594,7 @@ impl LanguageRegistry {
|
|||
grammar,
|
||||
lsp_adapters,
|
||||
get_queries,
|
||||
loaded: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -590,7 +603,7 @@ impl LanguageRegistry {
|
|||
let mut result = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.map(|l| l.config.name.to_string())
|
||||
.filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
|
||||
.chain(state.languages.iter().map(|l| l.config.name.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
|
||||
|
@ -603,6 +616,7 @@ impl LanguageRegistry {
|
|||
state
|
||||
.available_languages
|
||||
.iter()
|
||||
.filter(|l| !l.loaded)
|
||||
.flat_map(|l| l.lsp_adapters.clone())
|
||||
.chain(
|
||||
state
|
||||
|
@ -639,10 +653,17 @@ impl LanguageRegistry {
|
|||
self.state.read().subscription.1.clone()
|
||||
}
|
||||
|
||||
/// The number of times that the registry has been changed,
|
||||
/// by adding languages or reloading.
|
||||
pub fn version(&self) -> usize {
|
||||
self.state.read().version
|
||||
}
|
||||
|
||||
/// The number of times that the registry has been reloaded.
|
||||
pub fn reload_count(&self) -> usize {
|
||||
self.state.read().reload_count
|
||||
}
|
||||
|
||||
pub fn set_theme(&self, theme: Arc<Theme>) {
|
||||
let mut state = self.state.write();
|
||||
state.theme = Some(theme.clone());
|
||||
|
@ -721,7 +742,7 @@ impl LanguageRegistry {
|
|||
if let Some(language) = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.find(|l| callback(&l.config))
|
||||
.find(|l| !l.loaded && callback(&l.config))
|
||||
.cloned()
|
||||
{
|
||||
let txs = state
|
||||
|
@ -743,9 +764,7 @@ impl LanguageRegistry {
|
|||
let language = Arc::new(language);
|
||||
let mut state = this.state.write();
|
||||
state.add(language.clone());
|
||||
state
|
||||
.available_languages
|
||||
.retain(|language| language.id != id);
|
||||
state.mark_language_loaded(id);
|
||||
if let Some(mut txs) = state.loading_languages.remove(&id) {
|
||||
for tx in txs.drain(..) {
|
||||
let _ = tx.send(Ok(language.clone()));
|
||||
|
@ -753,10 +772,9 @@ impl LanguageRegistry {
|
|||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to load language {name} - {err}");
|
||||
let mut state = this.state.write();
|
||||
state
|
||||
.available_languages
|
||||
.retain(|language| language.id != id);
|
||||
state.mark_language_loaded(id);
|
||||
if let Some(mut txs) = state.loading_languages.remove(&id) {
|
||||
for tx in txs.drain(..) {
|
||||
let _ = tx.send(Err(anyhow!(
|
||||
|
@ -905,6 +923,28 @@ impl LanguageRegistryState {
|
|||
self.version += 1;
|
||||
*self.subscription.0.borrow_mut() = ();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn reload(&mut self) {
|
||||
self.languages.clear();
|
||||
self.version += 1;
|
||||
self.reload_count += 1;
|
||||
for language in &mut self.available_languages {
|
||||
language.loaded = false;
|
||||
}
|
||||
*self.subscription.0.borrow_mut() = ();
|
||||
}
|
||||
|
||||
/// Mark the given language a having been loaded, so that the
|
||||
/// language registry won't try to load it again.
|
||||
fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
|
||||
for language in &mut self.available_languages {
|
||||
if language.id == id {
|
||||
language.loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -1021,34 +1061,22 @@ impl Language {
|
|||
|
||||
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
|
||||
if let Some(query) = queries.highlights {
|
||||
self = self
|
||||
.with_highlights_query(query.as_ref())
|
||||
.expect("failed to evaluate highlights query");
|
||||
self = self.with_highlights_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.brackets {
|
||||
self = self
|
||||
.with_brackets_query(query.as_ref())
|
||||
.expect("failed to load brackets query");
|
||||
self = self.with_brackets_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.indents {
|
||||
self = self
|
||||
.with_indents_query(query.as_ref())
|
||||
.expect("failed to load indents query");
|
||||
self = self.with_indents_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.outline {
|
||||
self = self
|
||||
.with_outline_query(query.as_ref())
|
||||
.expect("failed to load outline query");
|
||||
self = self.with_outline_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.injections {
|
||||
self = self
|
||||
.with_injection_query(query.as_ref())
|
||||
.expect("failed to load injection query");
|
||||
self = self.with_injection_query(query.as_ref())?;
|
||||
}
|
||||
if let Some(query) = queries.overrides {
|
||||
self = self
|
||||
.with_override_query(query.as_ref())
|
||||
.expect("failed to load override query");
|
||||
self = self.with_override_query(query.as_ref())?;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
@ -1064,12 +1092,14 @@ impl Language {
|
|||
let mut item_capture_ix = None;
|
||||
let mut name_capture_ix = None;
|
||||
let mut context_capture_ix = None;
|
||||
let mut extra_context_capture_ix = None;
|
||||
get_capture_indices(
|
||||
&query,
|
||||
&mut [
|
||||
("item", &mut item_capture_ix),
|
||||
("name", &mut name_capture_ix),
|
||||
("context", &mut context_capture_ix),
|
||||
("context.extra", &mut extra_context_capture_ix),
|
||||
],
|
||||
);
|
||||
if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
|
||||
|
@ -1078,6 +1108,7 @@ impl Language {
|
|||
item_capture_ix,
|
||||
name_capture_ix,
|
||||
context_capture_ix,
|
||||
extra_context_capture_ix,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::{File, Language};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use globset::GlobMatcher;
|
||||
|
@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
|
|||
settings::register::<AllLanguageSettings>(cx);
|
||||
}
|
||||
|
||||
pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
|
||||
settings::get::<AllLanguageSettings>(cx).language(language)
|
||||
pub fn language_settings<'a>(
|
||||
language: Option<&Arc<Language>>,
|
||||
file: Option<&Arc<dyn File>>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language_name = language.map(|l| l.name());
|
||||
all_language_settings(file, cx).language(language_name.as_deref())
|
||||
}
|
||||
|
||||
pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
|
||||
settings::get::<AllLanguageSettings>(cx)
|
||||
pub fn all_language_settings<'a>(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a AllLanguageSettings {
|
||||
let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
|
||||
settings::get_local(location, cx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -155,7 +165,7 @@ impl AllLanguageSettings {
|
|||
.any(|glob| glob.is_match(path))
|
||||
}
|
||||
|
||||
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
|
||||
pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
|
||||
if !self.copilot.feature_enabled {
|
||||
return false;
|
||||
}
|
||||
|
@ -166,7 +176,8 @@ impl AllLanguageSettings {
|
|||
}
|
||||
}
|
||||
|
||||
self.language(language_name).show_copilot_suggestions
|
||||
self.language(language.map(|l| l.name()).as_deref())
|
||||
.show_copilot_suggestions
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,7 +264,7 @@ impl settings::Setting for AllLanguageSettings {
|
|||
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
|
||||
|
||||
// Create a schema for a 'languages overrides' object, associating editor
|
||||
// settings with specific langauges.
|
||||
// settings with specific languages.
|
||||
assert!(root_schema
|
||||
.definitions
|
||||
.contains_key("LanguageSettingsContent"));
|
||||
|
|
|
@ -773,7 +773,7 @@ impl<'a> SyntaxMapCaptures<'a> {
|
|||
} in layers
|
||||
{
|
||||
let grammar = match &language.grammar {
|
||||
Some(grammer) => grammer,
|
||||
Some(grammar) => grammar,
|
||||
None => continue,
|
||||
};
|
||||
let query = match query(&grammar) {
|
||||
|
@ -896,7 +896,7 @@ impl<'a> SyntaxMapMatches<'a> {
|
|||
} in layers
|
||||
{
|
||||
let grammar = match &language.grammar {
|
||||
Some(grammer) => grammer,
|
||||
Some(grammar) => grammar,
|
||||
None => continue,
|
||||
};
|
||||
let query = match query(&grammar) {
|
||||
|
|
|
@ -260,9 +260,10 @@ impl LanguageServer {
|
|||
buffer.clear();
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
stdout.read_until(b'\n', &mut buffer).await?;
|
||||
let message_len: usize = std::str::from_utf8(&buffer)?
|
||||
let header = std::str::from_utf8(&buffer)?;
|
||||
let message_len: usize = header
|
||||
.strip_prefix(CONTENT_LEN_HEADER)
|
||||
.ok_or_else(|| anyhow!("invalid header"))?
|
||||
.ok_or_else(|| anyhow!("invalid LSP message header {header:?}"))?
|
||||
.trim_end()
|
||||
.parse()?;
|
||||
|
||||
|
@ -301,7 +302,7 @@ impl LanguageServer {
|
|||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to deserialize message:\n{}",
|
||||
"failed to deserialize LSP message:\n{}",
|
||||
std::str::from_utf8(&buffer)?
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,4 +18,4 @@ metal = "0.21.0"
|
|||
objc = "0.2"
|
||||
|
||||
[build-dependencies]
|
||||
bindgen = "0.59.2"
|
||||
bindgen = "0.65.1"
|
||||
|
|
|
@ -11,7 +11,7 @@ use syn::{parse_macro_input, Block, FnArg, ForeignItemFn, Ident, ItemFn, Pat, Ty
|
|||
/// "Hello from Wasm".into()
|
||||
/// }
|
||||
/// ```
|
||||
/// This macro makes a function defined guest-side avaliable host-side.
|
||||
/// This macro makes a function defined guest-side available host-side.
|
||||
/// Note that all arguments and return types must be `serde`.
|
||||
#[proc_macro_attribute]
|
||||
pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
|
@ -92,7 +92,7 @@ pub fn export(args: TokenStream, function: TokenStream) -> TokenStream {
|
|||
/// #[import]
|
||||
/// pub fn operating_system_name() -> String;
|
||||
/// ```
|
||||
/// This macro makes a function defined host-side avaliable guest-side.
|
||||
/// This macro makes a function defined host-side available guest-side.
|
||||
/// Note that all arguments and return types must be `serde`.
|
||||
/// All that's provided is a signature, as the function is implemented host-side.
|
||||
#[proc_macro_attribute]
|
||||
|
|
|
@ -127,7 +127,7 @@ use plugin_handles::RopeHandle;
|
|||
pub fn append(rope: RopeHandle, string: &str);
|
||||
```
|
||||
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only aquire resources to handles we're given, so we'd need to expose a fuction that takes a handle.
|
||||
This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle.
|
||||
|
||||
To illustrate that point, here's an example. First, we'd define a plugin-side function as follows:
|
||||
|
||||
|
@ -177,7 +177,7 @@ So here's what calling `append_newline` would do, from the top:
|
|||
|
||||
6. And from here on out we return up the callstack, through Wasm, to Rust all the way back to where we started. Right before we return, we clear out the `ResourcePool`, so that we're no longer holding onto the underlying resource.
|
||||
|
||||
Throughout this entire chain of calls, the resource remain host-side. By temporarilty checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
|
||||
Throughout this entire chain of calls, the resource remain host-side. By temporarily checking it into a `ResourcePool`, we're able to keep a reference to the resource that we can use, while avoiding copying the uncopyable resource.
|
||||
|
||||
## Final Notes
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ impl PluginBuilder {
|
|||
"env",
|
||||
&format!("__{}", name),
|
||||
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
|
||||
// TODO: use try block once avaliable
|
||||
// TODO: use try block once available
|
||||
let result: Result<(WasiBuffer, Memory, _), Trap> = (|| {
|
||||
// grab a handle to the memory
|
||||
let plugin_memory = match caller.get_export("memory") {
|
||||
|
@ -211,7 +211,7 @@ impl PluginBuilder {
|
|||
"env",
|
||||
&format!("__{}", name),
|
||||
move |mut caller: Caller<'_, WasiCtxAlloc>, packed_buffer: u64| {
|
||||
// TODO: use try block once avaliable
|
||||
// TODO: use try block once available
|
||||
let result: Result<(WasiBuffer, Memory, Vec<u8>), Trap> = (|| {
|
||||
// grab a handle to the memory
|
||||
let plugin_memory = match caller.get_export("memory") {
|
||||
|
@ -297,7 +297,7 @@ pub enum PluginBinary<'a> {
|
|||
Precompiled(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Represents a WebAssembly plugin, with access to the WebAssembly System Inferface.
|
||||
/// Represents a WebAssembly plugin, with access to the WebAssembly System Interface.
|
||||
/// Build a new plugin using [`PluginBuilder`].
|
||||
pub struct Plugin {
|
||||
store: Store<WasiCtxAlloc>,
|
||||
|
@ -559,7 +559,7 @@ impl Plugin {
|
|||
.ok_or_else(|| anyhow!("Could not grab slice of plugin memory"))?;
|
||||
|
||||
// write the argument to linear memory
|
||||
// this returns a (ptr, lentgh) pair
|
||||
// this returns a (ptr, length) pair
|
||||
let arg_buffer = Self::bytes_to_buffer(
|
||||
self.store.data().alloc_buffer(),
|
||||
&mut plugin_memory,
|
||||
|
@ -569,7 +569,7 @@ impl Plugin {
|
|||
.await?;
|
||||
|
||||
// call the function, passing in the buffer and its length
|
||||
// this returns a ptr to a (ptr, lentgh) pair
|
||||
// this returns a ptr to a (ptr, length) pair
|
||||
let result_buffer = handle
|
||||
.function
|
||||
.call_async(&mut self.store, arg_buffer.into_u64())
|
||||
|
|
|
@ -1111,14 +1111,18 @@ impl LspCommand for GetHover {
|
|||
cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
Ok(message.and_then(|hover| {
|
||||
let range = hover.range.map(|range| {
|
||||
cx.read(|cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let token_start =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||
let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
||||
})
|
||||
let (language, range) = cx.read(|cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
(
|
||||
buffer.language().cloned(),
|
||||
hover.range.map(|range| {
|
||||
let token_start =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||
let token_end =
|
||||
buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
fn hover_blocks_from_marked_string(
|
||||
|
@ -1163,7 +1167,11 @@ impl LspCommand for GetHover {
|
|||
}],
|
||||
});
|
||||
|
||||
Some(Hover { contents, range })
|
||||
Some(Hover {
|
||||
contents,
|
||||
range,
|
||||
language,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1247,16 +1255,9 @@ impl LspCommand for GetHover {
|
|||
self,
|
||||
message: proto::GetHoverResponse,
|
||||
_: ModelHandle<Project>,
|
||||
_: ModelHandle<Buffer>,
|
||||
_: AsyncAppContext,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
||||
language::proto::deserialize_anchor(start)
|
||||
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let contents: Vec<_> = message
|
||||
.contents
|
||||
.into_iter()
|
||||
|
@ -1271,12 +1272,23 @@ impl LspCommand for GetHover {
|
|||
},
|
||||
})
|
||||
.collect();
|
||||
if contents.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(if contents.is_empty() {
|
||||
None
|
||||
let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
|
||||
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
||||
language::proto::deserialize_anchor(start)
|
||||
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
||||
} else {
|
||||
Some(Hover { contents, range })
|
||||
})
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Some(Hover {
|
||||
contents,
|
||||
range,
|
||||
language,
|
||||
}))
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64 {
|
||||
|
@ -1499,7 +1511,11 @@ impl LspCommand for GetCodeActions {
|
|||
type ProtoRequest = proto::GetCodeActions;
|
||||
|
||||
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
|
||||
capabilities.code_action_provider.is_some()
|
||||
match &capabilities.code_action_provider {
|
||||
None => false,
|
||||
Some(lsp::CodeActionProviderCapability::Simple(false)) => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
|
@ -1717,8 +1733,7 @@ impl LspCommand for OnTypeFormatting {
|
|||
.await?;
|
||||
|
||||
let tab_size = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
language_settings(buffer.language(), buffer.file(), cx).tab_size
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
|
|
|
@ -28,7 +28,7 @@ use gpui::{
|
|||
ModelHandle, Task, WeakModelHandle,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
|
||||
language_settings::{language_settings, FormatOnSave, Formatter},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
|
@ -72,7 +72,10 @@ use std::{
|
|||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
use terminals::Terminals;
|
||||
use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
|
||||
use util::{
|
||||
debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
|
||||
ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
pub use worktree::*;
|
||||
|
@ -356,6 +359,7 @@ pub enum HoverBlockKind {
|
|||
pub struct Hover {
|
||||
pub contents: Vec<HoverBlock>,
|
||||
pub range: Option<Range<language::Anchor>>,
|
||||
pub language: Option<Arc<Language>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -460,6 +464,7 @@ impl Project {
|
|||
client.add_model_request_handler(Self::handle_update_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
|
||||
client.add_model_message_handler(Self::handle_update_worktree);
|
||||
client.add_model_message_handler(Self::handle_update_worktree_settings);
|
||||
client.add_model_request_handler(Self::handle_create_project_entry);
|
||||
client.add_model_request_handler(Self::handle_rename_project_entry);
|
||||
client.add_model_request_handler(Self::handle_copy_project_entry);
|
||||
|
@ -519,7 +524,7 @@ impl Project {
|
|||
_subscriptions: vec![
|
||||
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
|
||||
],
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
||||
active_entry: None,
|
||||
languages,
|
||||
|
@ -588,7 +593,7 @@ impl Project {
|
|||
active_entry: None,
|
||||
collaborators: Default::default(),
|
||||
join_project_response_message_id: response.message_id,
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
|
||||
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
|
||||
_maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
|
||||
languages,
|
||||
user_store: user_store.clone(),
|
||||
|
@ -686,42 +691,37 @@ impl Project {
|
|||
}
|
||||
|
||||
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let settings = all_language_settings(cx);
|
||||
|
||||
let mut language_servers_to_start = Vec::new();
|
||||
for buffer in self.opened_buffers.values() {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
|
||||
{
|
||||
if settings
|
||||
.language(Some(&language.name()))
|
||||
.enable_language_server
|
||||
{
|
||||
let worktree = file.worktree.read(cx);
|
||||
language_servers_to_start.push((
|
||||
worktree.id(),
|
||||
worktree.as_local().unwrap().abs_path().clone(),
|
||||
language.clone(),
|
||||
));
|
||||
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
|
||||
let settings = language_settings(Some(language), Some(file), cx);
|
||||
if settings.enable_language_server {
|
||||
if let Some(file) = File::from_dyn(Some(file)) {
|
||||
language_servers_to_start
|
||||
.push((file.worktree.clone(), language.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut language_servers_to_stop = Vec::new();
|
||||
for language in self.languages.to_vec() {
|
||||
for lsp_adapter in language.lsp_adapters() {
|
||||
if !settings
|
||||
.language(Some(&language.name()))
|
||||
.enable_language_server
|
||||
{
|
||||
let lsp_name = &lsp_adapter.name;
|
||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||
if lsp_name == started_lsp_name {
|
||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||
}
|
||||
}
|
||||
let languages = self.languages.to_vec();
|
||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||
let language = languages.iter().find(|l| {
|
||||
l.lsp_adapters()
|
||||
.iter()
|
||||
.any(|adapter| &adapter.name == started_lsp_name)
|
||||
});
|
||||
if let Some(language) = language {
|
||||
let worktree = self.worktree_for_id(*worktree_id, cx);
|
||||
let file = worktree.and_then(|tree| {
|
||||
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
|
||||
});
|
||||
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
|
||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -733,8 +733,9 @@ impl Project {
|
|||
}
|
||||
|
||||
// Start all the newly-enabled language servers.
|
||||
for (worktree_id, worktree_path, language) in language_servers_to_start {
|
||||
self.start_language_servers(worktree_id, worktree_path, language, cx);
|
||||
for (worktree, language) in language_servers_to_start {
|
||||
let worktree_path = worktree.read(cx).abs_path();
|
||||
self.start_language_servers(&worktree, worktree_path, language, cx);
|
||||
}
|
||||
|
||||
if !self.copilot_enabled && Copilot::global(cx).is_some() {
|
||||
|
@ -1107,6 +1108,21 @@ impl Project {
|
|||
.log_err();
|
||||
}
|
||||
|
||||
let store = cx.global::<SettingsStore>();
|
||||
for worktree in self.worktrees(cx) {
|
||||
let worktree_id = worktree.read(cx).id().to_proto();
|
||||
for (path, content) in store.local_settings(worktree.id()) {
|
||||
self.client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path.to_string_lossy().into(),
|
||||
content: Some(content),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let (updates_tx, mut updates_rx) = mpsc::unbounded();
|
||||
let client = self.client.clone();
|
||||
self.client_state = Some(ProjectClientState::Local {
|
||||
|
@ -1219,6 +1235,14 @@ impl Project {
|
|||
message_id: u32,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
for worktree in &self.worktrees {
|
||||
store
|
||||
.clear_local_settings(worktree.handle_id(), cx)
|
||||
.log_err();
|
||||
}
|
||||
});
|
||||
|
||||
self.join_project_response_message_id = message_id;
|
||||
self.set_worktrees_from_proto(message.worktrees, cx)?;
|
||||
self.set_collaborators_from_proto(message.collaborators, cx)?;
|
||||
|
@ -2215,13 +2239,34 @@ impl Project {
|
|||
}
|
||||
|
||||
fn maintain_buffer_languages(
|
||||
languages: &LanguageRegistry,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<()> {
|
||||
let mut subscription = languages.subscribe();
|
||||
let mut prev_reload_count = languages.reload_count();
|
||||
cx.spawn_weak(|project, mut cx| async move {
|
||||
while let Some(()) = subscription.next().await {
|
||||
if let Some(project) = project.upgrade(&cx) {
|
||||
// If the language registry has been reloaded, then remove and
|
||||
// re-assign the languages on all open buffers.
|
||||
let reload_count = languages.reload_count();
|
||||
if reload_count > prev_reload_count {
|
||||
prev_reload_count = reload_count;
|
||||
project.update(&mut cx, |this, cx| {
|
||||
let buffers = this
|
||||
.opened_buffers
|
||||
.values()
|
||||
.filter_map(|b| b.upgrade(cx))
|
||||
.collect::<Vec<_>>();
|
||||
for buffer in buffers {
|
||||
if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() {
|
||||
this.unregister_buffer_from_language_servers(&buffer, &f, cx);
|
||||
buffer.update(cx, |buffer, cx| buffer.set_language(None, cx));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
let mut plain_text_buffers = Vec::new();
|
||||
let mut buffers_with_unknown_injections = Vec::new();
|
||||
|
@ -2321,25 +2366,34 @@ impl Project {
|
|||
});
|
||||
|
||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||
if let Some(worktree) = file.worktree.read(cx).as_local() {
|
||||
let worktree_id = worktree.id();
|
||||
let worktree_abs_path = worktree.abs_path().clone();
|
||||
self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
|
||||
let worktree = file.worktree.clone();
|
||||
if let Some(tree) = worktree.read(cx).as_local() {
|
||||
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_language_servers(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
worktree_path: Arc<Path>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if !language_settings(Some(&language.name()), cx).enable_language_server {
|
||||
if !language_settings(
|
||||
Some(&language),
|
||||
worktree
|
||||
.update(cx, |tree, cx| tree.root_file(cx))
|
||||
.map(|f| f as _)
|
||||
.as_ref(),
|
||||
cx,
|
||||
)
|
||||
.enable_language_server
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
for adapter in language.lsp_adapters() {
|
||||
let key = (worktree_id, adapter.name.clone());
|
||||
if self.language_server_ids.contains_key(&key) {
|
||||
|
@ -2748,23 +2802,22 @@ impl Project {
|
|||
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<()> {
|
||||
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
|
||||
let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = buffers
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let file = File::from_dyn(buffer.file())?;
|
||||
let worktree = file.worktree.read(cx).as_local()?;
|
||||
let full_path = file.full_path(cx);
|
||||
let language = self
|
||||
.languages
|
||||
.language_for_file(&full_path, Some(buffer.as_rope()))
|
||||
.now_or_never()?
|
||||
.ok()?;
|
||||
Some((worktree.id(), worktree.abs_path().clone(), language))
|
||||
Some((file.worktree.clone(), language))
|
||||
})
|
||||
.collect();
|
||||
for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
|
||||
self.restart_language_servers(worktree_id, worktree_abs_path, language, cx);
|
||||
for (worktree, language) in language_server_lookup_info {
|
||||
self.restart_language_servers(worktree, language, cx);
|
||||
}
|
||||
|
||||
None
|
||||
|
@ -2773,11 +2826,13 @@ impl Project {
|
|||
// TODO This will break in the case where the adapter's root paths and worktrees are not equal
|
||||
fn restart_language_servers(
|
||||
&mut self,
|
||||
worktree_id: WorktreeId,
|
||||
fallback_path: Arc<Path>,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let fallback_path = worktree.read(cx).abs_path();
|
||||
|
||||
let mut stops = Vec::new();
|
||||
for adapter in language.lsp_adapters() {
|
||||
stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
|
||||
|
@ -2807,7 +2862,7 @@ impl Project {
|
|||
.map(|path_buf| Arc::from(path_buf.as_path()))
|
||||
.unwrap_or(fallback_path);
|
||||
|
||||
this.start_language_servers(worktree_id, root_path, language.clone(), cx);
|
||||
this.start_language_servers(&worktree, root_path, language.clone(), cx);
|
||||
|
||||
// Lookup new server ids and set them for each of the orphaned worktrees
|
||||
for adapter in language.lsp_adapters() {
|
||||
|
@ -3432,8 +3487,7 @@ impl Project {
|
|||
let mut project_transaction = ProjectTransaction::default();
|
||||
for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
|
||||
let settings = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).clone()
|
||||
language_settings(buffer.language(), buffer.file(), cx).clone()
|
||||
});
|
||||
|
||||
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||
|
@ -4020,7 +4074,7 @@ impl Project {
|
|||
let end_within = range.start.cmp(&primary.end, buffer).is_le()
|
||||
&& range.end.cmp(&primary.end, buffer).is_ge();
|
||||
|
||||
//Skip addtional edits which overlap with the primary completion edit
|
||||
//Skip additional edits which overlap with the primary completion edit
|
||||
//https://github.com/zed-industries/zed/pull/1871
|
||||
if !start_within && !end_within {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
|
@ -4463,11 +4517,14 @@ impl Project {
|
|||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Transaction>>> {
|
||||
let tab_size = buffer.read_with(cx, |buffer, cx| {
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
|
||||
let position = position.to_point_utf16(buffer);
|
||||
(
|
||||
position,
|
||||
language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
|
||||
.tab_size,
|
||||
)
|
||||
});
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
OnTypeFormatting {
|
||||
|
@ -4873,6 +4930,7 @@ impl Project {
|
|||
worktree::Event::UpdatedEntries(changes) => {
|
||||
this.update_local_worktree_buffers(&worktree, changes, cx);
|
||||
this.update_local_worktree_language_servers(&worktree, changes, cx);
|
||||
this.update_local_worktree_settings(&worktree, changes, cx);
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
|
||||
|
@ -4893,8 +4951,12 @@ impl Project {
|
|||
.push(WorktreeHandle::Weak(worktree.downgrade()));
|
||||
}
|
||||
|
||||
cx.observe_release(worktree, |this, worktree, cx| {
|
||||
let handle_id = worktree.id();
|
||||
cx.observe_release(worktree, move |this, worktree, cx| {
|
||||
let _ = this.remove_worktree(worktree.id(), cx);
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.clear_local_settings(handle_id, cx).log_err()
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
@ -5179,6 +5241,71 @@ impl Project {
|
|||
.detach();
|
||||
}
|
||||
|
||||
fn update_local_worktree_settings(
|
||||
&mut self,
|
||||
worktree: &ModelHandle<Worktree>,
|
||||
changes: &UpdatedEntriesSet,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let project_id = self.remote_id();
|
||||
let worktree_id = worktree.id();
|
||||
let worktree = worktree.read(cx).as_local().unwrap();
|
||||
let remote_worktree_id = worktree.id();
|
||||
|
||||
let mut settings_contents = Vec::new();
|
||||
for (path, _, change) in changes.iter() {
|
||||
if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
|
||||
let settings_dir = Arc::from(
|
||||
path.ancestors()
|
||||
.nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
|
||||
.unwrap(),
|
||||
);
|
||||
let fs = self.fs.clone();
|
||||
let removed = *change == PathChange::Removed;
|
||||
let abs_path = worktree.absolutize(path);
|
||||
settings_contents.push(async move {
|
||||
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if settings_contents.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.spawn_weak(move |_, mut cx| async move {
|
||||
let settings_contents: Vec<(Arc<Path>, _)> =
|
||||
futures::future::join_all(settings_contents).await;
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
for (directory, file_content) in settings_contents {
|
||||
let file_content = file_content.and_then(|content| content.log_err());
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree_id,
|
||||
directory.clone(),
|
||||
file_content.as_ref().map(String::as_str),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
if let Some(remote_id) = project_id {
|
||||
client
|
||||
.send(proto::UpdateWorktreeSettings {
|
||||
project_id: remote_id,
|
||||
worktree_id: remote_worktree_id.to_proto(),
|
||||
path: directory.to_string_lossy().into_owned(),
|
||||
content: file_content,
|
||||
})
|
||||
.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)?;
|
||||
|
@ -5431,6 +5558,30 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_update_worktree_settings(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
|
||||
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree.id(),
|
||||
PathBuf::from(&envelope.payload.path).into(),
|
||||
envelope.payload.content.as_ref().map(String::as_str),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_create_project_entry(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::CreateProjectEntry>,
|
||||
|
@ -6521,8 +6672,8 @@ impl Project {
|
|||
}
|
||||
|
||||
self.metadata_changed(cx);
|
||||
for (id, _) in old_worktrees_by_id {
|
||||
cx.emit(Event::WorktreeRemoved(id));
|
||||
for id in old_worktrees_by_id.keys() {
|
||||
cx.emit(Event::WorktreeRemoved(*id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -6892,6 +7043,13 @@ impl WorktreeHandle {
|
|||
WorktreeHandle::Weak(handle) => handle.upgrade(cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_id(&self) -> usize {
|
||||
match self {
|
||||
WorktreeHandle::Strong(handle) => handle.id(),
|
||||
WorktreeHandle::Weak(handle) => handle.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenBuffer {
|
||||
|
|
|
@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_project_specific_settings(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 8 }"#
|
||||
},
|
||||
"a": {
|
||||
"a.rs": "fn a() {\n A\n}"
|
||||
},
|
||||
"b": {
|
||||
".zed": {
|
||||
"settings.json": r#"{ "tab_size": 2 }"#
|
||||
},
|
||||
"b.rs": "fn b() {\n B\n}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
|
||||
let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
|
||||
deterministic.run_until_parked();
|
||||
cx.read(|cx| {
|
||||
let tree = worktree.read(cx);
|
||||
|
||||
let settings_a = language_settings(
|
||||
None,
|
||||
Some(
|
||||
&(File::for_entry(
|
||||
tree.entry_for_path("a/a.rs").unwrap().clone(),
|
||||
worktree.clone(),
|
||||
) as _),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let settings_b = language_settings(
|
||||
None,
|
||||
Some(
|
||||
&(File::for_entry(
|
||||
tree.entry_for_path("b/b.rs").unwrap().clone(),
|
||||
worktree.clone(),
|
||||
) as _),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(settings_a.tab_size.get(), 8);
|
||||
assert_eq!(settings_b.tab_size.get(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_managing_language_servers(
|
||||
deterministic: Arc<Deterministic>,
|
||||
|
|
|
@ -571,6 +571,11 @@ impl Worktree {
|
|||
Worktree::Remote(worktree) => worktree.abs_path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<Arc<File>> {
|
||||
let entry = self.root_entry()?;
|
||||
Some(File::for_entry(entry.clone(), cx.handle()))
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalWorktree {
|
||||
|
@ -578,14 +583,6 @@ impl LocalWorktree {
|
|||
path.starts_with(&self.abs_path)
|
||||
}
|
||||
|
||||
fn absolutize(&self, path: &Path) -> PathBuf {
|
||||
if path.file_name().is_some() {
|
||||
self.abs_path.join(path)
|
||||
} else {
|
||||
self.abs_path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_buffer(
|
||||
&mut self,
|
||||
id: u64,
|
||||
|
@ -1429,6 +1426,14 @@ impl Snapshot {
|
|||
&self.abs_path
|
||||
}
|
||||
|
||||
pub fn absolutize(&self, path: &Path) -> PathBuf {
|
||||
if path.file_name().is_some() {
|
||||
self.abs_path.join(path)
|
||||
} else {
|
||||
self.abs_path.to_path_buf()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
|
@ -2360,6 +2365,10 @@ impl language::File for File {
|
|||
.unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
self.worktree.id()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
self.is_deleted
|
||||
}
|
||||
|
@ -2424,6 +2433,17 @@ impl language::LocalFile for File {
|
|||
}
|
||||
|
||||
impl File {
|
||||
pub fn for_entry(entry: Entry, worktree: ModelHandle<Worktree>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
worktree,
|
||||
path: entry.path.clone(),
|
||||
mtime: entry.mtime,
|
||||
entry_id: entry.id,
|
||||
is_local: true,
|
||||
is_deleted: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_proto(
|
||||
proto: rpc::proto::File,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
|
@ -2485,7 +2505,7 @@ pub enum EntryKind {
|
|||
File(CharBag),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum PathChange {
|
||||
/// A filesystem entry was was created.
|
||||
Added,
|
||||
|
@ -3617,7 +3637,7 @@ pub trait WorktreeHandle {
|
|||
|
||||
impl WorktreeHandle for ModelHandle<Worktree> {
|
||||
// When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
|
||||
// occurred before the worktree was constructed. These events can cause the worktree to perfrom
|
||||
// occurred before the worktree was constructed. These events can cause the worktree to perform
|
||||
// extra directory scans, and emit extra scan-state notifications.
|
||||
//
|
||||
// This function mutates the worktree's directory and waits for those mutations to be picked up,
|
||||
|
|
|
@ -276,7 +276,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set up fake langauge server to return fuzzy matches against
|
||||
// Set up fake language server to return fuzzy matches against
|
||||
// a fixed set of symbol names.
|
||||
let fake_symbols = [
|
||||
symbol("one", "/external"),
|
||||
|
|
|
@ -179,7 +179,11 @@ impl Rope {
|
|||
}
|
||||
|
||||
pub fn bytes_in_range(&self, range: Range<usize>) -> Bytes {
|
||||
Bytes::new(self, range)
|
||||
Bytes::new(self, range, false)
|
||||
}
|
||||
|
||||
pub fn reversed_bytes_in_range(&self, range: Range<usize>) -> Bytes {
|
||||
Bytes::new(self, range, true)
|
||||
}
|
||||
|
||||
pub fn chunks(&self) -> Chunks {
|
||||
|
@ -579,22 +583,33 @@ impl<'a> Iterator for Chunks<'a> {
|
|||
pub struct Bytes<'a> {
|
||||
chunks: sum_tree::Cursor<'a, Chunk, usize>,
|
||||
range: Range<usize>,
|
||||
reversed: bool,
|
||||
}
|
||||
|
||||
impl<'a> Bytes<'a> {
|
||||
pub fn new(rope: &'a Rope, range: Range<usize>) -> Self {
|
||||
pub fn new(rope: &'a Rope, range: Range<usize>, reversed: bool) -> Self {
|
||||
let mut chunks = rope.chunks.cursor();
|
||||
chunks.seek(&range.start, Bias::Right, &());
|
||||
Self { chunks, range }
|
||||
if reversed {
|
||||
chunks.seek(&range.end, Bias::Left, &());
|
||||
} else {
|
||||
chunks.seek(&range.start, Bias::Right, &());
|
||||
}
|
||||
Self {
|
||||
chunks,
|
||||
range,
|
||||
reversed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&'a [u8]> {
|
||||
let chunk = self.chunks.item()?;
|
||||
if self.reversed && self.range.start >= self.chunks.end(&()) {
|
||||
return None;
|
||||
}
|
||||
let chunk_start = *self.chunks.start();
|
||||
if self.range.end <= chunk_start {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = self.range.start.saturating_sub(chunk_start);
|
||||
let end = self.range.end - chunk_start;
|
||||
Some(&chunk.0.as_bytes()[start..chunk.0.len().min(end)])
|
||||
|
@ -607,7 +622,11 @@ impl<'a> Iterator for Bytes<'a> {
|
|||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.peek();
|
||||
if result.is_some() {
|
||||
self.chunks.next(&());
|
||||
if self.reversed {
|
||||
self.chunks.prev(&());
|
||||
} else {
|
||||
self.chunks.next(&());
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
@ -617,10 +636,21 @@ impl<'a> io::Read for Bytes<'a> {
|
|||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if let Some(chunk) = self.peek() {
|
||||
let len = cmp::min(buf.len(), chunk.len());
|
||||
buf[..len].copy_from_slice(&chunk[..len]);
|
||||
self.range.start += len;
|
||||
if self.reversed {
|
||||
buf[..len].copy_from_slice(&chunk[chunk.len() - len..]);
|
||||
buf[..len].reverse();
|
||||
self.range.end -= len;
|
||||
} else {
|
||||
buf[..len].copy_from_slice(&chunk[..len]);
|
||||
self.range.start += len;
|
||||
}
|
||||
|
||||
if len == chunk.len() {
|
||||
self.chunks.next(&());
|
||||
if self.reversed {
|
||||
self.chunks.prev(&());
|
||||
} else {
|
||||
self.chunks.next(&());
|
||||
}
|
||||
}
|
||||
Ok(len)
|
||||
} else {
|
||||
|
|
|
@ -132,6 +132,8 @@ message Envelope {
|
|||
|
||||
OnTypeFormatting on_type_formatting = 111;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 113;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,6 +341,13 @@ message UpdateWorktree {
|
|||
string abs_path = 10;
|
||||
}
|
||||
|
||||
message UpdateWorktreeSettings {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
string path = 3;
|
||||
optional string content = 4;
|
||||
}
|
||||
|
||||
message CreateProjectEntry {
|
||||
uint64 project_id = 1;
|
||||
uint64 worktree_id = 2;
|
||||
|
@ -467,7 +476,7 @@ message Symbol {
|
|||
string name = 4;
|
||||
int32 kind = 5;
|
||||
string path = 6;
|
||||
// Cannot use generate anchors for unopend files,
|
||||
// Cannot use generate anchors for unopened files,
|
||||
// so we are forced to use point coords instead
|
||||
PointUtf16 start = 7;
|
||||
PointUtf16 end = 8;
|
||||
|
|
|
@ -42,7 +42,7 @@ impl PublicKey {
|
|||
}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Decrypt a base64-encoded string that was encrypted by the correspoding public key.
|
||||
/// Decrypt a base64-encoded string that was encrypted by the corresponding public key.
|
||||
pub fn decrypt_string(&self, encrypted_string: &str) -> Result<String> {
|
||||
let encrypted_bytes = base64::decode_config(encrypted_string, base64::URL_SAFE)
|
||||
.context("failed to base64-decode encrypted string")?;
|
||||
|
|
|
@ -236,6 +236,7 @@ messages!(
|
|||
(UpdateProject, Foreground),
|
||||
(UpdateProjectCollaborator, Foreground),
|
||||
(UpdateWorktree, Foreground),
|
||||
(UpdateWorktreeSettings, Foreground),
|
||||
(UpdateDiffBase, Foreground),
|
||||
(GetPrivateUserInfo, Foreground),
|
||||
(GetPrivateUserInfoResponse, Foreground),
|
||||
|
@ -345,6 +346,7 @@ entity_messages!(
|
|||
UpdateProject,
|
||||
UpdateProjectCollaborator,
|
||||
UpdateWorktree,
|
||||
UpdateWorktreeSettings,
|
||||
UpdateDiffBase
|
||||
);
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ use std::{
|
|||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
mem,
|
||||
ops::Range,
|
||||
ops::{Not, Range},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -242,7 +242,13 @@ impl View for ProjectSearchView {
|
|||
|
||||
impl Item for ProjectSearchView {
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||
Some(self.query_editor.read(cx).text(cx).into())
|
||||
let query_text = self.query_editor.read(cx).text(cx);
|
||||
|
||||
query_text
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| query_text.into())
|
||||
.or_else(|| Some("Project Search".into()))
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
|
|
|
@ -12,7 +12,6 @@ doctest = false
|
|||
test-support = ["gpui/test-support", "fs/test-support"]
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
|
@ -25,6 +24,7 @@ futures.workspace = true
|
|||
json_comments = "0.2"
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use crate::settings_store::parse_json_with_comments;
|
||||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||
use anyhow::{Context, Result};
|
||||
use assets::Assets;
|
||||
use collections::BTreeMap;
|
||||
use gpui::{keymap_matcher::Binding, AppContext};
|
||||
use schemars::{
|
||||
|
@ -10,11 +9,11 @@ use schemars::{
|
|||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use util::ResultExt;
|
||||
use util::{asset_str, ResultExt};
|
||||
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct KeymapFileContent(Vec<KeymapBlock>);
|
||||
pub struct KeymapFile(Vec<KeymapBlock>);
|
||||
|
||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||
pub struct KeymapBlock {
|
||||
|
@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
|
|||
#[derive(Deserialize)]
|
||||
struct ActionWithData(Box<str>, Box<RawValue>);
|
||||
|
||||
impl KeymapFileContent {
|
||||
impl KeymapFile {
|
||||
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
||||
let content = Assets::get(asset_path).unwrap().data;
|
||||
let content_str = std::str::from_utf8(content.as_ref()).unwrap();
|
||||
Self::parse(content_str)?.add_to_cx(cx)
|
||||
let content = asset_str::<SettingsAssets>(asset_path);
|
||||
Self::parse(content.as_ref())?.add_to_cx(cx)
|
||||
}
|
||||
|
||||
pub fn parse(content: &str) -> Result<Self> {
|
||||
|
@ -83,40 +81,40 @@ impl KeymapFileContent {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value {
|
||||
let mut root_schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<KeymapFileContent>();
|
||||
pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
|
||||
let mut root_schema = SchemaSettings::draft07()
|
||||
.with(|settings| settings.option_add_null_type = false)
|
||||
.into_generator()
|
||||
.into_root_schema_for::<KeymapFile>();
|
||||
|
||||
let action_schema = Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
enum_values: Some(
|
||||
action_names
|
||||
.iter()
|
||||
.map(|name| Value::String(name.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||
..Default::default()
|
||||
}),
|
||||
]),
|
||||
let action_schema = Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
|
||||
enum_values: Some(
|
||||
action_names
|
||||
.iter()
|
||||
.map(|name| Value::String(name.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
|
||||
..Default::default()
|
||||
}),
|
||||
]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
root_schema
|
||||
.definitions
|
||||
.insert("KeymapAction".to_owned(), action_schema);
|
||||
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,37 @@ mod keymap_file;
|
|||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
||||
use gpui::AssetSource;
|
||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str};
|
||||
use util::asset_str;
|
||||
|
||||
pub use keymap_file::KeymapFile;
|
||||
pub use settings_file::*;
|
||||
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
|
||||
use std::{borrow::Cow, str};
|
||||
|
||||
pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
|
||||
pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../assets"]
|
||||
#[include = "settings/*"]
|
||||
#[include = "keymaps/*"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct SettingsAssets;
|
||||
|
||||
pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
|
||||
match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
}
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/default.json")
|
||||
}
|
||||
|
||||
pub fn default_keymap() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("keymaps/default.json")
|
||||
}
|
||||
|
||||
pub fn vim_keymap() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("keymaps/vim.json")
|
||||
}
|
||||
|
||||
pub fn initial_user_settings_content() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/initial_user_settings.json")
|
||||
}
|
||||
|
||||
pub fn initial_local_settings_content() -> Cow<'static, str> {
|
||||
asset_str::<SettingsAssets>("settings/initial_local_settings.json")
|
||||
}
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH};
|
||||
use crate::{settings_store::SettingsStore, Setting};
|
||||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, StreamExt};
|
||||
use gpui::{executor::Background, AppContext, AssetSource};
|
||||
use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
|
||||
use gpui::{executor::Background, AppContext};
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{paths, ResultExt};
|
||||
|
||||
pub fn register<T: Setting>(cx: &mut AppContext) {
|
||||
|
@ -17,11 +22,8 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
|
|||
cx.global::<SettingsStore>().get(None)
|
||||
}
|
||||
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
}
|
||||
pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
|
||||
cx.global::<SettingsStore>().get(location)
|
||||
}
|
||||
|
||||
pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
|
||||
|
@ -29,7 +31,7 @@ pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_settings() -> String {
|
||||
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
|
||||
default_settings().as_ref(),
|
||||
crate::default_settings().as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
util::merge_non_null_json_value_into(
|
||||
|
@ -55,15 +57,22 @@ pub fn watch_config_file(
|
|||
.spawn(async move {
|
||||
let events = fs.watch(&path, Duration::from_millis(100)).await;
|
||||
futures::pin_mut!(events);
|
||||
|
||||
let contents = fs.load(&path).await.unwrap_or_default();
|
||||
if tx.unbounded_send(contents).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
if events.next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok(contents) = fs.load(&path).await {
|
||||
if !tx.unbounded_send(contents).is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if events.next().await.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
@ -101,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
|
|||
Err(err) => {
|
||||
if let Some(e) = err.downcast_ref::<std::io::Error>() {
|
||||
if e.kind() == ErrorKind::NotFound {
|
||||
return Ok(crate::initial_user_settings_content(&Assets).to_string());
|
||||
return Ok(crate::initial_user_settings_content().to_string());
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
||||
use gpui::AppContext;
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> {
|
|||
}
|
||||
|
||||
/// A set of strongly-typed setting values defined via multiple JSON files.
|
||||
#[derive(Default)]
|
||||
pub struct SettingsStore {
|
||||
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
|
||||
default_deserialized_settings: Option<serde_json::Value>,
|
||||
user_deserialized_settings: Option<serde_json::Value>,
|
||||
local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
|
||||
default_deserialized_settings: serde_json::Value,
|
||||
user_deserialized_settings: serde_json::Value,
|
||||
local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
|
||||
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
|
||||
}
|
||||
|
||||
impl Default for SettingsStore {
|
||||
fn default() -> Self {
|
||||
SettingsStore {
|
||||
setting_values: Default::default(),
|
||||
default_deserialized_settings: serde_json::json!({}),
|
||||
user_deserialized_settings: serde_json::json!({}),
|
||||
local_deserialized_settings: Default::default(),
|
||||
tab_size_callback: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SettingValue<T> {
|
||||
global_value: Option<T>,
|
||||
local_values: Vec<(Arc<Path>, T)>,
|
||||
local_values: Vec<(usize, Arc<Path>, T)>,
|
||||
}
|
||||
|
||||
trait AnySettingValue {
|
||||
|
@ -109,9 +120,9 @@ trait AnySettingValue {
|
|||
custom: &[DeserializedSetting],
|
||||
cx: &AppContext,
|
||||
) -> Result<Box<dyn Any>>;
|
||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
|
||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
|
||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
|
||||
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
|
||||
fn json_schema(
|
||||
&self,
|
||||
generator: &mut SchemaGenerator,
|
||||
|
@ -136,27 +147,24 @@ impl SettingsStore {
|
|||
local_values: Vec::new(),
|
||||
}));
|
||||
|
||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
||||
if let Some(default_settings) = setting_value
|
||||
.deserialize_setting(default_settings)
|
||||
if let Some(default_settings) = setting_value
|
||||
.deserialize_setting(&self.default_deserialized_settings)
|
||||
.log_err()
|
||||
{
|
||||
let mut user_values_stack = Vec::new();
|
||||
|
||||
if let Some(user_settings) = setting_value
|
||||
.deserialize_setting(&self.user_deserialized_settings)
|
||||
.log_err()
|
||||
{
|
||||
let mut user_values_stack = Vec::new();
|
||||
user_values_stack = vec![user_settings];
|
||||
}
|
||||
|
||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
||||
if let Some(user_settings) =
|
||||
setting_value.deserialize_setting(user_settings).log_err()
|
||||
{
|
||||
user_values_stack = vec![user_settings];
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(setting) = setting_value
|
||||
.load_setting(&default_settings, &user_values_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(setting);
|
||||
}
|
||||
if let Some(setting) = setting_value
|
||||
.load_setting(&default_settings, &user_values_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +173,7 @@ impl SettingsStore {
|
|||
///
|
||||
/// Panics if the given setting type has not been registered, or if there is no
|
||||
/// value for this setting.
|
||||
pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
|
||||
pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
|
||||
self.setting_values
|
||||
.get(&TypeId::of::<T>())
|
||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||
|
@ -189,9 +197,7 @@ impl SettingsStore {
|
|||
/// This is only for debugging and reporting. For user-facing functionality,
|
||||
/// use the typed setting interface.
|
||||
pub fn untyped_user_settings(&self) -> &serde_json::Value {
|
||||
self.user_deserialized_settings
|
||||
.as_ref()
|
||||
.unwrap_or(&serde_json::Value::Null)
|
||||
&self.user_deserialized_settings
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -213,11 +219,7 @@ impl SettingsStore {
|
|||
cx: &AppContext,
|
||||
update: impl FnOnce(&mut T::FileContent),
|
||||
) {
|
||||
if self.user_deserialized_settings.is_none() {
|
||||
self.set_user_settings("{}", cx).unwrap();
|
||||
}
|
||||
let old_text =
|
||||
serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
|
||||
let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
|
||||
let new_text = self.new_text_for_update::<T>(old_text, update);
|
||||
self.set_user_settings(&new_text, cx).unwrap();
|
||||
}
|
||||
|
@ -250,11 +252,7 @@ impl SettingsStore {
|
|||
.setting_values
|
||||
.get(&setting_type_id)
|
||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||
.deserialize_setting(
|
||||
self.user_deserialized_settings
|
||||
.as_ref()
|
||||
.expect("no user settings loaded"),
|
||||
)
|
||||
.deserialize_setting(&self.user_deserialized_settings)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"could not deserialize setting type {} from user settings: {}",
|
||||
|
@ -323,10 +321,14 @@ impl SettingsStore {
|
|||
default_settings_content: &str,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
self.default_deserialized_settings =
|
||||
Some(parse_json_with_comments(default_settings_content)?);
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
|
||||
if settings.is_object() {
|
||||
self.default_deserialized_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("settings must be an object"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user settings via a JSON string.
|
||||
|
@ -335,28 +337,49 @@ impl SettingsStore {
|
|||
user_settings_content: &str,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
|
||||
if settings.is_object() {
|
||||
self.user_deserialized_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("settings must be an object"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add or remove a set of local settings via a JSON string.
|
||||
pub fn set_local_settings(
|
||||
&mut self,
|
||||
root_id: usize,
|
||||
path: Arc<Path>,
|
||||
settings_content: Option<&str>,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
if let Some(content) = settings_content {
|
||||
self.local_deserialized_settings
|
||||
.insert(path.clone(), parse_json_with_comments(content)?);
|
||||
.insert((root_id, path.clone()), parse_json_with_comments(content)?);
|
||||
} else {
|
||||
self.local_deserialized_settings.remove(&path);
|
||||
self.local_deserialized_settings
|
||||
.remove(&(root_id, path.clone()));
|
||||
}
|
||||
self.recompute_values(Some(&path), cx)?;
|
||||
self.recompute_values(Some((root_id, &path)), cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add or remove a set of local settings via a JSON string.
|
||||
pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
|
||||
self.local_deserialized_settings
|
||||
.retain(|k, _| k.0 != root_id);
|
||||
self.recompute_values(Some((root_id, "".as_ref())), cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
|
||||
self.local_deserialized_settings
|
||||
.range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
|
||||
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
|
||||
}
|
||||
|
||||
pub fn json_schema(
|
||||
&self,
|
||||
schema_params: &SettingsJsonSchemaParams,
|
||||
|
@ -436,72 +459,70 @@ impl SettingsStore {
|
|||
|
||||
fn recompute_values(
|
||||
&mut self,
|
||||
changed_local_path: Option<&Path>,
|
||||
changed_local_path: Option<(usize, &Path)>,
|
||||
cx: &AppContext,
|
||||
) -> Result<()> {
|
||||
// Reload the global and local values for every setting.
|
||||
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
|
||||
let mut paths_stack = Vec::<Option<&Path>>::new();
|
||||
let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
|
||||
for setting_value in self.setting_values.values_mut() {
|
||||
if let Some(default_settings) = &self.default_deserialized_settings {
|
||||
let default_settings = setting_value.deserialize_setting(default_settings)?;
|
||||
let default_settings =
|
||||
setting_value.deserialize_setting(&self.default_deserialized_settings)?;
|
||||
|
||||
user_settings_stack.clear();
|
||||
paths_stack.clear();
|
||||
user_settings_stack.clear();
|
||||
paths_stack.clear();
|
||||
|
||||
if let Some(user_settings) = &self.user_deserialized_settings {
|
||||
if let Some(user_settings) =
|
||||
setting_value.deserialize_setting(user_settings).log_err()
|
||||
{
|
||||
user_settings_stack.push(user_settings);
|
||||
paths_stack.push(None);
|
||||
if let Some(user_settings) = setting_value
|
||||
.deserialize_setting(&self.user_deserialized_settings)
|
||||
.log_err()
|
||||
{
|
||||
user_settings_stack.push(user_settings);
|
||||
paths_stack.push(None);
|
||||
}
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
for ((root_id, path), local_settings) in &self.local_deserialized_settings {
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_entry) = paths_stack.last() {
|
||||
if let Some((prev_root_id, prev_path)) = prev_entry {
|
||||
if root_id != prev_root_id || !path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
user_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
if let Some(local_settings) =
|
||||
setting_value.deserialize_setting(&local_settings).log_err()
|
||||
{
|
||||
paths_stack.push(Some((*root_id, path.as_ref())));
|
||||
user_settings_stack.push(local_settings);
|
||||
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
|
||||
*root_id != changed_root_id || !path.starts_with(changed_local_path)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_global_value(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the local values for the setting.
|
||||
for (path, local_settings) in &self.local_deserialized_settings {
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_path) = paths_stack.last() {
|
||||
if let Some(prev_path) = prev_path {
|
||||
if !path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
user_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(local_settings) =
|
||||
setting_value.deserialize_setting(&local_settings).log_err()
|
||||
{
|
||||
paths_stack.push(Some(path.as_ref()));
|
||||
user_settings_stack.push(local_settings);
|
||||
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(false, |changed_local_path| {
|
||||
!path.starts_with(changed_local_path)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(&default_settings, &user_settings_stack, cx)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(path.clone(), value);
|
||||
}
|
||||
setting_value.set_local_value(*root_id, path.clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -510,6 +531,24 @@ impl SettingsStore {
|
|||
}
|
||||
}
|
||||
|
||||
impl Debug for SettingsStore {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SettingsStore")
|
||||
.field(
|
||||
"types",
|
||||
&self
|
||||
.setting_values
|
||||
.values()
|
||||
.map(|value| value.setting_type_name())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.field("default_settings", &self.default_deserialized_settings)
|
||||
.field("user_settings", &self.user_deserialized_settings)
|
||||
.field("local_settings", &self.local_deserialized_settings)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Setting> AnySettingValue for SettingValue<T> {
|
||||
fn key(&self) -> Option<&'static str> {
|
||||
T::KEY
|
||||
|
@ -546,10 +585,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||
Ok(DeserializedSetting(Box::new(value)))
|
||||
}
|
||||
|
||||
fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
|
||||
if let Some(path) = path {
|
||||
for (settings_path, value) in self.local_values.iter().rev() {
|
||||
if path.starts_with(&settings_path) {
|
||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
|
||||
if let Some((root_id, path)) = path {
|
||||
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
|
||||
if root_id == *settings_root_id && path.starts_with(&settings_path) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
@ -563,11 +602,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||
self.global_value = Some(*value.downcast().unwrap());
|
||||
}
|
||||
|
||||
fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
|
||||
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
|
||||
let value = *value.downcast().unwrap();
|
||||
match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
|
||||
Ok(ix) => self.local_values[ix].1 = value,
|
||||
Err(ix) => self.local_values.insert(ix, (path, value)),
|
||||
match self
|
||||
.local_values
|
||||
.binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
|
||||
{
|
||||
Ok(ix) => self.local_values[ix].2 = value,
|
||||
Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -581,22 +623,6 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// impl Debug for SettingsStore {
|
||||
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// return f
|
||||
// .debug_struct("SettingsStore")
|
||||
// .field(
|
||||
// "setting_value_sets_by_type",
|
||||
// &self
|
||||
// .setting_values
|
||||
// .values()
|
||||
// .map(|set| (set.setting_type_name(), set))
|
||||
// .collect::<HashMap<_, _>>(),
|
||||
// )
|
||||
// .finish_non_exhaustive();
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update_value_in_json_text<'a>(
|
||||
text: &mut String,
|
||||
key_path: &mut Vec<&'a str>,
|
||||
|
@ -639,6 +665,10 @@ fn update_value_in_json_text<'a>(
|
|||
key_path.pop();
|
||||
}
|
||||
} else if old_value != new_value {
|
||||
let mut new_value = new_value.clone();
|
||||
if let Some(new_object) = new_value.as_object_mut() {
|
||||
new_object.retain(|_, v| !v.is_null());
|
||||
}
|
||||
let (range, replacement) =
|
||||
replace_value_in_json_text(text, &key_path, tab_size, &new_value);
|
||||
text.replace_range(range.clone(), &replacement);
|
||||
|
@ -650,7 +680,7 @@ fn replace_value_in_json_text(
|
|||
text: &str,
|
||||
key_path: &[&str],
|
||||
tab_size: usize,
|
||||
new_value: impl Serialize,
|
||||
new_value: &serde_json::Value,
|
||||
) -> (Range<usize>, String) {
|
||||
const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
|
||||
const LANGUAGES: &'static str = "languages";
|
||||
|
@ -884,6 +914,7 @@ mod tests {
|
|||
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root1").into(),
|
||||
Some(r#"{ "user": { "staff": true } }"#),
|
||||
cx,
|
||||
|
@ -891,6 +922,7 @@ mod tests {
|
|||
.unwrap();
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root1/subdir").into(),
|
||||
Some(r#"{ "user": { "name": "Jane Doe" } }"#),
|
||||
cx,
|
||||
|
@ -899,6 +931,7 @@ mod tests {
|
|||
|
||||
store
|
||||
.set_local_settings(
|
||||
1,
|
||||
Path::new("/root2").into(),
|
||||
Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
|
||||
cx,
|
||||
|
@ -906,7 +939,7 @@ mod tests {
|
|||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root1/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
|
||||
&UserSettings {
|
||||
name: "John Doe".to_string(),
|
||||
age: 31,
|
||||
|
@ -914,7 +947,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
|
||||
&UserSettings {
|
||||
name: "Jane Doe".to_string(),
|
||||
age: 31,
|
||||
|
@ -922,7 +955,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<UserSettings>(Some(Path::new("/root2/something"))),
|
||||
store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
|
||||
&UserSettings {
|
||||
name: "John Doe".to_string(),
|
||||
age: 42,
|
||||
|
@ -930,7 +963,7 @@ mod tests {
|
|||
}
|
||||
);
|
||||
assert_eq!(
|
||||
store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
|
||||
store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
|
||||
&MultiKeySettings {
|
||||
key1: "a".to_string(),
|
||||
key2: "b".to_string(),
|
||||
|
@ -994,24 +1027,32 @@ mod tests {
|
|||
r#"{
|
||||
"languages": {
|
||||
"JSON": {
|
||||
"is_enabled": true
|
||||
"language_setting_1": true
|
||||
}
|
||||
}
|
||||
}"#
|
||||
.unindent(),
|
||||
|settings| {
|
||||
settings.languages.get_mut("JSON").unwrap().is_enabled = false;
|
||||
settings
|
||||
.languages
|
||||
.insert("Rust".into(), LanguageSettingEntry { is_enabled: true });
|
||||
.get_mut("JSON")
|
||||
.unwrap()
|
||||
.language_setting_1 = Some(false);
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingEntry {
|
||||
language_setting_2: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
},
|
||||
r#"{
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"is_enabled": true
|
||||
"language_setting_2": true
|
||||
},
|
||||
"JSON": {
|
||||
"is_enabled": false
|
||||
"language_setting_1": false
|
||||
}
|
||||
}
|
||||
}"#
|
||||
|
@ -1074,6 +1115,23 @@ mod tests {
|
|||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
|
||||
check_settings_update::<UserSettings>(
|
||||
&mut store,
|
||||
r#"{
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| settings.age = Some(37),
|
||||
r#"{
|
||||
"user": {
|
||||
"age": 37
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn check_settings_update<T: Setting>(
|
||||
|
@ -1202,9 +1260,10 @@ mod tests {
|
|||
languages: HashMap<String, LanguageSettingEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
struct LanguageSettingEntry {
|
||||
is_enabled: bool,
|
||||
language_setting_1: Option<bool>,
|
||||
language_setting_2: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for LanguageSettings {
|
||||
|
|
|
@ -160,7 +160,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
|||
|
||||
// Create a one shot channel for the result of the queued write
|
||||
// so we can await on the result
|
||||
let (sender, reciever) = oneshot::channel();
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
|
||||
let thread_safe_connection = (*self).clone();
|
||||
write_channel(Box::new(move || {
|
||||
|
@ -168,7 +168,7 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
|||
let result = connection.with_write(|connection| callback(connection));
|
||||
sender.send(result).ok();
|
||||
}));
|
||||
reciever.map(|response| response.expect("Write queue unexpectedly closed"))
|
||||
receiver.map(|response| response.expect("Write queue unexpectedly closed"))
|
||||
}
|
||||
|
||||
pub(crate) fn create_connection(
|
||||
|
@ -245,10 +245,10 @@ pub fn background_thread_queue() -> WriteQueueConstructor {
|
|||
use std::sync::mpsc::channel;
|
||||
|
||||
Box::new(|| {
|
||||
let (sender, reciever) = channel::<QueuedWrite>();
|
||||
let (sender, receiver) = channel::<QueuedWrite>();
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(write) = reciever.recv() {
|
||||
while let Ok(write) = receiver.recv() {
|
||||
write()
|
||||
}
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color {
|
|||
}
|
||||
|
||||
///Converts an 8 bit ANSI color to it's GPUI equivalent.
|
||||
///Accepts usize for compatability with the alacritty::Colors interface,
|
||||
///Accepts usize for compatibility with the alacritty::Colors interface,
|
||||
///Other than that use case, should only be called with values in the [0,255] range
|
||||
pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
|
||||
match index {
|
||||
|
@ -78,7 +78,7 @@ pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color {
|
|||
let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks
|
||||
Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale
|
||||
}
|
||||
//For compatability with the alacritty::Colors interface
|
||||
//For compatibility with the alacritty::Colors interface
|
||||
256 => style.foreground,
|
||||
257 => style.background,
|
||||
258 => style.cursor,
|
||||
|
|
|
@ -37,8 +37,6 @@ lazy_static.workspace = true
|
|||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
|
|
@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal:
|
|||
|
||||
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
|
||||
|
||||
4. Pasted text has a seperate pathway.
|
||||
4. Pasted text has a separate pathway.
|
||||
|
||||
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
|
|
@ -40,7 +40,7 @@ function contrast_colour {
|
|||
|
||||
# Uncomment the below for more precise luminance calculations
|
||||
|
||||
# # Calculate percieved brightness
|
||||
# # Calculate perceived brightness
|
||||
# # See https://www.w3.org/TR/AERT#color-contrast
|
||||
# # and http://www.itu.int/rec/R-REC-BT.601
|
||||
# # Luminance is in range 0..5000 as each value is 0..5
|
||||
|
|
|
@ -34,7 +34,7 @@ use std::{mem, ops::Range};
|
|||
|
||||
use crate::TerminalView;
|
||||
|
||||
///The information generated during layout that is nescessary for painting
|
||||
///The information generated during layout that is necessary for painting
|
||||
pub struct LayoutState {
|
||||
cells: Vec<LayoutCell>,
|
||||
rects: Vec<LayoutRect>,
|
||||
|
@ -206,7 +206,7 @@ impl TerminalElement {
|
|||
//Expand background rect range
|
||||
{
|
||||
if matches!(bg, Named(NamedColor::Background)) {
|
||||
//Continue to next cell, resetting variables if nescessary
|
||||
//Continue to next cell, resetting variables if necessary
|
||||
cur_alac_color = None;
|
||||
if let Some(rect) = cur_rect {
|
||||
rects.push(rect);
|
||||
|
|
|
@ -70,6 +70,7 @@ impl TerminalPanel {
|
|||
.with_child(Pane::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
false,
|
||||
Some((
|
||||
"New Terminal".into(),
|
||||
Some(Box::new(workspace::NewTerminal)),
|
||||
|
@ -94,6 +95,7 @@ impl TerminalPanel {
|
|||
} else {
|
||||
"icons/maximize_8.svg"
|
||||
},
|
||||
pane.is_zoomed(),
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
|
|
|
@ -133,8 +133,8 @@ impl TerminalView {
|
|||
Event::Wakeup => {
|
||||
if !cx.is_self_focused() {
|
||||
this.has_new_content = true;
|
||||
cx.notify();
|
||||
}
|
||||
cx.notify();
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
Event::Bell => {
|
||||
|
@ -804,7 +804,7 @@ mod tests {
|
|||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
//Make sure enviroment is as expeted
|
||||
//Make sure environment is as expected
|
||||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_none());
|
||||
|
||||
|
@ -825,7 +825,7 @@ mod tests {
|
|||
let workspace = workspace.read(cx);
|
||||
let active_entry = project.read(cx).active_entry();
|
||||
|
||||
//Make sure enviroment is as expeted
|
||||
//Make sure environment is as expected
|
||||
assert!(active_entry.is_none());
|
||||
assert!(workspace.worktrees(cx).next().is_some());
|
||||
|
||||
|
@ -905,7 +905,10 @@ mod tests {
|
|||
cx: &mut TestAppContext,
|
||||
) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
|
||||
let params = cx.update(AppState::test);
|
||||
cx.update(|cx| theme::init((), cx));
|
||||
cx.update(|cx| {
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
});
|
||||
|
||||
let project = Project::test(params.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
|
|
@ -193,7 +193,7 @@ fn test_line_len() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_at_positionn() {
|
||||
fn test_common_prefix_at_position() {
|
||||
let text = "a = str; b = δα";
|
||||
let buffer = Buffer::new(0, 0, text.into());
|
||||
|
||||
|
@ -216,7 +216,7 @@ fn test_common_prefix_at_positionn() {
|
|||
empty_range_after(text, "str"),
|
||||
);
|
||||
|
||||
// prefix matching is case insenstive.
|
||||
// prefix matching is case insensitive.
|
||||
assert_eq!(
|
||||
buffer.common_prefix_at(offset1, "Strαngε"),
|
||||
range_of(text, "str"),
|
||||
|
|
|
@ -1749,6 +1749,12 @@ impl BufferSnapshot {
|
|||
self.visible_text.bytes_in_range(start..end)
|
||||
}
|
||||
|
||||
pub fn reversed_bytes_in_range<T: ToOffset>(&self, range: Range<T>) -> rope::Bytes<'_> {
|
||||
let start = range.start.to_offset(self);
|
||||
let end = range.end.to_offset(self);
|
||||
self.visible_text.reversed_bytes_in_range(start..end)
|
||||
}
|
||||
|
||||
pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Chunks<'_> {
|
||||
let start = range.start.to_offset(self);
|
||||
let end = range.end.to_offset(self);
|
||||
|
|
|
@ -60,6 +60,7 @@ pub struct Theme {
|
|||
pub incoming_call_notification: IncomingCallNotification,
|
||||
pub tooltip: TooltipStyle,
|
||||
pub terminal: TerminalStyle,
|
||||
pub assistant: AssistantStyle,
|
||||
pub feedback: FeedbackStyle,
|
||||
pub welcome: WelcomeStyle,
|
||||
pub color_scheme: ColorScheme,
|
||||
|
@ -968,6 +969,23 @@ pub struct TerminalStyle {
|
|||
pub dim_foreground: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct AssistantStyle {
|
||||
pub container: ContainerStyle,
|
||||
pub header: ContainerStyle,
|
||||
pub sent_at: ContainedText,
|
||||
pub user_sender: Interactive<ContainedText>,
|
||||
pub assistant_sender: Interactive<ContainedText>,
|
||||
pub system_sender: Interactive<ContainedText>,
|
||||
pub model_info_container: ContainerStyle,
|
||||
pub model: Interactive<ContainedText>,
|
||||
pub remaining_tokens: ContainedText,
|
||||
pub no_remaining_tokens: ContainedText,
|
||||
pub error_icon: Icon,
|
||||
pub api_key_editor: FieldEditor,
|
||||
pub api_key_prompt: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct FeedbackStyle {
|
||||
pub submit_button: Interactive<ContainedText>,
|
||||
|
|
|
@ -21,6 +21,7 @@ isahc.workspace = true
|
|||
smol.workspace = true
|
||||
url = "2.2"
|
||||
rand.workspace = true
|
||||
rust-embed.workspace = true
|
||||
tempdir = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
|
|
@ -15,6 +15,7 @@ lazy_static::lazy_static! {
|
|||
pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
|
||||
pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
|
||||
pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
|
||||
pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
|
||||
}
|
||||
|
||||
pub mod legacy {
|
||||
|
|
|
@ -7,8 +7,10 @@ pub mod paths;
|
|||
pub mod test;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
ops::{AddAssign, Range, RangeInclusive},
|
||||
panic::Location,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
@ -129,11 +131,13 @@ where
|
|||
{
|
||||
type Ok = T;
|
||||
|
||||
#[track_caller]
|
||||
fn log_err(self) -> Option<T> {
|
||||
match self {
|
||||
Ok(value) => Some(value),
|
||||
Err(error) => {
|
||||
log::error!("{:?}", error);
|
||||
let caller = Location::caller();
|
||||
log::error!("{}:{}: {:?}", caller.file(), caller.line(), error);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -281,6 +285,14 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get an embedded file as a string.
|
||||
pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
|
||||
match A::get(path).unwrap().data {
|
||||
Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
|
||||
Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
// copy unstable standard feature option unzip
|
||||
// https://github.com/rust-lang/rust/issues/87800
|
||||
// Remove when this ship in Rust 1.66 or 1.67
|
||||
|
|
|
@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f
|
|||
tokio = { version = "1.15", "optional" = true }
|
||||
serde_json.workspace = true
|
||||
|
||||
assets = { path = "../assets" }
|
||||
collections = { path = "../collections" }
|
||||
command_palette = { path = "../command_palette" }
|
||||
editor = { path = "../editor" }
|
||||
|
|
|
@ -400,7 +400,7 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Edito
|
|||
};
|
||||
|
||||
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
|
||||
let top_anchor = editor.scroll_manager.anchor().top_anchor;
|
||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.replace_cursors_with(|snapshot| {
|
||||
|
@ -756,7 +756,7 @@ mod test {
|
|||
ˇ
|
||||
The quick"})
|
||||
.await;
|
||||
// Indoc disallows trailing whitspace.
|
||||
// Indoc disallows trailing whitespace.
|
||||
cx.assert(" ˇ \nThe quick").await;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ use tokio::{
|
|||
use crate::state::Mode;
|
||||
use collections::VecDeque;
|
||||
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use thsi lock
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
lazy_static! {
|
||||
|
|
|
@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
|
|||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
});
|
||||
|
||||
// Setup search toolbars and keypress hook
|
||||
|
|
|
@ -19,7 +19,6 @@ test-support = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
db = { path = "../db" }
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
|
|
|
@ -175,6 +175,10 @@ impl Dock {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn position(&self) -> DockPosition {
|
||||
self.position
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
|
@ -184,6 +188,12 @@ impl Dock {
|
|||
.map_or(false, |panel| panel.has_focus(cx))
|
||||
}
|
||||
|
||||
pub fn panel<T: Panel>(&self) -> Option<ViewHandle<T>> {
|
||||
self.panel_entries
|
||||
.iter()
|
||||
.find_map(|entry| entry.panel.as_any().clone().downcast())
|
||||
}
|
||||
|
||||
pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
|
||||
self.panel_entries
|
||||
.iter()
|
||||
|
@ -472,11 +482,22 @@ impl View for PanelButtons {
|
|||
Flex::row()
|
||||
.with_children(panels.into_iter().enumerate().map(
|
||||
|(panel_ix, (view, context_menu))| {
|
||||
let (tooltip, tooltip_action) = view.icon_tooltip(cx);
|
||||
let is_active = is_open && panel_ix == active_ix;
|
||||
let (tooltip, tooltip_action) = if is_active {
|
||||
(
|
||||
format!("Close {} dock", dock_position.to_label()),
|
||||
Some(match dock_position {
|
||||
DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
|
||||
DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
|
||||
DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
view.icon_tooltip(cx)
|
||||
};
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
|
||||
let is_active = is_open && panel_ix == active_ix;
|
||||
let style = button_style.style_for(state, is_active);
|
||||
Flex::row()
|
||||
.with_child(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{Toast, Workspace};
|
||||
use collections::HashSet;
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
|
||||
use std::{any::TypeId, ops::DerefMut};
|
||||
|
||||
|
@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
|
|||
}
|
||||
}
|
||||
|
||||
struct NotificationTracker {
|
||||
notifications_sent: HashSet<TypeId>,
|
||||
pub(crate) struct NotificationTracker {
|
||||
notifications_sent: HashMap<TypeId, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for NotificationTracker {
|
||||
type Target = HashSet<TypeId>;
|
||||
type Target = HashMap<TypeId, Vec<usize>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.notifications_sent
|
||||
|
@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
|
|||
impl NotificationTracker {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
notifications_sent: HashSet::default(),
|
||||
notifications_sent: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn has_shown_notification_once<V: Notification>(
|
||||
&self,
|
||||
id: usize,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> bool {
|
||||
cx.global::<NotificationTracker>()
|
||||
.get(&TypeId::of::<V>())
|
||||
.map(|ids| ids.contains(&id))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn show_notification_once<V: Notification>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
|
||||
) {
|
||||
if !cx
|
||||
.global::<NotificationTracker>()
|
||||
.contains(&TypeId::of::<V>())
|
||||
{
|
||||
if !self.has_shown_notification_once::<V>(id, cx) {
|
||||
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
||||
tracker.insert(TypeId::of::<V>())
|
||||
let entry = tracker.entry(TypeId::of::<V>()).or_default();
|
||||
entry.push(id);
|
||||
});
|
||||
|
||||
self.show_notification::<V>(id, cx, build_notification)
|
||||
|
@ -154,9 +163,10 @@ pub mod simple_message_notification {
|
|||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
fonts::TextStyle,
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Element, Entity, View, ViewContext,
|
||||
AnyElement, AppContext, Element, Entity, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use serde::Deserialize;
|
||||
|
@ -184,8 +194,13 @@ pub mod simple_message_notification {
|
|||
)
|
||||
}
|
||||
|
||||
enum NotificationMessage {
|
||||
Text(Cow<'static, str>),
|
||||
Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
|
||||
}
|
||||
|
||||
pub struct MessageNotification {
|
||||
message: Cow<'static, str>,
|
||||
message: NotificationMessage,
|
||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
click_message: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
@ -204,7 +219,17 @@ pub mod simple_message_notification {
|
|||
S: Into<Cow<'static, str>>,
|
||||
{
|
||||
Self {
|
||||
message: message.into(),
|
||||
message: NotificationMessage::Text(message.into()),
|
||||
on_click: None,
|
||||
click_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_element(
|
||||
message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
|
||||
) -> MessageNotification {
|
||||
Self {
|
||||
message: NotificationMessage::Element(message),
|
||||
on_click: None,
|
||||
click_message: None,
|
||||
}
|
||||
|
@ -243,84 +268,90 @@ pub mod simple_message_notification {
|
|||
enum MessageNotificationTag {}
|
||||
|
||||
let click_message = self.click_message.clone();
|
||||
let message = self.message.clone();
|
||||
let message = match &self.message {
|
||||
NotificationMessage::Text(text) => {
|
||||
Text::new(text.to_owned(), theme.message.text.clone()).into_any()
|
||||
}
|
||||
NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
|
||||
};
|
||||
let on_click = self.on_click.clone();
|
||||
let has_click_action = on_click.is_some();
|
||||
|
||||
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(message, theme.message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.font_cache().line_height(theme.message.text.font_size),
|
||||
)
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
message
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float(),
|
||||
),
|
||||
)
|
||||
.with_children({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
if let Some(click_message) = click_message {
|
||||
Some(
|
||||
Flex::row().with_child(
|
||||
Text::new(click_message, style.text.clone())
|
||||
.left()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float(),
|
||||
),
|
||||
)
|
||||
.with_children({
|
||||
click_message
|
||||
.map(|click_message| {
|
||||
MouseEventHandler::<MessageNotificationTag, _>::new(
|
||||
0,
|
||||
cx,
|
||||
|state, _| {
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new(click_message, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.container),
|
||||
),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(on_click) = on_click.as_ref() {
|
||||
on_click(cx);
|
||||
this.dismiss(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
// Since we're not using a proper overlay, we have to capture these extra events
|
||||
.on_down(MouseButton::Left, |_, _, _| {})
|
||||
.on_up(MouseButton::Left, |_, _, _| {})
|
||||
.with_cursor_style(if has_click_action {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
})
|
||||
.into_iter()
|
||||
})
|
||||
.contained()
|
||||
})
|
||||
// Since we're not using a proper overlay, we have to capture these extra events
|
||||
.on_down(MouseButton::Left, |_, _, _| {})
|
||||
.on_up(MouseButton::Left, |_, _, _| {})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(on_click) = on_click.as_ref() {
|
||||
on_click(cx);
|
||||
this.dismiss(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(if has_click_action {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.into_any()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ mod dragged_item_receiver;
|
|||
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{
|
||||
item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile,
|
||||
NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
|
||||
item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
|
||||
NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
|
@ -268,6 +268,7 @@ impl Pane {
|
|||
.with_child(Self::render_tab_bar_button(
|
||||
0,
|
||||
"icons/plus_12.svg",
|
||||
false,
|
||||
Some(("New...".into(), None)),
|
||||
cx,
|
||||
|pane, cx| pane.deploy_new_menu(cx),
|
||||
|
@ -277,6 +278,7 @@ impl Pane {
|
|||
.with_child(Self::render_tab_bar_button(
|
||||
1,
|
||||
"icons/split_12.svg",
|
||||
false,
|
||||
Some(("Split Pane".into(), None)),
|
||||
cx,
|
||||
|pane, cx| pane.deploy_split_menu(cx),
|
||||
|
@ -290,6 +292,7 @@ impl Pane {
|
|||
} else {
|
||||
"icons/maximize_8.svg"
|
||||
},
|
||||
pane.is_zoomed(),
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
|
@ -536,6 +539,11 @@ impl Pane {
|
|||
}
|
||||
|
||||
pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||
// Potentially warn the user of the new keybinding
|
||||
let workspace_handle = self.workspace().clone();
|
||||
cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
|
||||
.detach();
|
||||
|
||||
if self.zoomed {
|
||||
cx.emit(Event::ZoomOut);
|
||||
} else if !self.items.is_empty() {
|
||||
|
@ -1014,7 +1022,7 @@ impl Pane {
|
|||
let is_active_item = target_item_id == active_item_id;
|
||||
let target_pane = cx.weak_handle();
|
||||
|
||||
// The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currenlty, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
|
||||
// The `CloseInactiveItems` action should really be called "CloseOthers" and the behaviour should be dynamically based on the tab the action is ran on. Currently, this is a weird action because you can run it on a non-active tab and it will close everything by the actual active tab
|
||||
|
||||
self.tab_context_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
|
@ -1143,7 +1151,8 @@ impl Pane {
|
|||
let theme = theme::current(cx).clone();
|
||||
let mut tooltip_theme = theme.tooltip.clone();
|
||||
tooltip_theme.max_text_width = None;
|
||||
let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
|
||||
let tab_tooltip_text =
|
||||
item.tab_tooltip_text(cx).map(|text| text.into_owned());
|
||||
|
||||
move |mouse_state, cx| {
|
||||
let tab_style =
|
||||
|
@ -1401,6 +1410,7 @@ impl Pane {
|
|||
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
|
||||
index: usize,
|
||||
icon: &'static str,
|
||||
active: bool,
|
||||
tooltip: Option<(String, Option<Box<dyn Action>>)>,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
on_click: F,
|
||||
|
@ -1410,7 +1420,7 @@ impl Pane {
|
|||
|
||||
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
|
||||
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
|
||||
let style = theme.pane_button.style_for(mouse_state, false);
|
||||
let style = theme.pane_button.style_for(mouse_state, active);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
|
|
@ -15,7 +15,6 @@ mod toolbar;
|
|||
mod workspace_settings;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assets::Assets;
|
||||
use call::ActiveCall;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
|
@ -60,7 +59,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
notifications::simple_message_notification::MessageNotification,
|
||||
notifications::{simple_message_notification::MessageNotification, NotificationTracker},
|
||||
persistence::model::{
|
||||
DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
|
||||
},
|
||||
|
@ -81,9 +80,9 @@ use serde::Deserialize;
|
|||
use shared_screen::SharedScreen;
|
||||
use status_bar::StatusBar;
|
||||
pub use status_bar::StatusItemView;
|
||||
use theme::Theme;
|
||||
use theme::{Theme, ThemeSettings};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
use util::{async_iife, paths, ResultExt};
|
||||
use util::{async_iife, ResultExt};
|
||||
pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
|
||||
|
||||
lazy_static! {
|
||||
|
@ -133,8 +132,6 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
actions!(zed, [OpenSettings]);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct OpenPaths {
|
||||
pub paths: Vec<PathBuf>,
|
||||
|
@ -295,17 +292,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
.detach();
|
||||
});
|
||||
|
||||
cx.add_action(
|
||||
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content(&Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
);
|
||||
|
||||
let client = &app_state.client;
|
||||
client.add_view_request_handler(Workspace::handle_follow);
|
||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||
|
@ -765,25 +751,21 @@ impl Workspace {
|
|||
DB.next_id().await.unwrap_or(0)
|
||||
};
|
||||
|
||||
let window_bounds_override =
|
||||
ZED_WINDOW_POSITION
|
||||
.zip(*ZED_WINDOW_SIZE)
|
||||
.map(|(position, size)| {
|
||||
WindowBounds::Fixed(RectF::new(
|
||||
cx.platform().screens()[0].bounds().origin() + position,
|
||||
size,
|
||||
))
|
||||
});
|
||||
|
||||
let build_workspace = |cx: &mut ViewContext<Workspace>| {
|
||||
Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx)
|
||||
};
|
||||
|
||||
let workspace = requesting_window_id
|
||||
.and_then(|window_id| {
|
||||
cx.update(|cx| cx.replace_root_view(window_id, |cx| build_workspace(cx)))
|
||||
cx.update(|cx| {
|
||||
cx.replace_root_view(window_id, |cx| {
|
||||
Workspace::new(
|
||||
workspace_id,
|
||||
project_handle.clone(),
|
||||
app_state.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let window_bounds_override = window_bounds_env_override(&cx);
|
||||
let (bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||
(Some(bounds), None)
|
||||
} else {
|
||||
|
@ -819,7 +801,14 @@ impl Workspace {
|
|||
// Use the serialized workspace to construct the new window
|
||||
cx.add_window(
|
||||
(app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
|
||||
|cx| build_workspace(cx),
|
||||
|cx| {
|
||||
Workspace::new(
|
||||
workspace_id,
|
||||
project_handle.clone(),
|
||||
app_state.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.1
|
||||
});
|
||||
|
@ -908,18 +897,24 @@ impl Workspace {
|
|||
}
|
||||
});
|
||||
} else if T::should_zoom_in_on_event(event) {
|
||||
this.zoom_out(cx);
|
||||
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
|
||||
if panel.has_focus(cx) {
|
||||
this.zoomed = Some(panel.downgrade().into_any());
|
||||
this.zoomed_position = Some(panel.read(cx).position(cx));
|
||||
}
|
||||
} else if T::should_zoom_out_on_event(event) {
|
||||
this.zoom_out(cx);
|
||||
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
|
||||
if this.zoomed_position == Some(prev_position) {
|
||||
this.zoomed = None;
|
||||
this.zoomed_position = None;
|
||||
}
|
||||
cx.notify();
|
||||
} else if T::is_focus_event(event) {
|
||||
let position = panel.read(cx).position(cx);
|
||||
this.dismiss_zoomed_items_to_reveal(Some(position), cx);
|
||||
if panel.is_zoomed(cx) {
|
||||
this.zoomed = Some(panel.downgrade().into_any());
|
||||
this.zoomed_position = Some(panel.read(cx).position(cx));
|
||||
this.zoomed_position = Some(position);
|
||||
} else {
|
||||
this.zoomed = None;
|
||||
this.zoomed_position = None;
|
||||
|
@ -968,9 +963,8 @@ impl Workspace {
|
|||
let timestamp = entry.timestamp;
|
||||
match history.entry(project_path) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
let (old_fs_path, old_timestamp) = entry.get();
|
||||
let (_, old_timestamp) = entry.get();
|
||||
if ×tamp > old_timestamp {
|
||||
assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
|
||||
entry.insert((fs_path, timestamp));
|
||||
}
|
||||
}
|
||||
|
@ -1592,7 +1586,7 @@ impl Workspace {
|
|||
DockPosition::Right => &self.right_dock,
|
||||
};
|
||||
let mut focus_center = false;
|
||||
let mut zoom_out = false;
|
||||
let mut reveal_dock = false;
|
||||
dock.update(cx, |dock, cx| {
|
||||
let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
|
||||
let was_visible = dock.is_open() && !other_is_zoomed;
|
||||
|
@ -1607,14 +1601,15 @@ impl Workspace {
|
|||
if active_panel.is_zoomed(cx) {
|
||||
cx.focus(active_panel.as_any());
|
||||
}
|
||||
zoom_out = true;
|
||||
reveal_dock = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if zoom_out {
|
||||
self.zoom_out_everything_except(dock_side, cx);
|
||||
if reveal_dock {
|
||||
self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
|
||||
}
|
||||
|
||||
if focus_center {
|
||||
cx.focus_self();
|
||||
}
|
||||
|
@ -1623,62 +1618,49 @@ impl Workspace {
|
|||
self.serialize_workspace(cx);
|
||||
}
|
||||
|
||||
/// Transfer focus to the panel of the given type.
|
||||
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
|
||||
self.show_or_hide_panel::<T>(cx, |_, _| true)?
|
||||
self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
|
||||
.as_any()
|
||||
.clone()
|
||||
.downcast()
|
||||
}
|
||||
|
||||
/// Focus the panel of the given type if it isn't already focused. If it is
|
||||
/// already focused, then transfer focus back to the workspace center.
|
||||
pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.show_or_hide_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
||||
self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
|
||||
}
|
||||
|
||||
fn show_or_hide_panel<T: Panel>(
|
||||
/// Focus or unfocus the given panel type, depending on the given callback.
|
||||
fn focus_or_unfocus_panel<T: Panel>(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
show: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
||||
should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
|
||||
) -> Option<Rc<dyn PanelHandle>> {
|
||||
for (dock, position) in [
|
||||
self.left_dock.clone(),
|
||||
self.bottom_dock.clone(),
|
||||
self.right_dock.clone(),
|
||||
]
|
||||
.into_iter()
|
||||
.zip(
|
||||
[
|
||||
DockPosition::Left,
|
||||
DockPosition::Bottom,
|
||||
DockPosition::Right,
|
||||
]
|
||||
.into_iter(),
|
||||
) {
|
||||
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||
if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
|
||||
let mut focus_center = false;
|
||||
let mut zoom_out = false;
|
||||
let mut reveal_dock = false;
|
||||
let panel = dock.update(cx, |dock, cx| {
|
||||
dock.activate_panel(panel_index, cx);
|
||||
|
||||
let panel = dock.active_panel().cloned();
|
||||
if let Some(panel) = panel.as_ref() {
|
||||
let should_show = show(&**panel, cx);
|
||||
if should_show {
|
||||
if should_focus(&**panel, cx) {
|
||||
dock.set_open(true, cx);
|
||||
cx.focus(panel.as_any());
|
||||
zoom_out = true;
|
||||
reveal_dock = true;
|
||||
} else {
|
||||
if panel.is_zoomed(cx) {
|
||||
dock.set_open(false, cx);
|
||||
}
|
||||
// if panel.is_zoomed(cx) {
|
||||
// dock.set_open(false, cx);
|
||||
// }
|
||||
focus_center = true;
|
||||
}
|
||||
}
|
||||
panel
|
||||
});
|
||||
|
||||
if zoom_out {
|
||||
self.zoom_out_everything_except(position, cx);
|
||||
}
|
||||
if focus_center {
|
||||
cx.focus_self();
|
||||
}
|
||||
|
@ -1691,6 +1673,16 @@ impl Workspace {
|
|||
None
|
||||
}
|
||||
|
||||
pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
|
||||
for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
|
||||
let dock = dock.read(cx);
|
||||
if let Some(panel) = dock.panel::<T>() {
|
||||
return Some(panel);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||
for pane in &self.panes {
|
||||
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
|
||||
|
@ -1705,28 +1697,38 @@ impl Workspace {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn zoom_out_everything_except(
|
||||
fn dismiss_zoomed_items_to_reveal(
|
||||
&mut self,
|
||||
except_position: DockPosition,
|
||||
dock_to_reveal: Option<DockPosition>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
// If a center pane is zoomed, unzoom it.
|
||||
for pane in &self.panes {
|
||||
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
|
||||
if pane != &self.active_pane || dock_to_reveal.is_some() {
|
||||
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
|
||||
}
|
||||
}
|
||||
|
||||
if except_position != DockPosition::Left {
|
||||
self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx));
|
||||
// If another dock is zoomed, hide it.
|
||||
let mut focus_center = false;
|
||||
for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
|
||||
dock.update(cx, |dock, cx| {
|
||||
if Some(dock.position()) != dock_to_reveal {
|
||||
if let Some(panel) = dock.active_panel() {
|
||||
if panel.is_zoomed(cx) {
|
||||
focus_center |= panel.has_focus(cx);
|
||||
dock.set_open(false, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if except_position != DockPosition::Bottom {
|
||||
self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
|
||||
if focus_center {
|
||||
cx.focus_self();
|
||||
}
|
||||
|
||||
if except_position != DockPosition::Right {
|
||||
self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
|
||||
}
|
||||
|
||||
if self.zoomed_position != Some(except_position) {
|
||||
if self.zoomed_position != dock_to_reveal {
|
||||
self.zoomed = None;
|
||||
self.zoomed_position = None;
|
||||
}
|
||||
|
@ -1937,6 +1939,7 @@ impl Workspace {
|
|||
self.last_active_center_pane = Some(pane.downgrade());
|
||||
}
|
||||
|
||||
self.dismiss_zoomed_items_to_reveal(None, cx);
|
||||
if pane.read(cx).is_zoomed() {
|
||||
self.zoomed = Some(pane.downgrade().into_any());
|
||||
} else {
|
||||
|
@ -1998,7 +2001,6 @@ impl Workspace {
|
|||
}
|
||||
pane::Event::ZoomIn => {
|
||||
if pane == self.active_pane {
|
||||
self.zoom_out(cx);
|
||||
pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
|
||||
if pane.read(cx).has_focus() {
|
||||
self.zoomed = Some(pane.downgrade().into_any());
|
||||
|
@ -2007,7 +2009,13 @@ impl Workspace {
|
|||
cx.notify();
|
||||
}
|
||||
}
|
||||
pane::Event::ZoomOut => self.zoom_out(cx),
|
||||
pane::Event::ZoomOut => {
|
||||
pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
|
||||
if self.zoomed_position.is_none() {
|
||||
self.zoomed = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
self.serialize_workspace(cx);
|
||||
|
@ -3101,6 +3109,17 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
|
||||
ZED_WINDOW_POSITION
|
||||
.zip(*ZED_WINDOW_SIZE)
|
||||
.map(|(position, size)| {
|
||||
WindowBounds::Fixed(RectF::new(
|
||||
cx.platform().screens()[0].bounds().origin() + position,
|
||||
size,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_items(
|
||||
serialized_workspace: Option<SerializedWorkspace>,
|
||||
workspace: &WeakViewHandle<Workspace>,
|
||||
|
@ -3190,6 +3209,87 @@ async fn open_items(
|
|||
opened_items
|
||||
}
|
||||
|
||||
fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||
const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
|
||||
const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
|
||||
const MESSAGE_ID: usize = 2;
|
||||
|
||||
if workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if db::kvp::KEY_VALUE_STORE
|
||||
.read_kvp(NEW_DOCK_HINT_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
if !workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
|
||||
let entry = tracker
|
||||
.entry(TypeId::of::<MessageNotification>())
|
||||
.or_default();
|
||||
if !entry.contains(&MESSAGE_ID) {
|
||||
entry.push(MESSAGE_ID);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cx.spawn(|_| async move {
|
||||
db::kvp::KEY_VALUE_STORE
|
||||
.write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
|
||||
.await
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_notification_once(2, cx, |cx| {
|
||||
cx.add_view(|_| {
|
||||
MessageNotification::new_element(|text, _| {
|
||||
Text::new(
|
||||
"Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
|
||||
text,
|
||||
)
|
||||
.with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
|
||||
let code_span_background_color = settings::get::<ThemeSettings>(cx)
|
||||
.theme
|
||||
.editor
|
||||
.document_highlight_read_background;
|
||||
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds,
|
||||
background: Some(code_span_background_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 2.0,
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
})
|
||||
.with_click_message("Read more about the new panel system")
|
||||
.on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
|
||||
})
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
|
||||
|
||||
|
@ -3206,7 +3306,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
|
|||
} else {
|
||||
let backup_path = (*db::BACKUP_DB_PATH).read();
|
||||
if let Some(backup_path) = backup_path.clone() {
|
||||
workspace.show_notification_once(0, cx, move |cx| {
|
||||
workspace.show_notification_once(1, cx, move |cx| {
|
||||
cx.add_view(move |_| {
|
||||
MessageNotification::new(format!(
|
||||
"Database file was corrupted. Old database backed up to {}",
|
||||
|
@ -3278,32 +3378,36 @@ impl View for Workspace {
|
|||
enum ZoomBackground {}
|
||||
let zoomed = zoomed.upgrade(cx)?;
|
||||
|
||||
let mut foreground_style;
|
||||
match self.zoomed_position {
|
||||
Some(DockPosition::Left) => {
|
||||
foreground_style =
|
||||
theme.workspace.zoomed_panel_foreground;
|
||||
foreground_style.margin.left = 0.;
|
||||
foreground_style.margin.top = 0.;
|
||||
foreground_style.margin.bottom = 0.;
|
||||
}
|
||||
Some(DockPosition::Right) => {
|
||||
foreground_style =
|
||||
theme.workspace.zoomed_panel_foreground;
|
||||
foreground_style.margin.right = 0.;
|
||||
foreground_style.margin.top = 0.;
|
||||
foreground_style.margin.bottom = 0.;
|
||||
}
|
||||
Some(DockPosition::Bottom) => {
|
||||
foreground_style =
|
||||
theme.workspace.zoomed_panel_foreground;
|
||||
foreground_style.margin.left = 0.;
|
||||
foreground_style.margin.right = 0.;
|
||||
foreground_style.margin.bottom = 0.;
|
||||
}
|
||||
None => {
|
||||
foreground_style =
|
||||
theme.workspace.zoomed_pane_foreground;
|
||||
let mut foreground_style =
|
||||
theme.workspace.zoomed_pane_foreground;
|
||||
if let Some(zoomed_dock_position) = self.zoomed_position {
|
||||
foreground_style =
|
||||
theme.workspace.zoomed_panel_foreground;
|
||||
let margin = foreground_style.margin.top;
|
||||
let border = foreground_style.border.top;
|
||||
|
||||
// Only include a margin and border on the opposite side.
|
||||
foreground_style.margin.top = 0.;
|
||||
foreground_style.margin.left = 0.;
|
||||
foreground_style.margin.bottom = 0.;
|
||||
foreground_style.margin.right = 0.;
|
||||
foreground_style.border.top = false;
|
||||
foreground_style.border.left = false;
|
||||
foreground_style.border.bottom = false;
|
||||
foreground_style.border.right = false;
|
||||
match zoomed_dock_position {
|
||||
DockPosition::Left => {
|
||||
foreground_style.margin.right = margin;
|
||||
foreground_style.border.right = border;
|
||||
}
|
||||
DockPosition::Right => {
|
||||
foreground_style.margin.left = margin;
|
||||
foreground_style.border.left = border;
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
foreground_style.margin.top = margin;
|
||||
foreground_style.border.top = border;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3548,8 +3652,13 @@ pub fn join_remote_project(
|
|||
})
|
||||
.await?;
|
||||
|
||||
let window_bounds_override = window_bounds_env_override(&cx);
|
||||
let (_, workspace) = cx.add_window(
|
||||
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|
||||
(app_state.build_window_options)(
|
||||
window_bounds_override,
|
||||
None,
|
||||
cx.platform().as_ref(),
|
||||
),
|
||||
|cx| Workspace::new(0, project, app_state.clone(), cx),
|
||||
);
|
||||
(app_state.initialize_workspace)(
|
||||
|
@ -4257,6 +4366,12 @@ mod tests {
|
|||
panel
|
||||
});
|
||||
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
pane.update(cx, |pane, cx| {
|
||||
let item = cx.add_view(|_| TestItem::new());
|
||||
pane.add_item(Box::new(item), true, true, None, cx);
|
||||
});
|
||||
|
||||
// Transfer focus from center to panel
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
|
@ -4324,7 +4439,7 @@ mod tests {
|
|||
assert!(!panel.has_focus(cx));
|
||||
});
|
||||
|
||||
// Transfering focus back to the panel keeps it zoomed
|
||||
// Transferring focus back to the panel keeps it zoomed
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
});
|
||||
|
@ -4358,6 +4473,25 @@ mod tests {
|
|||
assert!(workspace.zoomed.is_some());
|
||||
assert!(panel.has_focus(cx));
|
||||
});
|
||||
|
||||
// Unzoom and close the panel, zoom the active pane.
|
||||
panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_dock(DockPosition::Right, cx)
|
||||
});
|
||||
pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
|
||||
|
||||
// Opening a dock unzooms the pane.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_dock(DockPosition::Right, cx)
|
||||
});
|
||||
workspace.read_with(cx, |workspace, cx| {
|
||||
let pane = pane.read(cx);
|
||||
assert!(!pane.is_zoomed());
|
||||
assert!(pane.has_focus());
|
||||
assert!(workspace.right_dock().read(cx).is_open());
|
||||
assert!(workspace.zoomed.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.89.0"
|
||||
version = "0.91.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
@ -17,7 +17,6 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
activity_indicator = { path = "../activity_indicator" }
|
||||
assets = { path = "../assets" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
breadcrumbs = { path = "../breadcrumbs" }
|
||||
call = { path = "../call" }
|
||||
|
@ -107,7 +106,7 @@ 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-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
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 = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
|
||||
|
@ -120,7 +119,7 @@ tree-sitter-ruby = "0.20.0"
|
|||
tree-sitter-html = "0.19.0"
|
||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
|
||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "9050a4a4a847ed29e25485b1292a36eab8ae3492"}
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "5694b7f290cd9ef998829a0a6d8391a666370886"}
|
||||
tree-sitter-lua = "0.0.14"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
|
||||
|
||||
if let Ok(value) = std::env::var("ZED_MIXPANEL_TOKEN") {
|
||||
println!("cargo:rustc-env=ZED_MIXPANEL_TOKEN={value}");
|
||||
}
|
||||
if let Ok(value) = std::env::var("ZED_PREVIEW_CHANNEL") {
|
||||
println!("cargo:rustc-env=ZED_PREVIEW_CHANNEL={value}");
|
||||
}
|
||||
|
@ -24,4 +23,32 @@ fn main() {
|
|||
|
||||
// Register exported Objective-C selectors, protocols, etc
|
||||
println!("cargo:rustc-link-arg=-Wl,-ObjC");
|
||||
|
||||
// Install dependencies for theme-generation
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["install", "--no-save"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"failed to install theme dependencies {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
// Regenerate themes
|
||||
let output = Command::new("npm")
|
||||
.current_dir("../../styles")
|
||||
.args(["run", "build"])
|
||||
.output()
|
||||
.expect("failed to run npm");
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"build script failed {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=../../styles/src");
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@ use rust_embed::RustEmbed;
|
|||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../assets"]
|
||||
#[include = "fonts/**/*"]
|
||||
#[include = "icons/**/*"]
|
||||
#[include = "themes/**/*"]
|
||||
#[include = "*.md"]
|
||||
#[exclude = "*.DS_Store"]
|
||||
pub struct Assets;
|
||||
|
|
@ -3,6 +3,7 @@ pub use language::*;
|
|||
use node_runtime::NodeRuntime;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, str, sync::Arc};
|
||||
use util::asset_str;
|
||||
|
||||
mod c;
|
||||
mod elixir;
|
||||
|
@ -179,10 +180,7 @@ fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
|
|||
for path in LanguageDir::iter() {
|
||||
if let Some(remainder) = path.strip_prefix(name) {
|
||||
if remainder.starts_with(filename_prefix) {
|
||||
let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
|
||||
Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
|
||||
Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
|
||||
};
|
||||
let contents = asset_str::<LanguageDir>(path.as_ref());
|
||||
match &mut result {
|
||||
None => result = Some(contents),
|
||||
Some(r) => r.to_mut().push_str(contents.as_ref()),
|
||||
|
|
|
@ -1,20 +1,5 @@
|
|||
["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
|
||||
|
||||
(unary_operator
|
||||
operator: "@" @comment.doc
|
||||
operand: (call
|
||||
target: (identifier) @comment.doc.__attribute__
|
||||
(arguments
|
||||
[
|
||||
(string) @comment.doc
|
||||
(charlist) @comment.doc
|
||||
(sigil
|
||||
quoted_start: _ @comment.doc
|
||||
quoted_end: _ @comment.doc) @comment.doc
|
||||
(boolean) @comment.doc
|
||||
]))
|
||||
(#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$"))
|
||||
|
||||
(unary_operator
|
||||
operator: "&"
|
||||
operand: (integer) @operator)
|
||||
|
@ -84,6 +69,11 @@
|
|||
quoted_start: _ @string.special
|
||||
quoted_end: _ @string.special) @string.special
|
||||
|
||||
(
|
||||
(identifier) @comment.unused
|
||||
(#match? @comment.unused "^_")
|
||||
)
|
||||
|
||||
(call
|
||||
target: [
|
||||
(identifier) @function
|
||||
|
@ -99,17 +89,12 @@
|
|||
(binary_operator
|
||||
left: (identifier) @function
|
||||
operator: "when")
|
||||
(binary_operator
|
||||
operator: "|>"
|
||||
right: (identifier))
|
||||
])
|
||||
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
|
||||
|
||||
(call
|
||||
target: (identifier) @keyword
|
||||
(arguments
|
||||
(binary_operator
|
||||
operator: "|>"
|
||||
right: (identifier)))
|
||||
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
|
||||
|
||||
(binary_operator
|
||||
operator: "|>"
|
||||
right: (identifier) @function)
|
||||
|
@ -127,10 +112,18 @@
|
|||
(#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
|
||||
)
|
||||
|
||||
(
|
||||
(identifier) @comment.unused
|
||||
(#match? @comment.unused "^_")
|
||||
)
|
||||
(unary_operator
|
||||
operator: "@" @comment.doc
|
||||
operand: (call
|
||||
target: (identifier) @__attribute__ @comment.doc
|
||||
(arguments
|
||||
[
|
||||
(string)
|
||||
(charlist)
|
||||
(sigil)
|
||||
(boolean)
|
||||
] @comment.doc))
|
||||
(#match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
|
||||
|
||||
(comment) @comment
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
[
|
||||
(call)
|
||||
] @indent
|
||||
(call) @indent
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
|
|
|
@ -8,9 +8,19 @@
|
|||
(arguments
|
||||
[
|
||||
(identifier) @name
|
||||
(call target: (identifier) @name)
|
||||
(call
|
||||
target: (identifier) @name
|
||||
(arguments
|
||||
"(" @context.extra
|
||||
_* @context.extra
|
||||
")" @context.extra))
|
||||
(binary_operator
|
||||
left: (call target: (identifier) @name)
|
||||
left: (call
|
||||
target: (identifier) @name
|
||||
(arguments
|
||||
"(" @context.extra
|
||||
_* @context.extra
|
||||
")" @context.extra))
|
||||
operator: "when")
|
||||
])
|
||||
(#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
|
||||
|
|
|
@ -6,7 +6,7 @@ use gpui::AppContext;
|
|||
use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
|
||||
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
|
||||
use smol::fs;
|
||||
use staff_mode::StaffMode;
|
||||
use std::{
|
||||
|
@ -135,12 +135,15 @@ impl LspAdapter for JsonLspAdapter {
|
|||
},
|
||||
"schemas": [
|
||||
{
|
||||
"fileMatch": [schema_file_match(&paths::SETTINGS)],
|
||||
"fileMatch": [
|
||||
schema_file_match(&paths::SETTINGS),
|
||||
&*paths::LOCAL_SETTINGS_RELATIVE_PATH,
|
||||
],
|
||||
"schema": settings_schema,
|
||||
},
|
||||
{
|
||||
"fileMatch": [schema_file_match(&paths::KEYMAP)],
|
||||
"schema": keymap_file_json_schema(&action_names),
|
||||
"schema": KeymapFile::generate_json_schema(&action_names),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name = "Markdown"
|
||||
path_suffixes = ["md", "mdx", "zmd"]
|
||||
path_suffixes = ["md", "mdx"]
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
|
|
|
@ -14,11 +14,11 @@
|
|||
(list_marker_parenthesis)
|
||||
] @punctuation.list_marker
|
||||
|
||||
[
|
||||
(indented_code_block)
|
||||
(fenced_code_block)
|
||||
(code_span)
|
||||
] @text.literal
|
||||
(code_span) @text.literal
|
||||
|
||||
(fenced_code_block
|
||||
(info_string
|
||||
(language) @text.literal))
|
||||
|
||||
(link_destination) @link_uri
|
||||
(link_text) @link_text
|
||||
|
|
|
@ -207,7 +207,7 @@ impl LspAdapter for EsLintLspAdapter {
|
|||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
// At the time of writing the latest vscode-eslint release was released in 2020 and requires
|
||||
// special custom LSP protocol extensions be handled to fully initalize. Download the latest
|
||||
// special custom LSP protocol extensions be handled to fully initialize. Download the latest
|
||||
// prerelease instead to sidestep this issue
|
||||
let release = latest_github_release("microsoft/vscode-eslint", true, http).await?;
|
||||
Ok(Box::new(GitHubLspBinaryVersion {
|
||||
|
@ -327,10 +327,10 @@ mod tests {
|
|||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("function a ( )", 0),
|
||||
("async function a2 ( )", 1),
|
||||
("function a()", 0),
|
||||
("async function a2()", 1),
|
||||
("let b", 0),
|
||||
("function getB ( )", 0),
|
||||
("function getB()", 0),
|
||||
("const d", 0),
|
||||
]
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
|||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
|
||||
language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::Value;
|
||||
|
@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter {
|
|||
}
|
||||
|
||||
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
|
||||
let tab_size = all_language_settings(None, cx)
|
||||
.language(Some("YAML"))
|
||||
.tab_size;
|
||||
Some(
|
||||
future::ready(serde_json::json!({
|
||||
"yaml": {
|
||||
"keyOrdering": false
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.tabSize": language_settings(Some("YAML"), cx).tab_size,
|
||||
"editor.tabSize": tab_size,
|
||||
}
|
||||
}))
|
||||
.boxed(),
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Allow binary to be called Zed for a nice application menu when running executable direcly
|
||||
// Allow binary to be called Zed for a nice application menu when running executable directly
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use assets::Assets;
|
||||
use backtrace::Backtrace;
|
||||
use cli::{
|
||||
ipc::{self, IpcSender},
|
||||
|
@ -32,6 +31,7 @@ use std::{
|
|||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
io::Write as _,
|
||||
ops::Not,
|
||||
os::unix::prelude::OsStrExt,
|
||||
panic,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -41,7 +41,7 @@ use std::{
|
|||
Arc, Weak,
|
||||
},
|
||||
thread,
|
||||
time::Duration,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use sum_tree::Bias;
|
||||
use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
|
||||
|
@ -55,11 +55,10 @@ use fs::RealFs;
|
|||
#[cfg(debug_assertions)]
|
||||
use staff_mode::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace,
|
||||
};
|
||||
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
|
||||
use zed::{
|
||||
self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
|
||||
languages, menus,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
|
@ -70,10 +69,7 @@ fn main() {
|
|||
log::info!("========== starting zed ==========");
|
||||
let mut app = gpui::App::new(Assets).unwrap();
|
||||
|
||||
let app_version = ZED_APP_VERSION
|
||||
.or_else(|| app.platform().app_version().ok())
|
||||
.map_or("dev".to_string(), |v| v.to_string());
|
||||
init_panic_hook(app_version);
|
||||
init_panic_hook(&app);
|
||||
|
||||
app.background();
|
||||
|
||||
|
@ -164,6 +160,8 @@ fn main() {
|
|||
ai::init(cx);
|
||||
|
||||
cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
|
||||
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||
.detach();
|
||||
|
||||
languages.set_theme(theme::current(cx).clone());
|
||||
cx.observe_global::<SettingsStore, _>({
|
||||
|
@ -173,11 +171,6 @@ fn main() {
|
|||
.detach();
|
||||
|
||||
client.telemetry().start();
|
||||
client.telemetry().report_mixpanel_event(
|
||||
"start app",
|
||||
Default::default(),
|
||||
*settings::get::<TelemetrySettings>(cx),
|
||||
);
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
|
@ -374,32 +367,96 @@ struct Panic {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
location_data: Option<LocationData>,
|
||||
backtrace: Vec<String>,
|
||||
// TODO
|
||||
// stripped_backtrace: String,
|
||||
app_version: String,
|
||||
release_channel: String,
|
||||
os_name: String,
|
||||
os_version: Option<String>,
|
||||
architecture: String,
|
||||
panicked_on: u128,
|
||||
identifying_backtrace: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PanicRequest {
|
||||
panic: Panic,
|
||||
version: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
fn init_panic_hook(app_version: String) {
|
||||
fn init_panic_hook(app: &App) {
|
||||
let is_pty = stdout_is_a_pty();
|
||||
let platform = app.platform();
|
||||
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
let backtrace = Backtrace::new();
|
||||
let app_version = ZED_APP_VERSION
|
||||
.or_else(|| platform.app_version().ok())
|
||||
.map_or("dev".to_string(), |v| v.to_string());
|
||||
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("<unnamed>");
|
||||
|
||||
let payload = match info.payload().downcast_ref::<&'static str>() {
|
||||
Some(s) => *s,
|
||||
None => match info.payload().downcast_ref::<String>() {
|
||||
Some(s) => &**s,
|
||||
None => "Box<Any>",
|
||||
},
|
||||
};
|
||||
let payload = info.payload();
|
||||
let payload = None
|
||||
.or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
|
||||
.or_else(|| payload.downcast_ref::<String>().map(|s| s.clone()))
|
||||
.unwrap_or_else(|| "Box<Any>".to_string());
|
||||
|
||||
let backtrace = Backtrace::new();
|
||||
let backtrace = backtrace
|
||||
.frames()
|
||||
.iter()
|
||||
.filter_map(|frame| {
|
||||
let symbol = frame.symbols().first()?;
|
||||
let path = symbol.filename()?;
|
||||
Some((path, symbol.lineno(), format!("{:#}", symbol.name()?)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let this_file_path = Path::new(file!());
|
||||
|
||||
// Find the first frame in the backtrace for this panic hook itself. Exclude
|
||||
// that frame and all frames before it.
|
||||
let mut start_frame_ix = 0;
|
||||
let mut codebase_root_path = None;
|
||||
for (ix, (path, _, _)) in backtrace.iter().enumerate() {
|
||||
if path.ends_with(this_file_path) {
|
||||
start_frame_ix = ix + 1;
|
||||
codebase_root_path = path.ancestors().nth(this_file_path.components().count());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude any subsequent frames inside of rust's panic handling system.
|
||||
while let Some((path, _, _)) = backtrace.get(start_frame_ix) {
|
||||
if path.starts_with("/rustc") {
|
||||
start_frame_ix += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build two backtraces:
|
||||
// * one for display, which includes symbol names for all frames, and files
|
||||
// and line numbers for symbols in this codebase
|
||||
// * one for identification and de-duplication, which only includes symbol
|
||||
// names for symbols in this codebase.
|
||||
let mut display_backtrace = Vec::new();
|
||||
let mut identifying_backtrace = Vec::new();
|
||||
for (path, line, symbol) in &backtrace[start_frame_ix..] {
|
||||
display_backtrace.push(symbol.clone());
|
||||
|
||||
if let Some(codebase_root_path) = &codebase_root_path {
|
||||
if let Ok(suffix) = path.strip_prefix(&codebase_root_path) {
|
||||
identifying_backtrace.push(symbol.clone());
|
||||
|
||||
let display_path = suffix.to_string_lossy();
|
||||
if let Some(line) = line {
|
||||
display_backtrace.push(format!(" {display_path}:{line}"));
|
||||
} else {
|
||||
display_backtrace.push(format!(" {display_path}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let panic_data = Panic {
|
||||
thread: thread.into(),
|
||||
|
@ -408,11 +465,23 @@ fn init_panic_hook(app_version: String) {
|
|||
file: location.file().into(),
|
||||
line: location.line(),
|
||||
}),
|
||||
backtrace: format!("{:?}", backtrace)
|
||||
.split("\n")
|
||||
.map(|line| line.to_string())
|
||||
.collect(),
|
||||
// modified_backtrace: None,
|
||||
app_version: app_version.clone(),
|
||||
release_channel: RELEASE_CHANNEL.dev_name().into(),
|
||||
os_name: platform.os_name().into(),
|
||||
os_version: platform
|
||||
.os_version()
|
||||
.ok()
|
||||
.map(|os_version| os_version.to_string()),
|
||||
architecture: env::consts::ARCH.into(),
|
||||
panicked_on: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
backtrace: display_backtrace,
|
||||
identifying_backtrace: identifying_backtrace
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some(identifying_backtrace),
|
||||
};
|
||||
|
||||
if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
|
||||
|
@ -422,8 +491,7 @@ fn init_panic_hook(app_version: String) {
|
|||
}
|
||||
|
||||
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
|
||||
let panic_file_path =
|
||||
paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
|
||||
let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
|
||||
let panic_file = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
|
@ -458,15 +526,9 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
|
|||
continue;
|
||||
};
|
||||
|
||||
let mut components = filename.split('-');
|
||||
if components.next() != Some("zed") {
|
||||
if !filename.starts_with("zed") {
|
||||
continue;
|
||||
}
|
||||
let version = if let Some(version) = components.next() {
|
||||
version
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if telemetry_settings.diagnostics {
|
||||
let panic_data_text = smol::fs::read_to_string(&child_path)
|
||||
|
@ -475,7 +537,6 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
|
|||
|
||||
let body = serde_json::to_string(&PanicRequest {
|
||||
panic: serde_json::from_str(&panic_data_text)?,
|
||||
version: version.to_string(),
|
||||
token: ZED_SECRET_CLIENT_TOKEN.into(),
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -601,11 +662,30 @@ async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
|
|||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
|
||||
let mut events = fs
|
||||
.watch(
|
||||
"crates/zed/src/languages".as_ref(),
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.await;
|
||||
while (events.next().await).is_some() {
|
||||
languages.reload();
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn connect_to_cli(
|
||||
server_name: &str,
|
||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||
|
@ -816,6 +896,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
|||
("Go to file", &file_finder::Toggle),
|
||||
("Open command palette", &command_palette::Toggle),
|
||||
("Open recent projects", &recent_projects::OpenRecent),
|
||||
("Change your settings", &OpenSettings),
|
||||
("Change your settings", &zed::OpenSettings),
|
||||
]
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue