Merge branch 'main' into panels

This commit is contained in:
Antonio Scandurra 2023-05-22 13:52:50 +02:00
commit 146809eef0
183 changed files with 10202 additions and 5720 deletions

5
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,5 @@
[[PR Description]]
Release Notes:
* [[Added foo / Fixed bar / No notes]]

View file

@ -42,6 +42,7 @@ jobs:
runs-on: runs-on:
- self-hosted - self-hosted
- test - test
needs: rustfmt
env: env:
RUSTFLAGS: -D warnings RUSTFLAGS: -D warnings
steps: steps:
@ -62,6 +63,9 @@ jobs:
clean: false clean: false
submodules: 'recursive' submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Run check - name: Run check
run: cargo check --workspace run: cargo check --workspace
@ -82,7 +86,7 @@ jobs:
runs-on: runs-on:
- self-hosted - self-hosted
- bundle - bundle
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
needs: tests needs: tests
env: env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
@ -110,6 +114,9 @@ jobs:
clean: false clean: false
submodules: 'recursive' submodules: 'recursive'
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 70
- name: Determine version and release channel - name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }} if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: | run: |
@ -141,11 +148,11 @@ jobs:
- name: Create app bundle - name: Create app bundle
run: script/bundle run: script/bundle
- name: Upload app bundle to workflow run if main branch - name: Upload app bundle to workflow run if main branch or specifi label
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
if: ${{ github.ref == 'refs/heads/main' }} if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
with: with:
name: Zed.dmg name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg path: target/release/Zed.dmg
- uses: softprops/action-gh-release@v1 - uses: softprops/action-gh-release@v1

View file

@ -14,7 +14,7 @@ jobs:
content: | content: |
📣 Zed ${{ github.event.release.tag_name }} was just released! 📣 Zed ${{ github.event.release.tag_name }} was just released!
Restart your Zed or head to https://zed.dev/releases/latest to grab it. Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
```md ```md
# Changelog # Changelog

1795
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -77,7 +77,7 @@ async-trait = { version = "0.1" }
ctor = { version = "0.1" } ctor = { version = "0.1" }
env_logger = { version = "0.9" } env_logger = { version = "0.9" }
futures = { version = "0.3" } futures = { version = "0.3" }
glob = { version = "0.3.1" } globset = { version = "0.4" }
lazy_static = { version = "1.4.0" } lazy_static = { version = "1.4.0" }
log = { version = "0.4.16", features = ["kv_unstable_serde"] } log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = { version = "2.1.1" } ordered-float = { version = "2.1.1" }
@ -85,6 +85,7 @@ parking_lot = { version = "0.11.1" }
postage = { version = "0.5", features = ["futures-traits"] } postage = { version = "0.5", features = ["futures-traits"] }
rand = { version = "0.8.5" } rand = { version = "0.8.5" }
regex = { version = "1.5" } regex = { version = "1.5" }
schemars = { version = "0.8" }
serde = { version = "1.0", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
@ -93,6 +94,7 @@ smol = { version = "1.2" }
tempdir = { version = "0.3.7" } tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" } thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
unindent = { version = "0.1.7" } unindent = { version = "0.1.7" }
[patch.crates-io] [patch.crates-io]

View file

@ -192,7 +192,7 @@
} }
}, },
{ {
"context": "BufferSearchBar > Editor", "context": "BufferSearchBar",
"bindings": { "bindings": {
"escape": "buffer_search::Dismiss", "escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor", "tab": "buffer_search::FocusEditor",
@ -201,13 +201,13 @@
} }
}, },
{ {
"context": "ProjectSearchBar > Editor", "context": "ProjectSearchBar",
"bindings": { "bindings": {
"escape": "project_search::ToggleFocus" "escape": "project_search::ToggleFocus"
} }
}, },
{ {
"context": "ProjectSearchView > Editor", "context": "ProjectSearchView",
"bindings": { "bindings": {
"escape": "project_search::ToggleFocus" "escape": "project_search::ToggleFocus"
} }

View file

@ -11,6 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize", "ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize",
"cmd-d": "editor::DuplicateLine", "cmd-d": "editor::DuplicateLine",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown", "cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp", "cmd-pageup": "editor::MovePageUp",
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart", "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
@ -33,6 +34,7 @@
], ],
"shift-alt-up": "editor::MoveLineUp", "shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown", "shift-alt-down": "editor::MoveLineDown",
"cmd-alt-l": "editor::Format",
"cmd-[": "pane::GoBack", "cmd-[": "pane::GoBack",
"cmd-]": "pane::GoForward", "cmd-]": "pane::GoForward",
"alt-f7": "editor::FindAllReferences", "alt-f7": "editor::FindAllReferences",
@ -63,6 +65,7 @@
{ {
"context": "Workspace", "context": "Workspace",
"bindings": { "bindings": {
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle", "cmd-shift-a": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle", "cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftDock", "cmd-1": "workspace::ToggleLeftDock",

View file

@ -1,6 +1,15 @@
{ {
// The name of the Zed theme to use for the UI // The name of the Zed theme to use for the UI
"theme": "One Dark", "theme": "One Dark",
// The name of a base set of key bindings to use.
// This setting can take four values, each named after another
// text editor:
//
// 1. "VSCode"
// 2. "JetBrains"
// 3. "SublimeText"
// 4. "Atom"
"base_keymap": "VSCode",
// Features that can be globally enabled or disabled // Features that can be globally enabled or disabled
"features": { "features": {
// Show Copilot icon in status bar // Show Copilot icon in status bar
@ -43,6 +52,19 @@
// 3. Draw all invisible symbols: // 3. Draw all invisible symbols:
// "all" // "all"
"show_whitespaces": "selection", "show_whitespaces": "selection",
// Whether to show the scrollbar in the editor.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never"
"show_scrollbars": "auto",
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
// Whether to use language servers to provide code intelligence. // Whether to use language servers to provide code intelligence.
@ -106,11 +128,14 @@
}, },
// Automatically update Zed // Automatically update Zed
"auto_update": true, "auto_update": true,
// Git gutter behavior configuration. // Settings specific to the project panel
"project_panel": { "project_panel": {
"dock": "left", // Where to dock project panel. Can be 'left' or 'right'.
"default_width": 240 "dock": "left",
// Default width of the project panel.
"default_width": 240
}, },
// Git gutter behavior configuration.
"git": { "git": {
// Control whether the git gutter is shown. May take 2 values: // Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter // 1. Show the gutter
@ -155,6 +180,10 @@
"shell": "system", "shell": "system",
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'. // Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
"dock": "bottom", "dock": "bottom",
// Default width when the terminal is docked to the left or right.
"default_width": 640,
// Default height when the terminal is docked to the bottom.
"default_height": 320,
// What working directory to use when launching the terminal. // What working directory to use when launching the terminal.
// May take 4 values: // May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the // 1. Use the current file's project directory. Will Fallback to the

View file

@ -16,6 +16,11 @@ gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
futures.workspace = true futures.workspace = true
smallvec.workspace = true smallvec.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -9,7 +9,6 @@ use gpui::{
}; };
use language::{LanguageRegistry, LanguageServerBinaryStatus}; use language::{LanguageRegistry, LanguageServerBinaryStatus};
use project::{LanguageServerProgress, Project}; use project::{LanguageServerProgress, Project};
use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc}; use std::{cmp::Reverse, fmt::Write, sync::Arc};
use util::ResultExt; use util::ResultExt;
@ -325,12 +324,7 @@ impl View for ActivityIndicator {
} = self.content_to_render(cx); } = self.content_to_render(cx);
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| { let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &cx let theme = &theme::current(cx).workspace.status_bar.lsp_status;
.global::<Settings>()
.theme
.workspace
.status_bar
.lsp_status;
let style = if state.hovered() && on_click.is_some() { let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default) theme.hover.as_ref().unwrap_or(&theme.default)
} else { } else {

View file

@ -1,7 +1,7 @@
mod update_notification; mod update_notification;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@ -10,7 +10,7 @@ use gpui::{
use isahc::AsyncBody; use isahc::AsyncBody;
use serde::Deserialize; use serde::Deserialize;
use serde_derive::Serialize; use serde_derive::Serialize;
use settings::Settings; use settings::{Setting, SettingsStore};
use smol::{fs::File, io::AsyncReadExt, process::Command}; use smol::{fs::File, io::AsyncReadExt, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration}; use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification; use update_notification::UpdateNotification;
@ -58,18 +58,37 @@ impl Entity for AutoUpdater {
type Event = (); type Event = ();
} }
struct AutoUpdateSetting(bool);
impl Setting for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>;
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
_: &AppContext,
) -> Result<Self> {
Ok(Self(
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
))
}
}
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) { pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
settings::register::<AutoUpdateSetting>(cx);
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let auto_updater = cx.add_model(|cx| { let auto_updater = cx.add_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url); let updater = AutoUpdater::new(version, http_client, server_url);
let mut update_subscription = cx let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
.global::<Settings>() .0
.auto_update
.then(|| updater.start_polling(cx)); .then(|| updater.start_polling(cx));
cx.observe_global::<Settings, _>(move |updater, cx| { cx.observe_global::<SettingsStore, _>(move |updater, cx| {
if cx.global::<Settings>().auto_update { if settings::get::<AutoUpdateSetting>(cx).0 {
if update_subscription.is_none() { if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx)) update_subscription = Some(updater.start_polling(cx))
} }
@ -102,7 +121,7 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
{ {
format!("{server_url}/releases/preview/latest") format!("{server_url}/releases/preview/latest")
} else { } else {
format!("{server_url}/releases/latest") format!("{server_url}/releases/stable/latest")
}; };
cx.platform().open_url(&latest_release_url); cx.platform().open_url(&latest_release_url);
} }
@ -262,7 +281,7 @@ impl AutoUpdater {
let release_channel = cx let release_channel = cx
.has_global::<ReleaseChannel>() .has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name()); .then(|| cx.global::<ReleaseChannel>().display_name());
let telemetry = cx.global::<Settings>().telemetry().metrics(); let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
(installation_id, release_channel, telemetry) (installation_id, release_channel, telemetry)
}); });

View file

@ -5,7 +5,6 @@ use gpui::{
Element, Entity, View, ViewContext, Element, Entity, View, ViewContext,
}; };
use menu::Cancel; use menu::Cancel;
use settings::Settings;
use util::channel::ReleaseChannel; use util::channel::ReleaseChannel;
use workspace::notifications::Notification; use workspace::notifications::Notification;
@ -27,7 +26,7 @@ impl View for UpdateNotification {
} }
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let theme = &theme.update_notification; let theme = &theme.update_notification;
let app_name = cx.global::<ReleaseChannel>().display_name(); let app_name = cx.global::<ReleaseChannel>().display_name();

View file

@ -4,7 +4,6 @@ use gpui::{
}; };
use itertools::Itertools; use itertools::Itertools;
use search::ProjectSearchView; use search::ProjectSearchView;
use settings::Settings;
use workspace::{ use workspace::{
item::{ItemEvent, ItemHandle}, item::{ItemEvent, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemLocation, ToolbarItemView, Workspace,
@ -50,7 +49,7 @@ impl View for Breadcrumbs {
}; };
let not_editor = active_item.downcast::<editor::Editor>().is_none(); let not_editor = active_item.downcast::<editor::Editor>().is_none();
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let style = &theme.workspace.breadcrumbs; let style = &theme.workspace.breadcrumbs;
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {

View file

@ -19,6 +19,7 @@ dirs = "3.0"
ipc-channel = "0.16" ipc-channel = "0.16"
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
util = { path = "../util" }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9" core-foundation = "0.9"

View file

@ -1,6 +1,5 @@
pub use ipc_channel::ipc; pub use ipc_channel::ipc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IpcHandshake { pub struct IpcHandshake {
@ -10,7 +9,12 @@ pub struct IpcHandshake {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub enum CliRequest { pub enum CliRequest {
Open { paths: Vec<PathBuf>, wait: bool }, // The filed is named `path` for compatibility, but now CLI can request
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
//
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
Open { paths: Vec<String>, wait: bool },
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@ -20,3 +24,7 @@ pub enum CliResponse {
Stderr { message: String }, Stderr { message: String },
Exit { status: i32 }, Exit { status: i32 },
} }
/// When Zed started not as an *.app but as a binary (e.g. local development),
/// there's a possibility to tell it to behave "regularly".
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";

View file

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Context, Result};
use clap::Parser; use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake}; use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use core_foundation::{ use core_foundation::{
array::{CFArray, CFIndex}, array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8, string::kCFStringEncodingUTF8,
@ -16,16 +16,20 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
ptr, ptr,
}; };
use util::paths::PathLikeWithPosition;
#[derive(Parser)] #[derive(Parser)]
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))] #[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
struct Args { struct Args {
/// Wait for all of the given paths to be closed before exiting. /// Wait for all of the given paths to be opened/closed before exiting.
#[clap(short, long)] #[clap(short, long)]
wait: bool, wait: bool,
/// A sequence of space-separated paths that you want to open. /// A sequence of space-separated paths that you want to open.
#[clap()] ///
paths: Vec<PathBuf>, /// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
#[clap(value_parser = parse_path_with_position)]
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
/// Print Zed's version and the app path. /// Print Zed's version and the app path.
#[clap(short, long)] #[clap(short, long)]
version: bool, version: bool,
@ -34,6 +38,14 @@ struct Args {
bundle_path: Option<PathBuf>, bundle_path: Option<PathBuf>,
} }
fn parse_path_with_position(
argument_str: &str,
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
PathLikeWithPosition::parse_str(argument_str, |path_str| {
Ok(Path::new(path_str).to_path_buf())
})
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct InfoPlist { struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")] #[serde(rename = "CFBundleShortVersionString")]
@ -43,37 +55,37 @@ struct InfoPlist {
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
let bundle_path = if let Some(bundle_path) = args.bundle_path { let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
bundle_path.canonicalize()?
} else {
locate_bundle()?
};
if args.version { if args.version {
let plist_path = bundle_path.join("Contents/Info.plist"); println!("{}", bundle.zed_version_string());
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
println!(
"Zed {} {}",
plist.bundle_short_version_string,
bundle_path.to_string_lossy()
);
return Ok(()); return Ok(());
} }
for path in args.paths.iter() { for path in args
.paths_with_position
.iter()
.map(|path_with_position| &path_with_position.path_like)
{
if !path.exists() { if !path.exists() {
touch(path.as_path())?; touch(path.as_path())?;
} }
} }
let (tx, rx) = launch_app(bundle_path)?; let (tx, rx) = bundle.launch()?;
tx.send(CliRequest::Open { tx.send(CliRequest::Open {
paths: args paths: args
.paths .paths_with_position
.into_iter() .into_iter()
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error))) .map(|path_with_position| {
.collect::<Result<Vec<PathBuf>>>()?, let path_with_position = path_with_position.map_path_like(|path| {
fs::canonicalize(&path)
.with_context(|| format!("path {path:?} canonicalization"))
})?;
Ok(path_with_position.to_string(|path| path.display().to_string()))
})
.collect::<Result<_>>()?,
wait: args.wait, wait: args.wait,
})?; })?;
@ -89,6 +101,148 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
impl Bundle {
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
let bundle_path = if let Some(bundle_path) = args_bundle_path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Self::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist = plist::from_file::<_, InfoPlist>(&plist_path)
.with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
Ok(Self::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath {
executable: excutable,
..
} => excutable,
}
}
fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file =
fs::File::create(executable_parent.join("zed_dev.log"))
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
Ok((handshake.requests, handshake.responses))
}
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
}
fn touch(path: &Path) -> io::Result<()> { fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) { match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@ -106,38 +260,3 @@ fn locate_bundle() -> Result<PathBuf> {
} }
Ok(app_path) Ok(app_path)
} }
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
let url = format!("zed-cli://{server_name}");
let status = unsafe {
let app_url =
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
if status == 0 {
let (_, handshake) = server.accept()?;
Ok((handshake.requests, handshake.responses))
} else {
Err(anyhow!("cannot start {:?}", app_path))
}
}

View file

@ -31,6 +31,7 @@ log.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postage.workspace = true postage.workspace = true
rand.workspace = true rand.workspace = true
schemars.workspace = true
smol.workspace = true smol.workspace = true
thiserror.workspace = true thiserror.workspace = true
time.workspace = true time.workspace = true

View file

@ -15,19 +15,17 @@ use futures::{
TryStreamExt, TryStreamExt,
}; };
use gpui::{ use gpui::{
actions, actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle,
platform::AppVersion, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext,
serde_json::{self}, WeakViewHandle,
AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
ModelHandle, Task, View, ViewContext, WeakViewHandle,
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::RwLock; use parking_lot::RwLock;
use postage::watch; use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage}; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use serde::Deserialize; use schemars::JsonSchema;
use settings::Settings; use serde::{Deserialize, Serialize};
use std::{ use std::{
any::TypeId, any::TypeId,
collections::HashMap, collections::HashMap,
@ -72,25 +70,34 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [SignIn, SignOut]); actions!(client, [SignIn, SignOut]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) { pub fn init_settings(cx: &mut AppContext) {
settings::register::<TelemetrySettings>(cx);
}
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
init_settings(cx);
let client = Arc::downgrade(client);
cx.add_global_action({ cx.add_global_action({
let client = client.clone(); let client = client.clone();
move |_: &SignIn, cx| { move |_: &SignIn, cx| {
let client = client.clone(); if let Some(client) = client.upgrade() {
cx.spawn( cx.spawn(
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await }, |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
) )
.detach(); .detach();
}
} }
}); });
cx.add_global_action({ cx.add_global_action({
let client = client.clone(); let client = client.clone();
move |_: &SignOut, cx| { move |_: &SignOut, cx| {
let client = client.clone(); if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move { cx.spawn(|cx| async move {
client.disconnect(&cx); client.disconnect(&cx);
}) })
.detach(); .detach();
}
} }
}); });
} }
@ -326,6 +333,42 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
} }
} }
#[derive(Copy, Clone)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct TelemetrySettingsContent {
pub diagnostics: Option<bool>,
pub metrics: Option<bool>,
}
impl settings::Setting for TelemetrySettings {
const KEY: Option<&'static str> = Some("telemetry");
type FileContent = TelemetrySettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &AppContext,
) -> Result<Self> {
Ok(Self {
diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
default_value
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: user_values
.first()
.and_then(|v| v.metrics)
.unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
})
}
}
impl Client { impl Client {
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> { pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
Arc::new(Self { Arc::new(Self {
@ -447,9 +490,7 @@ impl Client {
})); }));
} }
Status::SignedOut | Status::UpgradeRequired => { Status::SignedOut | Status::UpgradeRequired => {
let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry()); cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx));
self.telemetry
.set_authenticated_user_info(None, false, telemetry_settings);
state._reconnect_task.take(); state._reconnect_task.take();
} }
_ => {} _ => {}
@ -740,7 +781,7 @@ impl Client {
self.telemetry().report_mixpanel_event( self.telemetry().report_mixpanel_event(
"read credentials from keychain", "read credentials from keychain",
Default::default(), Default::default(),
cx.global::<Settings>().telemetry(), *settings::get::<TelemetrySettings>(cx),
); );
}); });
} }
@ -1033,7 +1074,8 @@ impl Client {
let executor = cx.background(); let executor = cx.background();
let telemetry = self.telemetry.clone(); let telemetry = self.telemetry.clone();
let http = self.http.clone(); let http = self.http.clone();
let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
executor.clone().spawn(async move { executor.clone().spawn(async move {
// Generate a pair of asymmetric encryption keys. The public key will be used by the // Generate a pair of asymmetric encryption keys. The public key will be used by the
@ -1120,7 +1162,7 @@ impl Client {
telemetry.report_mixpanel_event( telemetry.report_mixpanel_event(
"authenticate with browser", "authenticate with browser",
Default::default(), Default::default(),
metrics_enabled, telemetry_settings,
); );
Ok(Credentials { Ok(Credentials {

View file

@ -1,4 +1,4 @@
use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
executor::Background, executor::Background,
@ -9,7 +9,6 @@ use lazy_static::lazy_static;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use settings::TelemetrySettings;
use std::{ use std::{
io::Write, io::Write,
mem, mem,
@ -86,6 +85,11 @@ pub enum ClickhouseEvent {
copilot_enabled: bool, copilot_enabled: bool,
copilot_enabled_for_language: bool, copilot_enabled_for_language: bool,
}, },
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
},
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@ -241,9 +245,9 @@ impl Telemetry {
self: &Arc<Self>, self: &Arc<Self>,
metrics_id: Option<String>, metrics_id: Option<String>,
is_staff: bool, is_staff: bool,
telemetry_settings: TelemetrySettings, cx: &AppContext,
) { ) {
if !telemetry_settings.metrics() { if !settings::get::<TelemetrySettings>(cx).metrics {
return; return;
} }
@ -285,7 +289,7 @@ impl Telemetry {
event: ClickhouseEvent, event: ClickhouseEvent,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
) { ) {
if !telemetry_settings.metrics() { if !telemetry_settings.metrics {
return; return;
} }
@ -321,7 +325,7 @@ impl Telemetry {
properties: Value, properties: Value,
telemetry_settings: TelemetrySettings, telemetry_settings: TelemetrySettings,
) { ) {
if !telemetry_settings.metrics() { if !telemetry_settings.metrics {
return; return;
} }

View file

@ -5,7 +5,6 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
use postage::{sink::Sink, watch}; use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse}; use rpc::proto::{RequestMessage, UsersResponse};
use settings::Settings;
use staff_mode::StaffMode; use staff_mode::StaffMode;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use util::http::HttpClient; use util::http::HttpClient;
@ -144,11 +143,13 @@ impl UserStore {
let fetch_metrics_id = let fetch_metrics_id =
client.request(proto::GetPrivateUserInfo {}).log_err(); client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) = futures::join!(fetch_user, fetch_metrics_id); let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
client.telemetry.set_authenticated_user_info( cx.read(|cx| {
info.as_ref().map(|info| info.metrics_id.clone()), client.telemetry.set_authenticated_user_info(
info.as_ref().map(|info| info.staff).unwrap_or(false), info.as_ref().map(|info| info.metrics_id.clone()),
cx.read(|cx| cx.global::<Settings>().telemetry()), info.as_ref().map(|info| info.staff).unwrap_or(false),
); cx,
)
});
cx.update(|cx| { cx.update(|cx| {
cx.update_default_global(|staff_mode: &mut StaffMode, _| { cx.update_default_global(|staff_mode: &mut StaffMode, _| {

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.12.0" version = "0.12.4"
publish = false publish = false
[[bin]] [[bin]]
@ -51,7 +51,7 @@ tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.17" tokio-tungstenite = "0.17"
tonic = "0.6" tonic = "0.6"
tower = "0.4" tower = "0.4"
toml = "0.5.8" toml.workspace = true
tracing = "0.1.34" tracing = "0.1.34"
tracing-log = "0.1.3" tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }

View file

@ -86,8 +86,8 @@ CREATE TABLE "worktree_repositories" (
"project_id" INTEGER NOT NULL, "project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL, "work_directory_id" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"branch" VARCHAR, "branch" VARCHAR,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL, "is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id), PRIMARY KEY(project_id, worktree_id, work_directory_id),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE, FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
@ -96,6 +96,23 @@ 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" ON "worktree_repositories" ("project_id");
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id"); CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL,
"work_directory_id" INTEGER NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INTEGER NOT NULL,
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
CREATE TABLE "worktree_diagnostic_summaries" ( CREATE TABLE "worktree_diagnostic_summaries" (
"project_id" INTEGER NOT NULL, "project_id" INTEGER NOT NULL,
"worktree_id" INTEGER NOT NULL, "worktree_id" INTEGER NOT NULL,

View file

@ -0,0 +1,15 @@
CREATE TABLE "worktree_repository_statuses" (
"project_id" INTEGER NOT NULL,
"worktree_id" INT8 NOT NULL,
"work_directory_id" INT8 NOT NULL,
"repo_path" VARCHAR NOT NULL,
"status" INT8 NOT NULL,
"scan_id" INT8 NOT NULL,
"is_deleted" BOOL NOT NULL,
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
);
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");

View file

@ -15,6 +15,7 @@ mod worktree;
mod worktree_diagnostic_summary; mod worktree_diagnostic_summary;
mod worktree_entry; mod worktree_entry;
mod worktree_repository; mod worktree_repository;
mod worktree_repository_statuses;
use crate::executor::Executor; use crate::executor::Executor;
use crate::{Error, Result}; use crate::{Error, Result};
@ -1513,6 +1514,7 @@ impl Database {
let mut db_entries = worktree_entry::Entity::find() let mut db_entries = worktree_entry::Entity::find()
.filter( .filter(
Condition::all() Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::WorktreeId.eq(worktree.id)) .add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter), .add(entry_filter),
) )
@ -1552,6 +1554,7 @@ impl Database {
let mut db_repositories = worktree_repository::Entity::find() let mut db_repositories = worktree_repository::Entity::find()
.filter( .filter(
Condition::all() Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id)) .add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter), .add(repository_entry_filter),
) )
@ -1568,6 +1571,54 @@ impl Database {
worktree.updated_repositories.push(proto::RepositoryEntry { worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64, work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch, branch: db_repository.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
});
}
}
}
// Repository Status Entries
for repository in worktree.updated_repositories.iter_mut() {
let repository_status_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository_statuses::Column::ScanId
.gt(rejoined_worktree.scan_id)
} else {
worktree_repository_statuses::Column::IsDeleted.eq(false)
};
let mut db_repository_statuses =
worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(
worktree_repository_statuses::Column::ProjectId
.eq(project.id),
)
.add(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree.id),
)
.add(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id),
)
.add(repository_status_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_repository_statuses.next().await {
let db_status_entry = db_status_entry?;
if db_status_entry.is_deleted {
repository
.removed_repo_paths
.push(db_status_entry.repo_path);
} else {
repository.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
}); });
} }
} }
@ -2395,6 +2446,68 @@ impl Database {
) )
.exec(&*tx) .exec(&*tx)
.await?; .await?;
for repository in update.updated_repositories.iter() {
if !repository.updated_statuses.is_empty() {
worktree_repository_statuses::Entity::insert_many(
repository.updated_statuses.iter().map(|status_entry| {
worktree_repository_statuses::ActiveModel {
project_id: ActiveValue::set(project_id),
worktree_id: ActiveValue::set(worktree_id),
work_directory_id: ActiveValue::set(
repository.work_directory_id as i64,
),
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
status: ActiveValue::set(status_entry.status as i64),
scan_id: ActiveValue::set(update.scan_id as i64),
is_deleted: ActiveValue::set(false),
}
}),
)
.on_conflict(
OnConflict::columns([
worktree_repository_statuses::Column::ProjectId,
worktree_repository_statuses::Column::WorktreeId,
worktree_repository_statuses::Column::WorkDirectoryId,
worktree_repository_statuses::Column::RepoPath,
])
.update_columns([
worktree_repository_statuses::Column::ScanId,
worktree_repository_statuses::Column::Status,
worktree_repository_statuses::Column::IsDeleted,
])
.to_owned(),
)
.exec(&*tx)
.await?;
}
if !repository.removed_repo_paths.is_empty() {
worktree_repository_statuses::Entity::update_many()
.filter(
worktree_repository_statuses::Column::ProjectId
.eq(project_id)
.and(
worktree_repository_statuses::Column::WorktreeId
.eq(worktree_id),
)
.and(
worktree_repository_statuses::Column::WorkDirectoryId
.eq(repository.work_directory_id as i64),
)
.and(worktree_repository_statuses::Column::RepoPath.is_in(
repository.removed_repo_paths.iter().map(String::as_str),
)),
)
.set(worktree_repository_statuses::ActiveModel {
is_deleted: ActiveValue::Set(true),
scan_id: ActiveValue::Set(update.scan_id as i64),
..Default::default()
})
.exec(&*tx)
.await?;
}
}
} }
if !update.removed_repositories.is_empty() { if !update.removed_repositories.is_empty() {
@ -2645,10 +2758,42 @@ impl Database {
if let Some(worktree) = if let Some(worktree) =
worktrees.get_mut(&(db_repository_entry.worktree_id as u64)) worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
{ {
worktree.repository_entries.push(proto::RepositoryEntry { worktree.repository_entries.insert(
work_directory_id: db_repository_entry.work_directory_id as u64, db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch, proto::RepositoryEntry {
}); work_directory_id: db_repository_entry.work_directory_id as u64,
branch: db_repository_entry.branch,
removed_repo_paths: Default::default(),
updated_statuses: Default::default(),
},
);
}
}
}
{
let mut db_status_entries = worktree_repository_statuses::Entity::find()
.filter(
Condition::all()
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
)
.stream(&*tx)
.await?;
while let Some(db_status_entry) = db_status_entries.next().await {
let db_status_entry = db_status_entry?;
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
{
if let Some(repository_entry) = worktree
.repository_entries
.get_mut(&(db_status_entry.work_directory_id as u64))
{
repository_entry.updated_statuses.push(proto::StatusEntry {
repo_path: db_status_entry.repo_path,
status: db_status_entry.status as i32,
});
}
} }
} }
} }
@ -3390,7 +3535,7 @@ pub struct Worktree {
pub root_name: String, pub root_name: String,
pub visible: bool, pub visible: bool,
pub entries: Vec<proto::Entry>, pub entries: Vec<proto::Entry>,
pub repository_entries: Vec<proto::RepositoryEntry>, pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>, pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
pub scan_id: u64, pub scan_id: u64,
pub completed_scan_id: u64, pub completed_scan_id: u64,

View file

@ -0,0 +1,23 @@
use super::ProjectId;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "worktree_repository_statuses")]
pub struct Model {
#[sea_orm(primary_key)]
pub project_id: ProjectId,
#[sea_orm(primary_key)]
pub worktree_id: i64,
#[sea_orm(primary_key)]
pub work_directory_id: i64,
#[sea_orm(primary_key)]
pub repo_path: String,
pub status: i64,
pub scan_id: i64,
pub is_deleted: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -51,7 +51,7 @@ use std::{
atomic::{AtomicBool, Ordering::SeqCst}, atomic::{AtomicBool, Ordering::SeqCst},
Arc, Arc,
}, },
time::Duration, time::{Duration, Instant},
}; };
use tokio::sync::{watch, Semaphore}; use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder; use tower::ServiceBuilder;
@ -397,10 +397,16 @@ impl Server {
"message received" "message received"
); );
}); });
let start_time = Instant::now();
let future = (handler)(*envelope, session); let future = (handler)(*envelope, session);
async move { async move {
if let Err(error) = future.await { let result = future.await;
tracing::error!(%error, "error handling message"); let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
match result {
Err(error) => {
tracing::error!(%error, ?duration_ms, "error handling message")
}
Ok(()) => tracing::info!(?duration_ms, "finished handling message"),
} }
} }
.instrument(span) .instrument(span)
@ -1385,7 +1391,7 @@ async fn join_project(
removed_entries: Default::default(), removed_entries: Default::default(),
scan_id: worktree.scan_id, scan_id: worktree.scan_id,
is_last_update: worktree.scan_id == worktree.completed_scan_id, is_last_update: worktree.scan_id == worktree.completed_scan_id,
updated_repositories: worktree.repository_entries, updated_repositories: worktree.repository_entries.into_values().collect(),
removed_repositories: Default::default(), removed_repositories: Default::default(),
}; };
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) { for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {

View file

@ -19,7 +19,7 @@ use gpui::{
use language::LanguageRegistry; use language::LanguageRegistry;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::{Project, WorktreeId}; use project::{Project, WorktreeId};
use settings::Settings; use settings::SettingsStore;
use std::{ use std::{
cell::{Ref, RefCell, RefMut}, cell::{Ref, RefCell, RefMut},
env, env,
@ -30,7 +30,6 @@ use std::{
Arc, Arc,
}, },
}; };
use theme::ThemeRegistry;
use util::http::FakeHttpClient; use util::http::FakeHttpClient;
use workspace::Workspace; use workspace::Workspace;
@ -102,7 +101,7 @@ impl TestServer {
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
cx.update(|cx| { cx.update(|cx| {
cx.set_global(Settings::test(cx)); cx.set_global(SettingsStore::test(cx));
}); });
let http = FakeHttpClient::with_404_response(); let http = FakeHttpClient::with_404_response();
@ -191,15 +190,18 @@ impl TestServer {
client: client.clone(), client: client.clone(),
user_store: user_store.clone(), user_store: user_store.clone(),
languages: Arc::new(LanguageRegistry::test()), languages: Arc::new(LanguageRegistry::test()),
themes: ThemeRegistry::new((), cx.font_cache()),
fs: fs.clone(), fs: fs.clone(),
build_window_options: |_, _, _| Default::default(), build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| unimplemented!(), initialize_workspace: |_, _, _, _| unimplemented!(),
background_actions: || &[], background_actions: || &[],
}); });
Project::init(&client);
cx.update(|cx| { cx.update(|cx| {
theme::init((), cx);
Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);
editor::init_settings(cx);
workspace::init(app_state.clone(), cx); workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx); call::init(client.clone(), user_store.clone(), cx);
}); });

View file

@ -10,7 +10,7 @@ use editor::{
ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions,
Undo, Undo,
}; };
use fs::{FakeFs, Fs as _, LineEnding, RemoveOptions}; use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions};
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{ use gpui::{
executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle, executor::Deterministic, geometry::vector::vec2f, test::EmptyView, AppContext, ModelHandle,
@ -18,6 +18,7 @@ use gpui::{
}; };
use indoc::indoc; use indoc::indoc;
use language::{ use language::{
language_settings::{AllLanguageSettings, Formatter},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope, LanguageConfig, OffsetRangeExt, Point, Rope,
}; };
@ -26,7 +27,7 @@ use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
use rand::prelude::*; use rand::prelude::*;
use serde_json::json; use serde_json::json;
use settings::{Formatter, Settings}; use settings::SettingsStore;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
env, future, mem, env, future, mem,
@ -1438,7 +1439,6 @@ async fn test_host_disconnect(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
cx_c: &mut TestAppContext, cx_c: &mut TestAppContext,
) { ) {
cx_b.update(editor::init);
deterministic.forbid_parking(); deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
@ -1448,6 +1448,8 @@ async fn test_host_disconnect(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await; .await;
cx_b.update(editor::init);
client_a client_a
.fs .fs
.insert_tree( .insert_tree(
@ -1545,7 +1547,6 @@ async fn test_project_reconnect(
cx_a: &mut TestAppContext, cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
cx_b.update(editor::init);
deterministic.forbid_parking(); deterministic.forbid_parking();
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
@ -1554,6 +1555,8 @@ async fn test_project_reconnect(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await; .await;
cx_b.update(editor::init);
client_a client_a
.fs .fs
.insert_tree( .insert_tree(
@ -2434,7 +2437,7 @@ async fn test_git_diff_base_change(
buffer_local_a.read_with(cx_a, |buffer, _| { buffer_local_a.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(1..2, "", "two\n")], &[(1..2, "", "two\n")],
@ -2454,7 +2457,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| { buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(1..2, "", "two\n")], &[(1..2, "", "two\n")],
@ -2478,7 +2481,7 @@ async fn test_git_diff_base_change(
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(2..3, "", "three\n")], &[(2..3, "", "three\n")],
@ -2489,7 +2492,7 @@ async fn test_git_diff_base_change(
buffer_remote_a.read_with(cx_b, |buffer, _| { buffer_remote_a.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(2..3, "", "three\n")], &[(2..3, "", "three\n")],
@ -2532,7 +2535,7 @@ async fn test_git_diff_base_change(
buffer_local_b.read_with(cx_a, |buffer, _| { buffer_local_b.read_with(cx_a, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(1..2, "", "two\n")], &[(1..2, "", "two\n")],
@ -2552,7 +2555,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| { buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(1..2, "", "two\n")], &[(1..2, "", "two\n")],
@ -2580,12 +2583,12 @@ async fn test_git_diff_base_change(
"{:?}", "{:?}",
buffer buffer
.snapshot() .snapshot()
.git_diff_hunks_in_row_range(0..4, false) .git_diff_hunks_in_row_range(0..4)
.collect::<Vec<_>>() .collect::<Vec<_>>()
); );
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(2..3, "", "three\n")], &[(2..3, "", "three\n")],
@ -2596,7 +2599,7 @@ async fn test_git_diff_base_change(
buffer_remote_b.read_with(cx_b, |buffer, _| { buffer_remote_b.read_with(cx_b, |buffer, _| {
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref())); assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
git::diff::assert_hunks( git::diff::assert_hunks(
buffer.snapshot().git_diff_hunks_in_row_range(0..4, false), buffer.snapshot().git_diff_hunks_in_row_range(0..4),
&buffer, &buffer,
&diff_base, &diff_base,
&[(2..3, "", "three\n")], &[(2..3, "", "three\n")],
@ -2690,6 +2693,154 @@ async fn test_git_branch_name(
}); });
} }
#[gpui::test]
async fn test_git_status_sync(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
cx_c: &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;
let client_c = server.create_client(cx_c, "user_c").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs
.insert_tree(
"/dir",
json!({
".git": {},
"a.txt": "a",
"b.txt": "b",
}),
)
.await;
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
client_a
.fs
.as_fake()
.set_status_for_repo(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Added),
(&Path::new(B_TXT), GitFileStatus::Added),
],
)
.await;
let (project_local, _worktree_id) = client_a.build_local_project("/dir", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| {
call.share_project(project_local.clone(), cx)
})
.await
.unwrap();
let project_remote = client_b.build_remote_project(project_id, cx_b).await;
// Wait for it to catch up to the new status
deterministic.run_until_parked();
#[track_caller]
fn assert_status(
file: &impl AsRef<Path>,
status: Option<GitFileStatus>,
project: &Project,
cx: &AppContext,
) {
let file = file.as_ref();
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
let worktree = worktrees[0].clone();
let snapshot = worktree.read(cx).snapshot();
let root_entry = snapshot.root_git_entry().unwrap();
assert_eq!(root_entry.status_for_file(&snapshot, file), status);
}
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(&Path::new(A_TXT), Some(GitFileStatus::Added), project, cx);
assert_status(&Path::new(B_TXT), Some(GitFileStatus::Added), project, cx);
});
client_a
.fs
.as_fake()
.set_status_for_repo(
Path::new("/dir/.git"),
&[
(&Path::new(A_TXT), GitFileStatus::Modified),
(&Path::new(B_TXT), GitFileStatus::Modified),
],
)
.await;
// Wait for buffer_local_a to receive it
deterministic.run_until_parked();
// Smoke test status reading
project_local.read_with(cx_a, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
project_remote.read_with(cx_b, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
// And synchronization while joining
let project_remote_c = client_c.build_remote_project(project_id, cx_c).await;
deterministic.run_until_parked();
project_remote_c.read_with(cx_c, |project, cx| {
assert_status(
&Path::new(A_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
assert_status(
&Path::new(B_TXT),
Some(GitFileStatus::Modified),
project,
cx,
);
});
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_fs_operations( async fn test_fs_operations(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
@ -4219,10 +4370,12 @@ async fn test_formatting_buffer(
// Ensure buffer can be formatted using an external command. Notice how the // Ensure buffer can be formatted using an external command. Notice how the
// host's configuration is honored as opposed to using the guest's settings. // host's configuration is honored as opposed to using the guest's settings.
cx_a.update(|cx| { cx_a.update(|cx| {
cx.update_global(|settings: &mut Settings, _| { cx.update_global(|store: &mut SettingsStore, cx| {
settings.editor_defaults.formatter = Some(Formatter::External { store.update_user_settings::<AllLanguageSettings>(cx, |file| {
command: "awk".to_string(), file.defaults.formatter = Some(Formatter::External {
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()], command: "awk".into(),
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
});
}); });
}); });
}); });
@ -4989,7 +5142,6 @@ async fn test_collaborating_with_code_actions(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -4998,6 +5150,8 @@ async fn test_collaborating_with_code_actions(
.await; .await;
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
// Set up a fake language server. // Set up a fake language server.
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -5202,7 +5356,6 @@ async fn test_collaborating_with_renames(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -5211,6 +5364,8 @@ async fn test_collaborating_with_renames(
.await; .await;
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
// Set up a fake language server. // Set up a fake language server.
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -5392,8 +5547,6 @@ async fn test_language_server_statuses(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -5402,6 +5555,8 @@ async fn test_language_server_statuses(
.await; .await;
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
// Set up a fake language server. // Set up a fake language server.
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -6109,8 +6264,6 @@ async fn test_basic_following(
cx_d: &mut TestAppContext, cx_d: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
@ -6128,6 +6281,9 @@ async fn test_basic_following(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
client_a client_a
.fs .fs
.insert_tree( .insert_tree(
@ -6706,9 +6862,6 @@ async fn test_following_tab_order(
cx_a: &mut TestAppContext, cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -6718,6 +6871,9 @@ async fn test_following_tab_order(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
client_a client_a
.fs .fs
.insert_tree( .insert_tree(
@ -6828,9 +6984,6 @@ async fn test_peers_following_each_other(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await; let client_b = server.create_client(cx_b, "user_b").await;
@ -6840,6 +6993,9 @@ async fn test_peers_following_each_other(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
// Client A shares a project. // Client A shares a project.
client_a client_a
.fs .fs
@ -6999,8 +7155,6 @@ async fn test_auto_unfollowing(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
// 2 clients connect to a server. // 2 clients connect to a server.
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
@ -7012,6 +7166,9 @@ async fn test_auto_unfollowing(
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
let active_call_b = cx_b.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
// Client A shares a project. // Client A shares a project.
client_a client_a
.fs .fs
@ -7166,8 +7323,6 @@ async fn test_peers_simultaneously_following_each_other(
cx_b: &mut TestAppContext, cx_b: &mut TestAppContext,
) { ) {
deterministic.forbid_parking(); deterministic.forbid_parking();
cx_a.update(editor::init);
cx_b.update(editor::init);
let mut server = TestServer::start(&deterministic).await; let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await; let client_a = server.create_client(cx_a, "user_a").await;
@ -7177,6 +7332,9 @@ async fn test_peers_simultaneously_following_each_other(
.await; .await;
let active_call_a = cx_a.read(ActiveCall::global); let active_call_a = cx_a.read(ActiveCall::global);
cx_a.update(editor::init);
cx_b.update(editor::init);
client_a.fs.insert_tree("/a", json!({})).await; client_a.fs.insert_tree("/a", json!({})).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let workspace_a = client_a.build_workspace(&project_a, cx_a); let workspace_a = client_a.build_workspace(&project_a, cx_a);

View file

@ -8,19 +8,20 @@ use call::ActiveCall;
use client::RECEIVE_TIMEOUT; use client::RECEIVE_TIMEOUT;
use collections::BTreeMap; use collections::BTreeMap;
use editor::Bias; use editor::Bias;
use fs::{FakeFs, Fs as _}; use fs::{repository::GitFileStatus, FakeFs, Fs as _};
use futures::StreamExt as _; use futures::StreamExt as _;
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext}; use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16}; use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
use lsp::FakeLanguageServer; use lsp::FakeLanguageServer;
use parking_lot::Mutex; use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use project::{search::SearchQuery, Project, ProjectPath}; use project::{search::SearchQuery, Project, ProjectPath};
use rand::{ use rand::{
distributions::{Alphanumeric, DistString}, distributions::{Alphanumeric, DistString},
prelude::*, prelude::*,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::SettingsStore;
use std::{ use std::{
env, env,
ops::Range, ops::Range,
@ -148,8 +149,9 @@ async fn test_random_collaboration(
for (client, mut cx) in clients { for (client, mut cx) in clients {
cx.update(|cx| { cx.update(|cx| {
let store = cx.remove_global::<SettingsStore>();
cx.clear_globals(); cx.clear_globals();
cx.set_global(Settings::test(cx)); cx.set_global(store);
drop(client); drop(client);
}); });
} }
@ -763,53 +765,85 @@ async fn apply_client_operation(
} }
} }
ClientOperation::WriteGitIndex { ClientOperation::GitOperation { operation } => match operation {
repo_path, GitOperation::WriteGitIndex {
contents,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git index for repo {:?}: {:?}",
client.username,
repo_path, repo_path,
contents contents,
); } => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
let dot_git_dir = repo_path.join(".git"); log::info!(
let contents = contents "{}: writing git index for repo {:?}: {:?}",
.iter() client.username,
.map(|(path, contents)| (path.as_path(), contents.clone())) repo_path,
.collect::<Vec<_>>(); contents
if client.fs.metadata(&dot_git_dir).await?.is_none() { );
client.fs.create_dir(&dot_git_dir).await?;
let dot_git_dir = repo_path.join(".git");
let contents = contents
.iter()
.map(|(path, contents)| (path.as_path(), contents.clone()))
.collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
} }
client.fs.set_index_for_repo(&dot_git_dir, &contents).await; GitOperation::WriteGitBranch {
}
ClientOperation::WriteGitBranch {
repo_path,
new_branch,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git branch for repo {:?}: {:?}",
client.username,
repo_path, repo_path,
new_branch new_branch,
); } => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
let dot_git_dir = repo_path.join(".git"); log::info!(
if client.fs.metadata(&dot_git_dir).await?.is_none() { "{}: writing git branch for repo {:?}: {:?}",
client.fs.create_dir(&dot_git_dir).await?; client.username,
repo_path,
new_branch
);
let dot_git_dir = repo_path.join(".git");
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
} }
client.fs.set_branch_name(&dot_git_dir, new_branch).await; GitOperation::WriteGitStatuses {
} repo_path,
statuses,
} => {
if !client.fs.directories().contains(&repo_path) {
return Err(TestError::Inapplicable);
}
log::info!(
"{}: writing git statuses for repo {:?}: {:?}",
client.username,
repo_path,
statuses
);
let dot_git_dir = repo_path.join(".git");
let statuses = statuses
.iter()
.map(|(path, val)| (path.as_path(), val.clone()))
.collect::<Vec<_>>();
if client.fs.metadata(&dot_git_dir).await?.is_none() {
client.fs.create_dir(&dot_git_dir).await?;
}
client
.fs
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
.await;
}
},
} }
Ok(()) Ok(())
} }
@ -1178,6 +1212,13 @@ enum ClientOperation {
is_dir: bool, is_dir: bool,
content: String, content: String,
}, },
GitOperation {
operation: GitOperation,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum GitOperation {
WriteGitIndex { WriteGitIndex {
repo_path: PathBuf, repo_path: PathBuf,
contents: Vec<(PathBuf, String)>, contents: Vec<(PathBuf, String)>,
@ -1186,6 +1227,10 @@ enum ClientOperation {
repo_path: PathBuf, repo_path: PathBuf,
new_branch: Option<String>, new_branch: Option<String>,
}, },
WriteGitStatuses {
repo_path: PathBuf,
statuses: Vec<(PathBuf, GitFileStatus)>,
},
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -1698,57 +1743,10 @@ impl TestPlan {
} }
} }
// Update a git index // Update a git related action
91..=93 => { 91..=95 => {
let repo_path = client break ClientOperation::GitOperation {
.fs operation: self.generate_git_operation(client),
.directories()
.into_iter()
.choose(&mut self.rng)
.unwrap()
.clone();
let mut file_paths = client
.fs
.files()
.into_iter()
.filter(|path| path.starts_with(&repo_path))
.collect::<Vec<_>>();
let count = self.rng.gen_range(0..=file_paths.len());
file_paths.shuffle(&mut self.rng);
file_paths.truncate(count);
let mut contents = Vec::new();
for abs_child_file_path in &file_paths {
let child_file_path = abs_child_file_path
.strip_prefix(&repo_path)
.unwrap()
.to_path_buf();
let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
contents.push((child_file_path, new_base));
}
break ClientOperation::WriteGitIndex {
repo_path,
contents,
};
}
// Update a git branch
94..=95 => {
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
let new_branch = (self.rng.gen_range(0..10) > 3)
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
break ClientOperation::WriteGitBranch {
repo_path,
new_branch,
}; };
} }
@ -1786,6 +1784,86 @@ impl TestPlan {
}) })
} }
fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
fn generate_file_paths(
repo_path: &Path,
rng: &mut StdRng,
client: &TestClient,
) -> Vec<PathBuf> {
let mut paths = client
.fs
.files()
.into_iter()
.filter(|path| path.starts_with(repo_path))
.collect::<Vec<_>>();
let count = rng.gen_range(0..=paths.len());
paths.shuffle(rng);
paths.truncate(count);
paths
.iter()
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
.collect::<Vec<_>>()
}
let repo_path = client
.fs
.directories()
.choose(&mut self.rng)
.unwrap()
.clone();
match self.rng.gen_range(0..100_u32) {
0..=25 => {
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
let contents = file_paths
.into_iter()
.map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
.collect();
GitOperation::WriteGitIndex {
repo_path,
contents,
}
}
26..=63 => {
let new_branch = (self.rng.gen_range(0..10) > 3)
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
GitOperation::WriteGitBranch {
repo_path,
new_branch,
}
}
64..=100 => {
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
let statuses = file_paths
.into_iter()
.map(|paths| {
(
paths,
match self.rng.gen_range(0..3_u32) {
0 => GitFileStatus::Added,
1 => GitFileStatus::Modified,
2 => GitFileStatus::Conflict,
_ => unreachable!(),
},
)
})
.collect::<Vec<_>>();
GitOperation::WriteGitStatuses {
repo_path,
statuses,
}
}
_ => unreachable!(),
}
}
fn next_root_dir_name(&mut self, user_id: UserId) -> String { fn next_root_dir_name(&mut self, user_id: UserId) -> String {
let user_ix = self let user_ix = self
.users .users

View file

@ -18,7 +18,6 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use project::Project; use project::Project;
use settings::Settings;
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme}; use theme::{AvatarStyle, Theme};
use util::ResultExt; use util::ResultExt;
@ -70,7 +69,7 @@ impl View for CollabTitlebarItem {
}; };
let project = self.project.read(cx); let project = self.project.read(cx);
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let mut left_container = Flex::row(); let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center(); let mut right_container = Flex::row().align_children_center();
@ -298,7 +297,7 @@ impl CollabTitlebarItem {
} }
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) { pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone(); let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone(); let item_style = theme.context_menu.item.disabled_style().clone();
self.user_menu.update(cx, |user_menu, cx| { self.user_menu.update(cx, |user_menu, cx| {
@ -866,7 +865,7 @@ impl CollabTitlebarItem {
) -> Option<AnyElement<Self>> { ) -> Option<AnyElement<Self>> {
enum ConnectionStatusButton {} enum ConnectionStatusButton {}
let theme = &cx.global::<Settings>().theme.clone(); let theme = &theme::current(cx).clone();
match status { match status {
client::Status::ConnectionError client::Status::ConnectionError
| client::Status::ConnectionLost | client::Status::ConnectionLost

View file

@ -1,7 +1,6 @@
use client::{ContactRequestStatus, User, UserStore}; use client::{ContactRequestStatus, User, UserStore};
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext}; use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::TryFutureExt; use util::TryFutureExt;
@ -98,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate {
selected: bool, selected: bool,
cx: &gpui::AppContext, cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let theme = &cx.global::<Settings>().theme; let theme = &theme::current(cx);
let user = &self.potential_contacts[ix]; let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user); let request_status = self.user_store.read(cx).contact_request_status(user);

View file

@ -14,7 +14,6 @@ use gpui::{
use menu::{Confirm, SelectNext, SelectPrev}; use menu::{Confirm, SelectNext, SelectPrev};
use project::Project; use project::Project;
use serde::Deserialize; use serde::Deserialize;
use settings::Settings;
use std::{mem, sync::Arc}; use std::{mem, sync::Arc};
use theme::IconButton; use theme::IconButton;
use workspace::Workspace; use workspace::Workspace;
@ -192,7 +191,7 @@ impl ContactList {
.detach(); .detach();
let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| { let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let is_selected = this.selection == Some(ix); let is_selected = this.selection == Some(ix);
let current_project_id = this.project.read(cx).remote_id(); let current_project_id = this.project.read(cx).remote_id();
@ -1313,7 +1312,7 @@ impl View for ContactList {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum AddContact {} enum AddContact {}
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
Flex::column() Flex::column()
.with_child( .with_child(

View file

@ -9,7 +9,6 @@ use gpui::{
}; };
use picker::PickerEvent; use picker::PickerEvent;
use project::Project; use project::Project;
use settings::Settings;
use workspace::Workspace; use workspace::Workspace;
actions!(contacts_popover, [ToggleContactFinder]); actions!(contacts_popover, [ToggleContactFinder]);
@ -108,7 +107,7 @@ impl View for ContactsPopover {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let child = match &self.child { let child = match &self.child {
Child::ContactList(child) => ChildView::new(child, cx), Child::ContactList(child) => ChildView::new(child, cx),
Child::ContactFinder(child) => ChildView::new(child, cx), Child::ContactFinder(child) => ChildView::new(child, cx),

View file

@ -9,7 +9,6 @@ use gpui::{
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AnyElement, AppContext, Entity, View, ViewContext, AnyElement, AppContext, Entity, View, ViewContext,
}; };
use settings::Settings;
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
@ -26,7 +25,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(incoming_call) = incoming_call { if let Some(incoming_call) = incoming_call {
const PADDING: f32 = 16.; const PADDING: f32 = 16.;
let window_size = cx.read(|cx| { let window_size = cx.read(|cx| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification; let theme = &theme::current(cx).incoming_call_notification;
vec2f(theme.window_width, theme.window_height) vec2f(theme.window_width, theme.window_height)
}); });
@ -107,7 +106,7 @@ impl IncomingCallNotification {
} }
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.incoming_call_notification; let theme = &theme::current(cx).incoming_call_notification;
let default_project = proto::ParticipantProject::default(); let default_project = proto::ParticipantProject::default();
let initial_project = self let initial_project = self
.call .call
@ -171,10 +170,11 @@ impl IncomingCallNotification {
enum Accept {} enum Accept {}
enum Decline {} enum Decline {}
let theme = theme::current(cx);
Flex::column() Flex::column()
.with_child( .with_child(
MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| { MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification; let theme = &theme.incoming_call_notification;
Label::new("Accept", theme.accept_button.text.clone()) Label::new("Accept", theme.accept_button.text.clone())
.aligned() .aligned()
.contained() .contained()
@ -187,8 +187,8 @@ impl IncomingCallNotification {
.flex(1., true), .flex(1., true),
) )
.with_child( .with_child(
MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| { MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
let theme = &cx.global::<Settings>().theme.incoming_call_notification; let theme = &theme.incoming_call_notification;
Label::new("Decline", theme.decline_button.text.clone()) Label::new("Decline", theme.decline_button.text.clone())
.aligned() .aligned()
.contained() .contained()
@ -201,12 +201,7 @@ impl IncomingCallNotification {
.flex(1., true), .flex(1., true),
) )
.constrained() .constrained()
.with_width( .with_width(theme.incoming_call_notification.button_width)
cx.global::<Settings>()
.theme
.incoming_call_notification
.button_width,
)
.into_any() .into_any()
} }
} }
@ -221,12 +216,7 @@ impl View for IncomingCallNotification {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let background = cx let background = theme::current(cx).incoming_call_notification.background;
.global::<Settings>()
.theme
.incoming_call_notification
.background;
Flex::row() Flex::row()
.with_child(self.render_caller(cx)) .with_child(self.render_caller(cx))
.with_child(self.render_buttons(cx)) .with_child(self.render_buttons(cx))

View file

@ -4,7 +4,6 @@ use gpui::{
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyElement, Element, View, ViewContext, AnyElement, Element, View, ViewContext,
}; };
use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
enum Dismiss {} enum Dismiss {}
@ -22,7 +21,7 @@ where
F: 'static + Fn(&mut V, &mut ViewContext<V>), F: 'static + Fn(&mut V, &mut ViewContext<V>),
V: View, V: View,
{ {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
let theme = &theme.contact_notification; let theme = &theme.contact_notification;
Flex::column() Flex::column()

View file

@ -7,7 +7,6 @@ use gpui::{
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
AppContext, Entity, View, ViewContext, AppContext, Entity, View, ViewContext,
}; };
use settings::Settings;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use workspace::AppState; use workspace::AppState;
@ -22,7 +21,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
worktree_root_names, worktree_root_names,
} => { } => {
const PADDING: f32 = 16.; const PADDING: f32 = 16.;
let theme = &cx.global::<Settings>().theme.project_shared_notification; let theme = &theme::current(cx).project_shared_notification;
let window_size = vec2f(theme.window_width, theme.window_height); let window_size = vec2f(theme.window_width, theme.window_height);
for screen in cx.platform().screens() { for screen in cx.platform().screens() {
@ -110,7 +109,7 @@ impl ProjectSharedNotification {
} }
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.project_shared_notification; let theme = &theme::current(cx).project_shared_notification;
Flex::row() Flex::row()
.with_children(self.owner.avatar.clone().map(|avatar| { .with_children(self.owner.avatar.clone().map(|avatar| {
Image::from_data(avatar) Image::from_data(avatar)
@ -168,10 +167,11 @@ impl ProjectSharedNotification {
enum Open {} enum Open {}
enum Dismiss {} enum Dismiss {}
let theme = theme::current(cx);
Flex::column() Flex::column()
.with_child( .with_child(
MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| { MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
let theme = &cx.global::<Settings>().theme.project_shared_notification; let theme = &theme.project_shared_notification;
Label::new("Open", theme.open_button.text.clone()) Label::new("Open", theme.open_button.text.clone())
.aligned() .aligned()
.contained() .contained()
@ -182,8 +182,8 @@ impl ProjectSharedNotification {
.flex(1., true), .flex(1., true),
) )
.with_child( .with_child(
MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, cx| { MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
let theme = &cx.global::<Settings>().theme.project_shared_notification; let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone()) Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned() .aligned()
.contained() .contained()
@ -196,12 +196,7 @@ impl ProjectSharedNotification {
.flex(1., true), .flex(1., true),
) )
.constrained() .constrained()
.with_width( .with_width(theme.project_shared_notification.button_width)
cx.global::<Settings>()
.theme
.project_shared_notification
.button_width,
)
.into_any() .into_any()
} }
} }
@ -216,11 +211,7 @@ impl View for ProjectSharedNotification {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
let background = cx let background = theme::current(cx).project_shared_notification.background;
.global::<Settings>()
.theme
.project_shared_notification
.background;
Flex::row() Flex::row()
.with_child(self.render_owner(cx)) .with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx)) .with_child(self.render_buttons(cx))

View file

@ -6,7 +6,7 @@ use gpui::{
platform::{Appearance, MouseButton}, platform::{Appearance, MouseButton},
AnyElement, AppContext, Element, Entity, View, ViewContext, AnyElement, AppContext, Element, Entity, View, ViewContext,
}; };
use settings::Settings; use workspace::WorkspaceSettings;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
@ -15,7 +15,9 @@ pub fn init(cx: &mut AppContext) {
cx.observe(&active_call, move |call, cx| { cx.observe(&active_call, move |call, cx| {
if let Some(room) = call.read(cx).room() { if let Some(room) = call.read(cx).room() {
if room.read(cx).is_screen_sharing() { if room.read(cx).is_screen_sharing() {
if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon { if status_indicator.is_none()
&& settings::get::<WorkspaceSettings>(cx).show_call_status_icon
{
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator)); status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
} }
} else if let Some((window_id, _)) = status_indicator.take() { } else if let Some((window_id, _)) = status_indicator.take() {

View file

@ -23,6 +23,7 @@ workspace = { path = "../workspace" }
[dev-dependencies] [dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
serde_json.workspace = true serde_json.workspace = true
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -5,7 +5,6 @@ use gpui::{
ViewContext, ViewContext,
}; };
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings;
use std::cmp; use std::cmp;
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -185,8 +184,7 @@ impl PickerDelegate for CommandPaletteDelegate {
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let mat = &self.matches[ix]; let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id]; let command = &self.actions[mat.candidate_id];
let settings = cx.global::<Settings>(); let theme = theme::current(cx);
let theme = &settings.theme;
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.style_for(mouse_state, selected);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected); let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing; let keystroke_spacing = theme.command_palette.keystroke_spacing;
@ -294,14 +292,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) { async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking(); let app_state = init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
});
let project = Project::test(app_state.fs.clone(), [], cx).await; let project = Project::test(app_state.fs.clone(), [], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@ -369,4 +360,16 @@ mod tests {
assert!(palette.delegate().matches.is_empty()) assert!(palette.delegate().matches.is_empty())
}); });
} }
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let app_state = AppState::test(cx);
theme::init((), cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
init(cx);
app_state
})
}
} }

View file

@ -8,7 +8,6 @@ use gpui::{
View, ViewContext, View, ViewContext,
}; };
use menu::*; use menu::*;
use settings::Settings;
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration}; use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
@ -323,7 +322,7 @@ impl ContextMenu {
} }
fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> { fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
let style = cx.global::<Settings>().theme.context_menu.clone(); let style = theme::current(cx).context_menu.clone();
Flex::row() Flex::row()
.with_child( .with_child(
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| { Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
@ -403,7 +402,7 @@ impl ContextMenu {
enum Menu {} enum Menu {}
enum MenuItem {} enum MenuItem {}
let style = cx.global::<Settings>().theme.context_menu.clone(); let style = theme::current(cx).context_menu.clone();
MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| { MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
Flex::column() Flex::column()

View file

@ -10,6 +10,7 @@ use gpui::{
actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
}; };
use language::{ use language::{
language_settings::{all_language_settings, language_settings},
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
ToPointUtf16, ToPointUtf16,
}; };
@ -17,7 +18,7 @@ use log::{debug, error};
use lsp::{LanguageServer, LanguageServerId}; use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use request::{LogMessage, StatusNotification}; use request::{LogMessage, StatusNotification};
use settings::Settings; use settings::SettingsStore;
use smol::{fs, io::BufReader, stream::StreamExt}; use smol::{fs, io::BufReader, stream::StreamExt};
use std::{ use std::{
ffi::OsString, ffi::OsString,
@ -258,7 +259,7 @@ impl RegisteredBuffer {
#[derive(Debug)] #[derive(Debug)]
pub struct Completion { pub struct Completion {
uuid: String, pub uuid: String,
pub range: Range<Anchor>, pub range: Range<Anchor>,
pub text: String, pub text: String,
} }
@ -302,56 +303,34 @@ impl Copilot {
node_runtime: Arc<NodeRuntime>, node_runtime: Arc<NodeRuntime>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
cx.observe_global::<Settings, _>({ let mut this = Self {
let http = http.clone(); http,
let node_runtime = node_runtime.clone(); node_runtime,
move |this, cx| { server: CopilotServer::Disabled,
if cx.global::<Settings>().features.copilot { buffers: Default::default(),
if matches!(this.server, CopilotServer::Disabled) { };
let start_task = cx this.enable_or_disable_copilot(cx);
.spawn({ cx.observe_global::<SettingsStore, _>(move |this, cx| this.enable_or_disable_copilot(cx))
let http = http.clone(); .detach();
let node_runtime = node_runtime.clone(); this
move |this, cx| { }
Self::start_language_server(http, node_runtime, this, cx)
}
})
.shared();
this.server = CopilotServer::Starting { task: start_task };
cx.notify();
}
} else {
this.server = CopilotServer::Disabled;
cx.notify();
}
}
})
.detach();
if cx.global::<Settings>().features.copilot { fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
let start_task = cx let http = self.http.clone();
.spawn({ let node_runtime = self.node_runtime.clone();
let http = http.clone(); if all_language_settings(cx).copilot_enabled(None, None) {
let node_runtime = node_runtime.clone(); if matches!(self.server, CopilotServer::Disabled) {
move |this, cx| async { let start_task = cx
Self::start_language_server(http, node_runtime, this, cx).await .spawn({
} move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
}) })
.shared(); .shared();
self.server = CopilotServer::Starting { task: start_task };
Self { cx.notify();
http,
node_runtime,
server: CopilotServer::Starting { task: start_task },
buffers: Default::default(),
} }
} else { } else {
Self { self.server = CopilotServer::Disabled;
http, cx.notify();
node_runtime,
server: CopilotServer::Disabled,
buffers: Default::default(),
}
} }
} }
@ -805,13 +784,13 @@ impl Copilot {
let snapshot = registered_buffer.report_changes(buffer, cx); let snapshot = registered_buffer.report_changes(buffer, cx);
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone(); let uri = registered_buffer.uri.clone();
let settings = cx.global::<Settings>();
let position = position.to_point_utf16(buffer); let position = position.to_point_utf16(buffer);
let language = buffer.language_at(position); let settings = language_settings(
let language_name = language.map(|language| language.name()); buffer.language_at(position).map(|l| l.name()).as_deref(),
let language_name = language_name.as_deref(); cx,
let tab_size = settings.tab_size(language_name); );
let hard_tabs = settings.hard_tabs(language_name); let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs;
let relative_path = buffer let relative_path = buffer
.file() .file()
.map(|file| file.path().to_path_buf()) .map(|file| file.path().to_path_buf())

View file

@ -6,7 +6,6 @@ use gpui::{
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
ViewHandle, ViewHandle,
}; };
use settings::Settings;
use theme::ui::modal; use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
@ -68,7 +67,7 @@ fn create_copilot_auth_window(
cx: &mut AppContext, cx: &mut AppContext,
status: &Status, status: &Status,
) -> ViewHandle<CopilotCodeVerification> { ) -> ViewHandle<CopilotCodeVerification> {
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions(); let window_size = theme::current(cx).copilot.modal.dimensions();
let window_options = WindowOptions { let window_options = WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
titlebar: None, titlebar: None,
@ -339,7 +338,7 @@ impl View for CopilotCodeVerification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
enum ConnectModal {} enum ConnectModal {}
let style = cx.global::<Settings>().theme.clone(); let style = theme::current(cx).clone();
modal::<ConnectModal, _, _, _, _>( modal::<ConnectModal, _, _, _, _>(
"Connect Copilot to Zed", "Connect Copilot to Zed",

View file

@ -12,8 +12,10 @@ doctest = false
assets = { path = "../assets" } assets = { path = "../assets" }
copilot = { path = "../copilot" } copilot = { path = "../copilot" }
editor = { path = "../editor" } editor = { path = "../editor" }
fs = { path = "../fs" }
context_menu = { path = "../context_menu" } context_menu = { path = "../context_menu" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
@ -21,3 +23,6 @@ workspace = { path = "../workspace" }
anyhow.workspace = true anyhow.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true futures.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -2,13 +2,15 @@ use anyhow::Result;
use context_menu::{ContextMenu, ContextMenuItem}; use context_menu::{ContextMenu, ContextMenuItem};
use copilot::{Copilot, SignOut, Status}; use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor}; use editor::{scroll::autoscroll::Autoscroll, Editor};
use fs::Fs;
use gpui::{ use gpui::{
elements::*, elements::*,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
}; };
use settings::{settings_file::SettingsFile, Settings}; use language::language_settings::{self, all_language_settings, AllLanguageSettings};
use settings::{update_settings_file, SettingsStore};
use std::{path::Path, sync::Arc}; use std::{path::Path, sync::Arc};
use util::{paths, ResultExt}; use util::{paths, ResultExt};
use workspace::{ use workspace::{
@ -26,6 +28,7 @@ pub struct CopilotButton {
editor_enabled: Option<bool>, editor_enabled: Option<bool>,
language: Option<Arc<str>>, language: Option<Arc<str>>,
path: Option<Arc<Path>>, path: Option<Arc<Path>>,
fs: Arc<dyn Fs>,
} }
impl Entity for CopilotButton { impl Entity for CopilotButton {
@ -38,13 +41,12 @@ impl View for CopilotButton {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let settings = cx.global::<Settings>(); let all_language_settings = &all_language_settings(cx);
if !all_language_settings.copilot.feature_enabled {
if !settings.features.copilot {
return Empty::new().into_any(); return Empty::new().into_any();
} }
let theme = settings.theme.clone(); let theme = theme::current(cx).clone();
let active = self.popup_menu.read(cx).visible(); let active = self.popup_menu.read(cx).visible();
let Some(copilot) = Copilot::global(cx) else { let Some(copilot) = Copilot::global(cx) else {
return Empty::new().into_any(); return Empty::new().into_any();
@ -53,7 +55,7 @@ impl View for CopilotButton {
let enabled = self let enabled = self
.editor_enabled .editor_enabled
.unwrap_or(settings.show_copilot_suggestions(None, None)); .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
Stack::new() Stack::new()
.with_child( .with_child(
@ -143,7 +145,7 @@ impl View for CopilotButton {
} }
impl CopilotButton { impl CopilotButton {
pub fn new(cx: &mut ViewContext<Self>) -> Self { pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
let button_view_id = cx.view_id(); let button_view_id = cx.view_id();
let menu = cx.add_view(|cx| { let menu = cx.add_view(|cx| {
let mut menu = ContextMenu::new(button_view_id, cx); let mut menu = ContextMenu::new(button_view_id, cx);
@ -155,7 +157,7 @@ impl CopilotButton {
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
cx.observe_global::<Settings, _>(move |_, cx| cx.notify()) cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
.detach(); .detach();
Self { Self {
@ -164,17 +166,19 @@ impl CopilotButton {
editor_enabled: None, editor_enabled: None,
language: None, language: None,
path: None, path: None,
fs,
} }
} }
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) { pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
let mut menu_options = Vec::with_capacity(2); let mut menu_options = Vec::with_capacity(2);
let fs = self.fs.clone();
menu_options.push(ContextMenuItem::handler("Sign In", |cx| { menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
initiate_sign_in(cx) initiate_sign_in(cx)
})); }));
menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| { menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
hide_copilot(cx) hide_copilot(fs.clone(), cx)
})); }));
self.popup_menu.update(cx, |menu, cx| { self.popup_menu.update(cx, |menu, cx| {
@ -188,22 +192,26 @@ impl CopilotButton {
} }
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) { pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>(); let fs = self.fs.clone();
let mut menu_options = Vec::with_capacity(8); let mut menu_options = Vec::with_capacity(8);
if let Some(language) = self.language.clone() { if let Some(language) = self.language.clone() {
let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref())); let fs = fs.clone();
let language_enabled =
language_settings::language_settings(Some(language.as_ref()), cx)
.show_copilot_suggestions;
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
format!( format!(
"{} Suggestions for {}", "{} Suggestions for {}",
if language_enabled { "Hide" } else { "Show" }, if language_enabled { "Hide" } else { "Show" },
language language
), ),
move |cx| toggle_copilot_for_language(language.clone(), cx), move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
)); ));
} }
let settings = settings::get::<AllLanguageSettings>(cx);
if let Some(path) = self.path.as_ref() { if let Some(path) = self.path.as_ref() {
let path_enabled = settings.copilot_enabled_for_path(path); let path_enabled = settings.copilot_enabled_for_path(path);
let path = path.clone(); let path = path.clone();
@ -228,19 +236,19 @@ impl CopilotButton {
)); ));
} }
let globally_enabled = cx.global::<Settings>().features.copilot; let globally_enabled = settings.copilot_enabled(None, None);
menu_options.push(ContextMenuItem::handler( menu_options.push(ContextMenuItem::handler(
if globally_enabled { if globally_enabled {
"Hide Suggestions for All Files" "Hide Suggestions for All Files"
} else { } else {
"Show Suggestions for All Files" "Show Suggestions for All Files"
}, },
|cx| toggle_copilot_globally(cx), move |cx| toggle_copilot_globally(fs.clone(), cx),
)); ));
menu_options.push(ContextMenuItem::Separator); menu_options.push(ContextMenuItem::Separator);
let icon_style = settings.theme.copilot.out_link_icon.clone(); let icon_style = theme::current(cx).copilot.out_link_icon.clone();
menu_options.push(ContextMenuItem::action( menu_options.push(ContextMenuItem::action(
move |state: &mut MouseState, style: &theme::ContextMenuItem| { move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row() Flex::row()
@ -266,22 +274,19 @@ impl CopilotButton {
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) { pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx); let editor = editor.read(cx);
let snapshot = editor.buffer().read(cx).snapshot(cx); let snapshot = editor.buffer().read(cx).snapshot(cx);
let settings = cx.global::<Settings>();
let suggestion_anchor = editor.selections.newest_anchor().start; let suggestion_anchor = editor.selections.newest_anchor().start;
let language_name = snapshot let language_name = snapshot
.language_at(suggestion_anchor) .language_at(suggestion_anchor)
.map(|language| language.name()); .map(|language| language.name());
let path = snapshot let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
.file_at(suggestion_anchor)
.map(|file| file.path().clone());
self.editor_enabled = self.editor_enabled = Some(
Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref())); all_language_settings(cx)
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
);
self.language = language_name; self.language = language_name;
self.path = path; self.path = path.cloned();
cx.notify() cx.notify()
} }
@ -310,7 +315,7 @@ async fn configure_disabled_globs(
let settings_editor = workspace let settings_editor = workspace
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
create_and_open_local_file(&paths::SETTINGS, cx, || { create_and_open_local_file(&paths::SETTINGS, cx, || {
Settings::initial_user_settings_content(&assets::Assets) settings::initial_user_settings_content(&assets::Assets)
.as_ref() .as_ref()
.into() .into()
}) })
@ -322,16 +327,17 @@ async fn configure_disabled_globs(
settings_editor.downgrade().update(&mut cx, |item, cx| { settings_editor.downgrade().update(&mut cx, |item, cx| {
let text = item.buffer().read(cx).snapshot(cx).text(); let text = item.buffer().read(cx).snapshot(cx).text();
let edits = SettingsFile::update_unsaved(&text, cx, |file| { let settings = cx.global::<SettingsStore>();
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
let copilot = file.copilot.get_or_insert_with(Default::default); let copilot = file.copilot.get_or_insert_with(Default::default);
let globs = copilot.disabled_globs.get_or_insert_with(|| { let globs = copilot.disabled_globs.get_or_insert_with(|| {
cx.global::<Settings>() settings
.get::<AllLanguageSettings>(None)
.copilot .copilot
.disabled_globs .disabled_globs
.clone()
.iter() .iter()
.map(|glob| glob.as_str().to_string()) .map(|glob| glob.glob().to_string())
.collect::<Vec<_>>() .collect()
}); });
if let Some(path_to_disable) = &path_to_disable { if let Some(path_to_disable) = &path_to_disable {
@ -356,32 +362,26 @@ async fn configure_disabled_globs(
anyhow::Ok(()) anyhow::Ok(())
} }
fn toggle_copilot_globally(cx: &mut AppContext) { fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None); let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
SettingsFile::update(cx, move |file_contents| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
}); });
} }
fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) { fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_copilot_suggestions = cx let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
.global::<Settings>() update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
.show_copilot_suggestions(Some(&language), None); file.languages
.entry(language)
SettingsFile::update(cx, move |file_contents| { .or_default()
file_contents.languages.insert( .show_copilot_suggestions = Some(!show_copilot_suggestions);
language,
settings::EditorSettings {
show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default()
},
);
}); });
} }
fn hide_copilot(cx: &mut AppContext) { fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
SettingsFile::update(cx, move |file_contents| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
file_contents.features.copilot = Some(false) file.features.get_or_insert(Default::default()).copilot = Some(false);
}); });
} }

View file

@ -31,6 +31,7 @@ language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
serde_json.workspace = true serde_json.workspace = true
unindent.workspace = true unindent.workspace = true

View file

@ -20,7 +20,6 @@ use language::{
use lsp::LanguageServerId; use lsp::LanguageServerId;
use project::{DiagnosticSummary, Project, ProjectPath}; use project::{DiagnosticSummary, Project, ProjectPath};
use serde_json::json; use serde_json::json;
use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
@ -30,6 +29,7 @@ use std::{
path::PathBuf, path::PathBuf,
sync::Arc, sync::Arc,
}; };
use theme::ThemeSettings;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@ -89,7 +89,7 @@ impl View for ProjectDiagnosticsEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if self.path_states.is_empty() { if self.path_states.is_empty() {
let theme = &cx.global::<Settings>().theme.project_diagnostics; let theme = &theme::current(cx).project_diagnostics;
Label::new("No problems in workspace", theme.empty_message.clone()) Label::new("No problems in workspace", theme.empty_message.clone())
.aligned() .aligned()
.contained() .contained()
@ -537,7 +537,7 @@ impl Item for ProjectDiagnosticsEditor {
render_summary( render_summary(
&self.summary, &self.summary,
&style.label.text, &style.label.text,
&cx.global::<Settings>().theme.project_diagnostics, &theme::current(cx).project_diagnostics,
) )
} }
@ -679,10 +679,10 @@ impl Item for ProjectDiagnosticsEditor {
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message); let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
Arc::new(move |cx| { Arc::new(move |cx| {
let settings = cx.global::<Settings>(); let settings = settings::get::<ThemeSettings>(cx);
let theme = &settings.theme.editor; let theme = &settings.theme.editor;
let style = theme.diagnostic_header.clone(); let style = theme.diagnostic_header.clone();
let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
let icon_width = cx.em_width * style.icon_width_factor; let icon_width = cx.em_width * style.icon_width_factor;
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
Svg::new("icons/circle_x_mark_12.svg") Svg::new("icons/circle_x_mark_12.svg")
@ -818,33 +818,35 @@ mod tests {
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
use project::FakeFs; use project::FakeFs;
use serde_json::json; use serde_json::json;
use settings::SettingsStore;
use unindent::Unindent as _; use unindent::Unindent as _;
#[gpui::test] #[gpui::test]
async fn test_diagnostics(cx: &mut TestAppContext) { async fn test_diagnostics(cx: &mut TestAppContext) {
Settings::test_async(cx); init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/test", "/test",
json!({ json!({
"consts.rs": " "consts.rs": "
const a: i32 = 'a'; const a: i32 = 'a';
const b: i32 = c; const b: i32 = c;
" "
.unindent(), .unindent(),
"main.rs": " "main.rs": "
fn main() { fn main() {
let x = vec![]; let x = vec![];
let y = vec![]; let y = vec![];
a(x); a(x);
b(y); b(y);
// comment 1 // comment 1
// comment 2 // comment 2
c(y); c(y);
d(x); d(x);
} }
" "
.unindent(), .unindent(),
}), }),
) )
@ -1225,7 +1227,8 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
Settings::test_async(cx); init_test(cx);
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/test", "/test",
@ -1489,6 +1492,16 @@ mod tests {
}); });
} }
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
client::init_settings(cx);
workspace::init_settings(cx);
});
}
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> { fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);

View file

@ -7,7 +7,6 @@ use gpui::{
}; };
use language::Diagnostic; use language::Diagnostic;
use lsp::LanguageServerId; use lsp::LanguageServerId;
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::ProjectDiagnosticsEditor; use crate::ProjectDiagnosticsEditor;
@ -92,13 +91,12 @@ impl View for DiagnosticIndicator {
enum Summary {} enum Summary {}
enum Message {} enum Message {}
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone(); let tooltip_style = theme::current(cx).tooltip.clone();
let in_progress = !self.in_progress_checks.is_empty(); let in_progress = !self.in_progress_checks.is_empty();
let mut element = Flex::row().with_child( let mut element = Flex::row().with_child(
MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| { MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
let style = cx let theme = theme::current(cx);
.global::<Settings>() let style = theme
.theme
.workspace .workspace
.status_bar .status_bar
.diagnostic_summary .diagnostic_summary
@ -184,7 +182,7 @@ impl View for DiagnosticIndicator {
.into_any(), .into_any(),
); );
let style = &cx.global::<Settings>().theme.workspace.status_bar; let style = &theme::current(cx).workspace.status_bar;
let item_spacing = style.item_spacing; let item_spacing = style.item_spacing;
if in_progress { if in_progress {

View file

@ -58,6 +58,7 @@ parking_lot.workspace = true
postage.workspace = true postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false } pulldown-cmark = { version = "0.9.2", default-features = false }
rand = { workspace = true, optional = true } rand = { workspace = true, optional = true }
schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
smallvec.workspace = true smallvec.workspace = true
@ -80,7 +81,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true
glob.workspace = true
rand.workspace = true rand.workspace = true
unindent.workspace = true unindent.workspace = true
tree-sitter = "0.20" tree-sitter = "0.20"

View file

@ -1,8 +1,8 @@
use std::time::Duration; use crate::EditorSettings;
use gpui::{Entity, ModelContext}; use gpui::{Entity, ModelContext};
use settings::Settings; use settings::SettingsStore;
use smol::Timer; use smol::Timer;
use std::time::Duration;
pub struct BlinkManager { pub struct BlinkManager {
blink_interval: Duration, blink_interval: Duration,
@ -15,8 +15,8 @@ pub struct BlinkManager {
impl BlinkManager { impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self { pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
cx.observe_global::<Settings, _>(move |this, cx| { // Make sure we blink the cursors if the setting is re-enabled
// Make sure we blink the cursors if the setting is re-enabled cx.observe_global::<SettingsStore, _>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx) this.blink_cursors(this.blink_epoch, cx)
}) })
.detach(); .detach();
@ -64,7 +64,7 @@ impl BlinkManager {
} }
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) { fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if cx.global::<Settings>().cursor_blink { if settings::get::<EditorSettings>(cx).cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused { if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible; self.visible = !self.visible;
cx.notify(); cx.notify();

View file

@ -13,8 +13,9 @@ use gpui::{
fonts::{FontId, HighlightStyle}, fonts::{FontId, HighlightStyle},
Entity, ModelContext, ModelHandle, Entity, ModelContext, ModelHandle,
}; };
use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; use language::{
use settings::Settings; language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
};
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
pub use suggestion_map::Suggestion; pub use suggestion_map::Suggestion;
use suggestion_map::SuggestionMap; use suggestion_map::SuggestionMap;
@ -276,8 +277,7 @@ impl DisplayMap {
.as_singleton() .as_singleton()
.and_then(|buffer| buffer.read(cx).language()) .and_then(|buffer| buffer.read(cx).language())
.map(|language| language.name()); .map(|language| language.name());
language_settings(language_name.as_deref(), cx).tab_size
cx.global::<Settings>().tab_size(language_name.as_deref())
} }
#[cfg(test)] #[cfg(test)]
@ -844,8 +844,12 @@ pub mod tests {
use super::*; use super::*;
use crate::{movement, test::marked_display_snapshot}; use crate::{movement, test::marked_display_snapshot};
use gpui::{color::Color, elements::*, test::observe, AppContext}; use gpui::{color::Color, elements::*, test::observe, AppContext};
use language::{Buffer, Language, LanguageConfig, SelectionGoal}; use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, SelectionGoal,
};
use rand::{prelude::*, Rng}; use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use theme::SyntaxTheme; use theme::SyntaxTheme;
@ -882,9 +886,7 @@ pub mod tests {
log::info!("wrap width: {:?}", wrap_width); log::info!("wrap width: {:?}", wrap_width);
cx.update(|cx| { cx.update(|cx| {
let mut settings = Settings::test(cx); init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
cx.set_global(settings)
}); });
let buffer = cx.update(|cx| { let buffer = cx.update(|cx| {
@ -939,9 +941,11 @@ pub mod tests {
tab_size = *tab_sizes.choose(&mut rng).unwrap(); tab_size = *tab_sizes.choose(&mut rng).unwrap();
log::info!("setting tab size to {:?}", tab_size); log::info!("setting tab size to {:?}", tab_size);
cx.update(|cx| { cx.update(|cx| {
let mut settings = Settings::test(cx); cx.update_global::<SettingsStore, _, _>(|store, cx| {
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size); store.update_user_settings::<AllLanguageSettings>(cx, |s| {
cx.set_global(settings) s.defaults.tab_size = NonZeroU32::new(tab_size);
});
});
}); });
} }
30..=44 => { 30..=44 => {
@ -1119,7 +1123,7 @@ pub mod tests {
#[gpui::test(retries = 5)] #[gpui::test(retries = 5)]
fn test_soft_wraps(cx: &mut AppContext) { fn test_soft_wraps(cx: &mut AppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
@ -1131,7 +1135,6 @@ pub mod tests {
.unwrap(); .unwrap();
let font_size = 12.0; let font_size = 12.0;
let wrap_width = Some(64.); let wrap_width = Some(64.);
cx.set_global(Settings::test(cx));
let text = "one two three four five\nsix seven eight"; let text = "one two three four five\nsix seven eight";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = MultiBuffer::build_simple(text, cx);
@ -1211,7 +1214,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_text_chunks(cx: &mut gpui::AppContext) { fn test_text_chunks(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx, |_| {});
let text = sample_text(6, 6, 'a'); let text = sample_text(6, 6, 'a');
let buffer = MultiBuffer::build_simple(&text, cx); let buffer = MultiBuffer::build_simple(&text, cx);
let family_id = cx let family_id = cx
@ -1225,6 +1229,7 @@ pub mod tests {
let font_size = 14.0; let font_size = 14.0;
let map = let map =
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
buffer.update(cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.edit( buffer.edit(
vec![ vec![
@ -1289,11 +1294,8 @@ pub mod tests {
.unwrap(), .unwrap(),
); );
language.set_theme(&theme); language.set_theme(&theme);
cx.update(|cx| {
let mut settings = Settings::test(cx); cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
cx.set_global(settings);
});
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await; buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@ -1382,7 +1384,7 @@ pub mod tests {
); );
language.set_theme(&theme); language.set_theme(&theme);
cx.update(|cx| cx.set_global(Settings::test(cx))); cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
buffer.condition(cx, |buf, _| !buf.is_parsing()).await; buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@ -1429,9 +1431,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| init_test(cx, |_| {}));
cx.update(|cx| cx.set_global(Settings::test(cx)));
let theme = SyntaxTheme::new(vec![ let theme = SyntaxTheme::new(vec![
("operator".to_string(), Color::red().into()), ("operator".to_string(), Color::red().into()),
("string".to_string(), Color::green().into()), ("string".to_string(), Color::green().into()),
@ -1510,7 +1511,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_clip_point(cx: &mut gpui::AppContext) { fn test_clip_point(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx, |_| {});
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
@ -1559,7 +1561,7 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx, |_| {});
fn assert(text: &str, cx: &mut gpui::AppContext) { fn assert(text: &str, cx: &mut gpui::AppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
@ -1578,7 +1580,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx, |_| {});
let text = "\t\tα\nβ\t\n🏀β\t\tγ"; let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = MultiBuffer::build_simple(text, cx); let buffer = MultiBuffer::build_simple(text, cx);
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
@ -1639,7 +1642,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_max_point(cx: &mut gpui::AppContext) { fn test_max_point(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx, |_| {});
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let family_id = font_cache let family_id = font_cache
@ -1718,4 +1722,13 @@ pub mod tests {
} }
chunks chunks
} }
fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking();
cx.set_global(SettingsStore::test(cx));
language::init(cx);
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
}
} }

View file

@ -993,7 +993,7 @@ mod tests {
use crate::multi_buffer::MultiBuffer; use crate::multi_buffer::MultiBuffer;
use gpui::{elements::Empty, Element}; use gpui::{elements::Empty, Element};
use rand::prelude::*; use rand::prelude::*;
use settings::Settings; use settings::SettingsStore;
use std::env; use std::env;
use util::RandomCharIter; use util::RandomCharIter;
@ -1013,7 +1013,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_basic_blocks(cx: &mut gpui::AppContext) { fn test_basic_blocks(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let family_id = cx let family_id = cx
.font_cache() .font_cache()
@ -1189,7 +1189,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) { fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let family_id = cx let family_id = cx
.font_cache() .font_cache()
@ -1239,7 +1239,7 @@ mod tests {
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) { fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
cx.set_global(Settings::test(cx)); init_test(cx);
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
@ -1647,6 +1647,11 @@ mod tests {
} }
} }
fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
}
impl TransformBlock { impl TransformBlock {
fn as_custom(&self) -> Option<&Block> { fn as_custom(&self) -> Option<&Block> {
match self { match self {

View file

@ -1204,7 +1204,7 @@ mod tests {
use crate::{MultiBuffer, ToPoint}; use crate::{MultiBuffer, ToPoint};
use collections::HashSet; use collections::HashSet;
use rand::prelude::*; use rand::prelude::*;
use settings::Settings; use settings::SettingsStore;
use std::{cmp::Reverse, env, mem, sync::Arc}; use std::{cmp::Reverse, env, mem, sync::Arc};
use sum_tree::TreeMap; use sum_tree::TreeMap;
use util::test::sample_text; use util::test::sample_text;
@ -1213,7 +1213,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_basic_folds(cx: &mut gpui::AppContext) { fn test_basic_folds(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(cx);
@ -1286,7 +1286,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_adjacent_folds(cx: &mut gpui::AppContext) { fn test_adjacent_folds(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(cx);
@ -1349,7 +1349,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) { fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx); let buffer_snapshot = buffer.read(cx).snapshot(cx);
@ -1400,7 +1400,7 @@ mod tests {
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) { fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
cx.set_global(Settings::test(cx)); init_test(cx);
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10); .unwrap_or(10);
@ -1676,6 +1676,10 @@ mod tests {
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]); assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
} }
fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx));
}
impl FoldMap { impl FoldMap {
fn merged_fold_ranges(&self) -> Vec<Range<usize>> { fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
let buffer = self.buffer.lock().clone(); let buffer = self.buffer.lock().clone();

View file

@ -578,7 +578,7 @@ mod tests {
use crate::{display_map::fold_map::FoldMap, MultiBuffer}; use crate::{display_map::fold_map::FoldMap, MultiBuffer};
use gpui::AppContext; use gpui::AppContext;
use rand::{prelude::StdRng, Rng}; use rand::{prelude::StdRng, Rng};
use settings::Settings; use settings::SettingsStore;
use std::{ use std::{
env, env,
ops::{Bound, RangeBounds}, ops::{Bound, RangeBounds},
@ -631,7 +631,8 @@ mod tests {
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) { fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
cx.set_global(Settings::test(cx)); init_test(cx);
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10); .unwrap_or(10);
@ -834,6 +835,11 @@ mod tests {
} }
} }
fn init_test(cx: &mut AppContext) {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
}
impl SuggestionMap { impl SuggestionMap {
pub fn randomly_mutate( pub fn randomly_mutate(
&self, &self,

View file

@ -1043,16 +1043,16 @@ mod tests {
}; };
use gpui::test::observe; use gpui::test::observe;
use rand::prelude::*; use rand::prelude::*;
use settings::Settings; use settings::SettingsStore;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cmp, env, num::NonZeroU32}; use std::{cmp, env, num::NonZeroU32};
use text::Rope; use text::Rope;
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) { async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx);
cx.foreground().set_block_on_ticks(0..=50); cx.foreground().set_block_on_ticks(0..=50);
cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10); .unwrap_or(10);
@ -1287,6 +1287,14 @@ mod tests {
wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty())); wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
} }
fn init_test(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
});
}
fn wrap_text( fn wrap_text(
unwrapped_text: &str, unwrapped_text: &str,
wrap_width: Option<f32>, wrap_width: Option<f32>,

View file

@ -1,5 +1,6 @@
mod blink_manager; mod blink_manager;
pub mod display_map; pub mod display_map;
mod editor_settings;
mod element; mod element;
mod git; mod git;
@ -19,15 +20,17 @@ mod editor_tests;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use blink_manager::BlinkManager; use blink_manager::BlinkManager;
use client::ClickhouseEvent; use client::{ClickhouseEvent, TelemetrySettings};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use copilot::Copilot; use copilot::Copilot;
pub use display_map::DisplayPoint; pub use display_map::DisplayPoint;
use display_map::*; use display_map::*;
pub use editor_settings::EditorSettings;
pub use element::*; pub use element::*;
use futures::FutureExt; use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
@ -51,6 +54,7 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools; use itertools::Itertools;
pub use language::{char_kind, CharKind}; pub use language::{char_kind, CharKind};
use language::{ use language::{
language_settings::{self, all_language_settings},
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@ -70,7 +74,7 @@ use scroll::{
}; };
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::SettingsStore;
use smallvec::SmallVec; use smallvec::SmallVec;
use snippet::Snippet; use snippet::Snippet;
use std::{ use std::{
@ -85,7 +89,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
pub use sum_tree::Bias; pub use sum_tree::Bias;
use theme::{DiagnosticStyle, Theme}; use theme::{DiagnosticStyle, Theme, ThemeSettings};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ItemNavHistory, ViewId, Workspace}; use workspace::{ItemNavHistory, ViewId, Workspace};
@ -286,7 +290,12 @@ pub enum Direction {
Next, Next,
} }
pub fn init_settings(cx: &mut AppContext) {
settings::register::<EditorSettings>(cx);
}
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
init_settings(cx);
cx.add_action(Editor::new_file); cx.add_action(Editor::new_file);
cx.add_action(Editor::cancel); cx.add_action(Editor::cancel);
cx.add_action(Editor::newline); cx.add_action(Editor::newline);
@ -436,7 +445,7 @@ pub enum EditorMode {
Full, Full,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum SoftWrap { pub enum SoftWrap {
None, None,
EditorWidth, EditorWidth,
@ -471,7 +480,7 @@ pub struct Editor {
select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>, select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
ime_transaction: Option<TransactionId>, ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>, active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<settings::SoftWrap>, soft_wrap_mode_override: Option<language_settings::SoftWrap>,
get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>, get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
override_text_style: Option<Box<OverrideTextStyle>>, override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<ModelHandle<Project>>, project: Option<ModelHandle<Project>>,
@ -516,6 +525,15 @@ pub struct EditorSnapshot {
ongoing_scroll: OngoingScroll, ongoing_scroll: OngoingScroll,
} }
impl EditorSnapshot {
fn has_scrollbar_info(&self) -> bool {
self.buffer_snapshot
.git_diff_hunks_in_range(0..self.max_point().row())
.next()
.is_some()
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct SelectionHistoryEntry { struct SelectionHistoryEntry {
selections: Arc<[Selection<Anchor>]>, selections: Arc<[Selection<Anchor>]>,
@ -1229,8 +1247,8 @@ impl Editor {
) -> Self { ) -> Self {
let editor_view_id = cx.view_id(); let editor_view_id = cx.view_id();
let display_map = cx.add_model(|cx| { let display_map = cx.add_model(|cx| {
let settings = cx.global::<Settings>(); let settings = settings::get::<ThemeSettings>(cx);
let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx); let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
DisplayMap::new( DisplayMap::new(
buffer.clone(), buffer.clone(),
style.text.font_id, style.text.font_id,
@ -1247,7 +1265,17 @@ impl Editor {
let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let soft_wrap_mode_override = let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None); (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
let mut project_subscription = None;
if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
if let Some(project) = project.as_ref() {
project_subscription = Some(cx.observe(project, |_, _, cx| {
cx.emit(Event::TitleChanged);
}))
}
}
let mut this = Self { let mut this = Self {
handle: cx.weak_handle(), handle: cx.weak_handle(),
buffer: buffer.clone(), buffer: buffer.clone(),
@ -1301,9 +1329,14 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event), cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global::<Settings, _>(Self::settings_changed), cx.observe_global::<SettingsStore, _>(Self::settings_changed),
], ],
}; };
if let Some(project_subscription) = project_subscription {
this._subscriptions.push(project_subscription);
}
this.end_selection(cx); this.end_selection(cx);
this.scroll_manager.show_scrollbar(cx); this.scroll_manager.show_scrollbar(cx);
@ -1315,7 +1348,7 @@ impl Editor {
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
} }
this.report_editor_event("open", cx); this.report_editor_event("open", None, cx);
this this
} }
@ -1395,7 +1428,7 @@ impl Editor {
fn style(&self, cx: &AppContext) -> EditorStyle { fn style(&self, cx: &AppContext) -> EditorStyle {
build_style( build_style(
cx.global::<Settings>(), settings::get::<ThemeSettings>(cx),
self.get_field_editor_theme.as_deref(), self.get_field_editor_theme.as_deref(),
self.override_text_style.as_deref(), self.override_text_style.as_deref(),
cx, cx,
@ -2353,7 +2386,7 @@ impl Editor {
} }
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) { fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
if !cx.global::<Settings>().show_completions_on_input { if !settings::get::<EditorSettings>(cx).show_completions_on_input {
return; return;
} }
@ -3082,6 +3115,8 @@ impl Editor {
copilot copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx)) .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx); .detach_and_log_err(cx);
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
} }
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx); self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
cx.notify(); cx.notify();
@ -3099,6 +3134,8 @@ impl Editor {
copilot.discard_completions(&self.copilot_state.completions, cx) copilot.discard_completions(&self.copilot_state.completions, cx)
}) })
.detach_and_log_err(cx); .detach_and_log_err(cx);
self.report_copilot_event(None, false, cx)
} }
self.display_map self.display_map
@ -3116,17 +3153,12 @@ impl Editor {
snapshot: &MultiBufferSnapshot, snapshot: &MultiBufferSnapshot,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
let settings = cx.global::<Settings>(); let path = snapshot.file_at(location).map(|file| file.path().as_ref());
let path = snapshot.file_at(location).map(|file| file.path());
let language_name = snapshot let language_name = snapshot
.language_at(location) .language_at(location)
.map(|language| language.name()); .map(|language| language.name());
if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) { let settings = all_language_settings(cx);
return false; settings.copilot_enabled(language_name.as_deref(), path)
}
true
} }
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@ -3427,12 +3459,9 @@ impl Editor {
{ {
let indent_size = let indent_size =
buffer.indent_size_for_line(line_buffer_range.start.row); buffer.indent_size_for_line(line_buffer_range.start.row);
let language_name = buffer
.language_at(line_buffer_range.start)
.map(|language| language.name());
let indent_len = match indent_size.kind { let indent_len = match indent_size.kind {
IndentKind::Space => { IndentKind::Space => {
cx.global::<Settings>().tab_size(language_name.as_deref()) buffer.settings_at(line_buffer_range.start, cx).tab_size
} }
IndentKind::Tab => NonZeroU32::new(1).unwrap(), IndentKind::Tab => NonZeroU32::new(1).unwrap(),
}; };
@ -3544,12 +3573,11 @@ impl Editor {
} }
// Otherwise, insert a hard or soft tab. // Otherwise, insert a hard or soft tab.
let settings = cx.global::<Settings>(); let settings = buffer.settings_at(cursor, cx);
let language_name = buffer.language_at(cursor, cx).map(|l| l.name()); let tab_size = if settings.hard_tabs {
let tab_size = if settings.hard_tabs(language_name.as_deref()) {
IndentSize::tab() IndentSize::tab()
} else { } else {
let tab_size = settings.tab_size(language_name.as_deref()).get(); let tab_size = settings.tab_size.get();
let char_column = snapshot let char_column = snapshot
.text_for_range(Point::new(cursor.row, 0)..cursor) .text_for_range(Point::new(cursor.row, 0)..cursor)
.flat_map(str::chars) .flat_map(str::chars)
@ -3602,10 +3630,9 @@ impl Editor {
delta_for_start_row: u32, delta_for_start_row: u32,
cx: &AppContext, cx: &AppContext,
) -> u32 { ) -> u32 {
let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); let settings = buffer.settings_at(selection.start, cx);
let settings = cx.global::<Settings>(); let tab_size = settings.tab_size.get();
let tab_size = settings.tab_size(language_name.as_deref()).get(); let indent_kind = if settings.hard_tabs {
let indent_kind = if settings.hard_tabs(language_name.as_deref()) {
IndentKind::Tab IndentKind::Tab
} else { } else {
IndentKind::Space IndentKind::Space
@ -3674,11 +3701,8 @@ impl Editor {
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
let snapshot = buffer.snapshot(cx); let snapshot = buffer.snapshot(cx);
for selection in &selections { for selection in &selections {
let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); let settings = buffer.settings_at(selection.start, cx);
let tab_size = cx let tab_size = settings.tab_size.get();
.global::<Settings>()
.tab_size(language_name.as_deref())
.get();
let mut rows = selection.spanned_rows(false, &display_map); let mut rows = selection.spanned_rows(false, &display_map);
// Avoid re-outdenting a row that has already been outdented by a // Avoid re-outdenting a row that has already been outdented by a
@ -5546,68 +5570,91 @@ impl Editor {
} }
fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) { fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
self.go_to_hunk_impl(Direction::Next, cx)
}
fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
self.go_to_hunk_impl(Direction::Prev, cx)
}
pub fn go_to_hunk_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
let snapshot = self let snapshot = self
.display_map .display_map
.update(cx, |display_map, cx| display_map.snapshot(cx)); .update(cx, |display_map, cx| display_map.snapshot(cx));
let selection = self.selections.newest::<Point>(cx); let selection = self.selections.newest::<Point>(cx);
fn seek_in_direction( if !self.seek_in_direction(
this: &mut Editor, &snapshot,
snapshot: &DisplaySnapshot, selection.head(),
initial_point: Point, false,
is_wrapped: bool, snapshot
direction: Direction, .buffer_snapshot
cx: &mut ViewContext<Editor>, .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
) -> bool { cx,
let hunks = if direction == Direction::Next { ) {
let wrapped_point = Point::zero();
self.seek_in_direction(
&snapshot,
wrapped_point,
true,
snapshot snapshot
.buffer_snapshot .buffer_snapshot
.git_diff_hunks_in_range(initial_point.row..u32::MAX, false) .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
} else { cx,
snapshot );
.buffer_snapshot
.git_diff_hunks_in_range(0..initial_point.row, true)
};
let display_point = initial_point.to_display_point(snapshot);
let mut hunks = hunks
.map(|hunk| diff_hunk_to_display(hunk, &snapshot))
.skip_while(|hunk| {
if is_wrapped {
false
} else {
hunk.contains_display_row(display_point.row())
}
})
.dedup();
if let Some(hunk) = hunks.next() {
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
let row = hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
});
true
} else {
false
}
} }
}
if !seek_in_direction(self, &snapshot, selection.head(), false, direction, cx) { fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
let wrapped_point = match direction { let snapshot = self
Direction::Next => Point::zero(), .display_map
Direction::Prev => snapshot.buffer_snapshot.max_point(), .update(cx, |display_map, cx| display_map.snapshot(cx));
}; let selection = self.selections.newest::<Point>(cx);
seek_in_direction(self, &snapshot, wrapped_point, true, direction, cx);
if !self.seek_in_direction(
&snapshot,
selection.head(),
false,
snapshot
.buffer_snapshot
.git_diff_hunks_in_range_rev(0..selection.head().row),
cx,
) {
let wrapped_point = snapshot.buffer_snapshot.max_point();
self.seek_in_direction(
&snapshot,
wrapped_point,
true,
snapshot
.buffer_snapshot
.git_diff_hunks_in_range_rev(0..wrapped_point.row),
cx,
);
}
}
fn seek_in_direction(
&mut self,
snapshot: &DisplaySnapshot,
initial_point: Point,
is_wrapped: bool,
hunks: impl Iterator<Item = DiffHunk<u32>>,
cx: &mut ViewContext<Editor>,
) -> bool {
let display_point = initial_point.to_display_point(snapshot);
let mut hunks = hunks
.map(|hunk| diff_hunk_to_display(hunk, &snapshot))
.skip_while(|hunk| {
if is_wrapped {
false
} else {
hunk.contains_display_row(display_point.row())
}
})
.dedup();
if let Some(hunk) = hunks.next() {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
let row = hunk.start_display_row();
let point = DisplayPoint::new(row, 0);
s.select_display_ranges([point..point]);
});
true
} else {
false
} }
} }
@ -6439,27 +6486,24 @@ impl Editor {
} }
pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
let language_name = self let settings = self.buffer.read(cx).settings_at(0, cx);
.buffer
.read(cx)
.as_singleton()
.and_then(|singleton_buffer| singleton_buffer.read(cx).language())
.map(|l| l.name());
let settings = cx.global::<Settings>();
let mode = self let mode = self
.soft_wrap_mode_override .soft_wrap_mode_override
.unwrap_or_else(|| settings.soft_wrap(language_name.as_deref())); .unwrap_or_else(|| settings.soft_wrap);
match mode { match mode {
settings::SoftWrap::None => SoftWrap::None, language_settings::SoftWrap::None => SoftWrap::None,
settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
settings::SoftWrap::PreferredLineLength => { language_settings::SoftWrap::PreferredLineLength => {
SoftWrap::Column(settings.preferred_line_length(language_name.as_deref())) SoftWrap::Column(settings.preferred_line_length)
} }
} }
} }
pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext<Self>) { pub fn set_soft_wrap_mode(
&mut self,
mode: language_settings::SoftWrap,
cx: &mut ViewContext<Self>,
) {
self.soft_wrap_mode_override = Some(mode); self.soft_wrap_mode_override = Some(mode);
cx.notify(); cx.notify();
} }
@ -6474,8 +6518,8 @@ impl Editor {
self.soft_wrap_mode_override.take(); self.soft_wrap_mode_override.take();
} else { } else {
let soft_wrap = match self.soft_wrap_mode(cx) { let soft_wrap = match self.soft_wrap_mode(cx) {
SoftWrap::None => settings::SoftWrap::EditorWidth, SoftWrap::None => language_settings::SoftWrap::EditorWidth,
SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None, SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None,
}; };
self.soft_wrap_mode_override = Some(soft_wrap); self.soft_wrap_mode_override = Some(soft_wrap);
} }
@ -6550,8 +6594,8 @@ impl Editor {
let buffer = &snapshot.buffer_snapshot; let buffer = &snapshot.buffer_snapshot;
let start = buffer.anchor_before(0); let start = buffer.anchor_before(0);
let end = buffer.anchor_after(buffer.len()); let end = buffer.anchor_after(buffer.len());
let theme = cx.global::<Settings>().theme.as_ref(); let theme = theme::current(cx);
self.background_highlights_in_range(start..end, &snapshot, theme) self.background_highlights_in_range(start..end, &snapshot, theme.as_ref())
} }
fn document_highlights_for_position<'a>( fn document_highlights_for_position<'a>(
@ -6861,44 +6905,88 @@ impl Editor {
.collect() .collect()
} }
fn report_editor_event(&self, name: &'static str, cx: &AppContext) { fn report_copilot_event(
if let Some((project, file)) = self.project.as_ref().zip( &self,
self.buffer suggestion_id: Option<String>,
.read(cx) suggestion_accepted: bool,
.as_singleton() cx: &AppContext,
.and_then(|b| b.read(cx).file()), ) {
) { let Some(project) = &self.project else {
let settings = cx.global::<Settings>(); return
};
let extension = Path::new(file.file_name(cx)) // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
.extension() let file_extension = self
.and_then(|e| e.to_str()); .buffer
let telemetry = project.read(cx).client().telemetry().clone(); .read(cx)
telemetry.report_mixpanel_event( .as_singleton()
match name { .and_then(|b| b.read(cx).file())
"open" => "open editor", .and_then(|file| Path::new(file.file_name(cx)).extension())
"save" => "save editor", .and_then(|e| e.to_str())
_ => name, .map(|a| a.to_string());
},
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }), let telemetry = project.read(cx).client().telemetry().clone();
settings.telemetry(), let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
);
let event = ClickhouseEvent::Editor { let event = ClickhouseEvent::Copilot {
file_extension: extension.map(ToString::to_string), suggestion_id,
vim_mode: settings.vim_mode, suggestion_accepted,
operation: name, file_extension,
copilot_enabled: settings.features.copilot, };
copilot_enabled_for_language: settings.show_copilot_suggestions( telemetry.report_clickhouse_event(event, telemetry_settings);
self.language_at(0, cx) }
.map(|language| language.name())
.as_deref(), fn report_editor_event(
self.file_at(0, cx) &self,
.map(|file| file.path().clone()) name: &'static str,
.as_deref(), file_extension: Option<String>,
), cx: &AppContext,
}; ) {
telemetry.report_clickhouse_event(event, settings.telemetry()) let Some(project) = &self.project else {
} return
};
// If None, we are in a file without an extension
let file_extension = file_extension.or(self
.buffer
.read(cx)
.as_singleton()
.and_then(|b| b.read(cx).file())
.and_then(|file| Path::new(file.file_name(cx)).extension())
.and_then(|e| e.to_str())
.map(|a| a.to_string()));
let vim_mode = cx
.global::<SettingsStore>()
.untyped_user_settings()
.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_for_language = self
.buffer
.read(cx)
.settings_at(0, cx)
.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,
operation: name,
copilot_enabled,
copilot_enabled_for_language,
};
telemetry.report_clickhouse_event(event, telemetry_settings)
} }
/// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@ -6930,7 +7018,7 @@ impl Editor {
let mut lines = Vec::new(); let mut lines = Vec::new();
let mut line: VecDeque<Chunk> = VecDeque::new(); let mut line: VecDeque<Chunk> = VecDeque::new();
let theme = &cx.global::<Settings>().theme.editor.syntax; let theme = &theme::current(cx).editor.syntax;
for chunk in chunks { for chunk in chunks {
let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme)); let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
@ -7352,7 +7440,7 @@ impl View for Editor {
} }
fn build_style( fn build_style(
settings: &Settings, settings: &ThemeSettings,
get_field_editor_theme: Option<&GetFieldEditorTheme>, get_field_editor_theme: Option<&GetFieldEditorTheme>,
override_text_style: Option<&OverrideTextStyle>, override_text_style: Option<&OverrideTextStyle>,
cx: &AppContext, cx: &AppContext,
@ -7382,7 +7470,7 @@ fn build_style(
let font_id = font_cache let font_id = font_cache
.select_font(font_family_id, &font_properties) .select_font(font_family_id, &font_properties)
.unwrap(); .unwrap();
let font_size = settings.buffer_font_size; let font_size = settings.buffer_font_size(cx);
EditorStyle { EditorStyle {
text: TextStyle { text: TextStyle {
color: settings.theme.editor.text_color, color: settings.theme.editor.text_color,
@ -7552,10 +7640,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
} }
Arc::new(move |cx: &mut BlockContext| { Arc::new(move |cx: &mut BlockContext| {
let settings = cx.global::<Settings>(); let settings = settings::get::<ThemeSettings>(cx);
let theme = &settings.theme.editor; let theme = &settings.theme.editor;
let style = diagnostic_style(diagnostic.severity, is_valid, theme); let style = diagnostic_style(diagnostic.severity, is_valid, theme);
let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
Flex::column() Flex::column()
.with_children(highlighted_lines.iter().map(|(line, highlights)| { .with_children(highlighted_lines.iter().map(|(line, highlights)| {
Label::new( Label::new(

View file

@ -0,0 +1,43 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Deserialize)]
pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_scrollbars: ShowScrollbars,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbars {
#[default]
Auto,
System,
Always,
Never,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_scrollbars: Option<ShowScrollbars>,
}
impl Setting for EditorSettings {
const KEY: Option<&'static str> = None;
type FileContent = EditorSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View file

@ -12,10 +12,12 @@ use gpui::{
serde_json, TestAppContext, serde_json, TestAppContext,
}; };
use indoc::indoc; use indoc::indoc;
use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point}; use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
};
use parking_lot::Mutex; use parking_lot::Mutex;
use project::FakeFs; use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use unindent::Unindent; use unindent::Unindent;
use util::{ use util::{
@ -29,7 +31,8 @@ use workspace::{
#[gpui::test] #[gpui::test]
fn test_edit_events(cx: &mut TestAppContext) { fn test_edit_events(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.add_model(|cx| { let buffer = cx.add_model(|cx| {
let mut buffer = language::Buffer::new(0, "123456", cx); let mut buffer = language::Buffer::new(0, "123456", cx);
buffer.set_group_interval(Duration::from_secs(1)); buffer.set_group_interval(Duration::from_secs(1));
@ -156,7 +159,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let mut now = Instant::now(); let mut now = Instant::now();
let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
@ -226,7 +230,8 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_ime_composition(cx: &mut TestAppContext) { fn test_ime_composition(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.add_model(|cx| { let buffer = cx.add_model(|cx| {
let mut buffer = language::Buffer::new(0, "abcde", cx); let mut buffer = language::Buffer::new(0, "abcde", cx);
// Ensure automatic grouping doesn't occur. // Ensure automatic grouping doesn't occur.
@ -328,7 +333,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_selection_with_mouse(cx: &mut TestAppContext) { fn test_selection_with_mouse(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
@ -395,7 +400,8 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_canceling_pending_selection(cx: &mut TestAppContext) { fn test_canceling_pending_selection(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -429,6 +435,8 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_clone(cx: &mut TestAppContext) { fn test_clone(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let (text, selection_ranges) = marked_text_ranges( let (text, selection_ranges) = marked_text_ranges(
indoc! {" indoc! {"
one one
@ -439,7 +447,6 @@ fn test_clone(cx: &mut TestAppContext) {
"}, "},
true, true,
); );
cx.update(|cx| cx.set_global(Settings::test(cx)));
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&text, cx); let buffer = MultiBuffer::build_simple(&text, cx);
@ -487,7 +494,8 @@ fn test_clone(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_navigation_history(cx: &mut TestAppContext) { async fn test_navigation_history(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
cx.set_global(DragAndDrop::<Workspace>::default()); cx.set_global(DragAndDrop::<Workspace>::default());
use workspace::item::Item; use workspace::item::Item;
@ -600,7 +608,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_cancel(cx: &mut TestAppContext) { fn test_cancel(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -642,7 +651,8 @@ fn test_cancel(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_fold_action(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
&" &"
@ -731,7 +741,8 @@ fn test_fold_action(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_move_cursor(cx: &mut TestAppContext) { fn test_move_cursor(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
@ -806,7 +817,8 @@ fn test_move_cursor(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_move_cursor_multibyte(cx: &mut TestAppContext) { fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
@ -910,7 +922,8 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
@ -959,7 +972,8 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_beginning_end_of_line(cx: &mut TestAppContext) { fn test_beginning_end_of_line(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\n def", cx); let buffer = MultiBuffer::build_simple("abc\n def", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -1121,7 +1135,8 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_prev_next_word_boundary(cx: &mut TestAppContext) { fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -1172,7 +1187,8 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -1229,6 +1245,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
@ -1343,6 +1360,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
cx.set_state("one «two threeˇ» four"); cx.set_state("one «two threeˇ» four");
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
@ -1353,7 +1371,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_delete_to_word_boundary(cx: &mut TestAppContext) { fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("one two three four", cx); let buffer = MultiBuffer::build_simple("one two three four", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
@ -1388,7 +1407,8 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_newline(cx: &mut TestAppContext) { fn test_newline(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
@ -1410,7 +1430,8 @@ fn test_newline(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_newline_with_old_selections(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
" "
@ -1491,11 +1512,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_newline_above(cx: &mut gpui::TestAppContext) { async fn test_newline_above(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |settings| {
cx.update(|cx| { settings.defaults.tab_size = NonZeroU32::new(4)
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
});
}); });
let language = Arc::new( let language = Arc::new(
@ -1506,8 +1524,9 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "(" ")" @end) @indent"#) .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(), .unwrap(),
); );
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let mut cx = EditorTestContext::new(cx);
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {" cx.set_state(indoc! {"
const a: ˇA = ( const a: ˇA = (
(ˇ (ˇ
@ -1516,6 +1535,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
)ˇ )ˇ
ˇ);ˇ ˇ);ˇ
"}); "});
cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
ˇ ˇ
@ -1540,11 +1560,8 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_newline_below(cx: &mut gpui::TestAppContext) { async fn test_newline_below(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |settings| {
cx.update(|cx| { settings.defaults.tab_size = NonZeroU32::new(4)
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
});
}); });
let language = Arc::new( let language = Arc::new(
@ -1555,8 +1572,9 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "(" ")" @end) @indent"#) .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
.unwrap(), .unwrap(),
); );
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let mut cx = EditorTestContext::new(cx);
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {" cx.set_state(indoc! {"
const a: ˇA = ( const a: ˇA = (
(ˇ (ˇ
@ -1565,6 +1583,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
)ˇ )ˇ
ˇ);ˇ ˇ);ˇ
"}); "});
cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
const a: A = ( const a: A = (
@ -1589,7 +1608,8 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_insert_with_old_selections(cx: &mut TestAppContext) { fn test_insert_with_old_selections(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
let mut editor = build_editor(buffer.clone(), cx); let mut editor = build_editor(buffer.clone(), cx);
@ -1615,12 +1635,11 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_tab(cx: &mut gpui::TestAppContext) { async fn test_tab(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |settings| {
cx.update(|cx| { settings.defaults.tab_size = NonZeroU32::new(3)
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
});
}); });
let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {" cx.set_state(indoc! {"
ˇabˇc ˇabˇc
ˇ🏀ˇ🏀ˇefg ˇ🏀ˇ🏀ˇefg
@ -1646,6 +1665,8 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
@ -1704,7 +1725,10 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp
#[gpui::test] #[gpui::test]
async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4)
});
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
LanguageConfig::default(), LanguageConfig::default(),
@ -1713,14 +1737,9 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
.with_indents_query(r#"(_ "{" "}" @end) @indent"#) .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
.unwrap(), .unwrap(),
); );
let mut cx = EditorTestContext::new(cx);
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.tab_size = Some(4.try_into().unwrap());
});
});
cx.set_state(indoc! {" cx.set_state(indoc! {"
fn a() { fn a() {
if b { if b {
@ -1741,6 +1760,10 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.tab_size = NonZeroU32::new(4);
});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {" cx.set_state(indoc! {"
@ -1810,13 +1833,12 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |settings| {
cx.update(|cx| { settings.defaults.hard_tabs = Some(true);
cx.update_global::<Settings, _, _>(|settings, _| {
settings.editor_overrides.hard_tabs = Some(true);
});
}); });
let mut cx = EditorTestContext::new(cx);
// select two ranges on one line // select two ranges on one line
cx.set_state(indoc! {" cx.set_state(indoc! {"
«oneˇ» «twoˇ» «oneˇ» «twoˇ»
@ -1907,25 +1929,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
cx.update(|cx| { init_test(cx, |settings| {
cx.set_global( settings.languages.extend([
Settings::test(cx) (
.with_language_defaults( "TOML".into(),
"TOML", LanguageSettingsContent {
EditorSettings { tab_size: NonZeroU32::new(2),
tab_size: Some(2.try_into().unwrap()), ..Default::default()
..Default::default() },
}, ),
) (
.with_language_defaults( "Rust".into(),
"Rust", LanguageSettingsContent {
EditorSettings { tab_size: NonZeroU32::new(4),
tab_size: Some(4.try_into().unwrap()), ..Default::default()
..Default::default() },
}, ),
), ]);
);
}); });
let toml_language = Arc::new(Language::new( let toml_language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
name: "TOML".into(), name: "TOML".into(),
@ -2020,6 +2042,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_backspace(cx: &mut gpui::TestAppContext) { async fn test_backspace(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
// Basic backspace // Basic backspace
@ -2067,8 +2091,9 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_delete(cx: &mut gpui::TestAppContext) { async fn test_delete(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx);
cx.set_state(indoc! {" cx.set_state(indoc! {"
onˇe two three onˇe two three
fou«» five six fou«» five six
@ -2095,7 +2120,8 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_delete_line(cx: &mut TestAppContext) { fn test_delete_line(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2119,7 +2145,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
); );
}); });
cx.update(|cx| cx.set_global(Settings::test(cx)));
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2139,7 +2164,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) { fn test_duplicate_line(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2191,7 +2217,8 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_move_line_up_down(cx: &mut TestAppContext) { fn test_move_line_up_down(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2289,7 +2316,8 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2315,7 +2343,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_transpose(cx: &mut TestAppContext) { fn test_transpose(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
_ = cx _ = cx
.add_window(|cx| { .add_window(|cx| {
@ -2417,6 +2445,8 @@ fn test_transpose(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_clipboard(cx: &mut gpui::TestAppContext) { async fn test_clipboard(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
@ -2497,6 +2527,8 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig::default(), LanguageConfig::default(),
@ -2609,7 +2641,8 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_select_all(cx: &mut TestAppContext) { fn test_select_all(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2625,7 +2658,8 @@ fn test_select_all(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_select_line(cx: &mut TestAppContext) { fn test_select_line(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2671,7 +2705,8 @@ fn test_select_line(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test_split_selection_into_lines(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2741,7 +2776,8 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_add_selection_above_below(cx: &mut TestAppContext) { fn test_add_selection_above_below(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, view) = cx.add_window(|cx| { let (_, view) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
build_editor(buffer, cx) build_editor(buffer, cx)
@ -2935,6 +2971,8 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_select_next(cx: &mut gpui::TestAppContext) { async fn test_select_next(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
cx.set_state("abc\nˇabc abc\ndefabc\nabc"); cx.set_state("abc\nˇabc abc\ndefabc\nabc");
@ -2959,7 +2997,8 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig::default(), LanguageConfig::default(),
Some(tree_sitter_rust::language()), Some(tree_sitter_rust::language()),
@ -3100,7 +3139,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
LanguageConfig { LanguageConfig {
@ -3160,6 +3200,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
@ -3329,6 +3371,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let html_language = Arc::new( let html_language = Arc::new(
@ -3563,6 +3607,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let rust_language = Arc::new( let rust_language = Arc::new(
@ -3660,7 +3706,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
brackets: BracketPairConfig { brackets: BracketPairConfig {
@ -3814,7 +3861,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
brackets: BracketPairConfig { brackets: BracketPairConfig {
@ -3919,7 +3967,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_snippets(cx: &mut gpui::TestAppContext) { async fn test_snippets(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (text, insertion_ranges) = marked_text_ranges( let (text, insertion_ranges) = marked_text_ranges(
indoc! {" indoc! {"
@ -4027,7 +4075,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -4111,16 +4159,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx))); 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 overriden tabsize is sent to language server
cx.update(|cx| { update_test_settings(cx, |settings| {
cx.update_global::<Settings, _, _>(|settings, _| { settings.languages.insert(
settings.language_overrides.insert( "Rust".into(),
"Rust".into(), LanguageSettingsContent {
EditorSettings { tab_size: NonZeroU32::new(8),
tab_size: Some(8.try_into().unwrap()), ..Default::default()
..Default::default() },
}, );
);
})
}); });
let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@ -4141,7 +4187,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -4227,16 +4273,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
assert!(!cx.read(|cx| editor.is_dirty(cx))); 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 overriden tabsize is sent to language server
cx.update(|cx| { update_test_settings(cx, |settings| {
cx.update_global::<Settings, _, _>(|settings, _| { settings.languages.insert(
settings.language_overrides.insert( "Rust".into(),
"Rust".into(), LanguageSettingsContent {
EditorSettings { tab_size: NonZeroU32::new(8),
tab_size: Some(8.try_into().unwrap()), ..Default::default()
..Default::default() },
}, );
);
})
}); });
let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@ -4257,7 +4301,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -4342,7 +4386,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
@ -4399,7 +4443,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
@ -4514,6 +4558,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
#[gpui::test] #[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) { async fn test_completion(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions { completion_provider: Some(lsp::CompletionOptions {
@ -4651,8 +4697,10 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
apply_additional_edits.await.unwrap(); apply_additional_edits.await.unwrap();
cx.update(|cx| { cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| { cx.update_global::<SettingsStore, _, _>(|settings, cx| {
settings.show_completions_on_input = false; settings.update_user_settings::<EditorSettings>(cx, |settings| {
settings.show_completions_on_input = Some(false);
});
}) })
}); });
cx.set_state("editorˇ"); cx.set_state("editorˇ");
@ -4681,7 +4729,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
line_comment: Some("// ".into()), line_comment: Some("// ".into()),
@ -4764,8 +4813,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
let mut cx = EditorTestContext::new(cx); init_test(cx, |_| {});
cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
@ -4778,6 +4826,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let registry = Arc::new(LanguageRegistry::test()); let registry = Arc::new(LanguageRegistry::test());
registry.add(language.clone()); registry.add(language.clone());
let mut cx = EditorTestContext::new(cx);
cx.update_buffer(|buffer, cx| { cx.update_buffer(|buffer, cx| {
buffer.set_language_registry(registry); buffer.set_language_registry(registry);
buffer.set_language(Some(language), cx); buffer.set_language(Some(language), cx);
@ -4897,6 +4946,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
#[gpui::test] #[gpui::test]
async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let html_language = Arc::new( let html_language = Arc::new(
@ -5021,7 +5072,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let multibuffer = cx.add_model(|cx| { let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0); let mut multibuffer = MultiBuffer::new(0);
@ -5067,7 +5119,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let markers = vec![('[', ']').into(), ('(', ')').into()]; let markers = vec![('[', ']').into(), ('(', ')').into()];
let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
indoc! {" indoc! {"
@ -5140,7 +5193,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_refresh_selections(cx: &mut TestAppContext) { fn test_refresh_selections(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let mut excerpt1_id = None; let mut excerpt1_id = None;
let multibuffer = cx.add_model(|cx| { let multibuffer = cx.add_model(|cx| {
@ -5224,7 +5278,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
let mut excerpt1_id = None; let mut excerpt1_id = None;
let multibuffer = cx.add_model(|cx| { let multibuffer = cx.add_model(|cx| {
@ -5282,7 +5337,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
LanguageConfig { LanguageConfig {
@ -5355,7 +5411,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
fn test_highlighted_ranges(cx: &mut TestAppContext) { fn test_highlighted_ranges(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
build_editor(buffer.clone(), cx) build_editor(buffer.clone(), cx)
@ -5395,7 +5452,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
let mut highlighted_ranges = editor.background_highlights_in_range( let mut highlighted_ranges = editor.background_highlights_in_range(
anchor_range(Point::new(3, 4)..Point::new(7, 4)), anchor_range(Point::new(3, 4)..Point::new(7, 4)),
&snapshot, &snapshot,
cx.global::<Settings>().theme.as_ref(), theme::current(cx).as_ref(),
); );
// Enforce a consistent ordering based on color without relying on the ordering of the // Enforce a consistent ordering based on color without relying on the ordering of the
// highlight's `TypeId` which is non-deterministic. // highlight's `TypeId` which is non-deterministic.
@ -5425,7 +5482,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
editor.background_highlights_in_range( editor.background_highlights_in_range(
anchor_range(Point::new(5, 6)..Point::new(6, 4)), anchor_range(Point::new(5, 6)..Point::new(6, 4)),
&snapshot, &snapshot,
cx.global::<Settings>().theme.as_ref(), theme::current(cx).as_ref(),
), ),
&[( &[(
DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5), DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
@ -5437,7 +5494,8 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_following(cx: &mut gpui::TestAppContext) { async fn test_following(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx); init_test(cx, |_| {});
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
@ -5459,10 +5517,12 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
}); });
let is_still_following = Rc::new(RefCell::new(true)); let is_still_following = Rc::new(RefCell::new(true));
let follower_edit_event_count = Rc::new(RefCell::new(0));
let pending_update = Rc::new(RefCell::new(None)); let pending_update = Rc::new(RefCell::new(None));
follower.update(cx, { follower.update(cx, {
let update = pending_update.clone(); let update = pending_update.clone();
let is_still_following = is_still_following.clone(); let is_still_following = is_still_following.clone();
let follower_edit_event_count = follower_edit_event_count.clone();
|_, cx| { |_, cx| {
cx.subscribe(&leader, move |_, leader, event, cx| { cx.subscribe(&leader, move |_, leader, event, cx| {
leader leader
@ -5475,6 +5535,9 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
if Editor::should_unfollow_on_event(event, cx) { if Editor::should_unfollow_on_event(event, cx) {
*is_still_following.borrow_mut() = false; *is_still_following.borrow_mut() = false;
} }
if let Event::BufferEdited = event {
*follower_edit_event_count.borrow_mut() += 1;
}
}) })
.detach(); .detach();
} }
@ -5494,6 +5557,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
assert_eq!(follower.selections.ranges(cx), vec![1..1]); assert_eq!(follower.selections.ranges(cx), vec![1..1]);
}); });
assert_eq!(*is_still_following.borrow(), true); assert_eq!(*is_still_following.borrow(), true);
assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the scroll position only // Update the scroll position only
leader.update(cx, |leader, cx| { leader.update(cx, |leader, cx| {
@ -5510,6 +5574,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
vec2f(1.5, 3.5) vec2f(1.5, 3.5)
); );
assert_eq!(*is_still_following.borrow(), true); assert_eq!(*is_still_following.borrow(), true);
assert_eq!(*follower_edit_event_count.borrow(), 0);
// Update the selections and scroll position. The follower's scroll position is updated // Update the selections and scroll position. The follower's scroll position is updated
// via autoscroll, not via the leader's exact scroll position. // via autoscroll, not via the leader's exact scroll position.
@ -5576,7 +5641,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
#[gpui::test] #[gpui::test]
async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx); init_test(cx, |_| {});
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@ -5805,6 +5871,8 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
#[gpui::test] #[gpui::test]
async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) { async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx); let mut cx = EditorTestContext::new(cx);
let diff_base = r#" let diff_base = r#"
@ -5924,6 +5992,8 @@ fn test_split_words() {
#[gpui::test] #[gpui::test]
async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
let mut assert = |before, after| { let mut assert = |before, after| {
let _state_context = cx.set_state(before); let _state_context = cx.set_state(before);
@ -5972,6 +6042,8 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) { async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let (copilot, copilot_lsp) = Copilot::fake(cx); let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot)); cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
@ -6223,6 +6295,8 @@ async fn test_copilot_completion_invalidation(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
init_test(cx, |_| {});
let (copilot, copilot_lsp) = Copilot::fake(cx); let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot)); cx.update(|cx| cx.set_global(copilot));
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
@ -6288,11 +6362,10 @@ async fn test_copilot_multibuffer(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
init_test(cx, |_| {});
let (copilot, copilot_lsp) = Copilot::fake(cx); let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| { cx.update(|cx| cx.set_global(copilot));
cx.set_global(Settings::test(cx));
cx.set_global(copilot)
});
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
@ -6392,14 +6465,16 @@ async fn test_copilot_disabled_globs(
deterministic: Arc<Deterministic>, deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
let (copilot, copilot_lsp) = Copilot::fake(cx); init_test(cx, |settings| {
cx.update(|cx| { settings
let mut settings = Settings::test(cx); .copilot
settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()]; .get_or_insert(Default::default())
cx.set_global(settings); .disabled_globs = Some(vec![".env*".to_string()]);
cx.set_global(copilot)
}); });
let (copilot, copilot_lsp) = Copilot::fake(cx);
cx.update(|cx| cx.set_global(copilot));
let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.background());
fs.insert_tree( fs.insert_tree(
"/test", "/test",
@ -6596,3 +6671,30 @@ fn handle_copilot_completion_request(
} }
}); });
} }
pub(crate) fn update_test_settings(
cx: &mut TestAppContext,
f: impl Fn(&mut AllLanguageSettingsContent),
) {
cx.update(|cx| {
cx.update_global::<SettingsStore, _, _>(|store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, f);
});
});
}
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
crate::init(cx);
});
update_test_settings(cx, f);
}

View file

@ -5,6 +5,7 @@ use super::{
}; };
use crate::{ use crate::{
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
editor_settings::ShowScrollbars,
git::{diff_hunk_to_display, DisplayDiffHunk}, git::{diff_hunk_to_display, DisplayDiffHunk},
hover_popover::{ hover_popover::{
hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@ -13,7 +14,7 @@ use crate::{
link_go_to_definition::{ link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
}, },
mouse_context_menu, EditorStyle, GutterHover, UnfoldAt, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
}; };
use clock::ReplicaId; use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
@ -35,9 +36,11 @@ use gpui::{
}; };
use itertools::Itertools; use itertools::Itertools;
use json::json; use json::json;
use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection}; use language::{
language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
Selection,
};
use project::ProjectPath; use project::ProjectPath;
use settings::{GitGutter, Settings, ShowWhitespaces};
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -47,7 +50,8 @@ use std::{
ops::Range, ops::Range,
sync::Arc, sync::Arc,
}; };
use workspace::item::Item; use text::Point;
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
enum FoldMarkers {} enum FoldMarkers {}
@ -547,11 +551,11 @@ impl EditorElement {
let scroll_top = scroll_position.y() * line_height; let scroll_top = scroll_position.y() * line_height;
let show_gutter = matches!( let show_gutter = matches!(
&cx.global::<Settings>() settings::get::<WorkspaceSettings>(cx)
.git_overrides .git
.git_gutter .git_gutter
.unwrap_or_default(), .unwrap_or_default(),
GitGutter::TrackedFiles GitGutterSetting::TrackedFiles
); );
if show_gutter { if show_gutter {
@ -608,7 +612,7 @@ impl EditorElement {
layout: &mut LayoutState, layout: &mut LayoutState,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) { ) {
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone(); let diff_style = &theme::current(cx).editor.diff.clone();
let line_height = layout.position_map.line_height; let line_height = layout.position_map.line_height;
let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_position = layout.position_map.snapshot.scroll_position();
@ -648,7 +652,7 @@ impl EditorElement {
//TODO: This rendering is entirely a horrible hack //TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => { DiffHunkStatus::Removed => {
let row = *display_row_range.start(); let row = display_row_range.start;
let offset = line_height / 2.; let offset = line_height / 2.;
let start_y = row as f32 * line_height - offset - scroll_top; let start_y = row as f32 * line_height - offset - scroll_top;
@ -670,11 +674,11 @@ impl EditorElement {
} }
}; };
let start_row = *display_row_range.start(); let start_row = display_row_range.start;
let end_row = *display_row_range.end(); let end_row = display_row_range.end;
let start_y = start_row as f32 * line_height - scroll_top; let start_y = start_row as f32 * line_height - scroll_top;
let end_y = end_row as f32 * line_height - scroll_top + line_height; let end_y = end_row as f32 * line_height - scroll_top;
let width = diff_style.width_em * line_height; let width = diff_style.width_em * line_height;
let highlight_origin = bounds.origin() + vec2f(-width, start_y); let highlight_origin = bounds.origin() + vec2f(-width, start_y);
@ -708,6 +712,7 @@ impl EditorElement {
let scroll_left = scroll_position.x() * max_glyph_width; let scroll_left = scroll_position.x() * max_glyph_width;
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
let line_end_overshoot = 0.15 * layout.position_map.line_height; let line_end_overshoot = 0.15 * layout.position_map.line_height;
let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
scene.push_layer(Some(bounds)); scene.push_layer(Some(bounds));
@ -882,9 +887,10 @@ impl EditorElement {
content_origin, content_origin,
scroll_left, scroll_left,
visible_text_bounds, visible_text_bounds,
cx, whitespace_setting,
&invisible_display_ranges, &invisible_display_ranges,
visible_bounds, visible_bounds,
cx,
) )
} }
} }
@ -1022,15 +1028,16 @@ impl EditorElement {
let mut first_row_y_offset = 0.0; let mut first_row_y_offset = 0.0;
// Impose a minimum height on the scrollbar thumb // Impose a minimum height on the scrollbar thumb
let row_height = height / max_row;
let min_thumb_height = let min_thumb_height =
style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
let thumb_height = (row_range.end - row_range.start) * height / max_row; let thumb_height = (row_range.end - row_range.start) * row_height;
if thumb_height < min_thumb_height { if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0; first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height; height -= min_thumb_height - thumb_height;
} }
let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row }; let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
let thumb_top = y_for_row(row_range.start) - first_row_y_offset; let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
@ -1044,6 +1051,54 @@ impl EditorElement {
background: style.track.background_color, background: style.track.background_color,
..Default::default() ..Default::default()
}); });
let diff_style = theme::current(cx).editor.diff.clone();
for hunk in layout
.position_map
.snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
{
let start_display = Point::new(hunk.buffer_range.start, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.buffer_range.end, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < 1. {
end_y = start_y + 1.;
}
let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
let color = match hunk.status() {
DiffHunkStatus::Added => diff_style.inserted,
DiffHunkStatus::Modified => diff_style.modified,
DiffHunkStatus::Removed => diff_style.deleted,
};
let border = Border {
width: 1.,
color: style.thumb.border.color,
overlay: false,
top: false,
right: true,
bottom: false,
left: true,
};
scene.push_quad(Quad {
bounds,
background: Some(color),
border,
corner_radius: style.thumb.corner_radius,
})
}
scene.push_quad(Quad { scene.push_quad(Quad {
bounds: thumb_bounds, bounds: thumb_bounds,
border: style.thumb.border, border: style.thumb.border,
@ -1219,7 +1274,7 @@ impl EditorElement {
.row; .row;
buffer_snapshot buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row, false) .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.map(|hunk| diff_hunk_to_display(hunk, snapshot)) .map(|hunk| diff_hunk_to_display(hunk, snapshot))
.dedup() .dedup()
.collect() .collect()
@ -1412,7 +1467,7 @@ impl EditorElement {
editor: &mut Editor, editor: &mut Editor,
cx: &mut LayoutContext<Editor>, cx: &mut LayoutContext<Editor>,
) -> (f32, Vec<BlockLayout>) { ) -> (f32, Vec<BlockLayout>) {
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone(); let tooltip_style = theme::current(cx).tooltip.clone();
let scroll_x = snapshot.scroll_anchor.offset.x(); let scroll_x = snapshot.scroll_anchor.offset.x();
let (fixed_blocks, non_fixed_blocks) = snapshot let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone()) .blocks_in_range(rows.clone())
@ -1738,9 +1793,10 @@ impl LineWithInvisibles {
content_origin: Vector2F, content_origin: Vector2F,
scroll_left: f32, scroll_left: f32,
visible_text_bounds: RectF, visible_text_bounds: RectF,
cx: &mut ViewContext<Editor>, whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>], selection_ranges: &[Range<DisplayPoint>],
visible_bounds: RectF, visible_bounds: RectF,
cx: &mut ViewContext<Editor>,
) { ) {
let line_height = layout.position_map.line_height; let line_height = layout.position_map.line_height;
let line_y = row as f32 * line_height - scroll_top; let line_y = row as f32 * line_height - scroll_top;
@ -1754,7 +1810,6 @@ impl LineWithInvisibles {
); );
self.draw_invisibles( self.draw_invisibles(
cx,
&selection_ranges, &selection_ranges,
layout, layout,
content_origin, content_origin,
@ -1764,12 +1819,13 @@ impl LineWithInvisibles {
scene, scene,
visible_bounds, visible_bounds,
line_height, line_height,
whitespace_setting,
cx,
); );
} }
fn draw_invisibles( fn draw_invisibles(
&self, &self,
cx: &mut ViewContext<Editor>,
selection_ranges: &[Range<DisplayPoint>], selection_ranges: &[Range<DisplayPoint>],
layout: &LayoutState, layout: &LayoutState,
content_origin: Vector2F, content_origin: Vector2F,
@ -1779,17 +1835,13 @@ impl LineWithInvisibles {
scene: &mut SceneBuilder, scene: &mut SceneBuilder,
visible_bounds: RectF, visible_bounds: RectF,
line_height: f32, line_height: f32,
whitespace_setting: ShowWhitespaceSetting,
cx: &mut ViewContext<Editor>,
) { ) {
let settings = cx.global::<Settings>(); let allowed_invisibles_regions = match whitespace_setting {
let allowed_invisibles_regions = match settings ShowWhitespaceSetting::None => return,
.editor_overrides ShowWhitespaceSetting::Selection => Some(selection_ranges),
.show_whitespaces ShowWhitespaceSetting::All => None,
.or(settings.editor_defaults.show_whitespaces)
.unwrap_or_default()
{
ShowWhitespaces::None => return,
ShowWhitespaces::Selection => Some(selection_ranges),
ShowWhitespaces::All => None,
}; };
for invisible in &self.invisibles { for invisible in &self.invisibles {
@ -1934,11 +1986,11 @@ impl Element<Editor> for EditorElement {
let is_singleton = editor.is_singleton(cx); let is_singleton = editor.is_singleton(cx);
let highlighted_rows = editor.highlighted_rows(); let highlighted_rows = editor.highlighted_rows();
let theme = cx.global::<Settings>().theme.as_ref(); let theme = theme::current(cx);
let highlighted_ranges = editor.background_highlights_in_range( let highlighted_ranges = editor.background_highlights_in_range(
start_anchor..end_anchor, start_anchor..end_anchor,
&snapshot.display_snapshot, &snapshot.display_snapshot,
theme, theme.as_ref(),
); );
fold_ranges.extend( fold_ranges.extend(
@ -2013,7 +2065,15 @@ impl Element<Editor> for EditorElement {
)); ));
} }
let show_scrollbars = editor.scroll_manager.scrollbars_visible(); let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
ShowScrollbars::Auto => {
snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
}
ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
ShowScrollbars::Always => true,
ShowScrollbars::Never => false,
};
let include_root = editor let include_root = editor
.project .project
.as_ref() .as_ref()
@ -2773,17 +2833,19 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
display_map::{BlockDisposition, BlockProperties}, display_map::{BlockDisposition, BlockProperties},
editor_tests::{init_test, update_test_settings},
Editor, MultiBuffer, Editor, MultiBuffer,
}; };
use gpui::TestAppContext; use gpui::TestAppContext;
use language::language_settings;
use log::info; use log::info;
use settings::Settings;
use std::{num::NonZeroU32, sync::Arc}; use std::{num::NonZeroU32, sync::Arc};
use util::test::sample_text; use util::test::sample_text;
#[gpui::test] #[gpui::test]
fn test_layout_line_numbers(cx: &mut TestAppContext) { fn test_layout_line_numbers(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
@ -2801,7 +2863,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
cx.update(|cx| cx.set_global(Settings::test(cx))); init_test(cx, |_| {});
let (_, editor) = cx.add_window(|cx| { let (_, editor) = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("", cx); let buffer = MultiBuffer::build_simple("", cx);
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
@ -2861,26 +2924,27 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_all_invisibles_drawing(cx: &mut TestAppContext) { fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
let tab_size = 4; const TAB_SIZE: u32 = 4;
let input_text = "\t \t|\t| a b"; let input_text = "\t \t|\t| a b";
let expected_invisibles = vec![ let expected_invisibles = vec![
Invisible::Tab { Invisible::Tab {
line_start_offset: 0, line_start_offset: 0,
}, },
Invisible::Whitespace { Invisible::Whitespace {
line_offset: tab_size as usize, line_offset: TAB_SIZE as usize,
}, },
Invisible::Tab { Invisible::Tab {
line_start_offset: tab_size as usize + 1, line_start_offset: TAB_SIZE as usize + 1,
}, },
Invisible::Tab { Invisible::Tab {
line_start_offset: tab_size as usize * 2 + 1, line_start_offset: TAB_SIZE as usize * 2 + 1,
}, },
Invisible::Whitespace { Invisible::Whitespace {
line_offset: tab_size as usize * 3 + 1, line_offset: TAB_SIZE as usize * 3 + 1,
}, },
Invisible::Whitespace { Invisible::Whitespace {
line_offset: tab_size as usize * 3 + 3, line_offset: TAB_SIZE as usize * 3 + 3,
}, },
]; ];
assert_eq!( assert_eq!(
@ -2892,12 +2956,11 @@ mod tests {
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'" "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
); );
cx.update(|cx| { init_test(cx, |s| {
let mut test_settings = Settings::test(cx); s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
cx.set_global(test_settings);
}); });
let actual_invisibles = let actual_invisibles =
collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
@ -2906,11 +2969,9 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
cx.update(|cx| { init_test(cx, |s| {
let mut test_settings = Settings::test(cx); s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); s.defaults.tab_size = NonZeroU32::new(4);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
cx.set_global(test_settings);
}); });
for editor_mode_without_invisibles in [ for editor_mode_without_invisibles in [
@ -2961,19 +3022,18 @@ mod tests {
); );
info!("Expected invisibles: {expected_invisibles:?}"); info!("Expected invisibles: {expected_invisibles:?}");
init_test(cx, |_| {});
// Put the same string with repeating whitespace pattern into editors of various size, // Put the same string with repeating whitespace pattern into editors of various size,
// take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
let resize_step = 10.0; let resize_step = 10.0;
let mut editor_width = 200.0; let mut editor_width = 200.0;
while editor_width <= 1000.0 { while editor_width <= 1000.0 {
cx.update(|cx| { update_test_settings(cx, |s| {
let mut test_settings = Settings::test(cx); s.defaults.tab_size = NonZeroU32::new(tab_size);
test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap()); s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); s.defaults.preferred_line_length = Some(editor_width as u32);
test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32); s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
test_settings.editor_defaults.soft_wrap =
Some(settings::SoftWrap::PreferredLineLength);
cx.set_global(test_settings);
}); });
let actual_invisibles = let actual_invisibles =
@ -3021,7 +3081,7 @@ mod tests {
let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
let (_, layout_state) = editor.update(cx, |editor, cx| { let (_, layout_state) = editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_wrap_width(Some(editor_width), cx); editor.set_wrap_width(Some(editor_width), cx);
let mut new_parents = Default::default(); let mut new_parents = Default::default();

View file

@ -1,4 +1,4 @@
use std::ops::RangeInclusive; use std::ops::Range;
use git::diff::{DiffHunk, DiffHunkStatus}; use git::diff::{DiffHunk, DiffHunkStatus};
use language::Point; use language::Point;
@ -15,7 +15,7 @@ pub enum DisplayDiffHunk {
}, },
Unfolded { Unfolded {
display_row_range: RangeInclusive<u32>, display_row_range: Range<u32>,
status: DiffHunkStatus, status: DiffHunkStatus,
}, },
} }
@ -26,7 +26,7 @@ impl DisplayDiffHunk {
&DisplayDiffHunk::Folded { display_row } => display_row, &DisplayDiffHunk::Folded { display_row } => display_row,
DisplayDiffHunk::Unfolded { DisplayDiffHunk::Unfolded {
display_row_range, .. display_row_range, ..
} => *display_row_range.start(), } => display_row_range.start,
} }
} }
@ -36,7 +36,7 @@ impl DisplayDiffHunk {
DisplayDiffHunk::Unfolded { DisplayDiffHunk::Unfolded {
display_row_range, .. display_row_range, ..
} => display_row_range.clone(), } => display_row_range.start..=display_row_range.end - 1,
}; };
range.contains(&display_row) range.contains(&display_row)
@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
} else { } else {
let start = hunk_start_point.to_display_point(snapshot).row(); let start = hunk_start_point.to_display_point(snapshot).row();
let hunk_end_row_inclusive = hunk let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
.buffer_range
.end
.saturating_sub(1)
.max(hunk.buffer_range.start);
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0); let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
let end = hunk_end_point.to_display_point(snapshot).row(); let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded { DisplayDiffHunk::Unfolded {
display_row_range: start..=end, display_row_range: start..end,
status: hunk.status(), status: hunk.status(),
} }
} }

View file

@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::editor_lsp_test_context::EditorLspTestContext; use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc; use indoc::indoc;
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
#[gpui::test] #[gpui::test]
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new( let mut cx = EditorLspTestContext::new(
Language::new( Language::new(
LanguageConfig { LanguageConfig {

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot, display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
EditorStyle, RangeToAnchorExt, EditorSnapshot, EditorStyle, RangeToAnchorExt,
}; };
use futures::FutureExt; use futures::FutureExt;
use gpui::{ use gpui::{
@ -12,7 +12,6 @@ use gpui::{
}; };
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, Project}; use project::{HoverBlock, HoverBlockKind, Project};
use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration}; use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt; use util::TryFutureExt;
@ -38,7 +37,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// The internal hover action dispatches between `show_hover` or `hide_hover` /// The internal hover action dispatches between `show_hover` or `hide_hover`
/// depending on whether a point to hover over is provided. /// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) { pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
if cx.global::<Settings>().hover_popover_enabled { if settings::get::<EditorSettings>(cx).hover_popover_enabled {
if let Some(point) = point { if let Some(point) = point {
show_hover(editor, point, false, cx); show_hover(editor, point, false, cx);
} else { } else {
@ -654,7 +653,7 @@ impl DiagnosticPopover {
_ => style.hover_popover.container, _ => style.hover_popover.container,
}; };
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone(); let tooltip_style = theme::current(cx).tooltip.clone();
MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| { MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
text.with_soft_wrap(true) text.with_soft_wrap(true)
@ -694,7 +693,7 @@ impl DiagnosticPopover {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::editor_lsp_test_context::EditorLspTestContext; use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use gpui::fonts::Weight; use gpui::fonts::Weight;
use indoc::indoc; use indoc::indoc;
use language::{Diagnostic, DiagnosticSet}; use language::{Diagnostic, DiagnosticSet};
@ -706,6 +705,8 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@ -773,6 +774,8 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@ -816,6 +819,8 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@ -882,7 +887,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_render_blocks(cx: &mut gpui::TestAppContext) { fn test_render_blocks(cx: &mut gpui::TestAppContext) {
Settings::test_async(cx); init_test(cx, |_| {});
cx.add_window(|cx| { cx.add_window(|cx| {
let editor = Editor::single_line(None, cx); let editor = Editor::single_line(None, cx);
let style = editor.style(cx); let style = editor.style(cx);
@ -1006,8 +1012,7 @@ mod tests {
.zip(expected_styles.iter().cloned()) .zip(expected_styles.iter().cloned())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
rendered.text, rendered.text, expected_text,
dbg!(expected_text),
"wrong text for input {blocks:?}" "wrong text for input {blocks:?}"
); );
assert_eq!( assert_eq!(

View file

@ -16,7 +16,6 @@ use language::{
}; };
use project::{FormatTrigger, Item as _, Project, ProjectPath}; use project::{FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@ -27,7 +26,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use text::Selection; use text::Selection;
use util::{ResultExt, TryFutureExt}; use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowableItemHandle};
use workspace::{ use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
@ -566,7 +565,7 @@ impl Item for Editor {
cx: &AppContext, cx: &AppContext,
) -> AnyElement<T> { ) -> AnyElement<T> {
Flex::row() Flex::row()
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).aligned()) .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
.with_children(detail.and_then(|detail| { .with_children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?; let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy(); let description = path.to_string_lossy();
@ -580,6 +579,7 @@ impl Item for Editor {
.aligned(), .aligned(),
) )
})) }))
.align_children_center()
.into_any() .into_any()
} }
@ -636,7 +636,7 @@ impl Item for Editor {
project: ModelHandle<Project>, project: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
self.report_editor_event("save", cx); self.report_editor_event("save", None, cx);
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx); let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = self.buffer().clone().read(cx).all_buffers();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
@ -685,6 +685,11 @@ impl Item for Editor {
.as_singleton() .as_singleton()
.expect("cannot call save_as on an excerpt list"); .expect("cannot call save_as on an excerpt list");
let file_extension = abs_path
.extension()
.map(|a| a.to_string_lossy().to_string());
self.report_editor_event("save", file_extension, cx);
project.update(cx, |project, cx| { project.update(cx, |project, cx| {
project.save_buffer_as(buffer, abs_path, cx) project.save_buffer_as(buffer, abs_path, cx)
}) })
@ -1110,8 +1115,12 @@ impl View for CursorPosition {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
if let Some(position) = self.position { if let Some(position) = self.position {
let theme = &cx.global::<Settings>().theme.workspace.status_bar; let theme = &theme::current(cx).workspace.status_bar;
let mut text = format!("{},{}", position.row + 1, position.column + 1); let mut text = format!(
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
position.row + 1,
position.column + 1
);
if self.selected_count > 0 { if self.selected_count > 0 {
write!(text, " ({} selected)", self.selected_count).unwrap(); write!(text, " ({} selected)", self.selected_count).unwrap();
} }

View file

@ -1,10 +1,8 @@
use std::ops::Range;
use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
use gpui::{Task, ViewContext}; use gpui::{Task, ViewContext};
use language::{Bias, ToOffset}; use language::{Bias, ToOffset};
use project::LocationLink; use project::LocationLink;
use settings::Settings; use std::ops::Range;
use util::TryFutureExt; use util::TryFutureExt;
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -211,7 +209,7 @@ pub fn show_link_definition(
}); });
// Highlight symbol using theme link definition highlight style // Highlight symbol using theme link definition highlight style
let style = cx.global::<Settings>().theme.editor.link_definition; let style = theme::current(cx).editor.link_definition;
this.highlight_text::<LinkGoToDefinitionState>( this.highlight_text::<LinkGoToDefinitionState>(
vec![highlight_range], vec![highlight_range],
style, style,
@ -297,6 +295,8 @@ fn go_to_fetched_definition_of_kind(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{
platform::{self, Modifiers, ModifiersChangedEvent}, platform::{self, Modifiers, ModifiersChangedEvent},
@ -305,12 +305,10 @@ mod tests {
use indoc::indoc; use indoc::indoc;
use lsp::request::{GotoDefinition, GotoTypeDefinition}; use lsp::request::{GotoDefinition, GotoTypeDefinition};
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*;
#[gpui::test] #[gpui::test]
async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@ -417,6 +415,8 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),

View file

@ -57,13 +57,14 @@ pub fn deploy_context_menu(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::test::editor_lsp_test_context::EditorLspTestContext;
use super::*; use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc; use indoc::indoc;
#[gpui::test] #[gpui::test]
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),

View file

@ -369,11 +369,12 @@ pub fn split_display_range_by_lines(
mod tests { mod tests {
use super::*; use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
use settings::Settings; use settings::SettingsStore;
#[gpui::test] #[gpui::test]
fn test_previous_word_start(cx: &mut gpui::AppContext) { fn test_previous_word_start(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) { fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -400,7 +401,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::AppContext) { fn test_previous_subword_start(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) { fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -434,7 +436,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert( fn assert(
marked_text: &str, marked_text: &str,
cx: &mut gpui::AppContext, cx: &mut gpui::AppContext,
@ -466,7 +469,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_next_word_end(cx: &mut gpui::AppContext) { fn test_next_word_end(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) { fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -490,7 +494,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_next_subword_end(cx: &mut gpui::AppContext) { fn test_next_subword_end(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) { fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -523,7 +528,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_find_boundary(cx: &mut gpui::AppContext) { fn test_find_boundary(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert( fn assert(
marked_text: &str, marked_text: &str,
cx: &mut gpui::AppContext, cx: &mut gpui::AppContext,
@ -555,7 +561,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_surrounding_word(cx: &mut gpui::AppContext) { fn test_surrounding_word(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) { fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -576,7 +583,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_test(cx);
let family_id = cx let family_id = cx
.font_cache() .font_cache()
.load_family(&["Helvetica"], &Default::default()) .load_family(&["Helvetica"], &Default::default())
@ -691,4 +699,11 @@ mod tests {
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
); );
} }
fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
crate::init(cx);
}
} }

View file

@ -9,7 +9,9 @@ use git::diff::DiffHunk;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
pub use language::Completion; pub use language::Completion;
use language::{ use language::{
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, char_kind,
language_settings::{language_settings, LanguageSettings},
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
@ -1165,6 +1167,9 @@ impl MultiBuffer {
) { ) {
self.sync(cx); self.sync(cx);
let ids = excerpt_ids.into_iter().collect::<Vec<_>>(); let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
if ids.is_empty() {
return;
}
let mut buffers = self.buffers.borrow_mut(); let mut buffers = self.buffers.borrow_mut();
let mut snapshot = self.snapshot.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut();
@ -1372,6 +1377,15 @@ impl MultiBuffer {
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
} }
pub fn settings_at<'a, T: ToOffset>(
&self,
point: T,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(point, cx);
language_settings(language.map(|l| l.name()).as_deref(), cx)
}
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) { pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
self.buffers self.buffers
.borrow() .borrow()
@ -2764,6 +2778,16 @@ impl MultiBufferSnapshot {
.and_then(|(buffer, offset)| buffer.language_at(offset)) .and_then(|(buffer, offset)| buffer.language_at(offset))
} }
pub fn settings_at<'a, T: ToOffset>(
&'a self,
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))
}
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> { pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
self.point_to_buffer_offset(point) self.point_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_scope_at(offset)) .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
@ -2817,20 +2841,15 @@ impl MultiBufferSnapshot {
}) })
} }
pub fn git_diff_hunks_in_range<'a>( pub fn git_diff_hunks_in_range_rev<'a>(
&'a self, &'a self,
row_range: Range<u32>, row_range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> { ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.excerpts.cursor::<Point>(); let mut cursor = self.excerpts.cursor::<Point>();
if reversed { cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &()); if cursor.item().is_none() {
if cursor.item().is_none() { cursor.prev(&());
cursor.prev(&());
}
} else {
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
} }
std::iter::from_fn(move || { std::iter::from_fn(move || {
@ -2860,7 +2879,7 @@ impl MultiBufferSnapshot {
let buffer_hunks = excerpt let buffer_hunks = excerpt
.buffer .buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed) .git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
.filter_map(move |hunk| { .filter_map(move |hunk| {
let start = multibuffer_start.row let start = multibuffer_start.row
+ hunk + hunk
@ -2880,12 +2899,70 @@ impl MultiBufferSnapshot {
}) })
}); });
if reversed { cursor.prev(&());
cursor.prev(&());
} else { Some(buffer_hunks)
cursor.next(&()); })
.flatten()
}
pub fn git_diff_hunks_in_range<'a>(
&'a self,
row_range: Range<u32>,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.excerpts.cursor::<Point>();
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
std::iter::from_fn(move || {
let excerpt = cursor.item()?;
let multibuffer_start = *cursor.start();
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
if multibuffer_start.row >= row_range.end {
return None;
} }
let mut buffer_start = excerpt.range.context.start;
let mut buffer_end = excerpt.range.context.end;
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
if row_range.start > multibuffer_start.row {
let buffer_start_point =
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
}
if row_range.end < multibuffer_end.row {
let buffer_end_point =
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
}
let buffer_hunks = excerpt
.buffer
.git_diff_hunks_intersecting_range(buffer_start..buffer_end)
.filter_map(move |hunk| {
let start = multibuffer_start.row
+ hunk
.buffer_range
.start
.saturating_sub(excerpt_start_point.row);
let end = multibuffer_start.row
+ hunk
.buffer_range
.end
.min(excerpt_end_point.row + 1)
.saturating_sub(excerpt_start_point.row);
Some(DiffHunk {
buffer_range: start..end,
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
})
});
cursor.next(&());
Some(buffer_hunks) Some(buffer_hunks)
}) })
.flatten() .flatten()
@ -3785,10 +3862,9 @@ mod tests {
use gpui::{AppContext, TestAppContext}; use gpui::{AppContext, TestAppContext};
use language::{Buffer, Rope}; use language::{Buffer, Rope};
use rand::prelude::*; use rand::prelude::*;
use settings::Settings; use settings::SettingsStore;
use std::{env, rc::Rc}; use std::{env, rc::Rc};
use unindent::Unindent; use unindent::Unindent;
use util::test::sample_text; use util::test::sample_text;
#[gpui::test] #[gpui::test]
@ -4080,19 +4156,25 @@ mod tests {
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let follower_edit_event_count = Rc::new(RefCell::new(0));
follower_multibuffer.update(cx, |_, cx| { follower_multibuffer.update(cx, |_, cx| {
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| { let follower_edit_event_count = follower_edit_event_count.clone();
match event.clone() { cx.subscribe(
&leader_multibuffer,
move |follower, _, event, cx| match event.clone() {
Event::ExcerptsAdded { Event::ExcerptsAdded {
buffer, buffer,
predecessor, predecessor,
excerpts, excerpts,
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
Event::Edited => {
*follower_edit_event_count.borrow_mut() += 1;
}
_ => {} _ => {}
} },
}) )
.detach(); .detach();
}); });
@ -4131,6 +4213,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(), leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(),
); );
assert_eq!(*follower_edit_event_count.borrow(), 2);
leader_multibuffer.update(cx, |leader, cx| { leader_multibuffer.update(cx, |leader, cx| {
let excerpt_ids = leader.excerpt_ids(); let excerpt_ids = leader.excerpt_ids();
@ -4140,6 +4223,27 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(), leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(),
); );
assert_eq!(*follower_edit_event_count.borrow(), 3);
// Removing an empty set of excerpts is a noop.
leader_multibuffer.update(cx, |leader, cx| {
leader.remove_excerpts([], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 3);
// Adding an empty set of excerpts is a noop.
leader_multibuffer.update(cx, |leader, cx| {
leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
});
assert_eq!(
leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(),
);
assert_eq!(*follower_edit_event_count.borrow(), 3);
leader_multibuffer.update(cx, |leader, cx| { leader_multibuffer.update(cx, |leader, cx| {
leader.clear(cx); leader.clear(cx);
@ -4148,6 +4252,7 @@ mod tests {
leader_multibuffer.read(cx).snapshot(cx).text(), leader_multibuffer.read(cx).snapshot(cx).text(),
follower_multibuffer.read(cx).snapshot(cx).text(), follower_multibuffer.read(cx).snapshot(cx).text(),
); );
assert_eq!(*follower_edit_event_count.borrow(), 4);
} }
#[gpui::test] #[gpui::test]
@ -4595,7 +4700,7 @@ mod tests {
assert_eq!( assert_eq!(
snapshot snapshot
.git_diff_hunks_in_range(0..12, false) .git_diff_hunks_in_range(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range)) .map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&expected, &expected,
@ -4603,7 +4708,7 @@ mod tests {
assert_eq!( assert_eq!(
snapshot snapshot
.git_diff_hunks_in_range(0..12, true) .git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range)) .map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
expected expected
@ -5034,7 +5139,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_history(cx: &mut AppContext) { fn test_history(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); cx.set_global(SettingsStore::test(cx));
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

View file

@ -34,13 +34,17 @@ impl<'a> EditorLspTestContext<'a> {
) -> EditorLspTestContext<'a> { ) -> EditorLspTestContext<'a> {
use json::json; use json::json;
let app_state = cx.update(AppState::test);
cx.update(|cx| { cx.update(|cx| {
theme::init((), cx);
language::init(cx);
crate::init(cx); crate::init(cx);
pane::init(cx); pane::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
}); });
let app_state = cx.update(AppState::test);
let file_name = format!( let file_name = format!(
"file.{}", "file.{}",
language language

View file

@ -1,19 +1,16 @@
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use futures::Future;
use gpui::{
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{Buffer, BufferSnapshot};
use std::{ use std::{
any::TypeId, any::TypeId,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
}; };
use futures::Future;
use indoc::indoc;
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use gpui::{
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
};
use language::{Buffer, BufferSnapshot};
use settings::Settings;
use util::{ use util::{
assert_set_eq, assert_set_eq,
test::{generate_marked_text, marked_text_ranges}, test::{generate_marked_text, marked_text_ranges},
@ -30,15 +27,10 @@ pub struct EditorTestContext<'a> {
impl<'a> EditorTestContext<'a> { impl<'a> EditorTestContext<'a> {
pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let (window_id, editor) = cx.update(|cx| { let (window_id, editor) = cx.update(|cx| {
cx.set_global(Settings::test(cx)); cx.add_window(Default::default(), |cx| {
crate::init(cx);
let (window_id, editor) = cx.add_window(Default::default(), |cx| {
cx.focus_self(); cx.focus_self();
build_editor(MultiBuffer::build_simple("", cx), cx) build_editor(MultiBuffer::build_simple("", cx), cx)
}); })
(window_id, editor)
}); });
Self { Self {
@ -212,6 +204,7 @@ impl<'a> EditorTestContext<'a> {
self.assert_selections(expected_selections, marked_text.to_string()) self.assert_selections(expected_selections, marked_text.to_string())
} }
#[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) { pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text); let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| { let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
@ -228,6 +221,7 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges); assert_set_eq!(actual_ranges, expected_ranges);
} }
#[track_caller]
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) { pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text); let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
@ -241,12 +235,14 @@ impl<'a> EditorTestContext<'a> {
assert_set_eq!(actual_ranges, expected_ranges); assert_set_eq!(actual_ranges, expected_ranges);
} }
#[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) { pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text = let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true); generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text) self.assert_selections(expected_selections, expected_marked_text)
} }
#[track_caller]
fn assert_selections( fn assert_selections(
&mut self, &mut self,
expected_selections: Vec<Range<usize>>, expected_selections: Vec<Range<usize>>,

View file

@ -35,3 +35,6 @@ serde_derive.workspace = true
sysinfo = "0.27.1" sysinfo = "0.27.1"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
urlencoding = "2.1.2" urlencoding = "2.1.2"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -3,7 +3,6 @@ use gpui::{
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Entity, View, ViewContext, WeakViewHandle, Entity, View, ViewContext, WeakViewHandle,
}; };
use settings::Settings;
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::feedback_editor::{FeedbackEditor, GiveFeedback}; use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
@ -33,7 +32,7 @@ impl View for DeployFeedbackButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let active = self.active; let active = self.active;
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
Stack::new() Stack::new()
.with_child( .with_child(
MouseEventHandler::<Self, Self>::new(0, cx, |state, _| { MouseEventHandler::<Self, Self>::new(0, cx, |state, _| {

View file

@ -3,7 +3,6 @@ use gpui::{
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyElement, Element, Entity, View, ViewContext, ViewHandle, AnyElement, Element, Entity, View, ViewContext, ViewHandle,
}; };
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo}; use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
@ -30,7 +29,7 @@ impl View for FeedbackInfoText {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
Flex::row() Flex::row()
.with_child( .with_child(

View file

@ -5,7 +5,6 @@ use gpui::{
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle, AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
}; };
use settings::Settings;
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
@ -46,7 +45,7 @@ impl View for SubmitFeedbackButton {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = cx.global::<Settings>().theme.clone(); let theme = theme::current(cx).clone();
enum SubmitFeedbackButton {} enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| { MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false); let style = theme.feedback.submit_button.style_for(state, false);

View file

@ -16,14 +16,19 @@ menu = { path = "../menu" }
picker = { path = "../picker" } picker = { path = "../picker" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" } settings = { path = "../settings" }
text = { path = "../text" }
util = { path = "../util" } util = { path = "../util" }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage.workspace = true postage.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
serde_json.workspace = true language = { path = "../language", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
serde_json.workspace = true
ctor.workspace = true ctor.workspace = true
env_logger.workspace = true env_logger.workspace = true

View file

@ -1,10 +1,10 @@
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{ use gpui::{
actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle, actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use settings::Settings;
use std::{ use std::{
path::Path, path::Path,
sync::{ sync::{
@ -12,7 +12,8 @@ use std::{
Arc, Arc,
}, },
}; };
use util::{post_inc, ResultExt}; use text::Point;
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace; use workspace::Workspace;
pub type FileFinder = Picker<FileFinderDelegate>; pub type FileFinder = Picker<FileFinderDelegate>;
@ -23,11 +24,12 @@ pub struct FileFinderDelegate {
search_count: usize, search_count: usize,
latest_search_id: usize, latest_search_id: usize,
latest_search_did_cancel: bool, latest_search_did_cancel: bool,
latest_search_query: String, latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
relative_to: Option<Arc<Path>>, currently_opened_path: Option<ProjectPath>,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
selected: Option<(usize, Arc<Path>)>, selected: Option<(usize, Arc<Path>)>,
cancel_flag: Arc<AtomicBool>, cancel_flag: Arc<AtomicBool>,
history_items: Vec<ProjectPath>,
} }
actions!(file_finder, [Toggle]); actions!(file_finder, [Toggle]);
@ -37,17 +39,26 @@ pub fn init(cx: &mut AppContext) {
FileFinder::init(cx); FileFinder::init(cx);
} }
const MAX_RECENT_SELECTIONS: usize = 20;
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| { workspace.toggle_modal(cx, |workspace, cx| {
let relative_to = workspace let history_items = workspace.recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx);
let currently_opened_path = workspace
.active_item(cx) .active_item(cx)
.and_then(|item| item.project_path(cx)) .and_then(|item| item.project_path(cx));
.map(|project_path| project_path.path.clone());
let project = workspace.project().clone(); let project = workspace.project().clone();
let workspace = cx.handle().downgrade(); let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| { let finder = cx.add_view(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new(workspace, project, relative_to, cx), FileFinderDelegate::new(
workspace,
project,
currently_opened_path,
history_items,
cx,
),
cx, cx,
) )
}); });
@ -60,6 +71,21 @@ pub enum Event {
Dismissed, Dismissed,
} }
#[derive(Debug, Clone)]
struct FileSearchQuery {
raw_query: String,
file_query_end: Option<usize>,
}
impl FileSearchQuery {
fn path_query(&self) -> &str {
match self.file_query_end {
Some(file_path_end) => &self.raw_query[..file_path_end],
None => &self.raw_query,
}
}
}
impl FileFinderDelegate { impl FileFinderDelegate {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) { fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path; let path = &path_match.path;
@ -90,7 +116,8 @@ impl FileFinderDelegate {
pub fn new( pub fn new(
workspace: WeakViewHandle<Workspace>, workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>, currently_opened_path: Option<ProjectPath>,
history_items: Vec<ProjectPath>,
cx: &mut ViewContext<FileFinder>, cx: &mut ViewContext<FileFinder>,
) -> Self { ) -> Self {
cx.observe(&project, |picker, _, cx| { cx.observe(&project, |picker, _, cx| {
@ -103,16 +130,24 @@ impl FileFinderDelegate {
search_count: 0, search_count: 0,
latest_search_id: 0, latest_search_id: 0,
latest_search_did_cancel: false, latest_search_did_cancel: false,
latest_search_query: String::new(), latest_search_query: None,
relative_to, currently_opened_path,
matches: Vec::new(), matches: Vec::new(),
selected: None, selected: None,
cancel_flag: Arc::new(AtomicBool::new(false)), cancel_flag: Arc::new(AtomicBool::new(false)),
history_items,
} }
} }
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> { fn spawn_search(
let relative_to = self.relative_to.clone(); &mut self,
query: PathLikeWithPosition<FileSearchQuery>,
cx: &mut ViewContext<FileFinder>,
) -> Task<()> {
let relative_to = self
.currently_opened_path
.as_ref()
.map(|project_path| Arc::clone(&project_path.path));
let worktrees = self let worktrees = self
.project .project
.read(cx) .read(cx)
@ -140,7 +175,7 @@ impl FileFinderDelegate {
cx.spawn(|picker, mut cx| async move { cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets( let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(), candidate_sets.as_slice(),
&query, query.path_like.path_query(),
relative_to, relative_to,
false, false,
100, 100,
@ -163,18 +198,24 @@ impl FileFinderDelegate {
&mut self, &mut self,
search_id: usize, search_id: usize,
did_cancel: bool, did_cancel: bool,
query: String, query: PathLikeWithPosition<FileSearchQuery>,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
cx: &mut ViewContext<FileFinder>, cx: &mut ViewContext<FileFinder>,
) { ) {
if search_id >= self.latest_search_id { if search_id >= self.latest_search_id {
self.latest_search_id = search_id; self.latest_search_id = search_id;
if self.latest_search_did_cancel && query == self.latest_search_query { if self.latest_search_did_cancel
&& Some(query.path_like.path_query())
== self
.latest_search_query
.as_ref()
.map(|query| query.path_like.path_query())
{
util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a)); util::extend_sorted(&mut self.matches, matches.into_iter(), 100, |a, b| b.cmp(a));
} else { } else {
self.matches = matches; self.matches = matches;
} }
self.latest_search_query = query; self.latest_search_query = Some(query);
self.latest_search_did_cancel = did_cancel; self.latest_search_did_cancel = did_cancel;
cx.notify(); cx.notify();
} }
@ -209,13 +250,42 @@ impl PickerDelegate for FileFinderDelegate {
cx.notify(); cx.notify();
} }
fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> { fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if query.is_empty() { if raw_query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count); self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear(); self.matches.clear();
self.matches = self
.currently_opened_path
.iter() // if exists, bubble the currently opened path to the top
.chain(self.history_items.iter().filter(|history_item| {
Some(*history_item) != self.currently_opened_path.as_ref()
}))
.enumerate()
.map(|(i, history_item)| PathMatch {
score: i as f64,
positions: Vec::new(),
worktree_id: history_item.worktree_id.to_usize(),
path: Arc::clone(&history_item.path),
path_prefix: "".into(),
distance_to_relative_ancestor: usize::MAX,
})
.collect();
cx.notify(); cx.notify();
Task::ready(()) Task::ready(())
} else { } else {
let raw_query = &raw_query;
let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: raw_query.to_owned(),
file_query_end: if path_like_str == raw_query {
None
} else {
Some(path_like_str.len())
},
})
})
.expect("infallible");
self.spawn_search(query, cx) self.spawn_search(query, cx)
} }
} }
@ -227,13 +297,48 @@ impl PickerDelegate for FileFinderDelegate {
worktree_id: WorktreeId::from_usize(m.worktree_id), worktree_id: WorktreeId::from_usize(m.worktree_id),
path: m.path.clone(), path: m.path.clone(),
}; };
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path.clone(), None, true, cx)
});
workspace.update(cx, |workspace, cx| { let workspace = workspace.downgrade();
let row = self
.latest_search_query
.as_ref()
.and_then(|query| query.row)
.map(|row| row.saturating_sub(1));
let col = self
.latest_search_query
.as_ref()
.and_then(|query| query.column)
.unwrap_or(0)
.saturating_sub(1);
cx.spawn(|_, mut cx| async move {
let item = open_task.await.log_err()?;
if let Some(row) = row {
if let Some(active_editor) = item.downcast::<Editor>() {
active_editor
.downgrade()
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let point = snapshot
.buffer_snapshot
.clip_point(Point::new(row, col), Bias::Left);
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([point..point])
});
})
.log_err();
}
}
workspace workspace
.open_path(project_path.clone(), None, true, cx) .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
.detach_and_log_err(cx); .log_err();
workspace.dismiss_modal(cx);
Some(())
}) })
.detach();
} }
} }
} }
@ -248,8 +353,8 @@ impl PickerDelegate for FileFinderDelegate {
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let path_match = &self.matches[ix]; let path_match = &self.matches[ix];
let settings = cx.global::<Settings>(); let theme = theme::current(cx);
let style = settings.theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.style_for(mouse_state, selected);
let (file_name, file_name_positions, full_path, full_path_positions) = let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match); self.labels_for_match(path_match);
Flex::column() Flex::column()
@ -268,8 +373,11 @@ impl PickerDelegate for FileFinderDelegate {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{assert_eq, collections::HashMap, time::Duration};
use super::*; use super::*;
use editor::Editor; use editor::Editor;
use gpui::{TestAppContext, ViewHandle};
use menu::{Confirm, SelectNext}; use menu::{Confirm, SelectNext};
use serde_json::json; use serde_json::json;
use workspace::{AppState, Workspace}; use workspace::{AppState, Workspace};
@ -282,13 +390,8 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_matching_paths(cx: &mut gpui::TestAppContext) { async fn test_matching_paths(cx: &mut TestAppContext) {
let app_state = cx.update(|cx| { let app_state = init_test(cx);
super::init(cx);
editor::init(cx);
AppState::test(cx)
});
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -338,8 +441,174 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) { async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test); let app_state = init_test(cx);
let first_file_name = "first.rs";
let first_file_contents = "// First Rust file";
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
first_file_name: first_file_contents,
"second.rs": "// Second Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3];
let file_row = 1;
let file_column = 3;
assert!(file_column <= first_file_contents.len());
let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(query_inside_file.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let finder = finder.delegate();
assert_eq!(finder.matches.len(), 1);
let latest_search_query = finder
.latest_search_query
.as_ref()
.expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
assert_eq!(
latest_search_query.path_like.file_query_end,
Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext);
cx.dispatch_action(window_id, Confirm);
active_pane
.condition(cx, |pane, _| pane.active_item().is_some())
.await;
let editor = cx.update(|cx| {
let active_item = active_pane.read(cx).active_item().unwrap();
active_item.downcast::<Editor>().unwrap()
});
cx.foreground().advance_clock(Duration::from_secs(2));
cx.foreground().start_waiting();
cx.foreground().finish_waiting();
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
assert_eq!(
all_selections.len(),
1,
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
);
let caret_selection = all_selections.into_iter().next().unwrap();
assert_eq!(caret_selection.start, caret_selection.end,
"Caret selection should have its start and end at the same position");
assert_eq!(file_row, caret_selection.start.row + 1,
"Query inside file should get caret with the same focus row");
assert_eq!(file_column, caret_selection.start.column as usize + 1,
"Query inside file should get caret with the same focus column");
});
}
#[gpui::test]
async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
let app_state = init_test(cx);
let first_file_name = "first.rs";
let first_file_contents = "// First Rust file";
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
first_file_name: first_file_contents,
"second.rs": "// Second Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
let file_query = &first_file_name[..3];
let file_row = 200;
let file_column = 300;
assert!(file_column > first_file_contents.len());
let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
finder
.update(cx, |finder, cx| {
finder
.delegate_mut()
.update_matches(query_outside_file.to_string(), cx)
})
.await;
finder.read_with(cx, |finder, _| {
let finder = finder.delegate();
assert_eq!(finder.matches.len(), 1);
let latest_search_query = finder
.latest_search_query
.as_ref()
.expect("Finder should have a query after the update_matches call");
assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
assert_eq!(
latest_search_query.path_like.file_query_end,
Some(file_query.len())
);
assert_eq!(latest_search_query.row, Some(file_row));
assert_eq!(latest_search_query.column, Some(file_column as u32));
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext);
cx.dispatch_action(window_id, Confirm);
active_pane
.condition(cx, |pane, _| pane.active_item().is_some())
.await;
let editor = cx.update(|cx| {
let active_item = active_pane.read(cx).active_item().unwrap();
active_item.downcast::<Editor>().unwrap()
});
cx.foreground().advance_clock(Duration::from_secs(2));
cx.foreground().start_waiting();
cx.foreground().finish_waiting();
editor.update(cx, |editor, cx| {
let all_selections = editor.selections.all_adjusted(cx);
assert_eq!(
all_selections.len(),
1,
"Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
);
let caret_selection = all_selections.into_iter().next().unwrap();
assert_eq!(caret_selection.start, caret_selection.end,
"Caret selection should have its start and end at the same position");
assert_eq!(0, caret_selection.start.row,
"Excessive rows (as in query outside file borders) should get trimmed to last file row");
assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
"Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
});
}
#[gpui::test]
async fn test_matching_cancellation(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -365,13 +634,14 @@ mod tests {
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
None, None,
Vec::new(),
cx, cx,
), ),
cx, cx,
) )
}); });
let query = "hi".to_string(); let query = test_path_like("hi");
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx)) .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
.await; .await;
@ -407,8 +677,8 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_ignored_files(cx: &mut gpui::TestAppContext) { async fn test_ignored_files(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test); let app_state = init_test(cx);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -449,20 +719,23 @@ mod tests {
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
None, None,
Vec::new(),
cx, cx,
), ),
cx, cx,
) )
}); });
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("hi".into(), cx)) .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("hi"), cx)
})
.await; .await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7)); finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
} }
#[gpui::test] #[gpui::test]
async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) { async fn test_single_file_worktrees(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test); let app_state = init_test(cx);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -482,6 +755,7 @@ mod tests {
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
None, None,
Vec::new(),
cx, cx,
), ),
cx, cx,
@ -491,7 +765,9 @@ mod tests {
// Even though there is only one worktree, that worktree's filename // Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file. // is included in the matching, because the worktree is a single file.
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf".into(), cx)) .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("thf"), cx)
})
.await; .await;
cx.read(|cx| { cx.read(|cx| {
let finder = finder.read(cx); let finder = finder.read(cx);
@ -509,16 +785,16 @@ mod tests {
// Since the worktree root is a file, searching for its name followed by a slash does // Since the worktree root is a file, searching for its name followed by a slash does
// not match anything. // not match anything.
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("thf/".into(), cx)) .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
})
.await; .await;
finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
} }
#[gpui::test] #[gpui::test]
async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) { async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); let app_state = init_test(cx);
let app_state = cx.update(AppState::test);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -545,6 +821,7 @@ mod tests {
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
None, None,
Vec::new(),
cx, cx,
), ),
cx, cx,
@ -553,7 +830,9 @@ mod tests {
// Run a search that matches two files with the same relative path. // Run a search that matches two files with the same relative path.
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("a.t".into(), cx)) .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
})
.await; .await;
// Can switch between different matches with the same relative path. // Can switch between different matches with the same relative path.
@ -569,10 +848,8 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) { async fn test_path_distance_ordering(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); let app_state = init_test(cx);
let app_state = cx.update(AppState::test);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -590,17 +867,26 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
WorktreeId::from_usize(worktrees[0].id())
});
// When workspace has an active item, sort items which are closer to that item // When workspace has an active item, sort items which are closer to that item
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier // so that one should be sorted earlier
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt"))); let b_path = Some(ProjectPath {
worktree_id,
path: Arc::from(Path::new("/root/dir2/b.txt")),
});
let (_, finder) = cx.add_window(|cx| { let (_, finder) = cx.add_window(|cx| {
Picker::new( Picker::new(
FileFinderDelegate::new( FileFinderDelegate::new(
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
b_path, b_path,
Vec::new(),
cx, cx,
), ),
cx, cx,
@ -609,7 +895,7 @@ mod tests {
finder finder
.update(cx, |f, cx| { .update(cx, |f, cx| {
f.delegate_mut().spawn_search("a.txt".into(), cx) f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
}) })
.await; .await;
@ -621,8 +907,8 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
let app_state = cx.update(AppState::test); let app_state = init_test(cx);
app_state app_state
.fs .fs
.as_fake() .as_fake()
@ -645,17 +931,288 @@ mod tests {
workspace.downgrade(), workspace.downgrade(),
workspace.read(cx).project().clone(), workspace.read(cx).project().clone(),
None, None,
Vec::new(),
cx, cx,
), ),
cx, cx,
) )
}); });
finder finder
.update(cx, |f, cx| f.delegate_mut().spawn_search("dir".into(), cx)) .update(cx, |f, cx| {
f.delegate_mut().spawn_search(test_path_like("dir"), cx)
})
.await; .await;
cx.read(|cx| { cx.read(|cx| {
let finder = finder.read(cx); let finder = finder.read(cx);
assert_eq!(finder.delegate().matches.len(), 0); assert_eq!(finder.delegate().matches.len(), 0);
}); });
} }
#[gpui::test]
async fn test_query_history(
deterministic: Arc<gpui::executor::Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
}
}),
)
.await;
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let worktree_id = cx.read(|cx| {
let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
WorktreeId::from_usize(worktrees[0].id())
});
// Open and close panels, getting their history items afterwards.
// Ensure history items get populated with opened items, and items are kept in a certain order.
// The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
//
// TODO: without closing, the opened items do not propagate their history changes for some reason
// it does work in real app though, only tests do not propagate.
let initial_history = open_close_queried_buffer(
"fir",
1,
"first.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert!(
initial_history.is_empty(),
"Should have no history before opening any files"
);
let history_after_first = open_close_queried_buffer(
"sec",
1,
"second.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
history_after_first,
vec![ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
}],
"Should show 1st opened item in the history when opening the 2nd item"
);
let history_after_second = open_close_queried_buffer(
"thi",
1,
"third.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
history_after_second,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
],
"Should show 1st and 2nd opened items in the history when opening the 3rd item. \
2nd item should be the first in the history, as the last opened."
);
let history_after_third = open_close_queried_buffer(
"sec",
1,
"second.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
history_after_third,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
3rd item should be the first in the history, as the last opened."
);
let history_after_second_again = open_close_queried_buffer(
"thi",
1,
"third.rs",
window_id,
&workspace,
&deterministic,
cx,
)
.await;
assert_eq!(
history_after_second_again,
vec![
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/second.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/third.rs")),
},
ProjectPath {
worktree_id,
path: Arc::from(Path::new("test/first.rs")),
},
],
"Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
2nd item, as the last opened, 3rd item should go next as it was opened right before."
);
}
async fn open_close_queried_buffer(
input: &str,
expected_matches: usize,
expected_editor_title: &str,
window_id: usize,
workspace: &ViewHandle<Workspace>,
deterministic: &gpui::executor::Deterministic,
cx: &mut gpui::TestAppContext,
) -> Vec<ProjectPath> {
cx.dispatch_action(window_id, Toggle);
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder
.update(cx, |finder, cx| {
finder.delegate_mut().update_matches(input.to_string(), cx)
})
.await;
let history_items = finder.read_with(cx, |finder, _| {
assert_eq!(
finder.delegate().matches.len(),
expected_matches,
"Unexpected number of matches found for query {input}"
);
finder.delegate().history_items.clone()
});
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
cx.dispatch_action(window_id, SelectNext);
cx.dispatch_action(window_id, Confirm);
deterministic.run_until_parked();
active_pane
.condition(cx, |pane, _| pane.active_item().is_some())
.await;
cx.read(|cx| {
let active_item = active_pane.read(cx).active_item().unwrap();
let active_editor_title = active_item
.as_any()
.downcast_ref::<Editor>()
.unwrap()
.read(cx)
.title(cx);
assert_eq!(
expected_editor_title, active_editor_title,
"Unexpected editor title for query {input}"
);
});
let mut original_items = HashMap::new();
cx.read(|cx| {
for pane in workspace.read(cx).panes() {
let pane_id = pane.id();
let pane = pane.read(cx);
let insertion_result = original_items.insert(pane_id, pane.items().count());
assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
}
});
active_pane
.update(cx, |pane, cx| {
pane.close_active_item(&workspace::CloseActiveItem, cx)
.unwrap()
})
.await
.unwrap();
deterministic.run_until_parked();
cx.read(|cx| {
for pane in workspace.read(cx).panes() {
let pane_id = pane.id();
let pane = pane.read(cx);
match original_items.remove(&pane_id) {
Some(original_items) => {
assert_eq!(
pane.items().count(),
original_items.saturating_sub(1),
"Pane id {pane_id} should have item closed"
);
}
None => panic!("Pane id {pane_id} not found in original items"),
}
}
});
history_items
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.foreground().forbid_parking();
cx.update(|cx| {
let state = AppState::test(cx);
theme::init((), cx);
language::init(cx);
super::init(cx);
editor::init(cx);
workspace::init_settings(cx);
state
})
}
fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
PathLikeWithPosition::parse_str(test_str, |path_like_str| {
Ok::<_, std::convert::Infallible>(FileSearchQuery {
raw_query: test_str.to_owned(),
file_query_end: if path_like_str == test_str {
None
} else {
Some(path_like_str.len())
},
})
})
.unwrap()
}
} }

View file

@ -13,6 +13,7 @@ gpui = { path = "../gpui" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
rope = { path = "../rope" } rope = { path = "../rope" }
util = { path = "../util" } util = { path = "../util" }
sum_tree = { path = "../sum_tree" }
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
futures.workspace = true futures.workspace = true

View file

@ -27,7 +27,7 @@ use util::ResultExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use collections::{btree_map, BTreeMap}; use collections::{btree_map, BTreeMap};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use repository::FakeGitRepositoryState; use repository::{FakeGitRepositoryState, GitFileStatus};
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
use std::sync::Weak; use std::sync::Weak;
@ -572,15 +572,15 @@ impl FakeFs {
Ok(()) Ok(())
} }
pub async fn pause_events(&self) { pub fn pause_events(&self) {
self.state.lock().events_paused = true; self.state.lock().events_paused = true;
} }
pub async fn buffered_event_count(&self) -> usize { pub fn buffered_event_count(&self) -> usize {
self.state.lock().buffered_events.len() self.state.lock().buffered_events.len()
} }
pub async fn flush_events(&self, count: usize) { pub fn flush_events(&self, count: usize) {
self.state.lock().flush_events(count); self.state.lock().flush_events(count);
} }
@ -654,6 +654,17 @@ impl FakeFs {
}); });
} }
pub async fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, GitFileStatus)]) {
self.with_git_state(dot_git, |state| {
state.worktree_statuses.clear();
state.worktree_statuses.extend(
statuses
.iter()
.map(|(path, content)| ((**path).into(), content.clone())),
);
});
}
pub fn paths(&self) -> Vec<PathBuf> { pub fn paths(&self) -> Vec<PathBuf> {
let mut result = Vec::new(); let mut result = Vec::new();
let mut queue = collections::VecDeque::new(); let mut queue = collections::VecDeque::new();
@ -821,14 +832,16 @@ impl Fs for FakeFs {
let old_path = normalize_path(old_path); let old_path = normalize_path(old_path);
let new_path = normalize_path(new_path); let new_path = normalize_path(new_path);
let mut state = self.state.lock(); let mut state = self.state.lock();
let moved_entry = state.write_path(&old_path, |e| { let moved_entry = state.write_path(&old_path, |e| {
if let btree_map::Entry::Occupied(e) = e { if let btree_map::Entry::Occupied(e) = e {
Ok(e.remove()) Ok(e.get().clone())
} else { } else {
Err(anyhow!("path does not exist: {}", &old_path.display())) Err(anyhow!("path does not exist: {}", &old_path.display()))
} }
})?; })?;
state.write_path(&new_path, |e| { state.write_path(&new_path, |e| {
match e { match e {
btree_map::Entry::Occupied(mut e) => { btree_map::Entry::Occupied(mut e) => {
@ -844,6 +857,17 @@ impl Fs for FakeFs {
} }
Ok(()) Ok(())
})?; })?;
state
.write_path(&old_path, |e| {
if let btree_map::Entry::Occupied(e) = e {
Ok(e.remove())
} else {
unreachable!()
}
})
.unwrap();
state.emit_event(&[old_path, new_path]); state.emit_event(&[old_path, new_path]);
Ok(()) Ok(())
} }

View file

@ -1,10 +1,15 @@
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde_derive::{Deserialize, Serialize};
use std::{ use std::{
cmp::Ordering,
ffi::OsStr,
os::unix::prelude::OsStrExt,
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use sum_tree::{MapSeekTarget, TreeMap};
use util::ResultExt; use util::ResultExt;
pub use git2::Repository as LibGitRepository; pub use git2::Repository as LibGitRepository;
@ -16,6 +21,10 @@ pub trait GitRepository: Send {
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>; fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
fn branch_name(&self) -> Option<String>; fn branch_name(&self) -> Option<String>;
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Option<GitFileStatus>;
} }
impl std::fmt::Debug for dyn GitRepository { impl std::fmt::Debug for dyn GitRepository {
@ -61,6 +70,48 @@ impl GitRepository for LibGitRepository {
let branch = String::from_utf8_lossy(head.shorthand_bytes()); let branch = String::from_utf8_lossy(head.shorthand_bytes());
Some(branch.to_string()) Some(branch.to_string())
} }
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let statuses = self.statuses(None).log_err()?;
let mut map = TreeMap::default();
for status in statuses
.iter()
.filter(|status| !status.status().contains(git2::Status::IGNORED))
{
let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
let Some(status) = read_status(status.status()) else {
continue
};
map.insert(path, status)
}
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
let status = self.status_file(path).log_err()?;
read_status(status)
}
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
if status.contains(git2::Status::CONFLICTED) {
Some(GitFileStatus::Conflict)
} else if status.intersects(
git2::Status::WT_MODIFIED
| git2::Status::WT_RENAMED
| git2::Status::INDEX_MODIFIED
| git2::Status::INDEX_RENAMED,
) {
Some(GitFileStatus::Modified)
} else if status.intersects(git2::Status::WT_NEW | git2::Status::INDEX_NEW) {
Some(GitFileStatus::Added)
} else {
None
}
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -71,6 +122,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct FakeGitRepositoryState { pub struct FakeGitRepositoryState {
pub index_contents: HashMap<PathBuf, String>, pub index_contents: HashMap<PathBuf, String>,
pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
pub branch_name: Option<String>, pub branch_name: Option<String>,
} }
@ -93,6 +145,20 @@ impl GitRepository for FakeGitRepository {
let state = self.state.lock(); let state = self.state.lock();
state.branch_name.clone() state.branch_name.clone()
} }
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let state = self.state.lock();
let mut map = TreeMap::default();
for (repo_path, status) in state.worktree_statuses.iter() {
map.insert(repo_path.to_owned(), status.to_owned());
}
Some(map)
}
fn status(&self, path: &RepoPath) -> Option<GitFileStatus> {
let state = self.state.lock();
state.worktree_statuses.get(path).cloned()
}
} }
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@ -123,3 +189,66 @@ fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
_ => Ok(()), _ => Ok(()),
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum GitFileStatus {
Added,
Modified,
Conflict,
}
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(PathBuf);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path)
}
}
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.to_path_buf())
}
}
impl From<PathBuf> for RepoPath {
fn from(value: PathBuf) -> Self {
RepoPath::new(value)
}
}
impl Default for RepoPath {
fn default() -> Self {
RepoPath(PathBuf::new())
}
}
impl AsRef<Path> for RepoPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl std::ops::Deref for RepoPath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a Path);
impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
if key.starts_with(&self.0) {
Ordering::Greater
} else {
self.0.cmp(key)
}
}
}

View file

@ -1,4 +1,4 @@
use std::ops::Range; use std::{iter, ops::Range};
use sum_tree::SumTree; use sum_tree::SumTree;
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
@ -75,18 +75,58 @@ impl BufferDiff {
&'a self, &'a self,
range: Range<u32>, range: Range<u32>,
buffer: &'a BufferSnapshot, buffer: &'a BufferSnapshot,
reversed: bool,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> { ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = buffer.anchor_before(Point::new(range.start, 0)); let start = buffer.anchor_before(Point::new(range.start, 0));
let end = buffer.anchor_after(Point::new(range.end, 0)); let end = buffer.anchor_after(Point::new(range.end, 0));
self.hunks_intersecting_range(start..end, buffer, reversed)
self.hunks_intersecting_range(start..end, buffer)
} }
pub fn hunks_intersecting_range<'a>( pub fn hunks_intersecting_range<'a>(
&'a self, &'a self,
range: Range<Anchor>, range: Range<Anchor>,
buffer: &'a BufferSnapshot, buffer: &'a BufferSnapshot,
reversed: bool, ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt();
!before_start && !after_end
});
let anchor_iter = std::iter::from_fn(move || {
cursor.next(buffer);
cursor.item()
})
.flat_map(move |hunk| {
[
(&hunk.buffer_range.start, hunk.diff_base_byte_range.start),
(&hunk.buffer_range.end, hunk.diff_base_byte_range.end),
]
.into_iter()
});
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || {
let (start_point, start_base) = summaries.next()?;
let (end_point, end_base) = summaries.next()?;
let end_row = if end_point.column > 0 {
end_point.row + 1
} else {
end_point.row
};
Some(DiffHunk {
buffer_range: start_point.row..end_row,
diff_base_byte_range: start_base..end_base,
})
})
}
pub fn hunks_intersecting_range_rev<'a>(
&'a self,
range: Range<Anchor>,
buffer: &'a BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk<u32>> { ) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt();
@ -95,14 +135,9 @@ impl BufferDiff {
}); });
std::iter::from_fn(move || { std::iter::from_fn(move || {
if reversed { cursor.prev(buffer);
cursor.prev(buffer);
} else {
cursor.next(buffer);
}
let hunk = cursor.item()?; let hunk = cursor.item()?;
let range = hunk.buffer_range.to_point(buffer); let range = hunk.buffer_range.to_point(buffer);
let end_row = if range.end.column > 0 { let end_row = if range.end.column > 0 {
range.end.row + 1 range.end.row + 1
@ -151,7 +186,7 @@ impl BufferDiff {
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> { fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
let start = text.anchor_before(Point::new(0, 0)); let start = text.anchor_before(Point::new(0, 0));
let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); let end = text.anchor_after(Point::new(u32::MAX, u32::MAX));
self.hunks_intersecting_range(start..end, text, false) self.hunks_intersecting_range(start..end, text)
} }
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> { fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
@ -279,6 +314,8 @@ pub fn assert_hunks<Iter>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::assert_eq;
use super::*; use super::*;
use text::Buffer; use text::Buffer;
use unindent::Unindent as _; use unindent::Unindent as _;
@ -365,7 +402,7 @@ mod tests {
assert_eq!(diff.hunks(&buffer).count(), 8); assert_eq!(diff.hunks(&buffer).count(), 8);
assert_hunks( assert_hunks(
diff.hunks_in_row_range(7..12, &buffer, false), diff.hunks_in_row_range(7..12, &buffer),
&buffer, &buffer,
&diff_base, &diff_base,
&[ &[

View file

@ -16,3 +16,8 @@ settings = { path = "../settings" }
text = { path = "../text" } text = { path = "../text" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage.workspace = true postage.workspace = true
theme = { path = "../theme" }
util = { path = "../util" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -1,13 +1,13 @@
use std::sync::Arc; use std::sync::Arc;
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{ use gpui::{
actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity, actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, AppContext, Axis, Entity,
View, ViewContext, ViewHandle, View, ViewContext, ViewHandle,
}; };
use menu::{Cancel, Confirm}; use menu::{Cancel, Confirm};
use settings::Settings;
use text::{Bias, Point}; use text::{Bias, Point};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, Workspace}; use workspace::{Modal, Workspace};
actions!(go_to_line, [Toggle]); actions!(go_to_line, [Toggle]);
@ -75,15 +75,16 @@ impl GoToLine {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
self.prev_scroll_position.take(); self.prev_scroll_position.take();
self.active_editor.update(cx, |active_editor, cx| { if let Some(point) = self.point_from_query(cx) {
if let Some(rows) = active_editor.highlighted_rows() { self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot; let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot); let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| { active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
s.select_ranges([position..position]) s.select_ranges([point..point])
}); });
} });
}); }
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }
@ -96,16 +97,7 @@ impl GoToLine {
match event { match event {
editor::Event::Blurred => cx.emit(Event::Dismissed), editor::Event::Blurred => cx.emit(Event::Dismissed),
editor::Event::BufferEdited { .. } => { editor::Event::BufferEdited { .. } => {
let line_editor = self.line_editor.read(cx).text(cx); if let Some(point) = self.point_from_query(cx) {
let mut components = line_editor.trim().split(&[',', ':'][..]);
let row = components.next().and_then(|row| row.parse::<u32>().ok());
let column = components.next().and_then(|row| row.parse::<u32>().ok());
if let Some(point) = row.map(|row| {
Point::new(
row.saturating_sub(1),
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
)
}) {
self.active_editor.update(cx, |active_editor, cx| { self.active_editor.update(cx, |active_editor, cx| {
let snapshot = active_editor.snapshot(cx).display_snapshot; let snapshot = active_editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
@ -120,6 +112,20 @@ impl GoToLine {
_ => {} _ => {}
} }
} }
fn point_from_query(&self, cx: &ViewContext<Self>) -> Option<Point> {
let line_editor = self.line_editor.read(cx).text(cx);
let mut components = line_editor
.splitn(2, FILE_ROW_COLUMN_DELIMITER)
.map(str::trim)
.fuse();
let row = components.next().and_then(|row| row.parse::<u32>().ok())?;
let column = components.next().and_then(|col| col.parse::<u32>().ok());
Some(Point::new(
row.saturating_sub(1),
column.unwrap_or(0).saturating_sub(1),
))
}
} }
impl Entity for GoToLine { impl Entity for GoToLine {
@ -144,10 +150,10 @@ impl View for GoToLine {
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &cx.global::<Settings>().theme.picker; let theme = &theme::current(cx).picker;
let label = format!( let label = format!(
"{},{} of {} lines", "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",
self.cursor_point.row + 1, self.cursor_point.row + 1,
self.cursor_point.column + 1, self.cursor_point.column + 1,
self.max_point.row + 1 self.max_point.row + 1

View file

@ -48,7 +48,7 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
time.workspace = true time.workspace = true
tiny-skia = "0.5" tiny-skia = "0.5"
usvg = "0.14" usvg = { version = "0.14", features = [] }
uuid = { version = "1.1.2", features = ["v4"] } uuid = { version = "1.1.2", features = ["v4"] }
waker-fn = "1.1.0" waker-fn = "1.1.0"
@ -72,7 +72,7 @@ cocoa = "0.24"
core-foundation = { version = "0.9.3", features = ["with-uuid"] } core-foundation = { version = "0.9.3", features = ["with-uuid"] }
core-graphics = "0.22.3" core-graphics = "0.22.3"
core-text = "19.2" core-text = "19.2"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "8eaf7a918eafa28b0a37dc759e2e0e7683fa24f1" } font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "b2f77d56f450338aa4f7dd2f0197d8c9acb0cf18" }
foreign-types = "0.3" foreign-types = "0.3"
log.workspace = true log.workspace = true
metal = "0.21.0" metal = "0.21.0"

View file

@ -1174,7 +1174,7 @@ impl AppContext {
this.notify_global(type_id); this.notify_global(type_id);
result result
} else { } else {
panic!("No global added for {}", std::any::type_name::<T>()); panic!("no global added for {}", std::any::type_name::<T>());
} }
} }
@ -1182,6 +1182,15 @@ impl AppContext {
self.globals.clear(); self.globals.clear();
} }
pub fn remove_global<T: 'static>(&mut self) -> T {
*self
.globals
.remove(&TypeId::of::<T>())
.unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<T>()))
.downcast()
.unwrap()
}
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T> pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
where where
T: Entity, T: Entity,

View file

@ -270,7 +270,7 @@ impl TestAppContext {
.borrow_mut() .borrow_mut()
.pop_front() .pop_front()
.expect("prompt was not called"); .expect("prompt was not called");
let _ = done_tx.try_send(answer); done_tx.try_send(answer).ok();
} }
pub fn has_pending_prompt(&self, window_id: usize) -> bool { pub fn has_pending_prompt(&self, window_id: usize) -> bool {

View file

@ -42,7 +42,7 @@ impl Color {
} }
pub fn yellow() -> Self { pub fn yellow() -> Self {
Self(ColorU::from_u32(0x00ffffff)) Self(ColorU::from_u32(0xffff00ff))
} }
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {

View file

@ -576,6 +576,15 @@ pub struct ComponentHost<V: View, C: Component<V>> {
view_type: PhantomData<V>, view_type: PhantomData<V>,
} }
impl<V: View, C: Component<V>> ComponentHost<V, C> {
pub fn new(c: C) -> Self {
Self {
component: c,
view_type: PhantomData,
}
}
}
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> { impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
type Target = C; type Target = C;

View file

@ -477,6 +477,14 @@ impl Deterministic {
state.rng = StdRng::seed_from_u64(state.seed); state.rng = StdRng::seed_from_u64(state.seed);
} }
pub fn allow_parking(&self) {
use rand::prelude::*;
let mut state = self.state.lock();
state.forbid_parking = false;
state.rng = StdRng::seed_from_u64(state.seed);
}
pub async fn simulate_random_delay(&self) { pub async fn simulate_random_delay(&self) {
use rand::prelude::*; use rand::prelude::*;
use smol::future::yield_now; use smol::future::yield_now;
@ -698,6 +706,14 @@ impl Foreground {
} }
} }
#[cfg(any(test, feature = "test-support"))]
pub fn allow_parking(&self) {
match self {
Self::Deterministic { executor, .. } => executor.allow_parking(),
_ => panic!("this method can only be called on a deterministic executor"),
}
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn advance_clock(&self, duration: Duration) { pub fn advance_clock(&self, duration: Duration) {
match self { match self {

View file

@ -11,6 +11,19 @@ pub struct Binding {
context_predicate: Option<KeymapContextPredicate>, context_predicate: Option<KeymapContextPredicate>,
} }
impl std::fmt::Debug for Binding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Binding {{ keystrokes: {:?}, action: {}::{}, context_predicate: {:?} }}",
self.keystrokes,
self.action.namespace(),
self.action.name(),
self.context_predicate
)
}
}
impl Clone for Binding { impl Clone for Binding {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {

View file

@ -755,7 +755,7 @@ impl platform::Window for Window {
let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap()); let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
} }
}); });
let block = block.copy();
let native_window = self.0.borrow().native_window; let native_window = self.0.borrow().native_window;
self.0 self.0
.borrow() .borrow()

View file

@ -13,9 +13,15 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
settings = { path = "../settings" }
anyhow.workspace = true anyhow.workspace = true
chrono = "0.4" chrono = "0.4"
dirs = "4.0" dirs = "4.0"
serde.workspace = true
schemars.workspace = true
log.workspace = true log.workspace = true
settings = { path = "../settings" }
shellexpand = "2.1.0" shellexpand = "2.1.0"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -1,7 +1,9 @@
use anyhow::Result;
use chrono::{Datelike, Local, NaiveTime, Timelike}; use chrono::{Datelike, Local, NaiveTime, Timelike};
use editor::{scroll::autoscroll::Autoscroll, Editor}; use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{actions, AppContext}; use gpui::{actions, AppContext};
use settings::{HourFormat, Settings}; use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{ use std::{
fs::OpenOptions, fs::OpenOptions,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -11,13 +13,48 @@ use workspace::AppState;
actions!(journal, [NewJournalEntry]); actions!(journal, [NewJournalEntry]);
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct JournalSettings {
pub path: Option<String>,
pub hour_format: Option<HourFormat>,
}
impl Default for JournalSettings {
fn default() -> Self {
Self {
path: Some("~".into()),
hour_format: Some(Default::default()),
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum HourFormat {
#[default]
Hour12,
Hour24,
}
impl settings::Setting for JournalSettings {
const KEY: Option<&'static str> = Some("journal");
type FileContent = Self;
fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
settings::register::<JournalSettings>(cx);
cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx)); cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
} }
pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) { pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
let settings = cx.global::<Settings>(); let settings = settings::get::<JournalSettings>(cx);
let journal_dir = match journal_dir(&settings) { let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
Some(journal_dir) => journal_dir, Some(journal_dir) => journal_dir,
None => { None => {
log::error!("Can't determine journal directory"); log::error!("Can't determine journal directory");
@ -31,8 +68,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
.join(format!("{:02}", now.month())); .join(format!("{:02}", now.month()));
let entry_path = month_dir.join(format!("{:02}.md", now.day())); let entry_path = month_dir.join(format!("{:02}.md", now.day()));
let now = now.time(); let now = now.time();
let hour_format = &settings.journal_overrides.hour_format; let entry_heading = heading_entry(now, &settings.hour_format);
let entry_heading = heading_entry(now, &hour_format);
let create_entry = cx.background().spawn(async move { let create_entry = cx.background().spawn(async move {
std::fs::create_dir_all(month_dir)?; std::fs::create_dir_all(month_dir)?;
@ -76,14 +112,8 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
.detach_and_log_err(cx); .detach_and_log_err(cx);
} }
fn journal_dir(settings: &Settings) -> Option<PathBuf> { fn journal_dir(path: &str) -> Option<PathBuf> {
let journal_dir = settings let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
.journal_overrides
.path
.as_ref()
.unwrap_or(settings.journal_defaults.path.as_ref()?);
let expanded_journal_dir = shellexpand::full(&journal_dir) //TODO handle this better
.ok() .ok()
.map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal")); .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));

View file

@ -36,16 +36,19 @@ sum_tree = { path = "../sum_tree" }
text = { path = "../text" } text = { path = "../text" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
anyhow.workspace = true anyhow.workspace = true
async-broadcast = "0.4" async-broadcast = "0.4"
async-trait.workspace = true async-trait.workspace = true
futures.workspace = true futures.workspace = true
globset.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true log.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
postage.workspace = true postage.workspace = true
rand = { workspace = true, optional = true } rand = { workspace = true, optional = true }
regex.workspace = true regex.workspace = true
schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true

View file

@ -5,6 +5,7 @@ pub use crate::{
}; };
use crate::{ use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings},
outline::OutlineItem, outline::OutlineItem,
syntax_map::{ syntax_map::{
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
@ -18,7 +19,6 @@ use futures::FutureExt as _;
use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
use lsp::LanguageServerId; use lsp::LanguageServerId;
use parking_lot::Mutex; use parking_lot::Mutex;
use settings::Settings;
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use smallvec::SmallVec; use smallvec::SmallVec;
use smol::future::yield_now; use smol::future::yield_now;
@ -1827,11 +1827,11 @@ impl BufferSnapshot {
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize { 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 language_name = self.language_at(position).map(|language| language.name());
let settings = cx.global::<Settings>(); let settings = language_settings(language_name.as_deref(), cx);
if settings.hard_tabs(language_name.as_deref()) { if settings.hard_tabs {
IndentSize::tab() IndentSize::tab()
} else { } else {
IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) IndentSize::spaces(settings.tab_size.get())
} }
} }
@ -2146,6 +2146,15 @@ impl BufferSnapshot {
.or(self.language.as_ref()) .or(self.language.as_ref())
} }
pub fn settings_at<'a, D: ToOffset>(
&self,
position: D,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language = self.language_at(position);
language_settings(language.map(|l| l.name()).as_deref(), cx)
}
pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> { pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
let offset = position.to_offset(self); let offset = position.to_offset(self);
@ -2500,18 +2509,22 @@ impl BufferSnapshot {
pub fn git_diff_hunks_in_row_range<'a>( pub fn git_diff_hunks_in_row_range<'a>(
&'a self, &'a self,
range: Range<u32>, range: Range<u32>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> { ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff.hunks_in_row_range(range, self, reversed) self.git_diff.hunks_in_row_range(range, self)
} }
pub fn git_diff_hunks_intersecting_range<'a>( pub fn git_diff_hunks_intersecting_range<'a>(
&'a self, &'a self,
range: Range<Anchor>, range: Range<Anchor>,
reversed: bool,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> { ) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff self.git_diff.hunks_intersecting_range(range, self)
.hunks_intersecting_range(range, self, reversed) }
pub fn git_diff_hunks_intersecting_range_rev<'a>(
&'a self,
range: Range<Anchor>,
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
self.git_diff.hunks_intersecting_range_rev(range, self)
} }
pub fn diagnostics_in_range<'a, T, O>( pub fn diagnostics_in_range<'a, T, O>(

View file

@ -1,3 +1,7 @@
use crate::language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
};
use super::*; use super::*;
use clock::ReplicaId; use clock::ReplicaId;
use collections::BTreeMap; use collections::BTreeMap;
@ -7,7 +11,7 @@ use indoc::indoc;
use proto::deserialize_operation; use proto::deserialize_operation;
use rand::prelude::*; use rand::prelude::*;
use regex::RegexBuilder; use regex::RegexBuilder;
use settings::Settings; use settings::SettingsStore;
use std::{ use std::{
cell::RefCell, cell::RefCell,
env, env,
@ -36,7 +40,8 @@ fn init_logger() {
#[gpui::test] #[gpui::test]
fn test_line_endings(cx: &mut gpui::AppContext) { fn test_line_endings(cx: &mut gpui::AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = let mut buffer =
Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
@ -862,8 +867,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
let settings = Settings::test(cx); init_settings(cx, |_| {});
cx.set_global(settings);
cx.add_model(|cx| { cx.add_model(|cx| {
let text = "fn a() {}"; let text = "fn a() {}";
@ -903,9 +907,9 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
let mut settings = Settings::test(cx); init_settings(cx, |settings| {
settings.editor_overrides.hard_tabs = Some(true); settings.defaults.hard_tabs = Some(true);
cx.set_global(settings); });
cx.add_model(|cx| { cx.add_model(|cx| {
let text = "fn a() {}"; let text = "fn a() {}";
@ -945,8 +949,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
let settings = Settings::test(cx); init_settings(cx, |_| {});
cx.set_global(settings);
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new( let mut buffer = Buffer::new(
@ -1082,8 +1085,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
#[gpui::test] #[gpui::test]
fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
let settings = Settings::test(cx); init_settings(cx, |_| {});
cx.set_global(settings);
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new( let mut buffer = Buffer::new(
@ -1145,7 +1147,8 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
#[gpui::test] #[gpui::test]
fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new( let mut buffer = Buffer::new(
0, 0,
@ -1201,7 +1204,8 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let text = "a\nb"; let text = "a\nb";
let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
@ -1217,7 +1221,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let text = " let text = "
const a: usize = 1; const a: usize = 1;
@ -1257,7 +1262,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_block_mode(cx: &mut AppContext) { fn test_autoindent_block_mode(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let text = r#" let text = r#"
fn a() { fn a() {
@ -1339,7 +1345,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let text = r#" let text = r#"
fn a() { fn a() {
@ -1417,7 +1424,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
#[gpui::test] #[gpui::test]
fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let text = " let text = "
* one * one
@ -1460,25 +1468,23 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut AppContext) { fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
cx.set_global({ init_settings(cx, |settings| {
let mut settings = Settings::test(cx); settings.languages.extend([
settings.language_overrides.extend([
( (
"HTML".into(), "HTML".into(),
settings::EditorSettings { LanguageSettingsContent {
tab_size: Some(2.try_into().unwrap()), tab_size: Some(2.try_into().unwrap()),
..Default::default() ..Default::default()
}, },
), ),
( (
"JavaScript".into(), "JavaScript".into(),
settings::EditorSettings { LanguageSettingsContent {
tab_size: Some(8.try_into().unwrap()), tab_size: Some(8.try_into().unwrap()),
..Default::default() ..Default::default()
}, },
), ),
]); ])
settings
}); });
let html_language = Arc::new( let html_language = Arc::new(
@ -1574,9 +1580,10 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
let mut settings = Settings::test(cx); init_settings(cx, |settings| {
settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); settings.defaults.tab_size = Some(2.try_into().unwrap());
cx.set_global(settings); });
cx.add_model(|cx| { cx.add_model(|cx| {
let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
@ -1617,7 +1624,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
#[gpui::test] #[gpui::test]
fn test_language_config_at(cx: &mut AppContext) { fn test_language_config_at(cx: &mut AppContext) {
cx.set_global(Settings::test(cx)); init_settings(cx, |_| {});
cx.add_model(|cx| { cx.add_model(|cx| {
let language = Language::new( let language = Language::new(
LanguageConfig { LanguageConfig {
@ -2199,7 +2207,6 @@ fn assert_bracket_pairs(
language: Language, language: Language,
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
cx.set_global(Settings::test(cx));
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer = cx.add_model(|cx| { let buffer = cx.add_model(|cx| {
Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
@ -2222,3 +2229,11 @@ fn assert_bracket_pairs(
bracket_pairs bracket_pairs
); );
} }
fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) {
cx.set_global(SettingsStore::test(cx));
crate::init(cx);
cx.update_global::<SettingsStore, _, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, f);
});
}

View file

@ -1,6 +1,7 @@
mod buffer; mod buffer;
mod diagnostic_set; mod diagnostic_set;
mod highlight_map; mod highlight_map;
pub mod language_settings;
mod outline; mod outline;
pub mod proto; pub mod proto;
mod syntax_map; mod syntax_map;
@ -58,6 +59,10 @@ pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem}; pub use outline::{Outline, OutlineItem};
pub use tree_sitter::{Parser, Tree}; pub use tree_sitter::{Parser, Tree};
pub fn init(cx: &mut AppContext) {
language_settings::init(cx);
}
thread_local! { thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new()); static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
} }

View file

@ -0,0 +1,338 @@
use anyhow::Result;
use collections::HashMap;
use globset::GlobMatcher;
use gpui::AppContext;
use schemars::{
schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
JsonSchema,
};
use serde::{Deserialize, Serialize};
use std::{num::NonZeroU32, path::Path, sync::Arc};
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 all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
settings::get::<AllLanguageSettings>(cx)
}
#[derive(Debug, Clone)]
pub struct AllLanguageSettings {
pub copilot: CopilotSettings,
defaults: LanguageSettings,
languages: HashMap<Arc<str>, LanguageSettings>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct LanguageSettings {
pub tab_size: NonZeroU32,
pub hard_tabs: bool,
pub soft_wrap: SoftWrap,
pub preferred_line_length: u32,
pub format_on_save: FormatOnSave,
pub remove_trailing_whitespace_on_save: bool,
pub ensure_final_newline_on_save: bool,
pub formatter: Formatter,
pub enable_language_server: bool,
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
}
#[derive(Clone, Debug, Default)]
pub struct CopilotSettings {
pub feature_enabled: bool,
pub disabled_globs: Vec<GlobMatcher>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct AllLanguageSettingsContent {
#[serde(default)]
pub features: Option<FeaturesContent>,
#[serde(default)]
pub copilot: Option<CopilotSettingsContent>,
#[serde(flatten)]
pub defaults: LanguageSettingsContent,
#[serde(default, alias = "language_overrides")]
pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent {
#[serde(default)]
pub tab_size: Option<NonZeroU32>,
#[serde(default)]
pub hard_tabs: Option<bool>,
#[serde(default)]
pub soft_wrap: Option<SoftWrap>,
#[serde(default)]
pub preferred_line_length: Option<u32>,
#[serde(default)]
pub format_on_save: Option<FormatOnSave>,
#[serde(default)]
pub remove_trailing_whitespace_on_save: Option<bool>,
#[serde(default)]
pub ensure_final_newline_on_save: Option<bool>,
#[serde(default)]
pub formatter: Option<Formatter>,
#[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(default)]
pub show_copilot_suggestions: Option<bool>,
#[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CopilotSettingsContent {
#[serde(default)]
pub disabled_globs: Option<Vec<String>>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct FeaturesContent {
pub copilot: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
None,
EditorWidth,
PreferredLineLength,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FormatOnSave {
On,
Off,
LanguageServer,
External {
command: Arc<str>,
arguments: Arc<[String]>,
},
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ShowWhitespaceSetting {
Selection,
None,
All,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Formatter {
LanguageServer,
External {
command: Arc<str>,
arguments: Arc<[String]>,
},
}
impl AllLanguageSettings {
pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
if let Some(name) = language_name {
if let Some(overrides) = self.languages.get(name) {
return overrides;
}
}
&self.defaults
}
pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
!self
.copilot
.disabled_globs
.iter()
.any(|glob| glob.is_match(path))
}
pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
if !self.copilot.feature_enabled {
return false;
}
if let Some(path) = path {
if !self.copilot_enabled_for_path(path) {
return false;
}
}
self.language(language_name).show_copilot_suggestions
}
}
impl settings::Setting for AllLanguageSettings {
const KEY: Option<&'static str> = None;
type FileContent = AllLanguageSettingsContent;
fn load(
default_value: &Self::FileContent,
user_settings: &[&Self::FileContent],
_: &AppContext,
) -> Result<Self> {
// A default is provided for all settings.
let mut defaults: LanguageSettings =
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
let mut languages = HashMap::default();
for (language_name, settings) in &default_value.languages {
let mut language_settings = defaults.clone();
merge_settings(&mut language_settings, &settings);
languages.insert(language_name.clone(), language_settings);
}
let mut copilot_enabled = default_value
.features
.as_ref()
.and_then(|f| f.copilot)
.ok_or_else(Self::missing_default)?;
let mut copilot_globs = default_value
.copilot
.as_ref()
.and_then(|c| c.disabled_globs.as_ref())
.ok_or_else(Self::missing_default)?;
for user_settings in user_settings {
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
copilot_enabled = copilot;
}
if let Some(globs) = user_settings
.copilot
.as_ref()
.and_then(|f| f.disabled_globs.as_ref())
{
copilot_globs = globs;
}
// A user's global settings override the default global settings and
// all default language-specific settings.
merge_settings(&mut defaults, &user_settings.defaults);
for language_settings in languages.values_mut() {
merge_settings(language_settings, &user_settings.defaults);
}
// A user's language-specific settings override default language-specific settings.
for (language_name, user_language_settings) in &user_settings.languages {
merge_settings(
languages
.entry(language_name.clone())
.or_insert_with(|| defaults.clone()),
&user_language_settings,
);
}
}
Ok(Self {
copilot: CopilotSettings {
feature_enabled: copilot_enabled,
disabled_globs: copilot_globs
.iter()
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
.collect(),
},
defaults,
languages,
})
}
fn json_schema(
generator: &mut schemars::gen::SchemaGenerator,
params: &settings::SettingsJsonSchemaParams,
_: &AppContext,
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
// Create a schema for a 'languages overrides' object, associating editor
// settings with specific langauges.
assert!(root_schema
.definitions
.contains_key("LanguageSettingsContent"));
let languages_object_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: params
.language_names
.iter()
.map(|name| {
(
name.clone(),
Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
)
})
.collect(),
..Default::default()
})),
..Default::default()
};
root_schema
.definitions
.extend([("Languages".into(), languages_object_schema.into())]);
root_schema
.schema
.object
.as_mut()
.unwrap()
.properties
.extend([
(
"languages".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
// For backward compatibility
(
"language_overrides".to_owned(),
Schema::new_ref("#/definitions/Languages".into()),
),
]);
root_schema
}
}
fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
merge(&mut settings.tab_size, src.tab_size);
merge(&mut settings.hard_tabs, src.hard_tabs);
merge(&mut settings.soft_wrap, src.soft_wrap);
merge(
&mut settings.preferred_line_length,
src.preferred_line_length,
);
merge(&mut settings.formatter, src.formatter.clone());
merge(&mut settings.format_on_save, src.format_on_save.clone());
merge(
&mut settings.remove_trailing_whitespace_on_save,
src.remove_trailing_whitespace_on_save,
);
merge(
&mut settings.ensure_final_newline_on_save,
src.ensure_final_newline_on_save,
);
merge(
&mut settings.enable_language_server,
src.enable_language_server,
);
merge(
&mut settings.show_copilot_suggestions,
src.show_copilot_suggestions,
);
merge(&mut settings.show_whitespaces, src.show_whitespaces);
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
}

View file

@ -1114,6 +1114,8 @@ fn get_injections(
let mut query_cursor = QueryCursorHandle::new(); let mut query_cursor = QueryCursorHandle::new();
let mut prev_match = None; let mut prev_match = None;
// Ensure that a `ParseStep` is created for every combined injection language, even
// if there currently no matches for that injection.
combined_injection_ranges.clear(); combined_injection_ranges.clear();
for pattern in &config.patterns { for pattern in &config.patterns {
if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) { if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
@ -1174,8 +1176,8 @@ fn get_injections(
if let Some(language) = language { if let Some(language) = language {
if combined { if combined {
combined_injection_ranges combined_injection_ranges
.get_mut(&language.clone()) .entry(language.clone())
.unwrap() .or_default()
.extend(content_ranges); .extend(content_ranges);
} else { } else {
queue.push(ParseStep { queue.push(ParseStep {

View file

@ -20,3 +20,6 @@ settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
anyhow.workspace = true anyhow.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }

View file

@ -4,7 +4,6 @@ use gpui::{
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
}; };
use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
@ -55,7 +54,7 @@ impl View for ActiveBufferLanguage {
}; };
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| { MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.workspace.status_bar; let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state, false); let style = theme.active_language.style_for(state, false);
Label::new(active_language_text, style.text.clone()) Label::new(active_language_text, style.text.clone())
.contained() .contained()

View file

@ -8,7 +8,6 @@ use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContex
use language::{Buffer, LanguageRegistry}; use language::{Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate, PickerEvent}; use picker::{Picker, PickerDelegate, PickerEvent};
use project::Project; use project::Project;
use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
@ -179,8 +178,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
selected: bool, selected: bool,
cx: &AppContext, cx: &AppContext,
) -> AnyElement<Picker<Self>> { ) -> AnyElement<Picker<Self>> {
let settings = cx.global::<Settings>(); let theme = theme::current(cx);
let theme = &settings.theme;
let mat = &self.matches[ix]; let mat = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected); let style = theme.picker.item.style_for(mouse_state, selected);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());

View file

@ -24,6 +24,7 @@ serde.workspace = true
anyhow.workspace = true anyhow.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
unindent.workspace = true unindent.workspace = true

Some files were not shown because too many files have changed in this diff Show more