Merge pull request #757 from zed-industries/restructure-settings

Enable language specific tab sizes
This commit is contained in:
Max Brunsfeld 2022-04-06 15:18:13 -07:00 committed by GitHub
commit 3d8e4adcde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2324 additions and 1920 deletions

34
Cargo.lock generated
View file

@ -729,6 +729,7 @@ dependencies = [
"language", "language",
"project", "project",
"search", "search",
"settings",
"theme", "theme",
"workspace", "workspace",
] ]
@ -881,6 +882,7 @@ dependencies = [
"editor", "editor",
"gpui", "gpui",
"postage", "postage",
"settings",
"theme", "theme",
"time 0.3.7", "time 0.3.7",
"util", "util",
@ -1131,6 +1133,7 @@ dependencies = [
"client", "client",
"gpui", "gpui",
"postage", "postage",
"settings",
"theme", "theme",
"workspace", "workspace",
] ]
@ -1480,6 +1483,7 @@ dependencies = [
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
"settings",
"theme", "theme",
"unindent", "unindent",
"util", "util",
@ -1634,6 +1638,7 @@ dependencies = [
"futures", "futures",
"fuzzy", "fuzzy",
"gpui", "gpui",
"indoc",
"itertools", "itertools",
"language", "language",
"lazy_static", "lazy_static",
@ -1646,6 +1651,7 @@ dependencies = [
"rand 0.8.3", "rand 0.8.3",
"rpc", "rpc",
"serde", "serde",
"settings",
"smallvec", "smallvec",
"smol", "smol",
"snippet", "snippet",
@ -1806,6 +1812,7 @@ dependencies = [
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
"settings",
"theme", "theme",
"util", "util",
"workspace", "workspace",
@ -2206,6 +2213,7 @@ dependencies = [
"editor", "editor",
"gpui", "gpui",
"postage", "postage",
"settings",
"text", "text",
"workspace", "workspace",
] ]
@ -3255,6 +3263,7 @@ dependencies = [
"language", "language",
"ordered-float", "ordered-float",
"postage", "postage",
"settings",
"smol", "smol",
"text", "text",
"workspace", "workspace",
@ -3624,6 +3633,7 @@ dependencies = [
"rpc", "rpc",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"sha2 0.10.2", "sha2 0.10.2",
"similar", "similar",
"smol", "smol",
@ -3643,6 +3653,7 @@ dependencies = [
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
"settings",
"theme", "theme",
"util", "util",
"workspace", "workspace",
@ -3659,6 +3670,7 @@ dependencies = [
"ordered-float", "ordered-float",
"postage", "postage",
"project", "project",
"settings",
"smol", "smol",
"text", "text",
"util", "util",
@ -4258,6 +4270,7 @@ dependencies = [
"postage", "postage",
"project", "project",
"serde_json", "serde_json",
"settings",
"theme", "theme",
"unindent", "unindent",
"util", "util",
@ -4406,6 +4419,21 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "settings"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"schemars",
"serde",
"serde_json",
"serde_path_to_error",
"theme",
"toml",
"util",
]
[[package]] [[package]]
name = "sha-1" name = "sha-1"
version = "0.8.2" version = "0.8.2"
@ -5142,6 +5170,7 @@ dependencies = [
"log", "log",
"parking_lot", "parking_lot",
"postage", "postage",
"settings",
"smol", "smol",
"theme", "theme",
"workspace", "workspace",
@ -5719,6 +5748,7 @@ dependencies = [
"language", "language",
"log", "log",
"project", "project",
"settings",
"util", "util",
"workspace", "workspace",
] ]
@ -5948,9 +5978,9 @@ dependencies = [
"parking_lot", "parking_lot",
"postage", "postage",
"project", "project",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"smallvec", "smallvec",
"theme", "theme",
"util", "util",
@ -6034,6 +6064,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_path_to_error", "serde_path_to_error",
"settings",
"simplelog", "simplelog",
"smallvec", "smallvec",
"smol", "smol",
@ -6099,6 +6130,7 @@ dependencies = [
"scrypt", "scrypt",
"serde", "serde",
"serde_json", "serde_json",
"settings",
"sha-1 0.9.6", "sha-1 0.9.6",
"sqlx 0.5.5", "sqlx 0.5.5",
"surf", "surf",

View file

@ -14,6 +14,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
project = { path = "../project" } project = { path = "../project" }
search = { path = "../search" } search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -6,8 +6,9 @@ use gpui::{
use language::{Buffer, OutlineItem}; use language::{Buffer, OutlineItem};
use project::Project; use project::Project;
use search::ProjectSearchView; use search::ProjectSearchView;
use settings::Settings;
use theme::SyntaxTheme; use theme::SyntaxTheme;
use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView}; use workspace::{ItemHandle, ToolbarItemLocation, ToolbarItemView};
pub enum Event { pub enum Event {
UpdateLocation, UpdateLocation,

View file

@ -11,6 +11,7 @@ doctest = false
client = { path = "../client" } client = { path = "../client" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -13,10 +13,10 @@ use gpui::{
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
use postage::prelude::Stream; use postage::prelude::Stream;
use settings::{Settings, SoftWrap};
use std::sync::Arc; use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{settings::SoftWrap, Settings};
const MESSAGE_LOADING_THRESHOLD: usize = 50; const MESSAGE_LOADING_THRESHOLD: usize = 50;

View file

@ -10,6 +10,7 @@ doctest = false
[dependencies] [dependencies]
client = { path = "../client" } client = { path = "../client" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] } postage = { version = "0.4.1", features = ["futures-traits"] }

View file

@ -8,7 +8,8 @@ use gpui::{
Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View, Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
ViewContext, ViewContext,
}; };
use workspace::{AppState, JoinProject, JoinProjectParams, Settings}; use workspace::{AppState, JoinProject, JoinProjectParams};
use settings::Settings;
pub struct ContactsPanel { pub struct ContactsPanel {
contacts: ListState, contacts: ListState,

View file

@ -14,6 +14,7 @@ editor = { path = "../editor" }
language = { path = "../language" } language = { path = "../language" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -25,7 +25,8 @@ use std::{
sync::Arc, sync::Arc,
}; };
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace}; use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
use settings::Settings;
action!(Deploy); action!(Deploy);

View file

@ -3,7 +3,8 @@ use gpui::{
elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext, elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
}; };
use project::Project; use project::Project;
use workspace::{Settings, StatusItemView}; use workspace::{StatusItemView};
use settings::Settings;
pub struct DiagnosticSummary { pub struct DiagnosticSummary {
summary: project::DiagnosticSummary, summary: project::DiagnosticSummary,

View file

@ -28,6 +28,7 @@ language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
project = { path = "../project" } project = { path = "../project" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
settings = { path = "../settings" }
snippet = { path = "../snippet" } snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
theme = { path = "../theme" } theme = { path = "../theme" }
@ -36,6 +37,7 @@ workspace = { path = "../workspace" }
aho-corasick = "0.7" aho-corasick = "0.7"
anyhow = "1.0" anyhow = "1.0"
futures = "0.3" futures = "0.3"
indoc = "1.0.4"
itertools = "0.10" itertools = "0.10"
lazy_static = "1.4" lazy_static = "1.4"
log = "0.4" log = "0.4"
@ -54,6 +56,7 @@ lsp = { path = "../lsp", 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"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
ctor = "0.1" ctor = "0.1"
env_logger = "0.8" env_logger = "0.8"

View file

@ -12,6 +12,7 @@ use gpui::{
Entity, ModelContext, ModelHandle, Entity, ModelContext, ModelHandle,
}; };
use language::{Point, Subscription as BufferSubscription}; use language::{Point, Subscription as BufferSubscription};
use settings::Settings;
use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc}; use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap}; use sum_tree::{Bias, TreeMap};
use tab_map::TabMap; use tab_map::TabMap;
@ -46,7 +47,6 @@ impl Entity for DisplayMap {
impl DisplayMap { impl DisplayMap {
pub fn new( pub fn new(
buffer: ModelHandle<MultiBuffer>, buffer: ModelHandle<MultiBuffer>,
tab_size: usize,
font_id: FontId, font_id: FontId,
font_size: f32, font_size: f32,
wrap_width: Option<f32>, wrap_width: Option<f32>,
@ -55,6 +55,8 @@ impl DisplayMap {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Self { ) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let tab_size = Self::tab_size(&buffer, cx);
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx)); let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx); let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
@ -76,7 +78,9 @@ impl DisplayMap {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner(); let edits = self.buffer_subscription.consume().into_inner();
let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits); let (folds_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits, tab_size);
let (wraps_snapshot, edits) = self let (wraps_snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx)); .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
@ -100,14 +104,15 @@ impl DisplayMap {
) { ) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner(); let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits); self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges); let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@ -122,14 +127,15 @@ impl DisplayMap {
) { ) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner(); let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits); self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive); let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@ -143,8 +149,9 @@ impl DisplayMap {
) -> Vec<BlockId> { ) -> Vec<BlockId> {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner(); let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@ -159,8 +166,9 @@ impl DisplayMap {
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) { pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner(); let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.fold_map.read(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self let (snapshot, edits) = self
.wrap_map .wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx)); .update(cx, |map, cx| map.sync(snapshot, edits, cx));
@ -195,6 +203,16 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
} }
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> u32 {
let language_name = buffer
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).language())
.map(|language| language.name());
cx.global::<Settings>().tab_size(language_name.as_deref())
}
#[cfg(test)] #[cfg(test)]
pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool { pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
self.wrap_map.read(cx).is_rewrapping() self.wrap_map.read(cx).is_rewrapping()
@ -536,6 +554,8 @@ pub mod tests {
log::info!("tab size: {}", tab_size); log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width); log::info!("wrap width: {:?}", wrap_width);
cx.update(|cx| cx.set_global(Settings::test(cx)));
let buffer = cx.update(|cx| { let buffer = cx.update(|cx| {
if rng.gen() { if rng.gen() {
let len = rng.gen_range(0..10); let len = rng.gen_range(0..10);
@ -549,7 +569,6 @@ pub mod tests {
let map = cx.add_model(|cx| { let map = cx.add_model(|cx| {
DisplayMap::new( DisplayMap::new(
buffer.clone(), buffer.clone(),
tab_size,
font_id, font_id,
font_size, font_size,
wrap_width, wrap_width,
@ -759,27 +778,18 @@ pub mod tests {
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let tab_size = 4;
let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.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);
let map = cx.add_model(|cx| { let map = cx.add_model(|cx| {
DisplayMap::new( DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
buffer.clone(),
tab_size,
font_id,
font_size,
wrap_width,
1,
1,
cx,
)
}); });
let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
@ -847,18 +857,17 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_text_chunks(cx: &mut gpui::MutableAppContext) { fn test_text_chunks(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::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 tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx let font_id = cx
.font_cache() .font_cache()
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { let map =
DisplayMap::new(buffer.clone(), tab_size, 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![
@ -923,12 +932,17 @@ pub mod tests {
.unwrap(), .unwrap(),
); );
language.set_theme(&theme); language.set_theme(&theme);
cx.update(|cx| {
cx.set_global(Settings {
tab_size: 2,
..Settings::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;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_size = 2;
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache let font_id = font_cache
@ -936,8 +950,7 @@ pub mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
vec![ vec![
@ -1011,22 +1024,22 @@ pub mod tests {
); );
language.set_theme(&theme); language.set_theme(&theme);
cx.update(|cx| cx.set_global(Settings::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;
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap(); let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 16.0; let font_size = 16.0;
let map = cx.add_model(|cx| { let map =
DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx) cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
});
assert_eq!( assert_eq!(
cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
[ [
@ -1058,6 +1071,7 @@ pub mod tests {
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.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
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()),
@ -1090,14 +1104,12 @@ pub mod tests {
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap(); let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 16.0; let font_size = 16.0;
let map = cx let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx));
enum MyType {} enum MyType {}
@ -1136,6 +1148,7 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) { fn test_clip_point(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) { fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) {
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
@ -1187,6 +1200,8 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) { fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(text: &str, cx: &mut gpui::MutableAppContext) { fn assert(text: &str, cx: &mut gpui::MutableAppContext) {
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
unmarked_snapshot.clip_at_line_ends = true; unmarked_snapshot.clip_at_line_ends = true;
@ -1204,9 +1219,9 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) { fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::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 tab_size = 4;
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache let font_id = font_cache
@ -1214,9 +1229,8 @@ pub mod tests {
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { let map =
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx) cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
});
let map = map.update(cx, |map, cx| map.snapshot(cx)); let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ"); assert_eq!(map.text(), "α\nβ \n🏀β γ");
assert_eq!( assert_eq!(
@ -1264,17 +1278,16 @@ pub mod tests {
#[gpui::test] #[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) { fn test_max_point(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let tab_size = 4;
let font_cache = cx.font_cache(); let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache let font_id = font_cache
.select_font(family_id, &Default::default()) .select_font(family_id, &Default::default())
.unwrap(); .unwrap();
let font_size = 14.0; let font_size = 14.0;
let map = cx.add_model(|cx| { let map =
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx) cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
});
assert_eq!( assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(), map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11) DisplayPoint::new(1, 11)

View file

@ -969,6 +969,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 std::env; use std::env;
use text::RandomCharIter; use text::RandomCharIter;
@ -988,6 +989,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_basic_blocks(cx: &mut gpui::MutableAppContext) { fn test_basic_blocks(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx let font_id = cx
.font_cache() .font_cache()
@ -1157,7 +1160,7 @@ mod tests {
let (folds_snapshot, fold_edits) = let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot, subscription.consume().into_inner()); fold_map.read(buffer_snapshot, subscription.consume().into_inner());
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, 4);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx) wrap_map.sync(tabs_snapshot, tab_edits, cx)
}); });
@ -1167,6 +1170,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) { fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx let font_id = cx
.font_cache() .font_cache()
@ -1209,6 +1214,8 @@ mod tests {
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
cx.set_global(Settings::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);
@ -1296,7 +1303,8 @@ mod tests {
let (folds_snapshot, fold_edits) = let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]); fold_map.read(buffer_snapshot.clone(), vec![]);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx) wrap_map.sync(tabs_snapshot, tab_edits, cx)
}); });
@ -1318,7 +1326,8 @@ mod tests {
let (folds_snapshot, fold_edits) = let (folds_snapshot, fold_edits) =
fold_map.read(buffer_snapshot.clone(), vec![]); fold_map.read(buffer_snapshot.clone(), vec![]);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx) wrap_map.sync(tabs_snapshot, tab_edits, cx)
}); });
@ -1338,7 +1347,7 @@ mod tests {
} }
let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits); let (folds_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tabs_snapshot, tab_edits, cx) wrap_map.sync(tabs_snapshot, tab_edits, cx)
}); });

View file

@ -1210,6 +1210,7 @@ mod tests {
use super::*; use super::*;
use crate::{MultiBuffer, ToPoint}; use crate::{MultiBuffer, ToPoint};
use rand::prelude::*; use rand::prelude::*;
use settings::Settings;
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 text::RandomCharIter; use text::RandomCharIter;
@ -1218,6 +1219,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_basic_folds(cx: &mut gpui::MutableAppContext) { fn test_basic_folds(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::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);
@ -1291,6 +1293,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) { fn test_adjacent_folds(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::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);
@ -1354,6 +1357,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) { fn test_merging_folds_via_edit(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::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);
@ -1404,6 +1408,7 @@ mod tests {
#[gpui::test(iterations = 100)] #[gpui::test(iterations = 100)]
fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { fn test_random_folds(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
cx.set_global(Settings::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);

View file

@ -12,7 +12,7 @@ use text::Point;
pub struct TabMap(Mutex<TabSnapshot>); pub struct TabMap(Mutex<TabSnapshot>);
impl TabMap { impl TabMap {
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, TabSnapshot) { pub fn new(input: FoldSnapshot, tab_size: u32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot { let snapshot = TabSnapshot {
fold_snapshot: input, fold_snapshot: input,
tab_size, tab_size,
@ -24,12 +24,13 @@ impl TabMap {
&self, &self,
fold_snapshot: FoldSnapshot, fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>, mut fold_edits: Vec<FoldEdit>,
tab_size: u32,
) -> (TabSnapshot, Vec<TabEdit>) { ) -> (TabSnapshot, Vec<TabEdit>) {
let mut old_snapshot = self.0.lock(); let mut old_snapshot = self.0.lock();
let max_offset = old_snapshot.fold_snapshot.len(); let max_offset = old_snapshot.fold_snapshot.len();
let new_snapshot = TabSnapshot { let new_snapshot = TabSnapshot {
fold_snapshot, fold_snapshot,
tab_size: old_snapshot.tab_size, tab_size,
}; };
let mut tab_edits = Vec::with_capacity(fold_edits.len()); let mut tab_edits = Vec::with_capacity(fold_edits.len());
@ -87,7 +88,7 @@ impl TabMap {
#[derive(Clone)] #[derive(Clone)]
pub struct TabSnapshot { pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot, pub fold_snapshot: FoldSnapshot,
pub tab_size: usize, pub tab_size: u32,
} }
impl TabSnapshot { impl TabSnapshot {
@ -234,7 +235,7 @@ impl TabSnapshot {
.to_buffer_point(&self.fold_snapshot) .to_buffer_point(&self.fold_snapshot)
} }
fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize { fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: u32) -> usize {
let mut expanded_chars = 0; let mut expanded_chars = 0;
let mut expanded_bytes = 0; let mut expanded_bytes = 0;
let mut collapsed_bytes = 0; let mut collapsed_bytes = 0;
@ -243,7 +244,7 @@ impl TabSnapshot {
break; break;
} }
if c == '\t' { if c == '\t' {
let tab_len = tab_size - expanded_chars % tab_size; let tab_len = tab_size as usize - expanded_chars % tab_size as usize;
expanded_bytes += tab_len; expanded_bytes += tab_len;
expanded_chars += tab_len; expanded_chars += tab_len;
} else { } else {
@ -259,7 +260,7 @@ impl TabSnapshot {
mut chars: impl Iterator<Item = char>, mut chars: impl Iterator<Item = char>,
column: usize, column: usize,
bias: Bias, bias: Bias,
tab_size: usize, tab_size: u32,
) -> (usize, usize, usize) { ) -> (usize, usize, usize) {
let mut expanded_bytes = 0; let mut expanded_bytes = 0;
let mut expanded_chars = 0; let mut expanded_chars = 0;
@ -270,7 +271,7 @@ impl TabSnapshot {
} }
if c == '\t' { if c == '\t' {
let tab_len = tab_size - (expanded_chars % tab_size); let tab_len = tab_size as usize - (expanded_chars % tab_size as usize);
expanded_chars += tab_len; expanded_chars += tab_len;
expanded_bytes += tab_len; expanded_bytes += tab_len;
if expanded_bytes > column { if expanded_bytes > column {
@ -383,7 +384,7 @@ pub struct TabChunks<'a> {
column: usize, column: usize,
output_position: Point, output_position: Point,
max_output_position: Point, max_output_position: Point,
tab_size: usize, tab_size: u32,
skip_leading_tab: bool, skip_leading_tab: bool,
} }
@ -415,16 +416,16 @@ impl<'a> Iterator for TabChunks<'a> {
}); });
} else { } else {
self.chunk.text = &self.chunk.text[1..]; self.chunk.text = &self.chunk.text[1..];
let mut len = self.tab_size - self.column % self.tab_size; let mut len = self.tab_size - self.column as u32 % self.tab_size;
let next_output_position = cmp::min( let next_output_position = cmp::min(
self.output_position + Point::new(0, len as u32), self.output_position + Point::new(0, len),
self.max_output_position, self.max_output_position,
); );
len = (next_output_position.column - self.output_position.column) as usize; len = next_output_position.column - self.output_position.column;
self.column += len; self.column += len as usize;
self.output_position = next_output_position; self.output_position = next_output_position;
return Some(Chunk { return Some(Chunk {
text: &SPACES[0..len], text: &SPACES[0..len as usize],
..self.chunk ..self.chunk
}); });
} }

View file

@ -1014,12 +1014,14 @@ mod tests {
use gpui::test::observe; use gpui::test::observe;
use language::RandomCharIter; use language::RandomCharIter;
use rand::prelude::*; use rand::prelude::*;
use settings::Settings;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cmp, env}; use std::{cmp, env};
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)));
cx.foreground().set_block_on_ticks(0..=50); cx.foreground().set_block_on_ticks(0..=50);
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS") let operations = env::var("OPERATIONS")
@ -1104,7 +1106,8 @@ mod tests {
} }
20..=39 => { 20..=39 => {
for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { for (folds_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) =
tab_map.sync(folds_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) = let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants(); snapshot.check_invariants();
@ -1129,7 +1132,7 @@ mod tests {
"Unwrapped text (unexpanded tabs): {:?}", "Unwrapped text (unexpanded tabs): {:?}",
folds_snapshot.text() folds_snapshot.text()
); );
let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits, tab_size);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text()); log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text(); let unwrapped_text = tabs_snapshot.text();

View file

@ -41,6 +41,7 @@ pub use multi_buffer::{
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::{Project, ProjectTransaction}; use project::{Project, ProjectTransaction};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings;
use smallvec::SmallVec; use smallvec::SmallVec;
use smol::Timer; use smol::Timer;
use snippet::Snippet; use snippet::Snippet;
@ -57,7 +58,7 @@ pub use sum_tree::Bias;
use text::rope::TextDimension; use text::rope::TextDimension;
use theme::DiagnosticStyle; use theme::DiagnosticStyle;
use util::{post_inc, ResultExt, TryFutureExt}; use util::{post_inc, ResultExt, TryFutureExt};
use workspace::{settings, ItemNavHistory, Settings, Workspace}; use workspace::{ItemNavHistory, Workspace};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024; const MAX_LINE_LEN: usize = 1024;
@ -1008,7 +1009,6 @@ impl Editor {
let style = build_style(&*settings, get_field_editor_theme, None, cx); let style = build_style(&*settings, get_field_editor_theme, None, cx);
DisplayMap::new( DisplayMap::new(
buffer.clone(), buffer.clone(),
settings.tab_size,
style.text.font_id, style.text.font_id,
style.text.font_size, style.text.font_size,
None, None,
@ -1130,8 +1130,12 @@ impl Editor {
} }
} }
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> { pub fn language_at<'a, T: ToOffset>(
self.buffer.read(cx).language(cx) &self,
point: T,
cx: &'a AppContext,
) -> Option<&'a Arc<Language>> {
self.buffer.read(cx).language_at(point, cx)
} }
fn style(&self, cx: &AppContext) -> EditorStyle { fn style(&self, cx: &AppContext) -> EditorStyle {
@ -2945,8 +2949,9 @@ impl Editor {
.buffer_line_for_row(old_head.row) .buffer_line_for_row(old_head.row)
{ {
let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row);
let language_name = buffer.language().map(|language| language.name());
let indent = cx.global::<Settings>().tab_size(language_name.as_deref());
if old_head.column <= indent_column && old_head.column > 0 { if old_head.column <= indent_column && old_head.column > 0 {
let indent = buffer.indent_size();
new_head = cmp::min( new_head = cmp::min(
new_head, new_head,
Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), Point::new(old_head.row, ((old_head.column - 1) / indent) * indent),
@ -2991,12 +2996,15 @@ impl Editor {
return; return;
} }
let tab_size = cx.global::<Settings>().tab_size;
let mut selections = self.local_selections::<Point>(cx); let mut selections = self.local_selections::<Point>(cx);
if selections.iter().all(|s| s.is_empty()) { if selections.iter().all(|s| s.is_empty()) {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| { this.buffer.update(cx, |buffer, cx| {
for selection in &mut selections { for selection in &mut selections {
let language_name =
buffer.language_at(selection.start, cx).map(|l| l.name());
let tab_size =
cx.global::<Settings>().tab_size(language_name.as_deref());
let char_column = buffer let char_column = buffer
.read(cx) .read(cx)
.text_for_range( .text_for_range(
@ -3004,13 +3012,14 @@ impl Editor {
) )
.flat_map(str::chars) .flat_map(str::chars)
.count(); .count();
let chars_to_next_tab_stop = tab_size - (char_column % tab_size); let chars_to_next_tab_stop =
tab_size - (char_column as u32 % tab_size);
buffer.edit( buffer.edit(
[selection.start..selection.start], [selection.start..selection.start],
" ".repeat(chars_to_next_tab_stop), " ".repeat(chars_to_next_tab_stop as usize),
cx, cx,
); );
selection.start.column += chars_to_next_tab_stop as u32; selection.start.column += chars_to_next_tab_stop;
selection.end = selection.start; selection.end = selection.start;
} }
}); });
@ -3024,12 +3033,14 @@ impl Editor {
} }
pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) { pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
let tab_size = cx.global::<Settings>().tab_size;
let mut selections = self.local_selections::<Point>(cx); let mut selections = self.local_selections::<Point>(cx);
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let mut last_indent = None; let mut last_indent = None;
this.buffer.update(cx, |buffer, cx| { this.buffer.update(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
for selection in &mut selections { for selection in &mut selections {
let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
let tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
let mut start_row = selection.start.row; let mut start_row = selection.start.row;
let mut end_row = selection.end.row + 1; let mut end_row = selection.end.row + 1;
@ -3053,12 +3064,12 @@ impl Editor {
} }
for row in start_row..end_row { for row in start_row..end_row {
let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; let indent_column = snapshot.indent_column_for_line(row);
let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); let columns_to_next_tab_stop = tab_size - (indent_column % tab_size);
let row_start = Point::new(row, 0); let row_start = Point::new(row, 0);
buffer.edit( buffer.edit(
[row_start..row_start], [row_start..row_start],
" ".repeat(columns_to_next_tab_stop), " ".repeat(columns_to_next_tab_stop as usize),
cx, cx,
); );
@ -3080,14 +3091,16 @@ impl Editor {
} }
pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) { pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
let tab_size = cx.global::<Settings>().tab_size;
let selections = self.local_selections::<Point>(cx); let selections = self.local_selections::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut deletion_ranges = Vec::new(); let mut deletion_ranges = Vec::new();
let mut last_outdent = None; let mut last_outdent = None;
{ {
let buffer = self.buffer.read(cx).read(cx); let buffer = self.buffer.read(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 tab_size = cx.global::<Settings>().tab_size(language_name.as_deref());
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
@ -3099,11 +3112,11 @@ impl Editor {
} }
for row in rows { for row in rows {
let column = buffer.indent_column_for_line(row) as usize; let column = snapshot.indent_column_for_line(row);
if column > 0 { if column > 0 {
let mut deletion_len = (column % tab_size) as u32; let mut deletion_len = column % tab_size;
if deletion_len == 0 { if deletion_len == 0 {
deletion_len = tab_size as u32; deletion_len = tab_size;
} }
deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len)); deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
last_outdent = Some(row); last_outdent = Some(row);
@ -4243,24 +4256,26 @@ impl Editor {
} }
pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) { pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
// Get the line comment prefix. Split its trailing whitespace into a separate string,
// as that portion won't be used for detecting if a line is a comment.
let full_comment_prefix =
if let Some(prefix) = self.language(cx).and_then(|l| l.line_comment_prefix()) {
prefix.to_string()
} else {
return;
};
let comment_prefix = full_comment_prefix.trim_end_matches(' ');
let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let mut selections = this.local_selections::<Point>(cx); let mut selections = this.local_selections::<Point>(cx);
let mut all_selection_lines_are_comments = true; let mut all_selection_lines_are_comments = true;
let mut edit_ranges = Vec::new(); let mut edit_ranges = Vec::new();
let mut last_toggled_row = None; let mut last_toggled_row = None;
this.buffer.update(cx, |buffer, cx| { this.buffer.update(cx, |buffer, cx| {
// TODO: Handle selections that cross excerpts
for selection in &mut selections { for selection in &mut selections {
// Get the line comment prefix. Split its trailing whitespace into a separate string,
// as that portion won't be used for detecting if a line is a comment.
let full_comment_prefix = if let Some(prefix) = buffer
.language_at(selection.start, cx)
.and_then(|l| l.line_comment_prefix())
{
prefix.to_string()
} else {
return;
};
let comment_prefix = full_comment_prefix.trim_end_matches(' ');
let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
edit_ranges.clear(); edit_ranges.clear();
let snapshot = buffer.snapshot(cx); let snapshot = buffer.snapshot(cx);
@ -5668,16 +5683,22 @@ impl Editor {
} }
pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
let language = self.language(cx); let language_name = self
.buffer
.read(cx)
.as_singleton()
.and_then(|singleton_buffer| singleton_buffer.read(cx).language())
.map(|l| l.name());
let settings = cx.global::<Settings>(); 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)); .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref()));
match mode { match mode {
settings::SoftWrap::None => SoftWrap::None, settings::SoftWrap::None => SoftWrap::None,
settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
settings::SoftWrap::PreferredLineLength => { settings::SoftWrap::PreferredLineLength => {
SoftWrap::Column(settings.preferred_line_length(language)) SoftWrap::Column(settings.preferred_line_length(language_name.as_deref()))
} }
} }
} }
@ -6461,14 +6482,18 @@ pub fn styled_runs_for_code_label<'a>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::test::{assert_text_with_selections, select_ranges};
use super::*; use super::*;
use gpui::{ use gpui::{
geometry::rect::RectF, geometry::rect::RectF,
platform::{WindowBounds, WindowOptions}, platform::{WindowBounds, WindowOptions},
}; };
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig}; use language::{FakeLspAdapter, LanguageConfig};
use lsp::FakeLanguageServer; use lsp::FakeLanguageServer;
use project::FakeFs; use project::FakeFs;
use settings::LanguageOverride;
use smol::stream::StreamExt; use smol::stream::StreamExt;
use std::{cell::RefCell, rc::Rc, time::Instant}; use std::{cell::RefCell, rc::Rc, time::Instant};
use text::Point; use text::Point;
@ -6478,7 +6503,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) { fn test_edit_events(cx: &mut MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
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 events = Rc::new(RefCell::new(Vec::new())); let events = Rc::new(RefCell::new(Vec::new()));
@ -6586,7 +6611,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::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(cx).transaction_group_interval(); let group_interval = buffer.read(cx).transaction_group_interval();
@ -6655,7 +6680,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -6720,7 +6745,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) { fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -6752,7 +6777,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_navigation_history(cx: &mut gpui::MutableAppContext) { fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
use workspace::Item; use workspace::Item;
let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default()));
let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx);
@ -6812,7 +6837,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_cancel(cx: &mut gpui::MutableAppContext) { fn test_cancel(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -6852,7 +6877,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_fold(cx: &mut gpui::MutableAppContext) { fn test_fold(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
&" &"
impl Foo { impl Foo {
@ -6937,7 +6962,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_cursor(cx: &mut gpui::MutableAppContext) { fn test_move_cursor(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@ -7011,7 +7036,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) { fn test_move_cursor_multibyte(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@ -7112,7 +7137,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) { fn test_move_cursor_different_line_lengths(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7157,7 +7182,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) { fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abc\n def", cx); let buffer = MultiBuffer::build_simple("abc\n def", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7298,7 +7323,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) { fn test_prev_next_word_boundary(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(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);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7403,7 +7428,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(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);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -7456,7 +7481,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) { fn test_delete_to_word_boundary(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("one two three four", cx); let buffer = MultiBuffer::build_simple("one two three four", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@ -7493,7 +7518,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_newline(cx: &mut gpui::MutableAppContext) { fn test_newline(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@ -7514,7 +7539,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) { fn test_newline_with_old_selections(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple( let buffer = MultiBuffer::build_simple(
" "
a a
@ -7599,7 +7624,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) { fn test_insert_with_old_selections(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(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 (_, editor) = cx.add_window(Default::default(), |cx| { let (_, editor) = cx.add_window(Default::default(), |cx| {
let mut editor = build_editor(buffer.clone(), cx); let mut editor = build_editor(buffer.clone(), cx);
@ -7626,81 +7651,226 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_indent_outdent(cx: &mut gpui::MutableAppContext) { fn test_indent_outdent(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(" one two\nthree\n four", cx); let buffer = MultiBuffer::build_simple(
indoc! {"
one two
three
four"},
cx,
);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
// two selections on the same line // two selections on the same line
view.select_display_ranges( select_ranges(
&[ view,
DisplayPoint::new(0, 2)..DisplayPoint::new(0, 5), indoc! {"
DisplayPoint::new(0, 6)..DisplayPoint::new(0, 9), [one] [two]
], three
four"},
cx, cx,
); );
// indent from mid-tabstop to full tabstop // indent from mid-tabstop to full tabstop
view.tab(&Tab(Direction::Next), cx); view.tab(&Tab(Direction::Next), cx);
assert_eq!(view.text(cx), " one two\nthree\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[ [one] [two]
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), three
DisplayPoint::new(0, 8)..DisplayPoint::new(0, 11), four"},
] cx,
); );
// outdent from 1 tabstop to 0 tabstops // outdent from 1 tabstop to 0 tabstops
view.tab(&Tab(Direction::Prev), cx); view.tab(&Tab(Direction::Prev), cx);
assert_eq!(view.text(cx), "one two\nthree\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[ [one] [two]
DisplayPoint::new(0, 0)..DisplayPoint::new(0, 3), three
DisplayPoint::new(0, 4)..DisplayPoint::new(0, 7), four"},
] cx,
); );
// select across line ending // select across line ending
view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); select_ranges(
view,
indoc! {"
one two
t[hree
] four"},
cx,
);
// indent and outdent affect only the preceding line // indent and outdent affect only the preceding line
view.tab(&Tab(Direction::Next), cx); view.tab(&Tab(Direction::Next), cx);
assert_eq!(view.text(cx), "one two\n three\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] one two
t[hree
] four"},
cx,
); );
view.tab(&Tab(Direction::Prev), cx); view.tab(&Tab(Direction::Prev), cx);
assert_eq!(view.text(cx), "one two\nthree\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)] one two
t[hree
] four"},
cx,
); );
// Ensure that indenting/outdenting works when the cursor is at column 0. // Ensure that indenting/outdenting works when the cursor is at column 0.
view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); select_ranges(
view,
indoc! {"
one two
[]three
four"},
cx,
);
view.tab(&Tab(Direction::Next), cx); view.tab(&Tab(Direction::Next), cx);
assert_eq!(view.text(cx), "one two\n three\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)] one two
[]three
four"},
cx,
); );
view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); select_ranges(
view,
indoc! {"
one two
[] three
four"},
cx,
);
view.tab(&Tab(Direction::Prev), cx); view.tab(&Tab(Direction::Prev), cx);
assert_eq!(view.text(cx), "one two\nthree\n four"); assert_text_with_selections(
assert_eq!( view,
view.selected_display_ranges(cx), indoc! {"
&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)] one two
[]three
four"},
cx,
); );
}); });
} }
#[gpui::test]
fn test_indent_outdent_with_excerpts(cx: &mut gpui::MutableAppContext) {
cx.set_global(
Settings::test(cx)
.with_overrides(
"TOML",
LanguageOverride {
tab_size: Some(2),
..Default::default()
},
)
.with_overrides(
"Rust",
LanguageOverride {
tab_size: Some(4),
..Default::default()
},
),
);
let toml_language = Arc::new(Language::new(
LanguageConfig {
name: "TOML".into(),
..Default::default()
},
None,
));
let rust_language = Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
..Default::default()
},
None,
));
let toml_buffer = cx
.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx));
let rust_buffer = cx.add_model(|cx| {
Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx)
});
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
toml_buffer.clone(),
[Point::new(0, 0)..Point::new(2, 0)],
cx,
);
multibuffer.push_excerpts(
rust_buffer.clone(),
[Point::new(0, 0)..Point::new(1, 0)],
cx,
);
multibuffer
});
cx.add_window(Default::default(), |cx| {
let mut editor = build_editor(multibuffer, cx);
assert_eq!(
editor.text(cx),
indoc! {"
a = 1
b = 2
const c: usize = 3;
"}
);
select_ranges(
&mut editor,
indoc! {"
[a] = 1
b = 2
[const c:] usize = 3;
"},
cx,
);
editor.tab(&Tab(Direction::Next), cx);
assert_text_with_selections(
&mut editor,
indoc! {"
[a] = 1
b = 2
[const c:] usize = 3;
"},
cx,
);
editor.tab(&Tab(Direction::Prev), cx);
assert_text_with_selections(
&mut editor,
indoc! {"
[a] = 1
b = 2
[const c:] usize = 3;
"},
cx,
);
editor
});
}
#[gpui::test] #[gpui::test]
fn test_backspace(cx: &mut gpui::MutableAppContext) { fn test_backspace(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let (_, view) = cx.add_window(Default::default(), |cx| { let (_, view) = cx.add_window(Default::default(), |cx| {
build_editor(MultiBuffer::build_simple("", cx), cx) build_editor(MultiBuffer::build_simple("", cx), cx)
}); });
@ -7745,7 +7915,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_delete(cx: &mut gpui::MutableAppContext) { fn test_delete(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = let buffer =
MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
@ -7773,7 +7943,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_delete_line(cx: &mut gpui::MutableAppContext) { fn test_delete_line(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7796,7 +7966,7 @@ mod tests {
); );
}); });
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7812,7 +7982,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_duplicate_line(cx: &mut gpui::MutableAppContext) { fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7862,7 +8032,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) { fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -7958,7 +8128,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) { fn test_move_line_up_down_with_blocks(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
let snapshot = buffer.read(cx).snapshot(cx); let snapshot = buffer.read(cx).snapshot(cx);
let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -7979,7 +8149,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_clipboard(cx: &mut gpui::MutableAppContext) { fn test_clipboard(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx); let buffer = MultiBuffer::build_simple("one✅ two three four five six ", cx);
let view = cx let view = cx
.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)) .add_window(Default::default(), |cx| build_editor(buffer.clone(), cx))
@ -8108,7 +8278,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_select_all(cx: &mut gpui::MutableAppContext) { fn test_select_all(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -8122,7 +8292,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_select_line(cx: &mut gpui::MutableAppContext) { fn test_select_line(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -8167,7 +8337,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) { fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
@ -8233,7 +8403,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) { fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(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);
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
@ -8417,7 +8587,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_select_next(cx: &mut gpui::MutableAppContext) { fn test_select_next(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::test(cx));
let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]");
let buffer = MultiBuffer::build_simple(&text, cx); let buffer = MultiBuffer::build_simple(&text, cx);
@ -8447,7 +8617,7 @@ mod tests {
#[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(populate_settings); cx.update(|cx| cx.set_global(Settings::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()),
@ -8588,7 +8758,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new( let language = Arc::new(
Language::new( Language::new(
LanguageConfig { LanguageConfig {
@ -8645,7 +8815,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
brackets: vec![ brackets: vec![
@ -8792,7 +8962,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_snippets(cx: &mut gpui::TestAppContext) { async fn test_snippets(cx: &mut gpui::TestAppContext) {
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let text = " let text = "
a. b a. b
@ -8900,7 +9070,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_format_during_save(cx: &mut gpui::TestAppContext) { async fn test_format_during_save(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -8952,6 +9122,7 @@ mod tests {
params.text_document.uri, params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap() lsp::Url::from_file_path("/file.rs").unwrap()
); );
assert_eq!(params.options.tab_size, 4);
Ok(Some(vec![lsp::TextEdit::new( Ok(Some(vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
", ".to_string(), ", ".to_string(),
@ -8988,11 +9159,39 @@ mod tests {
"one\ntwo\nthree\n" "one\ntwo\nthree\n"
); );
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
cx.update(|cx| {
cx.update_global::<Settings, _, _>(|settings, _| {
settings.language_overrides.insert(
"Rust".into(),
LanguageOverride {
tab_size: Some(8),
..Default::default()
},
);
})
});
let save = cx.update(|cx| editor.save(project.clone(), cx));
fake_server
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
);
assert_eq!(params.options.tab_size, 8);
Ok(Some(vec![]))
})
.next()
.await;
cx.foreground().start_waiting();
save.await.unwrap();
} }
#[gpui::test] #[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) { async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {
@ -9233,7 +9432,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
cx.update(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
line_comment: Some("// ".to_string()), line_comment: Some("// ".to_string()),
@ -9313,7 +9512,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) { fn test_editing_disjoint_excerpts(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::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);
@ -9356,7 +9555,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) { fn test_editing_overlapping_excerpts(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::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);
@ -9411,7 +9610,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_refresh_selections(cx: &mut gpui::MutableAppContext) { fn test_refresh_selections(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::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| {
@ -9489,7 +9688,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) {
populate_settings(cx); cx.set_global(Settings::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| {
@ -9543,7 +9742,7 @@ mod tests {
#[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(populate_settings); cx.update(|cx| cx.set_global(Settings::test(cx)));
let language = Arc::new(Language::new( let language = Arc::new(Language::new(
LanguageConfig { LanguageConfig {
brackets: vec![ brackets: vec![
@ -9611,7 +9810,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) { fn test_highlighted_ranges(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
populate_settings(cx);
cx.set_global(Settings::test(cx));
let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
@ -9690,7 +9890,8 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_following(cx: &mut gpui::MutableAppContext) { fn test_following(cx: &mut gpui::MutableAppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
populate_settings(cx);
cx.set_global(Settings::test(cx));
let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx));
let (_, follower) = cx.add_window( let (_, follower) = cx.add_window(
@ -9857,11 +10058,6 @@ mod tests {
Editor::new(EditorMode::Full, buffer, None, None, cx) Editor::new(EditorMode::Full, buffer, None, None, cx)
} }
fn populate_settings(cx: &mut gpui::MutableAppContext) {
let settings = Settings::test(cx);
cx.set_global(settings);
}
fn assert_selection_ranges( fn assert_selection_ranges(
marked_text: &str, marked_text: &str,
selection_marker_pairs: Vec<(char, char)>, selection_marker_pairs: Vec<(char, char)>,

View file

@ -1494,8 +1494,8 @@ mod tests {
display_map::{BlockDisposition, BlockProperties}, display_map::{BlockDisposition, BlockProperties},
Editor, MultiBuffer, Editor, MultiBuffer,
}; };
use settings::Settings;
use util::test::sample_text; use util::test::sample_text;
use workspace::Settings;
#[gpui::test] #[gpui::test]
fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {

View file

@ -8,12 +8,11 @@ use gpui::{
use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal};
use project::{File, Project, ProjectEntryId, ProjectPath}; use project::{File, Project, ProjectEntryId, ProjectPath};
use rpc::proto::{self, update_view}; use rpc::proto::{self, update_view};
use settings::Settings;
use std::{fmt::Write, path::PathBuf, time::Duration}; use std::{fmt::Write, path::PathBuf, time::Duration};
use text::{Point, Selection}; use text::{Point, Selection};
use util::TryFutureExt; use util::TryFutureExt;
use workspace::{ use workspace::{FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, StatusItemView};
FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView,
};
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);

View file

@ -268,9 +268,11 @@ mod tests {
use super::*; use super::*;
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer}; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer};
use language::Point; use language::Point;
use settings::Settings;
#[gpui::test] #[gpui::test]
fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { fn test_previous_word_start(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -297,6 +299,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) { fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -330,6 +333,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) { fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert( fn assert(
marked_text: &str, marked_text: &str,
cx: &mut gpui::MutableAppContext, cx: &mut gpui::MutableAppContext,
@ -361,6 +365,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_next_word_end(cx: &mut gpui::MutableAppContext) { fn test_next_word_end(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -384,6 +389,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_next_subword_end(cx: &mut gpui::MutableAppContext) { fn test_next_subword_end(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -416,6 +422,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_find_boundary(cx: &mut gpui::MutableAppContext) { fn test_find_boundary(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert( fn assert(
marked_text: &str, marked_text: &str,
cx: &mut gpui::MutableAppContext, cx: &mut gpui::MutableAppContext,
@ -447,6 +454,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!( assert_eq!(
@ -467,6 +475,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) {
cx.set_global(Settings::test(cx));
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx let font_id = cx
.font_cache() .font_cache()
@ -487,7 +496,7 @@ mod tests {
multibuffer multibuffer
}); });
let display_map = let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx)); cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");

View file

@ -11,6 +11,7 @@ use language::{
Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _,
ToPointUtf16 as _, TransactionId, ToPointUtf16 as _, TransactionId,
}; };
use settings::Settings;
use std::{ use std::{
cell::{Ref, RefCell}, cell::{Ref, RefCell},
cmp, fmt, io, cmp, fmt, io,
@ -297,8 +298,10 @@ impl MultiBuffer {
.into_iter() .into_iter()
.map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot)); .map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot));
return buffer.update(cx, |buffer, cx| { return buffer.update(cx, |buffer, cx| {
let language_name = buffer.language().map(|language| language.name());
let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
if autoindent { if autoindent {
buffer.edit_with_autoindent(ranges, new_text, cx); buffer.edit_with_autoindent(ranges, new_text, indent_size, cx);
} else { } else {
buffer.edit(ranges, new_text, cx); buffer.edit(ranges, new_text, cx);
} }
@ -392,10 +395,12 @@ impl MultiBuffer {
); );
} }
} }
let language_name = buffer.language().map(|l| l.name());
let indent_size = cx.global::<Settings>().tab_size(language_name.as_deref());
if autoindent { if autoindent {
buffer.edit_with_autoindent(deletions, "", cx); buffer.edit_with_autoindent(deletions, "", indent_size, cx);
buffer.edit_with_autoindent(insertions, new_text.clone(), cx); buffer.edit_with_autoindent(insertions, new_text.clone(), indent_size, cx);
} else { } else {
buffer.edit(deletions, "", cx); buffer.edit(deletions, "", cx);
buffer.edit(insertions, new_text.clone(), cx); buffer.edit(insertions, new_text.clone(), cx);
@ -861,6 +866,29 @@ impl MultiBuffer {
}) })
} }
// If point is at the end of the buffer, the last excerpt is returned
pub fn point_to_buffer_offset<'a, T: ToOffset>(
&'a self,
point: T,
cx: &AppContext,
) -> Option<(ModelHandle<Buffer>, usize)> {
let snapshot = self.read(cx);
let offset = point.to_offset(&snapshot);
let mut cursor = snapshot.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
cursor.item().map(|excerpt| {
let excerpt_start = excerpt.range.start.to_offset(&excerpt.buffer);
let buffer_point = excerpt_start + offset - *cursor.start();
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
(buffer, buffer_point)
})
}
pub fn range_to_buffer_ranges<'a, T: ToOffset>( pub fn range_to_buffer_ranges<'a, T: ToOffset>(
&'a self, &'a self,
range: Range<T>, range: Range<T>,
@ -1057,12 +1085,13 @@ impl MultiBuffer {
.unwrap_or(false) .unwrap_or(false)
} }
pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> { pub fn language_at<'a, T: ToOffset>(
self.buffers &self,
.borrow() point: T,
.values() cx: &'a AppContext,
.next() ) -> Option<&'a Arc<Language>> {
.and_then(|state| state.buffer.read(cx).language()) self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, _)| buffer.read(cx).language())
} }
pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> { pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> {
@ -3760,6 +3789,7 @@ mod tests {
#[gpui::test] #[gpui::test]
fn test_history(cx: &mut MutableAppContext) { fn test_history(cx: &mut MutableAppContext) {
cx.set_global(Settings::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

@ -1,8 +1,9 @@
use util::test::marked_text; use gpui::ViewContext;
use util::test::{marked_text, marked_text_ranges};
use crate::{ use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, MultiBuffer, DisplayPoint, Editor, MultiBuffer,
}; };
#[cfg(test)] #[cfg(test)]
@ -20,7 +21,6 @@ pub fn marked_display_snapshot(
) -> (DisplaySnapshot, Vec<DisplayPoint>) { ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text(text); let (unmarked_text, markers) = marked_text(text);
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx let font_id = cx
.font_cache() .font_cache()
@ -30,7 +30,7 @@ pub fn marked_display_snapshot(
let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
let display_map = let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
let markers = markers let markers = markers
.into_iter() .into_iter()
@ -39,3 +39,20 @@ pub fn marked_display_snapshot(
(snapshot, markers) (snapshot, markers)
} }
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
assert_eq!(editor.text(cx), umarked_text);
editor.select_ranges(text_ranges, None, cx);
}
pub fn assert_text_with_selections(
editor: &mut Editor,
marked_text: &str,
cx: &mut ViewContext<Editor>,
) {
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
assert_eq!(editor.text(cx), unmarked_text);
assert_eq!(editor.selected_ranges(cx), text_ranges);
}

View file

@ -12,6 +12,7 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
util = { path = "../util" } util = { path = "../util" }
theme = { path = "../theme" } theme = { path = "../theme" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -8,6 +8,7 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use project::{Project, ProjectPath, WorktreeId}; use project::{Project, ProjectPath, WorktreeId};
use settings::Settings;
use std::{ use std::{
cmp, cmp,
path::Path, path::Path,
@ -19,7 +20,7 @@ use std::{
use util::post_inc; use util::post_inc;
use workspace::{ use workspace::{
menu::{Confirm, SelectNext, SelectPrev}, menu::{Confirm, SelectNext, SelectPrev},
Settings, Workspace, Workspace,
}; };
pub struct FileFinder { pub struct FileFinder {

View file

@ -11,5 +11,6 @@ doctest = false
text = { path = "../text" } text = { path = "../text" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
postage = { version = "0.4", features = ["futures-traits"] } postage = { version = "0.4", features = ["futures-traits"] }

View file

@ -3,8 +3,9 @@ use gpui::{
action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity, action, elements::*, geometry::vector::Vector2F, keymap::Binding, Axis, Entity,
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
}; };
use settings::Settings;
use text::{Bias, Point}; use text::{Bias, Point};
use workspace::{Settings, Workspace}; use workspace::Workspace;
action!(Toggle); action!(Toggle);
action!(Confirm); action!(Confirm);

View file

@ -1019,7 +1019,10 @@ impl MutableAppContext {
.insert(TypeId::of::<A>(), handler) .insert(TypeId::of::<A>(), handler)
.is_some() .is_some()
{ {
panic!("registered multiple global handlers for the same action type"); panic!(
"registered multiple global handlers for {}",
type_name::<A>()
);
} }
} }
@ -2355,11 +2358,11 @@ impl AppContext {
} }
pub fn global<T: 'static>(&self) -> &T { pub fn global<T: 'static>(&self) -> &T {
self.globals if let Some(global) = self.globals.get(&TypeId::of::<T>()) {
.get(&TypeId::of::<T>()) global.downcast_ref().unwrap()
.expect("no app state has been added for this type") } else {
.downcast_ref() panic!("no global has been added for {}", type_name::<T>());
.unwrap() }
} }
} }

View file

@ -66,7 +66,6 @@ pub struct Buffer {
file_update_count: usize, file_update_count: usize,
completion_triggers: Vec<String>, completion_triggers: Vec<String>,
deferred_ops: OperationQueue<Operation>, deferred_ops: OperationQueue<Operation>,
indent_size: u32,
} }
pub struct BufferSnapshot { pub struct BufferSnapshot {
@ -80,7 +79,6 @@ pub struct BufferSnapshot {
selections_update_count: usize, selections_update_count: usize,
language: Option<Arc<Language>>, language: Option<Arc<Language>>,
parse_count: usize, parse_count: usize,
indent_size: u32,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -214,6 +212,7 @@ struct AutoindentRequest {
before_edit: BufferSnapshot, before_edit: BufferSnapshot,
edited: Vec<Anchor>, edited: Vec<Anchor>,
inserted: Option<Vec<Range<Anchor>>>, inserted: Option<Vec<Range<Anchor>>>,
indent_size: u32,
} }
#[derive(Debug)] #[derive(Debug)]
@ -427,8 +426,6 @@ impl Buffer {
file_update_count: 0, file_update_count: 0,
completion_triggers: Default::default(), completion_triggers: Default::default(),
deferred_ops: OperationQueue::new(), deferred_ops: OperationQueue::new(),
// TODO: make this configurable
indent_size: 4,
} }
} }
@ -444,7 +441,6 @@ impl Buffer {
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
selections_update_count: self.selections_update_count, selections_update_count: self.selections_update_count,
indent_size: self.indent_size,
} }
} }
@ -786,7 +782,7 @@ impl Buffer {
.indent_column_for_line(suggestion.basis_row) .indent_column_for_line(suggestion.basis_row)
}); });
let delta = if suggestion.indent { let delta = if suggestion.indent {
snapshot.indent_size request.indent_size
} else { } else {
0 0
}; };
@ -809,7 +805,7 @@ impl Buffer {
.flatten(); .flatten();
for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
let delta = if suggestion.indent { let delta = if suggestion.indent {
snapshot.indent_size request.indent_size
} else { } else {
0 0
}; };
@ -845,7 +841,7 @@ impl Buffer {
.flatten(); .flatten();
for (row, suggestion) in inserted_row_range.zip(suggestions) { for (row, suggestion) in inserted_row_range.zip(suggestions) {
let delta = if suggestion.indent { let delta = if suggestion.indent {
snapshot.indent_size request.indent_size
} else { } else {
0 0
}; };
@ -1055,7 +1051,7 @@ impl Buffer {
where where
T: Into<String>, T: Into<String>,
{ {
self.edit_internal([0..self.len()], text, false, cx) self.edit_internal([0..self.len()], text, None, cx)
} }
pub fn edit<I, S, T>( pub fn edit<I, S, T>(
@ -1069,13 +1065,14 @@ impl Buffer {
S: ToOffset, S: ToOffset,
T: Into<String>, T: Into<String>,
{ {
self.edit_internal(ranges_iter, new_text, false, cx) self.edit_internal(ranges_iter, new_text, None, cx)
} }
pub fn edit_with_autoindent<I, S, T>( pub fn edit_with_autoindent<I, S, T>(
&mut self, &mut self,
ranges_iter: I, ranges_iter: I,
new_text: T, new_text: T,
indent_size: u32,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<clock::Local> ) -> Option<clock::Local>
where where
@ -1083,14 +1080,14 @@ impl Buffer {
S: ToOffset, S: ToOffset,
T: Into<String>, T: Into<String>,
{ {
self.edit_internal(ranges_iter, new_text, true, cx) self.edit_internal(ranges_iter, new_text, Some(indent_size), cx)
} }
pub fn edit_internal<I, S, T>( pub fn edit_internal<I, S, T>(
&mut self, &mut self,
ranges_iter: I, ranges_iter: I,
new_text: T, new_text: T,
autoindent: bool, autoindent_size: Option<u32>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<clock::Local> ) -> Option<clock::Local>
where where
@ -1122,23 +1119,27 @@ impl Buffer {
self.start_transaction(); self.start_transaction();
self.pending_autoindent.take(); self.pending_autoindent.take();
let autoindent_request = if autoindent && self.language.is_some() { let autoindent_request =
let before_edit = self.snapshot(); self.language
let edited = ranges .as_ref()
.iter() .and_then(|_| autoindent_size)
.filter_map(|range| { .map(|autoindent_size| {
let start = range.start.to_point(self); let before_edit = self.snapshot();
if new_text.starts_with('\n') && start.column == self.line_len(start.row) { let edited = ranges
None .iter()
} else { .filter_map(|range| {
Some(self.anchor_before(range.start)) let start = range.start.to_point(self);
} if new_text.starts_with('\n')
}) && start.column == self.line_len(start.row)
.collect(); {
Some((before_edit, edited)) None
} else { } else {
None Some(self.anchor_before(range.start))
}; }
})
.collect();
(before_edit, edited, autoindent_size)
});
let first_newline_ix = new_text.find('\n'); let first_newline_ix = new_text.find('\n');
let new_text_len = new_text.len(); let new_text_len = new_text.len();
@ -1146,7 +1147,7 @@ impl Buffer {
let edit = self.text.edit(ranges.iter().cloned(), new_text); let edit = self.text.edit(ranges.iter().cloned(), new_text);
let edit_id = edit.local_timestamp(); let edit_id = edit.local_timestamp();
if let Some((before_edit, edited)) = autoindent_request { if let Some((before_edit, edited, size)) = autoindent_request {
let mut inserted = None; let mut inserted = None;
if let Some(first_newline_ix) = first_newline_ix { if let Some(first_newline_ix) = first_newline_ix {
let mut delta = 0isize; let mut delta = 0isize;
@ -1169,6 +1170,7 @@ impl Buffer {
before_edit, before_edit,
edited, edited,
inserted, inserted,
indent_size: size,
})); }));
} }
@ -1925,10 +1927,6 @@ impl BufferSnapshot {
pub fn file_update_count(&self) -> usize { pub fn file_update_count(&self) -> usize {
self.file_update_count self.file_update_count
} }
pub fn indent_size(&self) -> u32 {
self.indent_size
}
} }
impl Clone for BufferSnapshot { impl Clone for BufferSnapshot {
@ -1944,7 +1942,6 @@ impl Clone for BufferSnapshot {
file_update_count: self.file_update_count, file_update_count: self.file_update_count,
language: self.language.clone(), language: self.language.clone(),
parse_count: self.parse_count, parse_count: self.parse_count,
indent_size: self.indent_size,
} }
} }
} }

View file

@ -576,13 +576,13 @@ fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
let text = "fn a() {}"; let text = "fn a() {}";
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);
buffer.edit_with_autoindent([8..8], "\n\n", cx); buffer.edit_with_autoindent([8..8], "\n\n", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n \n}"); assert_eq!(buffer.text(), "fn a() {\n \n}");
buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx); buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n b()\n \n}"); assert_eq!(buffer.text(), "fn a() {\n b()\n \n}");
buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx); buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", 4, cx);
assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}"); assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}");
buffer buffer
@ -604,7 +604,12 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines, // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
// their indentation is not adjusted. // their indentation is not adjusted.
buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx); buffer.edit_with_autoindent(
[empty(Point::new(1, 1)), empty(Point::new(2, 1))],
"()",
4,
cx,
);
assert_eq!( assert_eq!(
buffer.text(), buffer.text(),
" "
@ -621,6 +626,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut Muta
buffer.edit_with_autoindent( buffer.edit_with_autoindent(
[empty(Point::new(1, 1)), empty(Point::new(2, 1))], [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
"\n.f\n.g", "\n.f\n.g",
4,
cx, cx,
); );
assert_eq!( assert_eq!(
@ -651,7 +657,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
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);
buffer.edit_with_autoindent([5..5], "\nb", cx); buffer.edit_with_autoindent([5..5], "\nb", 4, cx);
assert_eq!( assert_eq!(
buffer.text(), buffer.text(),
" "
@ -663,7 +669,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppConte
// The indentation suggestion changed because `@end` node (a close paren) // The indentation suggestion changed because `@end` node (a close paren)
// is now at the beginning of the line. // is now at the beginning of the line.
buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx); buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", 4, cx);
assert_eq!( assert_eq!(
buffer.text(), buffer.text(),
" "

View file

@ -12,6 +12,7 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
settings = { path = "../settings" }
text = { path = "../text" } text = { path = "../text" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
ordered-float = "2.1.1" ordered-float = "2.1.1"

View file

@ -13,10 +13,11 @@ use gpui::{
}; };
use language::Outline; use language::Outline;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use settings::Settings;
use std::cmp::{self, Reverse}; use std::cmp::{self, Reverse};
use workspace::{ use workspace::{
menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
Settings, Workspace, Workspace,
}; };
action!(Toggle); action!(Toggle);

View file

@ -25,6 +25,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
util = { path = "../util" } util = { path = "../util" }
aho-corasick = "0.7" aho-corasick = "0.7"

View file

@ -28,6 +28,7 @@ use parking_lot::Mutex;
use postage::watch; use postage::watch;
use rand::prelude::*; use rand::prelude::*;
use search::SearchQuery; use search::SearchQuery;
use settings::Settings;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use similar::{ChangeTag, TextDiff}; use similar::{ChangeTag, TextDiff};
use std::{ use std::{
@ -2173,6 +2174,10 @@ impl Project {
lsp::Url::from_file_path(&buffer_abs_path).unwrap(), lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
); );
let capabilities = &language_server.capabilities(); let capabilities = &language_server.capabilities();
let tab_size = cx.update(|cx| {
let language_name = buffer.read(cx).language().map(|language| language.name());
cx.global::<Settings>().tab_size(language_name.as_deref())
});
let lsp_edits = if capabilities let lsp_edits = if capabilities
.document_formatting_provider .document_formatting_provider
.as_ref() .as_ref()
@ -2182,7 +2187,7 @@ impl Project {
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams { .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
text_document, text_document,
options: lsp::FormattingOptions { options: lsp::FormattingOptions {
tab_size: 4, tab_size,
insert_spaces: true, insert_spaces: true,
insert_final_newline: Some(true), insert_final_newline: Some(true),
..Default::default() ..Default::default()

View file

@ -10,6 +10,7 @@ doctest = false
[dependencies] [dependencies]
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -10,6 +10,7 @@ use gpui::{
ViewHandle, WeakViewHandle, ViewHandle, WeakViewHandle,
}; };
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use settings::Settings;
use std::{ use std::{
collections::{hash_map, HashMap}, collections::{hash_map, HashMap},
ffi::OsStr, ffi::OsStr,
@ -17,7 +18,7 @@ use std::{
}; };
use workspace::{ use workspace::{
menu::{SelectNext, SelectPrev}, menu::{SelectNext, SelectPrev},
Settings, Workspace, Workspace,
}; };
pub struct ProjectPanel { pub struct ProjectPanel {

View file

@ -13,6 +13,7 @@ fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
project = { path = "../project" } project = { path = "../project" }
text = { path = "../text" } text = { path = "../text" }
settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
util = { path = "../util" } util = { path = "../util" }
anyhow = "1.0.38" anyhow = "1.0.38"

View file

@ -11,6 +11,7 @@ use gpui::{
}; };
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use project::{Project, Symbol}; use project::{Project, Symbol};
use settings::Settings;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::{self, Reverse}, cmp::{self, Reverse},
@ -18,7 +19,7 @@ use std::{
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev}, menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
Settings, Workspace, Workspace,
}; };
action!(Toggle); action!(Toggle);

View file

@ -13,6 +13,7 @@ editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }

View file

@ -8,8 +8,9 @@ use gpui::{
}; };
use language::OffsetRangeExt; use language::OffsetRangeExt;
use project::search::SearchQuery; use project::search::SearchQuery;
use settings::Settings;
use std::ops::Range; use std::ops::Range;
use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView}; use workspace::{ItemHandle, Pane, ToolbarItemLocation, ToolbarItemView};
action!(Deploy, bool); action!(Deploy, bool);
action!(Dismiss); action!(Dismiss);

View file

@ -10,15 +10,14 @@ use gpui::{
ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
}; };
use project::{search::SearchQuery, Project}; use project::{search::SearchQuery, Project};
use settings::Settings;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
ops::Range, ops::Range,
path::PathBuf, path::PathBuf,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use workspace::{ use workspace::{Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace};
Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
};
action!(Deploy); action!(Deploy);
action!(Search); action!(Search);

View file

@ -14,6 +14,7 @@ required-features = ["seed-support"]
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
settings = { path = "../settings" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
anyhow = "1.0.40" anyhow = "1.0.40"
async-io = "1.3" async-io = "1.3"

View file

@ -1104,6 +1104,7 @@ mod tests {
use rand::prelude::*; use rand::prelude::*;
use rpc::PeerId; use rpc::PeerId;
use serde_json::json; use serde_json::json;
use settings::Settings;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use std::{ use std::{
cell::Cell, cell::Cell,
@ -1117,7 +1118,7 @@ mod tests {
}, },
time::Duration, time::Duration,
}; };
use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams}; use workspace::{Item, SplitDirection, Workspace, WorkspaceParams};
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]

View file

@ -0,0 +1,22 @@
[package]
name = "settings"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/settings.rs"
doctest = false
[features]
test-support = []
[dependencies]
gpui = { path = "../gpui" }
theme = { path = "../theme" }
util = { path = "../util" }
anyhow = "1.0.38"
schemars = "0.8"
serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
serde_path_to_error = "0.1.4"
toml = "0.5"

View file

@ -0,0 +1,171 @@
use anyhow::Result;
use gpui::font_cache::{FamilyId, FontCache};
use schemars::{schema_for, JsonSchema};
use serde::Deserialize;
use std::{collections::HashMap, sync::Arc};
use theme::{Theme, ThemeRegistry};
use util::ResultExt as _;
#[derive(Clone)]
pub struct Settings {
pub buffer_font_family: FamilyId,
pub buffer_font_size: f32,
pub vim_mode: bool,
pub tab_size: u32,
pub soft_wrap: SoftWrap,
pub preferred_line_length: u32,
pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
pub theme: Arc<Theme>,
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct LanguageOverride {
pub tab_size: Option<u32>,
pub soft_wrap: Option<SoftWrap>,
pub preferred_line_length: Option<u32>,
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
None,
EditorWidth,
PreferredLineLength,
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct SettingsFileContent {
#[serde(default)]
pub buffer_font_family: Option<String>,
#[serde(default)]
pub buffer_font_size: Option<f32>,
#[serde(default)]
pub vim_mode: Option<bool>,
#[serde(flatten)]
pub editor: LanguageOverride,
#[serde(default)]
pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
#[serde(default)]
pub theme: Option<String>,
}
impl Settings {
pub fn new(
buffer_font_family: &str,
font_cache: &FontCache,
theme: Arc<Theme>,
) -> Result<Self> {
Ok(Self {
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
buffer_font_size: 15.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
language_overrides: Default::default(),
theme,
})
}
pub fn file_json_schema() -> serde_json::Value {
serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
}
pub fn with_overrides(
mut self,
language_name: impl Into<Arc<str>>,
overrides: LanguageOverride,
) -> Self {
self.language_overrides
.insert(language_name.into(), overrides);
self
}
pub fn tab_size(&self, language: Option<&str>) -> u32 {
language
.and_then(|language| self.language_overrides.get(language))
.and_then(|settings| settings.tab_size)
.unwrap_or(self.tab_size)
}
pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
language
.and_then(|language| self.language_overrides.get(language))
.and_then(|settings| settings.soft_wrap)
.unwrap_or(self.soft_wrap)
}
pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
language
.and_then(|language| self.language_overrides.get(language))
.and_then(|settings| settings.preferred_line_length)
.unwrap_or(self.preferred_line_length)
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
language_overrides: Default::default(),
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
}
}
pub fn merge(
&mut self,
data: &SettingsFileContent,
theme_registry: &ThemeRegistry,
font_cache: &FontCache,
) {
if let Some(value) = &data.buffer_font_family {
if let Some(id) = font_cache.load_family(&[value]).log_err() {
self.buffer_font_family = id;
}
}
if let Some(value) = &data.theme {
if let Some(theme) = theme_registry.get(value).log_err() {
self.theme = theme;
}
}
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.soft_wrap, data.editor.soft_wrap);
merge(&mut self.tab_size, data.editor.tab_size);
merge(
&mut self.preferred_line_length,
data.editor.preferred_line_length,
);
for (language_name, settings) in &data.language_overrides {
let target = self
.language_overrides
.entry(language_name.clone())
.or_default();
merge_option(&mut target.tab_size, settings.tab_size);
merge_option(&mut target.soft_wrap, settings.soft_wrap);
merge_option(
&mut target.preferred_line_length,
settings.preferred_line_length,
);
}
}
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
if value.is_some() {
*target = value;
}
}

View file

@ -12,6 +12,7 @@ editor = { path = "../editor" }
fuzzy = { path = "../fuzzy" } fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
theme = { path = "../theme" } theme = { path = "../theme" }
settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
log = "0.4" log = "0.4"
parking_lot = "0.11.1" parking_lot = "0.11.1"

View file

@ -9,9 +9,10 @@ use gpui::{
}; };
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use theme::{Theme, ThemeRegistry}; use theme::{Theme, ThemeRegistry};
use settings::Settings;
use workspace::{ use workspace::{
menu::{Confirm, SelectNext, SelectPrev}, menu::{Confirm, SelectNext, SelectPrev},
Settings, Workspace, Workspace,
}; };
pub struct ThemeSelector { pub struct ThemeSelector {

View file

@ -12,6 +12,7 @@ collections = { path = "../collections" }
editor = { path = "../editor" } editor = { path = "../editor" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
settings = { path = "../settings" }
workspace = { path = "../workspace" } workspace = { path = "../workspace" }
log = "0.4" log = "0.4"
@ -19,7 +20,8 @@ log = "0.4"
indoc = "1.0.4" indoc = "1.0.4"
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] } language = { path = "../language", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
settings = { path = "../settings" }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }

View file

@ -10,7 +10,8 @@ use editor::{CursorShape, Editor};
use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle};
use mode::Mode; use mode::Mode;
use workspace::{self, Settings, Workspace}; use settings::Settings;
use workspace::{self, Workspace};
action!(SwitchMode, Mode); action!(SwitchMode, Mode);

View file

@ -17,6 +17,7 @@ collections = { path = "../collections" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
project = { path = "../project" } project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
anyhow = "1.0.38" anyhow = "1.0.38"
@ -24,7 +25,6 @@ futures = "0.3"
log = "0.4" log = "0.4"
parking_lot = "0.11.1" parking_lot = "0.11.1"
postage = { version = "0.4.1", features = ["futures-traits"] } postage = { version = "0.4.1", features = ["futures-traits"] }
schemars = "0.8"
serde = { version = "1", features = ["derive", "rc"] } serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["preserve_order"] } serde_json = { version = "1", features = ["preserve_order"] }
smallvec = { version = "1.6", features = ["union"] } smallvec = { version = "1.6", features = ["union"] }
@ -33,3 +33,4 @@ smallvec = { version = "1.6", features = ["union"] }
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }

View file

@ -1,4 +1,4 @@
use crate::{ItemHandle, Settings, StatusItemView}; use crate::{ItemHandle, StatusItemView};
use futures::StreamExt; use futures::StreamExt;
use gpui::AppContext; use gpui::AppContext;
use gpui::{ use gpui::{
@ -7,6 +7,7 @@ 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; use std::cmp::Reverse;
use std::fmt::Write; use std::fmt::Write;

View file

@ -1,5 +1,5 @@
use super::{ItemHandle, SplitDirection}; use super::{ItemHandle, SplitDirection};
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace}; use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
use anyhow::Result; use anyhow::Result;
use collections::{HashMap, VecDeque}; use collections::{HashMap, VecDeque};
use futures::StreamExt; use futures::StreamExt;
@ -13,6 +13,7 @@ use gpui::{
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use project::{ProjectEntryId, ProjectPath}; use project::{ProjectEntryId, ProjectPath};
use settings::Settings;
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc}; use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
use util::ResultExt; use util::ResultExt;

View file

@ -1,325 +0,0 @@
use anyhow::Result;
use futures::{stream, SinkExt, StreamExt as _};
use gpui::{
executor,
font_cache::{FamilyId, FontCache},
};
use language::Language;
use postage::{prelude::Stream, watch};
use project::Fs;
use schemars::{schema_for, JsonSchema};
use serde::Deserialize;
use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
use theme::{Theme, ThemeRegistry};
use util::ResultExt;
#[derive(Clone)]
pub struct Settings {
pub buffer_font_family: FamilyId,
pub buffer_font_size: f32,
pub vim_mode: bool,
pub tab_size: usize,
pub soft_wrap: SoftWrap,
pub preferred_line_length: u32,
pub language_overrides: HashMap<Arc<str>, LanguageOverride>,
pub theme: Arc<Theme>,
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct LanguageOverride {
pub tab_size: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
pub preferred_line_length: Option<u32>,
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
None,
EditorWidth,
PreferredLineLength,
}
#[derive(Clone)]
pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct SettingsFileContent {
#[serde(default)]
buffer_font_family: Option<String>,
#[serde(default)]
buffer_font_size: Option<f32>,
#[serde(default)]
vim_mode: Option<bool>,
#[serde(flatten)]
editor: LanguageOverride,
#[serde(default)]
language_overrides: HashMap<Arc<str>, LanguageOverride>,
#[serde(default)]
theme: Option<String>,
}
impl SettingsFile {
pub async fn new(
fs: Arc<dyn Fs>,
executor: &executor::Background,
path: impl Into<Arc<Path>>,
) -> Self {
let path = path.into();
let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
let mut events = fs.watch(&path, Duration::from_millis(500)).await;
let (mut tx, rx) = watch::channel_with(settings);
executor
.spawn(async move {
while events.next().await.is_some() {
if let Some(settings) = Self::load(fs.clone(), &path).await {
if tx.send(settings).await.is_err() {
break;
}
}
}
})
.detach();
Self(rx)
}
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
if fs.is_file(&path).await {
fs.load(&path)
.await
.log_err()
.and_then(|data| serde_json::from_str(&data).log_err())
} else {
Some(SettingsFileContent::default())
}
}
}
impl Settings {
pub fn file_json_schema() -> serde_json::Value {
serde_json::to_value(schema_for!(SettingsFileContent)).unwrap()
}
pub fn from_files(
defaults: Self,
sources: Vec<SettingsFile>,
theme_registry: Arc<ThemeRegistry>,
font_cache: Arc<FontCache>,
) -> impl futures::stream::Stream<Item = Self> {
stream::select_all(sources.iter().enumerate().map(|(i, source)| {
let mut rx = source.0.clone();
// Consume the initial item from all of the constituent file watches but one.
// This way, the stream will yield exactly one item for the files' initial
// state, and won't return any more items until the files change.
if i > 0 {
rx.try_recv().ok();
}
rx
}))
.map(move |_| {
let mut settings = defaults.clone();
for source in &sources {
settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
}
settings
})
}
pub fn new(
buffer_font_family: &str,
font_cache: &FontCache,
theme: Arc<Theme>,
) -> Result<Self> {
Ok(Self {
buffer_font_family: font_cache.load_family(&[buffer_font_family])?,
buffer_font_size: 15.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
language_overrides: Default::default(),
theme,
})
}
pub fn with_overrides(
mut self,
language_name: impl Into<Arc<str>>,
overrides: LanguageOverride,
) -> Self {
self.language_overrides
.insert(language_name.into(), overrides);
self
}
pub fn tab_size(&self, language: Option<&Arc<Language>>) -> usize {
language
.and_then(|language| self.language_overrides.get(language.name().as_ref()))
.and_then(|settings| settings.tab_size)
.unwrap_or(self.tab_size)
}
pub fn soft_wrap(&self, language: Option<&Arc<Language>>) -> SoftWrap {
language
.and_then(|language| self.language_overrides.get(language.name().as_ref()))
.and_then(|settings| settings.soft_wrap)
.unwrap_or(self.soft_wrap)
}
pub fn preferred_line_length(&self, language: Option<&Arc<Language>>) -> u32 {
language
.and_then(|language| self.language_overrides.get(language.name().as_ref()))
.and_then(|settings| settings.preferred_line_length)
.unwrap_or(self.preferred_line_length)
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &gpui::AppContext) -> Settings {
Settings {
buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
buffer_font_size: 14.,
vim_mode: false,
tab_size: 4,
soft_wrap: SoftWrap::None,
preferred_line_length: 80,
language_overrides: Default::default(),
theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), || Default::default()),
}
}
fn merge(
&mut self,
data: &SettingsFileContent,
theme_registry: &ThemeRegistry,
font_cache: &FontCache,
) {
if let Some(value) = &data.buffer_font_family {
if let Some(id) = font_cache.load_family(&[value]).log_err() {
self.buffer_font_family = id;
}
}
if let Some(value) = &data.theme {
if let Some(theme) = theme_registry.get(value).log_err() {
self.theme = theme;
}
}
merge(&mut self.buffer_font_size, data.buffer_font_size);
merge(&mut self.vim_mode, data.vim_mode);
merge(&mut self.soft_wrap, data.editor.soft_wrap);
merge(&mut self.tab_size, data.editor.tab_size);
merge(
&mut self.preferred_line_length,
data.editor.preferred_line_length,
);
for (language_name, settings) in &data.language_overrides {
let target = self
.language_overrides
.entry(language_name.clone())
.or_default();
merge_option(&mut target.tab_size, settings.tab_size);
merge_option(&mut target.soft_wrap, settings.soft_wrap);
merge_option(
&mut target.preferred_line_length,
settings.preferred_line_length,
);
}
}
}
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) {
if value.is_some() {
*target = value;
}
}
#[cfg(test)]
mod tests {
use super::*;
use project::FakeFs;
#[gpui::test]
async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
fs.save(
"/settings1.json".as_ref(),
&r#"
{
"buffer_font_size": 24,
"soft_wrap": "editor_width",
"language_overrides": {
"Markdown": {
"preferred_line_length": 100,
"soft_wrap": "preferred_line_length"
}
}
}
"#
.into(),
)
.await
.unwrap();
let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
let mut settings_rx = Settings::from_files(
cx.read(Settings::test),
vec![source1, source2, source3],
ThemeRegistry::new((), cx.font_cache()),
cx.font_cache(),
);
let settings = settings_rx.next().await.unwrap();
let md_settings = settings.language_overrides.get("Markdown").unwrap();
assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.tab_size, 4);
assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
assert_eq!(md_settings.preferred_line_length, Some(100));
fs.save(
"/settings2.json".as_ref(),
&r#"
{
"tab_size": 2,
"soft_wrap": "none",
"language_overrides": {
"Markdown": {
"preferred_line_length": 120
}
}
}
"#
.into(),
)
.await
.unwrap();
let settings = settings_rx.next().await.unwrap();
let md_settings = settings.language_overrides.get("Markdown").unwrap();
assert_eq!(settings.soft_wrap, SoftWrap::None);
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.tab_size, 2);
assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
assert_eq!(md_settings.preferred_line_length, Some(120));
fs.remove_file("/settings2.json".as_ref(), Default::default())
.await
.unwrap();
let settings = settings_rx.next().await.unwrap();
assert_eq!(settings.tab_size, 4);
}
}

View file

@ -1,4 +1,5 @@
use crate::{ItemHandle, Pane, Settings}; use crate::{ItemHandle, Pane};
use settings::Settings;
use gpui::{ use gpui::{
elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription,
View, ViewContext, ViewHandle, View, ViewContext, ViewHandle,

View file

@ -1,8 +1,9 @@
use crate::{ItemHandle, Settings}; use crate::ItemHandle;
use gpui::{ use gpui::{
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext, elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle, View, ViewContext, ViewHandle,
}; };
use settings::Settings;
pub trait ToolbarItemView: View { pub trait ToolbarItemView: View {
fn set_active_pane_item( fn set_active_pane_item(

View file

@ -2,7 +2,6 @@ pub mod lsp_status;
pub mod menu; pub mod menu;
pub mod pane; pub mod pane;
pub mod pane_group; pub mod pane_group;
pub mod settings;
pub mod sidebar; pub mod sidebar;
mod status_bar; mod status_bar;
mod toolbar; mod toolbar;
@ -31,7 +30,7 @@ pub use pane::*;
pub use pane_group::*; pub use pane_group::*;
use postage::prelude::Stream; use postage::prelude::Stream;
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree}; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
pub use settings::Settings; use settings::Settings;
use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}; use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus};
use status_bar::StatusBar; use status_bar::StatusBar;
pub use status_bar::StatusItemView; pub use status_bar::StatusItemView;

View file

@ -51,6 +51,7 @@ project = { path = "../project" }
project_panel = { path = "../project_panel" } project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" } project_symbols = { path = "../project_symbols" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
text = { path = "../text" } text = { path = "../text" }
theme = { path = "../theme" } theme = { path = "../theme" }
@ -111,6 +112,7 @@ lsp = { path = "../lsp", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] } project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] }
client = { path = "../client", features = ["test-support"] } client = { path = "../client", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] } util = { path = "../util", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] }
env_logger = "0.8" env_logger = "0.8"

View file

@ -9,17 +9,19 @@ use gpui::{App, AssetSource, Task};
use log::LevelFilter; use log::LevelFilter;
use parking_lot::Mutex; use parking_lot::Mutex;
use project::Fs; use project::Fs;
use settings::{self, Settings};
use smol::process::Command; use smol::process::Command;
use std::{env, fs, path::PathBuf, sync::Arc}; use std::{env, fs, path::PathBuf, sync::Arc};
use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{self, AppState, OpenNew, OpenParams, OpenPaths};
self,
settings::{self, SettingsFile},
AppState, OpenNew, OpenParams, OpenPaths, Settings,
};
use zed::{ use zed::{
self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, self,
assets::Assets,
build_window_options, build_workspace,
fs::RealFs,
languages, menus,
settings_file::{settings_from_files, SettingsFile},
}; };
fn main() { fn main() {
@ -46,6 +48,20 @@ fn main() {
soft_wrap: Some(settings::SoftWrap::PreferredLineLength), soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
..Default::default() ..Default::default()
}, },
)
.with_overrides(
"Rust",
settings::LanguageOverride {
tab_size: Some(4),
..Default::default()
},
)
.with_overrides(
"TypeScript",
settings::LanguageOverride {
tab_size: Some(2),
..Default::default()
},
); );
let settings_file = load_settings_file(&app, fs.clone()); let settings_file = load_settings_file(&app, fs.clone());
@ -97,7 +113,7 @@ fn main() {
.detach_and_log_err(cx); .detach_and_log_err(cx);
let settings_file = cx.background().block(settings_file).unwrap(); let settings_file = cx.background().block(settings_file).unwrap();
let mut settings_rx = Settings::from_files( let mut settings_rx = settings_from_files(
default_settings, default_settings,
vec![settings_file], vec![settings_file],
themes.clone(), themes.clone(),

View file

@ -0,0 +1,157 @@
use futures::{stream, StreamExt};
use gpui::{executor, FontCache};
use postage::sink::Sink as _;
use postage::{prelude::Stream, watch};
use project::Fs;
use settings::{Settings, SettingsFileContent};
use std::{path::Path, sync::Arc, time::Duration};
use theme::ThemeRegistry;
use util::ResultExt;
#[derive(Clone)]
pub struct SettingsFile(watch::Receiver<SettingsFileContent>);
impl SettingsFile {
pub async fn new(
fs: Arc<dyn Fs>,
executor: &executor::Background,
path: impl Into<Arc<Path>>,
) -> Self {
let path = path.into();
let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
let mut events = fs.watch(&path, Duration::from_millis(500)).await;
let (mut tx, rx) = watch::channel_with(settings);
executor
.spawn(async move {
while events.next().await.is_some() {
if let Some(settings) = Self::load(fs.clone(), &path).await {
if tx.send(settings).await.is_err() {
break;
}
}
}
})
.detach();
Self(rx)
}
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<SettingsFileContent> {
if fs.is_file(&path).await {
fs.load(&path)
.await
.log_err()
.and_then(|data| serde_json::from_str(&data).log_err())
} else {
Some(SettingsFileContent::default())
}
}
}
pub fn settings_from_files(
defaults: Settings,
sources: Vec<SettingsFile>,
theme_registry: Arc<ThemeRegistry>,
font_cache: Arc<FontCache>,
) -> impl futures::stream::Stream<Item = Settings> {
stream::select_all(sources.iter().enumerate().map(|(i, source)| {
let mut rx = source.0.clone();
// Consume the initial item from all of the constituent file watches but one.
// This way, the stream will yield exactly one item for the files' initial
// state, and won't return any more items until the files change.
if i > 0 {
rx.try_recv().ok();
}
rx
}))
.map(move |_| {
let mut settings = defaults.clone();
for source in &sources {
settings.merge(&*source.0.borrow(), &theme_registry, &font_cache);
}
settings
})
}
#[cfg(test)]
mod tests {
use super::*;
use project::FakeFs;
use settings::SoftWrap;
#[gpui::test]
async fn test_settings_from_files(cx: &mut gpui::TestAppContext) {
let executor = cx.background();
let fs = FakeFs::new(executor.clone());
fs.save(
"/settings1.json".as_ref(),
&r#"
{
"buffer_font_size": 24,
"soft_wrap": "editor_width",
"language_overrides": {
"Markdown": {
"preferred_line_length": 100,
"soft_wrap": "preferred_line_length"
}
}
}
"#
.into(),
)
.await
.unwrap();
let source1 = SettingsFile::new(fs.clone(), &executor, "/settings1.json".as_ref()).await;
let source2 = SettingsFile::new(fs.clone(), &executor, "/settings2.json".as_ref()).await;
let source3 = SettingsFile::new(fs.clone(), &executor, "/settings3.json".as_ref()).await;
let mut settings_rx = settings_from_files(
cx.read(Settings::test),
vec![source1, source2, source3],
ThemeRegistry::new((), cx.font_cache()),
cx.font_cache(),
);
let settings = settings_rx.next().await.unwrap();
let md_settings = settings.language_overrides.get("Markdown").unwrap();
assert_eq!(settings.soft_wrap, SoftWrap::EditorWidth);
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.tab_size, 4);
assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
assert_eq!(md_settings.preferred_line_length, Some(100));
fs.save(
"/settings2.json".as_ref(),
&r#"
{
"tab_size": 2,
"soft_wrap": "none",
"language_overrides": {
"Markdown": {
"preferred_line_length": 120
}
}
}
"#
.into(),
)
.await
.unwrap();
let settings = settings_rx.next().await.unwrap();
let md_settings = settings.language_overrides.get("Markdown").unwrap();
assert_eq!(settings.soft_wrap, SoftWrap::None);
assert_eq!(settings.buffer_font_size, 24.0);
assert_eq!(settings.tab_size, 2);
assert_eq!(md_settings.soft_wrap, Some(SoftWrap::PreferredLineLength));
assert_eq!(md_settings.preferred_line_length, Some(120));
fs.remove_file("/settings2.json".as_ref(), Default::default())
.await
.unwrap();
let settings = settings_rx.next().await.unwrap();
assert_eq!(settings.tab_size, 4);
}
}

View file

@ -3,9 +3,9 @@ use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
use gpui::MutableAppContext; use gpui::MutableAppContext;
use language::LanguageRegistry; use language::LanguageRegistry;
use project::fs::FakeFs; use project::fs::FakeFs;
use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use theme::ThemeRegistry; use theme::ThemeRegistry;
use workspace::Settings;
#[cfg(test)] #[cfg(test)]
#[ctor::ctor] #[ctor::ctor]

View file

@ -1,6 +1,7 @@
pub mod assets; pub mod assets;
pub mod languages; pub mod languages;
pub mod menus; pub mod menus;
pub mod settings_file;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub mod test; pub mod test;
@ -23,9 +24,10 @@ use project::Project;
pub use project::{self, fs}; pub use project::{self, fs};
use project_panel::ProjectPanel; use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar}; use search::{BufferSearchBar, ProjectSearchBar};
use settings::Settings;
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
pub use workspace; pub use workspace;
use workspace::{AppState, Settings, Workspace, WorkspaceParams}; use workspace::{AppState, Workspace, WorkspaceParams};
action!(About); action!(About);
action!(Quit); action!(Quit);
@ -576,7 +578,7 @@ mod tests {
assert!(!editor.is_dirty(cx)); assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "untitled"); assert_eq!(editor.title(cx), "untitled");
assert!(Arc::ptr_eq( assert!(Arc::ptr_eq(
editor.language(cx).unwrap(), editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT &languages::PLAIN_TEXT
)); ));
editor.handle_input(&editor::Input("hi".into()), cx); editor.handle_input(&editor::Input("hi".into()), cx);
@ -600,7 +602,7 @@ mod tests {
editor.read_with(cx, |editor, cx| { editor.read_with(cx, |editor, cx| {
assert!(!editor.is_dirty(cx)); assert!(!editor.is_dirty(cx));
assert_eq!(editor.title(cx), "the-new-name.rs"); assert_eq!(editor.title(cx), "the-new-name.rs");
assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust"); assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
}); });
// Edit the file and save it again. This time, there is no filename prompt. // Edit the file and save it again. This time, there is no filename prompt.
@ -666,7 +668,7 @@ mod tests {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
assert!(Arc::ptr_eq( assert!(Arc::ptr_eq(
editor.language(cx).unwrap(), editor.language_at(0, cx).unwrap(),
&languages::PLAIN_TEXT &languages::PLAIN_TEXT
)); ));
editor.handle_input(&editor::Input("hi".into()), cx); editor.handle_input(&editor::Input("hi".into()), cx);
@ -680,7 +682,7 @@ mod tests {
// The buffer is not dirty anymore and the language is assigned based on the path. // The buffer is not dirty anymore and the language is assigned based on the path.
editor.read_with(cx, |editor, cx| { editor.read_with(cx, |editor, cx| {
assert!(!editor.is_dirty(cx)); assert!(!editor.is_dirty(cx));
assert_eq!(editor.language(cx).unwrap().name().as_ref(), "Rust") assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
}); });
} }

View file

@ -7,11 +7,11 @@ import snakeCase from "./utils/snakeCase";
const themes = [dark, light]; const themes = [dark, light];
for (let theme of themes) { for (let theme of themes) {
let styleTree = snakeCase(app(theme)); let styleTree = snakeCase(app(theme));
let styleTreeJSON = JSON.stringify(styleTree, null, 2); let styleTreeJSON = JSON.stringify(styleTree, null, 2);
let outPath = path.resolve( let outPath = path.resolve(
`${__dirname}/../../crates/zed/assets/themes/${theme.name}.json` `${__dirname}/../../crates/zed/assets/themes/${theme.name}.json`
); );
fs.writeFileSync(outPath, styleTreeJSON); fs.writeFileSync(outPath, styleTreeJSON);
console.log(`- ${outPath} created`); console.log(`- ${outPath} created`);
} }

View file

@ -7,80 +7,80 @@ import { colors, fontFamilies, fontSizes, fontWeights } from "./tokens";
// Organize theme tokens // Organize theme tokens
function themeTokens(theme: Theme) { function themeTokens(theme: Theme) {
return { return {
meta: { meta: {
themeName: theme.name, themeName: theme.name,
}, },
text: theme.textColor, text: theme.textColor,
icon: theme.iconColor, icon: theme.iconColor,
background: theme.backgroundColor, background: theme.backgroundColor,
border: theme.borderColor, border: theme.borderColor,
editor: theme.editor, editor: theme.editor,
syntax: { syntax: {
primary: { primary: {
value: theme.syntax.primary.color.value, value: theme.syntax.primary.color.value,
type: "color", type: "color",
}, },
comment: { comment: {
value: theme.syntax.comment.color.value, value: theme.syntax.comment.color.value,
type: "color", type: "color",
}, },
keyword: { keyword: {
value: theme.syntax.keyword.color.value, value: theme.syntax.keyword.color.value,
type: "color", type: "color",
}, },
function: { function: {
value: theme.syntax.function.color.value, value: theme.syntax.function.color.value,
type: "color", type: "color",
}, },
type: { type: {
value: theme.syntax.type.color.value, value: theme.syntax.type.color.value,
type: "color", type: "color",
}, },
variant: { variant: {
value: theme.syntax.variant.color.value, value: theme.syntax.variant.color.value,
type: "color", type: "color",
}, },
property: { property: {
value: theme.syntax.property.color.value, value: theme.syntax.property.color.value,
type: "color", type: "color",
}, },
enum: { enum: {
value: theme.syntax.enum.color.value, value: theme.syntax.enum.color.value,
type: "color", type: "color",
}, },
operator: { operator: {
value: theme.syntax.operator.color.value, value: theme.syntax.operator.color.value,
type: "color", type: "color",
}, },
string: { string: {
value: theme.syntax.string.color.value, value: theme.syntax.string.color.value,
type: "color", type: "color",
}, },
number: { number: {
value: theme.syntax.number.color.value, value: theme.syntax.number.color.value,
type: "color", type: "color",
}, },
boolean: { boolean: {
value: theme.syntax.boolean.color.value, value: theme.syntax.boolean.color.value,
type: "color", type: "color",
}, },
}, },
player: theme.player, player: theme.player,
shadowAlpha: theme.shadowAlpha, shadowAlpha: theme.shadowAlpha,
}; };
} }
// Organize core tokens // Organize core tokens
const coreTokens = { const coreTokens = {
color: { color: {
...colors, ...colors,
}, },
text: { text: {
family: fontFamilies, family: fontFamilies,
weight: fontWeights, weight: fontWeights,
}, },
size: fontSizes, size: fontSizes,
}; };
const combinedTokens: any = {}; const combinedTokens: any = {};
@ -98,10 +98,10 @@ combinedTokens.core = coreTokens;
// We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly. // We write `${theme}.json` as a separate file for the design team's convenience, but it isn't consumed by Figma Tokens directly.
let themes = [dark, light]; let themes = [dark, light];
themes.forEach((theme) => { themes.forEach((theme) => {
const themePath = `${distPath}/${theme.name}.json` const themePath = `${distPath}/${theme.name}.json`
fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2)); fs.writeFileSync(themePath, JSON.stringify(themeTokens(theme), null, 2));
console.log(`- ${themePath} created`); console.log(`- ${themePath} created`);
combinedTokens[theme.name] = themeTokens(theme); combinedTokens[theme.name] = themeTokens(theme);
}); });
// Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app. // Write combined tokens to `tokens.json`. This file is consumed by the Figma Tokens plugin to keep our designs consistent with the app.

View file

@ -9,35 +9,35 @@ import selectorModal from "./selectorModal";
import workspace from "./workspace"; import workspace from "./workspace";
export const panel = { export const panel = {
padding: { top: 12, left: 12, bottom: 12, right: 12 }, padding: { top: 12, left: 12, bottom: 12, right: 12 },
}; };
export default function app(theme: Theme): Object { export default function app(theme: Theme): Object {
return { return {
selector: selectorModal(theme), selector: selectorModal(theme),
workspace: workspace(theme), workspace: workspace(theme),
editor: editor(theme), editor: editor(theme),
projectDiagnostics: { projectDiagnostics: {
tabIconSpacing: 4, tabIconSpacing: 4,
tabIconWidth: 13, tabIconWidth: 13,
tabSummarySpacing: 10, tabSummarySpacing: 10,
emptyMessage: text(theme, "sans", "primary", { size: "lg" }), emptyMessage: text(theme, "sans", "primary", { size: "lg" }),
statusBarItem: { statusBarItem: {
...text(theme, "sans", "muted"), ...text(theme, "sans", "muted"),
margin: { margin: {
right: 10, right: 10,
},
},
}, },
projectPanel: projectPanel(theme), },
chatPanel: chatPanel(theme), },
contactsPanel: contactsPanel(theme), projectPanel: projectPanel(theme),
search: search(theme), chatPanel: chatPanel(theme),
breadcrumbs: { contactsPanel: contactsPanel(theme),
...text(theme, "sans", "primary"), search: search(theme),
padding: { breadcrumbs: {
left: 6, ...text(theme, "sans", "primary"),
}, padding: {
} left: 6,
}; },
}
};
} }

View file

@ -1,108 +1,108 @@
import Theme from "../themes/theme"; import Theme from "../themes/theme";
import { panel } from "./app"; import { panel } from "./app";
import { import {
backgroundColor, backgroundColor,
border, border,
player, player,
shadow, shadow,
text, text,
TextColor TextColor
} from "./components"; } from "./components";
export default function chatPanel(theme: Theme) { export default function chatPanel(theme: Theme) {
function channelSelectItem( function channelSelectItem(
theme: Theme, theme: Theme,
textColor: TextColor, textColor: TextColor,
hovered: boolean hovered: boolean
) { ) {
return {
name: text(theme, "sans", textColor),
padding: 4,
hash: {
...text(theme, "sans", "muted"),
margin: {
right: 8,
},
},
background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
cornerRadius: hovered ? 6 : 0,
};
}
const message = {
body: text(theme, "sans", "secondary"),
timestamp: text(theme, "sans", "muted", { size: "sm" }),
padding: {
bottom: 6,
},
sender: {
...text(theme, "sans", "primary", { weight: "bold" }),
margin: {
right: 8,
},
},
};
return { return {
...panel, name: text(theme, "sans", textColor),
channelName: text(theme, "sans", "primary", { weight: "bold" }), padding: 4,
channelNameHash: { hash: {
...text(theme, "sans", "muted"), ...text(theme, "sans", "muted"),
padding: { margin: {
right: 8, right: 8,
},
},
channelSelect: {
header: {
...channelSelectItem(theme, "primary", false),
padding: {
bottom: 4,
left: 0,
},
},
item: channelSelectItem(theme, "secondary", false),
hoveredItem: channelSelectItem(theme, "secondary", true),
activeItem: channelSelectItem(theme, "primary", false),
hoveredActiveItem: channelSelectItem(theme, "primary", true),
menu: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
padding: 4,
border: border(theme, "primary"),
shadow: shadow(theme),
},
},
signInPrompt: text(theme, "sans", "secondary", { underline: true }),
hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
message,
pendingMessage: {
...message,
body: {
...message.body,
color: theme.textColor.muted.value,
},
sender: {
...message.sender,
color: theme.textColor.muted.value,
},
timestamp: {
...message.timestamp,
color: theme.textColor.muted.value,
},
},
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
padding: {
bottom: 7,
left: 8,
right: 8,
top: 7,
},
}, },
},
background: hovered ? backgroundColor(theme, 300, "hovered") : undefined,
cornerRadius: hovered ? 6 : 0,
}; };
}
const message = {
body: text(theme, "sans", "secondary"),
timestamp: text(theme, "sans", "muted", { size: "sm" }),
padding: {
bottom: 6,
},
sender: {
...text(theme, "sans", "primary", { weight: "bold" }),
margin: {
right: 8,
},
},
};
return {
...panel,
channelName: text(theme, "sans", "primary", { weight: "bold" }),
channelNameHash: {
...text(theme, "sans", "muted"),
padding: {
right: 8,
},
},
channelSelect: {
header: {
...channelSelectItem(theme, "primary", false),
padding: {
bottom: 4,
left: 0,
},
},
item: channelSelectItem(theme, "secondary", false),
hoveredItem: channelSelectItem(theme, "secondary", true),
activeItem: channelSelectItem(theme, "primary", false),
hoveredActiveItem: channelSelectItem(theme, "primary", true),
menu: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
padding: 4,
border: border(theme, "primary"),
shadow: shadow(theme),
},
},
signInPrompt: text(theme, "sans", "secondary", { underline: true }),
hoveredSignInPrompt: text(theme, "sans", "primary", { underline: true }),
message,
pendingMessage: {
...message,
body: {
...message.body,
color: theme.textColor.muted.value,
},
sender: {
...message.sender,
color: theme.textColor.muted.value,
},
timestamp: {
...message.timestamp,
color: theme.textColor.muted.value,
},
},
inputEditor: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
text: text(theme, "mono", "primary"),
placeholderText: text(theme, "mono", "placeholder", { size: "sm" }),
selection: player(theme, 1).selection,
border: border(theme, "secondary"),
padding: {
bottom: 7,
left: 8,
right: 8,
top: 7,
},
},
};
} }

View file

@ -5,89 +5,89 @@ import { Color } from "../utils/color";
export type TextColor = keyof Theme["textColor"]; export type TextColor = keyof Theme["textColor"];
export function text( export function text(
theme: Theme, theme: Theme,
fontFamily: keyof typeof fontFamilies, fontFamily: keyof typeof fontFamilies,
color: TextColor, color: TextColor,
properties?: { properties?: {
size?: keyof typeof fontSizes; size?: keyof typeof fontSizes;
weight?: FontWeight; weight?: FontWeight;
underline?: boolean; underline?: boolean;
} }
) { ) {
let size = fontSizes[properties?.size || "sm"].value; let size = fontSizes[properties?.size || "sm"].value;
return { return {
family: fontFamilies[fontFamily].value, family: fontFamilies[fontFamily].value,
color: theme.textColor[color].value, color: theme.textColor[color].value,
...properties, ...properties,
size, size,
}; };
} }
export function textColor(theme: Theme, color: TextColor) { export function textColor(theme: Theme, color: TextColor) {
return theme.textColor[color].value; return theme.textColor[color].value;
} }
export type BorderColor = keyof Theme["borderColor"]; export type BorderColor = keyof Theme["borderColor"];
export interface BorderOptions { export interface BorderOptions {
width?: number; width?: number;
top?: boolean; top?: boolean;
bottom?: boolean; bottom?: boolean;
left?: boolean; left?: boolean;
right?: boolean; right?: boolean;
overlay?: boolean; overlay?: boolean;
} }
export function border( export function border(
theme: Theme, theme: Theme,
color: BorderColor, color: BorderColor,
options?: BorderOptions options?: BorderOptions
) { ) {
return { return {
color: borderColor(theme, color), color: borderColor(theme, color),
width: 1, width: 1,
...options, ...options,
}; };
} }
export function borderColor(theme: Theme, color: BorderColor) { export function borderColor(theme: Theme, color: BorderColor) {
return theme.borderColor[color].value; return theme.borderColor[color].value;
} }
export type IconColor = keyof Theme["iconColor"]; export type IconColor = keyof Theme["iconColor"];
export function iconColor(theme: Theme, color: IconColor) { export function iconColor(theme: Theme, color: IconColor) {
return theme.iconColor[color].value; return theme.iconColor[color].value;
} }
export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; export type PlayerIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export interface Player { export interface Player {
selection: { selection: {
cursor: Color; cursor: Color;
selection: Color; selection: Color;
}; };
} }
export function player( export function player(
theme: Theme, theme: Theme,
playerNumber: PlayerIndex, playerNumber: PlayerIndex,
): Player { ): Player {
return { return {
selection: { selection: {
cursor: theme.player[playerNumber].cursorColor.value, cursor: theme.player[playerNumber].cursorColor.value,
selection: theme.player[playerNumber].selectionColor.value, selection: theme.player[playerNumber].selectionColor.value,
}, },
}; };
} }
export type BackgroundColor = keyof Theme["backgroundColor"]; export type BackgroundColor = keyof Theme["backgroundColor"];
export type BackgroundState = keyof BackgroundColorSet; export type BackgroundState = keyof BackgroundColorSet;
export function backgroundColor( export function backgroundColor(
theme: Theme, theme: Theme,
name: BackgroundColor, name: BackgroundColor,
state?: BackgroundState, state?: BackgroundState,
): Color { ): Color {
return theme.backgroundColor[name][state || "base"].value; return theme.backgroundColor[name][state || "base"].value;
} }
export function shadow(theme: Theme) { export function shadow(theme: Theme) {
return { return {
blur: 16, blur: 16,
color: chroma("black").alpha(theme.shadowAlpha.value).hex(), color: chroma("black").alpha(theme.shadowAlpha.value).hex(),
offset: [0, 2], offset: [0, 2],
}; };
} }

View file

@ -3,60 +3,60 @@ import { panel } from "./app";
import { backgroundColor, borderColor, text } from "./components"; import { backgroundColor, borderColor, text } from "./components";
export default function(theme: Theme) { export default function(theme: Theme) {
const project = { const project = {
guestAvatarSpacing: 4, guestAvatarSpacing: 4,
height: 24, height: 24,
guestAvatar: { guestAvatar: {
cornerRadius: 8, cornerRadius: 8,
width: 14, width: 14,
}, },
name: { name: {
...text(theme, "mono", "placeholder", { size: "sm" }), ...text(theme, "mono", "placeholder", { size: "sm" }),
margin: { margin: {
right: 6, right: 6,
}, },
}, },
padding: { padding: {
left: 8, left: 8,
}, },
}; };
const sharedProject = { const sharedProject = {
...project, ...project,
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
cornerRadius: 6, cornerRadius: 6,
name: { name: {
...project.name, ...project.name,
...text(theme, "mono", "secondary", { size: "sm" }), ...text(theme, "mono", "secondary", { size: "sm" }),
}, },
}; };
return { return {
...panel, ...panel,
hostRowHeight: 28, hostRowHeight: 28,
treeBranchColor: borderColor(theme, "muted"), treeBranchColor: borderColor(theme, "muted"),
treeBranchWidth: 1, treeBranchWidth: 1,
hostAvatar: { hostAvatar: {
cornerRadius: 10, cornerRadius: 10,
width: 18, width: 18,
}, },
hostUsername: { hostUsername: {
...text(theme, "mono", "primary", { size: "sm" }), ...text(theme, "mono", "primary", { size: "sm" }),
padding: { padding: {
left: 8, left: 8,
}, },
}, },
project, project,
sharedProject, sharedProject,
hoveredSharedProject: { hoveredSharedProject: {
...sharedProject, ...sharedProject,
background: backgroundColor(theme, 300, "hovered"), background: backgroundColor(theme, 300, "hovered"),
cornerRadius: 6, cornerRadius: 6,
}, },
unsharedProject: project, unsharedProject: project,
hoveredUnsharedProject: { hoveredUnsharedProject: {
...project, ...project,
cornerRadius: 6, cornerRadius: 6,
}, },
} }
} }

View file

@ -1,146 +1,146 @@
import Theme from "../themes/theme"; import Theme from "../themes/theme";
import { import {
backgroundColor, backgroundColor,
border, border,
iconColor, iconColor,
player, player,
text, text,
TextColor TextColor
} from "./components"; } from "./components";
export default function editor(theme: Theme) { export default function editor(theme: Theme) {
const autocompleteItem = { const autocompleteItem = {
cornerRadius: 6, cornerRadius: 6,
padding: { padding: {
bottom: 2, bottom: 2,
left: 6, left: 6,
right: 6, right: 6,
top: 2, top: 2,
}, },
}; };
function diagnostic(theme: Theme, color: TextColor) {
return {
textScaleFactor: 0.857,
header: {
border: border(theme, "primary", {
top: true,
}),
},
message: {
text: text(theme, "sans", color, { size: "sm" }),
highlightText: text(theme, "sans", color, {
size: "sm",
weight: "bold",
}),
},
};
}
function diagnostic(theme: Theme, color: TextColor) {
return { return {
// textColor: theme.syntax.primary.color, textScaleFactor: 0.857,
textColor: theme.syntax.primary.color.value, header: {
background: backgroundColor(theme, 500), border: border(theme, "primary", {
activeLineBackground: theme.editor.line.active.value, top: true,
codeActionsIndicator: iconColor(theme, "muted"), }),
diffBackgroundDeleted: backgroundColor(theme, "error"), },
diffBackgroundInserted: backgroundColor(theme, "ok"), message: {
documentHighlightReadBackground: theme.editor.highlight.occurrence.value, text: text(theme, "sans", color, { size: "sm" }),
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value, highlightText: text(theme, "sans", color, {
errorColor: theme.textColor.error.value, size: "sm",
gutterBackground: backgroundColor(theme, 500), weight: "bold",
gutterPaddingFactor: 3.5, }),
highlightedLineBackground: theme.editor.line.highlighted.value, },
lineNumber: theme.editor.gutter.primary.value,
lineNumberActive: theme.editor.gutter.active.value,
renameFade: 0.6,
unnecessaryCodeFade: 0.5,
selection: player(theme, 1).selection,
guestSelections: [
player(theme, 2).selection,
player(theme, 3).selection,
player(theme, 4).selection,
player(theme, 5).selection,
player(theme, 6).selection,
player(theme, 7).selection,
player(theme, 8).selection,
],
autocomplete: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
padding: 6,
border: border(theme, "secondary"),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "hovered"),
},
margin: {
left: -14,
},
matchHighlight: text(theme, "mono", "feature"),
selectedItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "active"),
},
},
diagnosticHeader: {
background: backgroundColor(theme, 300),
iconWidthFactor: 1.5,
textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
border: border(theme, "secondary", {
bottom: true,
top: true,
}),
code: {
...text(theme, "mono", "muted", { size: "sm" }),
margin: {
left: 10,
},
},
message: {
highlightText: text(theme, "sans", "primary", {
size: "sm",
weight: "bold",
}),
text: text(theme, "sans", "secondary", { size: "sm" }),
},
},
diagnosticPathHeader: {
background: theme.editor.line.active.value,
textScaleFactor: 0.857,
filename: text(theme, "mono", "primary", { size: "sm" }),
path: {
...text(theme, "mono", "muted", { size: "sm" }),
margin: {
left: 12,
},
},
},
errorDiagnostic: diagnostic(theme, "error"),
warningDiagnostic: diagnostic(theme, "warning"),
informationDiagnostic: diagnostic(theme, "info"),
hintDiagnostic: diagnostic(theme, "info"),
invalidErrorDiagnostic: diagnostic(theme, "muted"),
invalidHintDiagnostic: diagnostic(theme, "muted"),
invalidInformationDiagnostic: diagnostic(theme, "muted"),
invalidWarningDiagnostic: diagnostic(theme, "muted"),
syntax: {
keyword: theme.syntax.keyword.color.value,
function: theme.syntax.function.color.value,
string: theme.syntax.string.color.value,
type: theme.syntax.type.color.value,
number: theme.syntax.number.color.value,
comment: theme.syntax.comment.color.value,
property: theme.syntax.property.color.value,
variant: theme.syntax.variant.color.value,
constant: theme.syntax.constant.color.value,
title: { color: theme.syntax.title.color.value, weight: "bold" },
emphasis: theme.textColor.feature.value,
"emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
link_text: { color: theme.syntax.linkText.color.value, italic: true },
list_marker: theme.syntax.punctuation.color.value,
},
}; };
}
return {
// textColor: theme.syntax.primary.color,
textColor: theme.syntax.primary.color.value,
background: backgroundColor(theme, 500),
activeLineBackground: theme.editor.line.active.value,
codeActionsIndicator: iconColor(theme, "muted"),
diffBackgroundDeleted: backgroundColor(theme, "error"),
diffBackgroundInserted: backgroundColor(theme, "ok"),
documentHighlightReadBackground: theme.editor.highlight.occurrence.value,
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence.value,
errorColor: theme.textColor.error.value,
gutterBackground: backgroundColor(theme, 500),
gutterPaddingFactor: 3.5,
highlightedLineBackground: theme.editor.line.highlighted.value,
lineNumber: theme.editor.gutter.primary.value,
lineNumberActive: theme.editor.gutter.active.value,
renameFade: 0.6,
unnecessaryCodeFade: 0.5,
selection: player(theme, 1).selection,
guestSelections: [
player(theme, 2).selection,
player(theme, 3).selection,
player(theme, 4).selection,
player(theme, 5).selection,
player(theme, 6).selection,
player(theme, 7).selection,
player(theme, 8).selection,
],
autocomplete: {
background: backgroundColor(theme, 500),
cornerRadius: 6,
padding: 6,
border: border(theme, "secondary"),
item: autocompleteItem,
hoveredItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "hovered"),
},
margin: {
left: -14,
},
matchHighlight: text(theme, "mono", "feature"),
selectedItem: {
...autocompleteItem,
background: backgroundColor(theme, 500, "active"),
},
},
diagnosticHeader: {
background: backgroundColor(theme, 300),
iconWidthFactor: 1.5,
textScaleFactor: 0.857, // NateQ: Will we need dynamic sizing for text? If so let's create tokens for these.
border: border(theme, "secondary", {
bottom: true,
top: true,
}),
code: {
...text(theme, "mono", "muted", { size: "sm" }),
margin: {
left: 10,
},
},
message: {
highlightText: text(theme, "sans", "primary", {
size: "sm",
weight: "bold",
}),
text: text(theme, "sans", "secondary", { size: "sm" }),
},
},
diagnosticPathHeader: {
background: theme.editor.line.active.value,
textScaleFactor: 0.857,
filename: text(theme, "mono", "primary", { size: "sm" }),
path: {
...text(theme, "mono", "muted", { size: "sm" }),
margin: {
left: 12,
},
},
},
errorDiagnostic: diagnostic(theme, "error"),
warningDiagnostic: diagnostic(theme, "warning"),
informationDiagnostic: diagnostic(theme, "info"),
hintDiagnostic: diagnostic(theme, "info"),
invalidErrorDiagnostic: diagnostic(theme, "muted"),
invalidHintDiagnostic: diagnostic(theme, "muted"),
invalidInformationDiagnostic: diagnostic(theme, "muted"),
invalidWarningDiagnostic: diagnostic(theme, "muted"),
syntax: {
keyword: theme.syntax.keyword.color.value,
function: theme.syntax.function.color.value,
string: theme.syntax.string.color.value,
type: theme.syntax.type.color.value,
number: theme.syntax.number.color.value,
comment: theme.syntax.comment.color.value,
property: theme.syntax.property.color.value,
variant: theme.syntax.variant.color.value,
constant: theme.syntax.constant.color.value,
title: { color: theme.syntax.title.color.value, weight: "bold" },
emphasis: theme.textColor.feature.value,
"emphasis.strong": { color: theme.textColor.feature.value, weight: "bold" },
link_uri: { color: theme.syntax.linkUrl.color.value, underline: true },
link_text: { color: theme.syntax.linkText.color.value, italic: true },
list_marker: theme.syntax.punctuation.color.value,
},
};
} }

View file

@ -4,34 +4,34 @@ import { panel } from "./app";
import { backgroundColor, iconColor, text, TextColor } from "./components"; import { backgroundColor, iconColor, text, TextColor } from "./components";
export default function projectPanel(theme: Theme) { export default function projectPanel(theme: Theme) {
function entry(theme: Theme, textColor: TextColor, background?: Color) { function entry(theme: Theme, textColor: TextColor, background?: Color) {
return {
height: 22,
background,
iconColor: iconColor(theme, "muted"),
iconSize: 8,
iconSpacing: 8,
text: text(theme, "mono", textColor, { size: "sm" }),
};
}
return { return {
...panel, height: 22,
entry: entry(theme, "secondary"), background,
hoveredEntry: entry( iconColor: iconColor(theme, "muted"),
theme, iconSize: 8,
"secondary", iconSpacing: 8,
backgroundColor(theme, 300, "hovered") text: text(theme, "mono", textColor, { size: "sm" }),
),
selectedEntry: entry(theme, "primary"),
hoveredSelectedEntry: entry(
theme,
"primary",
backgroundColor(theme, 300, "hovered")
),
padding: {
top: 6,
left: 12,
},
}; };
}
return {
...panel,
entry: entry(theme, "secondary"),
hoveredEntry: entry(
theme,
"secondary",
backgroundColor(theme, 300, "hovered")
),
selectedEntry: entry(theme, "primary"),
hoveredSelectedEntry: entry(
theme,
"primary",
backgroundColor(theme, 300, "hovered")
),
padding: {
top: 6,
left: 12,
},
};
} }

View file

@ -2,78 +2,78 @@ import Theme from "../themes/theme";
import { backgroundColor, border, player, text } from "./components"; import { backgroundColor, border, player, text } from "./components";
export default function search(theme: Theme) { export default function search(theme: Theme) {
const optionButton = { const optionButton = {
...text(theme, "mono", "secondary"), ...text(theme, "mono", "secondary"),
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
cornerRadius: 6, cornerRadius: 6,
border: border(theme, "primary"), border: border(theme, "primary"),
margin: { margin: {
left: 1, left: 1,
right: 1, right: 1,
}, },
padding: { padding: {
bottom: 1, bottom: 1,
left: 6, left: 6,
right: 6, right: 6,
top: 1, top: 1,
}, },
}; };
const editor = { const editor = {
background: backgroundColor(theme, 500), background: backgroundColor(theme, 500),
cornerRadius: 6, cornerRadius: 6,
minWidth: 200, minWidth: 200,
maxWidth: 500, maxWidth: 500,
placeholderText: text(theme, "mono", "placeholder"), placeholderText: text(theme, "mono", "placeholder"),
selection: player(theme, 1).selection, selection: player(theme, 1).selection,
text: text(theme, "mono", "primary"), text: text(theme, "mono", "primary"),
border: border(theme, "secondary"), border: border(theme, "secondary"),
margin: { margin: {
right: 5, right: 5,
}, },
padding: { padding: {
top: 3, top: 3,
bottom: 3, bottom: 3,
left: 14, left: 14,
right: 14, right: 14,
}, },
}; };
return { return {
matchBackground: theme.editor.highlight.match.value, matchBackground: theme.editor.highlight.match.value,
tabIconSpacing: 4, tabIconSpacing: 4,
tabIconWidth: 14, tabIconWidth: 14,
activeHoveredOptionButton: { activeHoveredOptionButton: {
...optionButton, ...optionButton,
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
}, },
activeOptionButton: { activeOptionButton: {
...optionButton, ...optionButton,
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
}, },
editor, editor,
hoveredOptionButton: { hoveredOptionButton: {
...optionButton, ...optionButton,
background: backgroundColor(theme, 100), background: backgroundColor(theme, 100),
}, },
invalidEditor: { invalidEditor: {
...editor, ...editor,
border: border(theme, "error"), border: border(theme, "error"),
}, },
matchIndex: { matchIndex: {
...text(theme, "mono", "muted"), ...text(theme, "mono", "muted"),
padding: 6, padding: 6,
}, },
optionButton, optionButton,
optionButtonGroup: { optionButtonGroup: {
padding: { padding: {
left: 2, left: 2,
right: 2, right: 2,
}, },
}, },
resultsStatus: { resultsStatus: {
...text(theme, "mono", "primary"), ...text(theme, "mono", "primary"),
size: 18, size: 18,
}, },
}; };
} }

View file

@ -2,58 +2,58 @@ import Theme from "../themes/theme";
import { backgroundColor, border, player, shadow, text } from "./components"; import { backgroundColor, border, player, shadow, text } from "./components";
export default function selectorModal(theme: Theme): Object { export default function selectorModal(theme: Theme): Object {
const item = { const item = {
padding: { padding: {
bottom: 4, bottom: 4,
left: 16, left: 16,
right: 16, right: 16,
top: 4, top: 4,
}, },
cornerRadius: 6, cornerRadius: 6,
text: text(theme, "sans", "secondary"), text: text(theme, "sans", "secondary"),
highlightText: text(theme, "sans", "feature", { weight: "bold" }), highlightText: text(theme, "sans", "feature", { weight: "bold" }),
}; };
const activeItem = { const activeItem = {
...item, ...item,
background: backgroundColor(theme, 300, "active"), background: backgroundColor(theme, 300, "active"),
text: text(theme, "sans", "primary"), text: text(theme, "sans", "primary"),
}; };
return { return {
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
cornerRadius: 6, cornerRadius: 6,
padding: 8, padding: 8,
item, item,
activeItem, activeItem,
border: border(theme, "primary"), border: border(theme, "primary"),
empty: { empty: {
text: text(theme, "sans", "placeholder"), text: text(theme, "sans", "placeholder"),
padding: { padding: {
bottom: 4, bottom: 4,
left: 16, left: 16,
right: 16, right: 16,
top: 8, top: 8,
}, },
}, },
inputEditor: { inputEditor: {
background: backgroundColor(theme, 500), background: backgroundColor(theme, 500),
corner_radius: 6, corner_radius: 6,
placeholderText: text(theme, "sans", "placeholder"), placeholderText: text(theme, "sans", "placeholder"),
selection: player(theme, 1).selection, selection: player(theme, 1).selection,
text: text(theme, "mono", "primary"), text: text(theme, "mono", "primary"),
border: border(theme, "secondary"), border: border(theme, "secondary"),
padding: { padding: {
bottom: 7, bottom: 7,
left: 16, left: 16,
right: 16, right: 16,
top: 7, top: 7,
}, },
}, },
margin: { margin: {
bottom: 52, bottom: 52,
top: 52, top: 52,
}, },
shadow: shadow(theme), shadow: shadow(theme),
}; };
} }

View file

@ -1,150 +1,150 @@
import Theme from "../themes/theme"; import Theme from "../themes/theme";
import { backgroundColor, border, borderColor, iconColor, text } from "./components"; import { backgroundColor, border, iconColor, text } from "./components";
export default function workspace(theme: Theme) { export default function workspace(theme: Theme) {
const signInPrompt = { const signInPrompt = {
...text(theme, "sans", "secondary", { size: "xs" }), ...text(theme, "sans", "secondary", { size: "xs" }),
underline: true, underline: true,
padding: { padding: {
right: 8, right: 8,
}, },
}; };
const tab = { const tab = {
height: 32, height: 32,
background: backgroundColor(theme, 300), background: backgroundColor(theme, 300),
iconClose: iconColor(theme, "muted"), iconClose: iconColor(theme, "muted"),
iconCloseActive: iconColor(theme, "active"), iconCloseActive: iconColor(theme, "active"),
iconConflict: iconColor(theme, "warning"), iconConflict: iconColor(theme, "warning"),
iconDirty: iconColor(theme, "info"), iconDirty: iconColor(theme, "info"),
iconWidth: 8, iconWidth: 8,
spacing: 10, spacing: 10,
text: text(theme, "mono", "secondary", { size: "sm" }), text: text(theme, "mono", "secondary", { size: "sm" }),
border: border(theme, "primary", { border: border(theme, "primary", {
left: true, left: true,
bottom: true, bottom: true,
overlay: true, overlay: true,
}), }),
padding: { padding: {
left: 12, left: 12,
right: 12, right: 12,
}, },
}; };
const activeTab = { const activeTab = {
...tab, ...tab,
background: backgroundColor(theme, 500), background: backgroundColor(theme, 500),
text: text(theme, "mono", "active", { size: "sm" }), text: text(theme, "mono", "active", { size: "sm" }),
border: {
...tab.border,
bottom: false,
},
};
const sidebarItem = {
height: 32,
iconColor: iconColor(theme, "secondary"),
iconSize: 18,
};
const sidebar = {
width: 30,
background: backgroundColor(theme, 300),
border: border(theme, "primary", { right: true }),
item: sidebarItem,
activeItem: {
...sidebarItem,
iconColor: iconColor(theme, "active"),
},
resizeHandle: {
background: border(theme, "primary").color,
padding: {
left: 1,
},
},
};
return {
background: backgroundColor(theme, 300),
leaderBorderOpacity: 0.7,
leaderBorderWidth: 2.0,
tab,
activeTab,
leftSidebar: {
...sidebar,
border: border(theme, "primary", { right: true }),
},
rightSidebar: {
...sidebar,
border: border(theme, "primary", { left: true }),
},
paneDivider: {
color: border(theme, "secondary").color,
width: 1,
},
status_bar: {
height: 24,
itemSpacing: 8,
padding: {
left: 6,
right: 6,
},
border: border(theme, "primary", { top: true, overlay: true }),
cursorPosition: text(theme, "sans", "muted"),
diagnosticMessage: text(theme, "sans", "muted"),
lspMessage: text(theme, "sans", "muted"),
},
titlebar: {
avatarWidth: 18,
height: 32,
background: backgroundColor(theme, 100),
shareIconColor: iconColor(theme, "secondary"),
shareIconActiveColor: iconColor(theme, "feature"),
title: text(theme, "sans", "primary"),
avatar: {
cornerRadius: 10,
border: { border: {
...tab.border, color: "#00000088",
bottom: false, width: 1,
}, },
}; },
avatarRibbon: {
const sidebarItem = { height: 3,
height: 32, width: 12,
iconColor: iconColor(theme, "secondary"), // TODO: The background for this ideally should be
iconSize: 18, // set with a token, not hardcoded in rust
}; },
const sidebar = { border: border(theme, "primary", { bottom: true }),
width: 30, signInPrompt,
background: backgroundColor(theme, 300), hoveredSignInPrompt: {
border: border(theme, "primary", { right: true }), ...signInPrompt,
item: sidebarItem, ...text(theme, "sans", "active", { size: "xs" }),
activeItem: { },
...sidebarItem, offlineIcon: {
iconColor: iconColor(theme, "active"), color: iconColor(theme, "secondary"),
width: 16,
padding: {
right: 4,
}, },
resizeHandle: { },
background: border(theme, "primary").color, outdatedWarning: {
padding: { ...text(theme, "sans", "warning"),
left: 1, size: 13,
}, },
}, },
}; toolbar: {
height: 34,
return { background: backgroundColor(theme, 500),
background: backgroundColor(theme, 300), border: border(theme, "secondary", { bottom: true }),
leaderBorderOpacity: 0.7, itemSpacing: 8,
leaderBorderWidth: 2.0, padding: { left: 16, right: 8, top: 4, bottom: 4 },
tab, },
activeTab, breadcrumbs: {
leftSidebar: { ...text(theme, "mono", "secondary"),
...sidebar, padding: { left: 6 },
border: border(theme, "primary", { right: true }), },
}, disconnectedOverlay: {
rightSidebar: { ...text(theme, "sans", "active"),
...sidebar, background: "#000000aa",
border: border(theme, "primary", { left: true }), },
}, };
paneDivider: {
color: border(theme, "secondary").color,
width: 1,
},
status_bar: {
height: 24,
itemSpacing: 8,
padding: {
left: 6,
right: 6,
},
border: border(theme, "primary", { top: true, overlay: true }),
cursorPosition: text(theme, "sans", "muted"),
diagnosticMessage: text(theme, "sans", "muted"),
lspMessage: text(theme, "sans", "muted"),
},
titlebar: {
avatarWidth: 18,
height: 32,
background: backgroundColor(theme, 100),
shareIconColor: iconColor(theme, "secondary"),
shareIconActiveColor: iconColor(theme, "feature"),
title: text(theme, "sans", "primary"),
avatar: {
cornerRadius: 10,
border: {
color: "#00000088",
width: 1,
},
},
avatarRibbon: {
height: 3,
width: 12,
// TODO: The background for this ideally should be
// set with a token, not hardcoded in rust
},
border: border(theme, "primary", { bottom: true }),
signInPrompt,
hoveredSignInPrompt: {
...signInPrompt,
...text(theme, "sans", "active", { size: "xs" }),
},
offlineIcon: {
color: iconColor(theme, "secondary"),
width: 16,
padding: {
right: 4,
},
},
outdatedWarning: {
...text(theme, "sans", "warning"),
size: 13,
},
},
toolbar: {
height: 34,
background: backgroundColor(theme, 500),
border: border(theme, "secondary", { bottom: true }),
itemSpacing: 8,
padding: { left: 16, right: 8, top: 4, bottom: 4 },
},
breadcrumbs: {
...text(theme, "mono", "secondary"),
padding: { left: 6 },
},
disconnectedOverlay: {
...text(theme, "sans", "active"),
background: "#000000aa",
},
};
} }

View file

@ -3,227 +3,227 @@ import { withOpacity } from "../utils/color";
import Theme, { buildPlayer, Syntax } from "./theme"; import Theme, { buildPlayer, Syntax } from "./theme";
const backgroundColor = { const backgroundColor = {
100: { 100: {
base: colors.neutral[750], base: colors.neutral[750],
hovered: colors.neutral[725], hovered: colors.neutral[725],
active: colors.neutral[800], active: colors.neutral[800],
focused: colors.neutral[675], focused: colors.neutral[675],
}, },
300: { 300: {
base: colors.neutral[800], base: colors.neutral[800],
hovered: colors.neutral[775], hovered: colors.neutral[775],
active: colors.neutral[750], active: colors.neutral[750],
focused: colors.neutral[775], focused: colors.neutral[775],
}, },
500: { 500: {
base: colors.neutral[900], base: colors.neutral[900],
hovered: withOpacity(colors.neutral[0], 0.08), hovered: withOpacity(colors.neutral[0], 0.08),
active: withOpacity(colors.neutral[0], 0.12), active: withOpacity(colors.neutral[0], 0.12),
focused: colors.neutral[825], focused: colors.neutral[825],
}, },
ok: { ok: {
base: colors.green[600], base: colors.green[600],
hovered: colors.green[600], hovered: colors.green[600],
active: colors.green[600], active: colors.green[600],
focused: colors.green[600], focused: colors.green[600],
}, },
error: { error: {
base: colors.red[400], base: colors.red[400],
hovered: colors.red[400], hovered: colors.red[400],
active: colors.red[400], active: colors.red[400],
focused: colors.red[400], focused: colors.red[400],
}, },
warning: { warning: {
base: colors.amber[300], base: colors.amber[300],
hovered: colors.amber[300], hovered: colors.amber[300],
active: colors.amber[300], active: colors.amber[300],
focused: colors.amber[300], focused: colors.amber[300],
}, },
info: { info: {
base: colors.blue[500], base: colors.blue[500],
hovered: colors.blue[500], hovered: colors.blue[500],
active: colors.blue[500], active: colors.blue[500],
focused: colors.blue[500], focused: colors.blue[500],
}, },
}; };
const borderColor = { const borderColor = {
primary: colors.neutral[875], primary: colors.neutral[875],
secondary: colors.neutral[775], secondary: colors.neutral[775],
muted: colors.neutral[675], muted: colors.neutral[675],
focused: colors.neutral[500], focused: colors.neutral[500],
active: colors.neutral[900], active: colors.neutral[900],
ok: colors.green[500], ok: colors.green[500],
error: colors.red[500], error: colors.red[500],
warning: colors.amber[500], warning: colors.amber[500],
info: colors.blue[500], info: colors.blue[500],
}; };
const textColor = { const textColor = {
primary: colors.neutral[50], primary: colors.neutral[50],
secondary: colors.neutral[350], secondary: colors.neutral[350],
muted: colors.neutral[450], muted: colors.neutral[450],
placeholder: colors.neutral[650], placeholder: colors.neutral[650],
active: colors.neutral[0], active: colors.neutral[0],
//TODO: (design) define feature and it's correct value //TODO: (design) define feature and it's correct value
feature: colors.sky[500], feature: colors.sky[500],
ok: colors.green[600], ok: colors.green[600],
error: colors.red[400], error: colors.red[400],
warning: colors.amber[300], warning: colors.amber[300],
info: colors.blue[500], info: colors.blue[500],
}; };
const iconColor = { const iconColor = {
primary: colors.neutral[200], primary: colors.neutral[200],
secondary: colors.neutral[350], secondary: colors.neutral[350],
muted: colors.neutral[600], muted: colors.neutral[600],
placeholder: colors.neutral[700], placeholder: colors.neutral[700],
active: colors.neutral[0], active: colors.neutral[0],
//TODO: (design) define feature and it's correct value //TODO: (design) define feature and it's correct value
feature: colors.blue[500], feature: colors.blue[500],
ok: colors.green[600], ok: colors.green[600],
error: colors.red[500], error: colors.red[500],
warning: colors.amber[400], warning: colors.amber[400],
info: colors.blue[600], info: colors.blue[600],
}; };
const player = { const player = {
1: buildPlayer(colors.blue[500]), 1: buildPlayer(colors.blue[500]),
2: buildPlayer(colors.lime[500]), 2: buildPlayer(colors.lime[500]),
3: buildPlayer(colors.fuschia[500]), 3: buildPlayer(colors.fuschia[500]),
4: buildPlayer(colors.orange[500]), 4: buildPlayer(colors.orange[500]),
5: buildPlayer(colors.purple[500]), 5: buildPlayer(colors.purple[500]),
6: buildPlayer(colors.teal[400]), 6: buildPlayer(colors.teal[400]),
7: buildPlayer(colors.pink[400]), 7: buildPlayer(colors.pink[400]),
8: buildPlayer(colors.yellow[400]), 8: buildPlayer(colors.yellow[400]),
}; };
const editor = { const editor = {
background: backgroundColor[500].base, background: backgroundColor[500].base,
indent_guide: borderColor.muted, indent_guide: borderColor.muted,
indent_guide_active: borderColor.secondary, indent_guide_active: borderColor.secondary,
line: { line: {
active: withOpacity(colors.neutral[0], 0.07), active: withOpacity(colors.neutral[0], 0.07),
highlighted: withOpacity(colors.neutral[0], 0.12), highlighted: withOpacity(colors.neutral[0], 0.12),
inserted: backgroundColor.ok.active, inserted: backgroundColor.ok.active,
deleted: backgroundColor.error.active, deleted: backgroundColor.error.active,
modified: backgroundColor.info.active, modified: backgroundColor.info.active,
}, },
highlight: { highlight: {
selection: player[1].selectionColor, selection: player[1].selectionColor,
occurrence: withOpacity(colors.neutral[0], 0.12), occurrence: withOpacity(colors.neutral[0], 0.12),
activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side activeOccurrence: withOpacity(colors.neutral[0], 0.16), // TODO: This is not correctly hooked up to occurences on the rust side
matchingBracket: backgroundColor[500].active, matchingBracket: backgroundColor[500].active,
match: withOpacity(colors.sky[500], 0.16), match: withOpacity(colors.sky[500], 0.16),
activeMatch: withOpacity(colors.sky[800], 0.32), activeMatch: withOpacity(colors.sky[800], 0.32),
related: backgroundColor[500].focused, related: backgroundColor[500].focused,
}, },
gutter: { gutter: {
primary: textColor.placeholder, primary: textColor.placeholder,
active: textColor.active, active: textColor.active,
}, },
}; };
const syntax: Syntax = { const syntax: Syntax = {
primary: { primary: {
color: colors.neutral[150], color: colors.neutral[150],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
comment: { comment: {
color: colors.neutral[300], color: colors.neutral[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
punctuation: { punctuation: {
color: colors.neutral[200], color: colors.neutral[200],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
constant: { constant: {
color: colors.neutral[150], color: colors.neutral[150],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
keyword: { keyword: {
color: colors.blue[400], color: colors.blue[400],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
function: { function: {
color: colors.yellow[200], color: colors.yellow[200],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
type: { type: {
color: colors.teal[300], color: colors.teal[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
variant: { variant: {
color: colors.sky[300], color: colors.sky[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
property: { property: {
color: colors.blue[400], color: colors.blue[400],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
enum: { enum: {
color: colors.orange[500], color: colors.orange[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
operator: { operator: {
color: colors.orange[500], color: colors.orange[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
string: { string: {
color: colors.orange[300], color: colors.orange[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
number: { number: {
color: colors.lime[300], color: colors.lime[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
boolean: { boolean: {
color: colors.lime[300], color: colors.lime[300],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
predictive: { predictive: {
color: textColor.muted, color: textColor.muted,
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
title: { title: {
color: colors.amber[500], color: colors.amber[500],
weight: fontWeights.bold, weight: fontWeights.bold,
}, },
emphasis: { emphasis: {
color: textColor.active, color: textColor.active,
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
emphasisStrong: { emphasisStrong: {
color: textColor.active, color: textColor.active,
weight: fontWeights.bold, weight: fontWeights.bold,
}, },
linkUrl: { linkUrl: {
color: colors.lime[500], color: colors.lime[500],
weight: fontWeights.normal, weight: fontWeights.normal,
// TODO: add underline // TODO: add underline
}, },
linkText: { linkText: {
color: colors.orange[500], color: colors.orange[500],
weight: fontWeights.normal, weight: fontWeights.normal,
// TODO: add italic // TODO: add italic
}, },
}; };
const shadowAlpha: NumberToken = { const shadowAlpha: NumberToken = {
value: 0.32, value: 0.32,
type: "number", type: "number",
}; };
const theme: Theme = { const theme: Theme = {
name: "dark", name: "dark",
backgroundColor, backgroundColor,
borderColor, borderColor,
textColor, textColor,
iconColor, iconColor,
editor, editor,
syntax, syntax,
player, player,
shadowAlpha, shadowAlpha,
}; };
export default theme; export default theme;

View file

@ -3,225 +3,225 @@ import { withOpacity } from "../utils/color";
import Theme, { buildPlayer, Syntax } from "./theme"; import Theme, { buildPlayer, Syntax } from "./theme";
const backgroundColor = { const backgroundColor = {
100: { 100: {
base: colors.neutral[75], base: colors.neutral[75],
hovered: colors.neutral[100], hovered: colors.neutral[100],
active: colors.neutral[150], active: colors.neutral[150],
focused: colors.neutral[100], focused: colors.neutral[100],
}, },
300: { 300: {
base: colors.neutral[25], base: colors.neutral[25],
hovered: colors.neutral[75], hovered: colors.neutral[75],
active: colors.neutral[125], active: colors.neutral[125],
focused: colors.neutral[75], focused: colors.neutral[75],
}, },
500: { 500: {
base: colors.neutral[0], base: colors.neutral[0],
hovered: withOpacity(colors.neutral[900], 0.03), hovered: withOpacity(colors.neutral[900], 0.03),
active: withOpacity(colors.neutral[900], 0.06), active: withOpacity(colors.neutral[900], 0.06),
focused: colors.neutral[50], focused: colors.neutral[50],
}, },
ok: { ok: {
base: colors.green[100], base: colors.green[100],
hovered: colors.green[100], hovered: colors.green[100],
active: colors.green[100], active: colors.green[100],
focused: colors.green[100], focused: colors.green[100],
}, },
error: { error: {
base: colors.red[100], base: colors.red[100],
hovered: colors.red[100], hovered: colors.red[100],
active: colors.red[100], active: colors.red[100],
focused: colors.red[100], focused: colors.red[100],
}, },
warning: { warning: {
base: colors.yellow[100], base: colors.yellow[100],
hovered: colors.yellow[100], hovered: colors.yellow[100],
active: colors.yellow[100], active: colors.yellow[100],
focused: colors.yellow[100], focused: colors.yellow[100],
}, },
info: { info: {
base: colors.blue[100], base: colors.blue[100],
hovered: colors.blue[100], hovered: colors.blue[100],
active: colors.blue[100], active: colors.blue[100],
focused: colors.blue[100], focused: colors.blue[100],
}, },
}; };
const borderColor = { const borderColor = {
primary: colors.neutral[150], primary: colors.neutral[150],
secondary: colors.neutral[150], secondary: colors.neutral[150],
muted: colors.neutral[100], muted: colors.neutral[100],
focused: colors.neutral[100], focused: colors.neutral[100],
active: colors.neutral[250], active: colors.neutral[250],
ok: colors.green[200], ok: colors.green[200],
error: colors.red[200], error: colors.red[200],
warning: colors.yellow[200], warning: colors.yellow[200],
info: colors.blue[200], info: colors.blue[200],
}; };
const textColor = { const textColor = {
primary: colors.neutral[750], primary: colors.neutral[750],
secondary: colors.neutral[650], secondary: colors.neutral[650],
muted: colors.neutral[550], muted: colors.neutral[550],
placeholder: colors.neutral[450], placeholder: colors.neutral[450],
active: colors.neutral[900], active: colors.neutral[900],
feature: colors.indigo[600], feature: colors.indigo[600],
ok: colors.green[500], ok: colors.green[500],
error: colors.red[500], error: colors.red[500],
warning: colors.yellow[500], warning: colors.yellow[500],
info: colors.blue[500], info: colors.blue[500],
}; };
const iconColor = { const iconColor = {
primary: colors.neutral[700], primary: colors.neutral[700],
secondary: colors.neutral[500], secondary: colors.neutral[500],
muted: colors.neutral[350], muted: colors.neutral[350],
placeholder: colors.neutral[300], placeholder: colors.neutral[300],
active: colors.neutral[900], active: colors.neutral[900],
feature: colors.indigo[500], feature: colors.indigo[500],
ok: colors.green[600], ok: colors.green[600],
error: colors.red[600], error: colors.red[600],
warning: colors.yellow[400], warning: colors.yellow[400],
info: colors.blue[600], info: colors.blue[600],
}; };
const player = { const player = {
1: buildPlayer(colors.blue[500]), 1: buildPlayer(colors.blue[500]),
2: buildPlayer(colors.emerald[400]), 2: buildPlayer(colors.emerald[400]),
3: buildPlayer(colors.fuschia[400]), 3: buildPlayer(colors.fuschia[400]),
4: buildPlayer(colors.orange[400]), 4: buildPlayer(colors.orange[400]),
5: buildPlayer(colors.purple[400]), 5: buildPlayer(colors.purple[400]),
6: buildPlayer(colors.teal[400]), 6: buildPlayer(colors.teal[400]),
7: buildPlayer(colors.pink[400]), 7: buildPlayer(colors.pink[400]),
8: buildPlayer(colors.yellow[400]), 8: buildPlayer(colors.yellow[400]),
}; };
const editor = { const editor = {
background: backgroundColor[500].base, background: backgroundColor[500].base,
indent_guide: borderColor.muted, indent_guide: borderColor.muted,
indent_guide_active: borderColor.secondary, indent_guide_active: borderColor.secondary,
line: { line: {
active: withOpacity(colors.neutral[900], 0.06), active: withOpacity(colors.neutral[900], 0.06),
highlighted: withOpacity(colors.neutral[900], 0.12), highlighted: withOpacity(colors.neutral[900], 0.12),
inserted: backgroundColor.ok.active, inserted: backgroundColor.ok.active,
deleted: backgroundColor.error.active, deleted: backgroundColor.error.active,
modified: backgroundColor.info.active, modified: backgroundColor.info.active,
}, },
highlight: { highlight: {
selection: player[1].selectionColor, selection: player[1].selectionColor,
occurrence: withOpacity(colors.neutral[900], 0.06), occurrence: withOpacity(colors.neutral[900], 0.06),
activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side activeOccurrence: withOpacity(colors.neutral[900], 0.16), // TODO: This is not hooked up to occurences on the rust side
matchingBracket: colors.neutral[0], matchingBracket: colors.neutral[0],
match: withOpacity(colors.red[500], 0.2), match: withOpacity(colors.red[500], 0.2),
activeMatch: withOpacity(colors.indigo[400], 0.36), // TODO: This is not hooked up to occurences on the rust side activeMatch: withOpacity(colors.indigo[400], 0.36), // TODO: This is not hooked up to occurences on the rust side
related: colors.neutral[0], related: colors.neutral[0],
}, },
gutter: { gutter: {
primary: colors.neutral[300], primary: colors.neutral[300],
active: textColor.active, active: textColor.active,
}, },
}; };
const syntax: Syntax = { const syntax: Syntax = {
primary: { primary: {
color: colors.neutral[800], color: colors.neutral[800],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
comment: { comment: {
color: colors.neutral[500], color: colors.neutral[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
punctuation: { punctuation: {
color: colors.neutral[600], color: colors.neutral[600],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
constant: { constant: {
color: colors.neutral[800], color: colors.neutral[800],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
keyword: { keyword: {
color: colors.indigo[700], color: colors.indigo[700],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
function: { function: {
color: colors.orange[600], color: colors.orange[600],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
type: { type: {
color: colors.yellow[600], color: colors.yellow[600],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
variant: { variant: {
color: colors.rose[700], color: colors.rose[700],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
property: { property: {
color: colors.emerald[700], color: colors.emerald[700],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
enum: { enum: {
color: colors.red[500], color: colors.red[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
operator: { operator: {
color: colors.red[500], color: colors.red[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
string: { string: {
color: colors.red[500], color: colors.red[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
number: { number: {
color: colors.indigo[500], color: colors.indigo[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
boolean: { boolean: {
color: colors.red[500], color: colors.red[500],
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
predictive: { predictive: {
color: textColor.placeholder, color: textColor.placeholder,
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
title: { title: {
color: colors.sky[500], color: colors.sky[500],
weight: fontWeights.bold, weight: fontWeights.bold,
}, },
emphasis: { emphasis: {
color: textColor.active, color: textColor.active,
weight: fontWeights.normal, weight: fontWeights.normal,
}, },
emphasisStrong: { emphasisStrong: {
color: textColor.active, color: textColor.active,
weight: fontWeights.bold, weight: fontWeights.bold,
}, },
linkUrl: { linkUrl: {
color: colors.lime[500], color: colors.lime[500],
weight: fontWeights.normal, weight: fontWeights.normal,
// TODO: add underline // TODO: add underline
}, },
linkText: { linkText: {
color: colors.red[500], color: colors.red[500],
weight: fontWeights.normal, weight: fontWeights.normal,
// TODO: add italic // TODO: add italic
}, },
}; };
const shadowAlpha: NumberToken = { const shadowAlpha: NumberToken = {
value: 0.12, value: 0.12,
type: "number", type: "number",
}; };
const theme: Theme = { const theme: Theme = {
name: "light", name: "light",
backgroundColor, backgroundColor,
borderColor, borderColor,
textColor, textColor,
iconColor, iconColor,
editor, editor,
syntax, syntax,
player, player,
shadowAlpha, shadowAlpha,
}; };
export default theme; export default theme;

View file

@ -2,144 +2,144 @@ import { ColorToken, FontWeightToken, NumberToken } from "../tokens";
import { withOpacity } from "../utils/color"; import { withOpacity } from "../utils/color";
export interface SyntaxHighlightStyle { export interface SyntaxHighlightStyle {
color: ColorToken; color: ColorToken;
weight: FontWeightToken; weight: FontWeightToken;
} }
export interface Player { export interface Player {
baseColor: ColorToken; baseColor: ColorToken;
cursorColor: ColorToken; cursorColor: ColorToken;
selectionColor: ColorToken; selectionColor: ColorToken;
borderColor: ColorToken; borderColor: ColorToken;
} }
export function buildPlayer( export function buildPlayer(
color: ColorToken, color: ColorToken,
cursorOpacity?: number, cursorOpacity?: number,
selectionOpacity?: number, selectionOpacity?: number,
borderOpacity?: number borderOpacity?: number
) { ) {
return { return {
baseColor: color, baseColor: color,
cursorColor: withOpacity(color, cursorOpacity || 1.0), cursorColor: withOpacity(color, cursorOpacity || 1.0),
selectionColor: withOpacity(color, selectionOpacity || 0.24), selectionColor: withOpacity(color, selectionOpacity || 0.24),
borderColor: withOpacity(color, borderOpacity || 0.8), borderColor: withOpacity(color, borderOpacity || 0.8),
} }
} }
export interface BackgroundColorSet { export interface BackgroundColorSet {
base: ColorToken; base: ColorToken;
hovered: ColorToken; hovered: ColorToken;
active: ColorToken; active: ColorToken;
focused: ColorToken; focused: ColorToken;
} }
export interface Syntax { export interface Syntax {
primary: SyntaxHighlightStyle; primary: SyntaxHighlightStyle;
comment: SyntaxHighlightStyle; comment: SyntaxHighlightStyle;
punctuation: SyntaxHighlightStyle; punctuation: SyntaxHighlightStyle;
constant: SyntaxHighlightStyle; constant: SyntaxHighlightStyle;
keyword: SyntaxHighlightStyle; keyword: SyntaxHighlightStyle;
function: SyntaxHighlightStyle; function: SyntaxHighlightStyle;
type: SyntaxHighlightStyle; type: SyntaxHighlightStyle;
variant: SyntaxHighlightStyle; variant: SyntaxHighlightStyle;
property: SyntaxHighlightStyle; property: SyntaxHighlightStyle;
enum: SyntaxHighlightStyle; enum: SyntaxHighlightStyle;
operator: SyntaxHighlightStyle; operator: SyntaxHighlightStyle;
string: SyntaxHighlightStyle; string: SyntaxHighlightStyle;
number: SyntaxHighlightStyle; number: SyntaxHighlightStyle;
boolean: SyntaxHighlightStyle; boolean: SyntaxHighlightStyle;
predictive: SyntaxHighlightStyle; predictive: SyntaxHighlightStyle;
// TODO: Either move the following or rename // TODO: Either move the following or rename
title: SyntaxHighlightStyle; title: SyntaxHighlightStyle;
emphasis: SyntaxHighlightStyle; emphasis: SyntaxHighlightStyle;
emphasisStrong: SyntaxHighlightStyle; emphasisStrong: SyntaxHighlightStyle;
linkUrl: SyntaxHighlightStyle; linkUrl: SyntaxHighlightStyle;
linkText: SyntaxHighlightStyle; linkText: SyntaxHighlightStyle;
}; };
export default interface Theme { export default interface Theme {
name: string; name: string;
backgroundColor: { backgroundColor: {
100: BackgroundColorSet; 100: BackgroundColorSet;
300: BackgroundColorSet; 300: BackgroundColorSet;
500: BackgroundColorSet; 500: BackgroundColorSet;
ok: BackgroundColorSet; ok: BackgroundColorSet;
error: BackgroundColorSet; error: BackgroundColorSet;
warning: BackgroundColorSet; warning: BackgroundColorSet;
info: BackgroundColorSet; info: BackgroundColorSet;
};
borderColor: {
primary: ColorToken;
secondary: ColorToken;
muted: ColorToken;
focused: ColorToken;
active: ColorToken;
ok: ColorToken;
error: ColorToken;
warning: ColorToken;
info: ColorToken;
};
textColor: {
primary: ColorToken;
secondary: ColorToken;
muted: ColorToken;
placeholder: ColorToken;
active: ColorToken;
feature: ColorToken;
ok: ColorToken;
error: ColorToken;
warning: ColorToken;
info: ColorToken;
};
iconColor: {
primary: ColorToken;
secondary: ColorToken;
muted: ColorToken;
placeholder: ColorToken;
active: ColorToken;
feature: ColorToken;
ok: ColorToken;
error: ColorToken;
warning: ColorToken;
info: ColorToken;
};
editor: {
background: ColorToken;
indent_guide: ColorToken;
indent_guide_active: ColorToken;
line: {
active: ColorToken;
highlighted: ColorToken;
inserted: ColorToken;
deleted: ColorToken;
modified: ColorToken;
}; };
borderColor: { highlight: {
primary: ColorToken; selection: ColorToken;
secondary: ColorToken; occurrence: ColorToken;
muted: ColorToken; activeOccurrence: ColorToken;
focused: ColorToken; matchingBracket: ColorToken;
active: ColorToken; match: ColorToken;
ok: ColorToken; activeMatch: ColorToken;
error: ColorToken; related: ColorToken;
warning: ColorToken;
info: ColorToken;
}; };
textColor: { gutter: {
primary: ColorToken; primary: ColorToken;
secondary: ColorToken; active: ColorToken;
muted: ColorToken;
placeholder: ColorToken;
active: ColorToken;
feature: ColorToken;
ok: ColorToken;
error: ColorToken;
warning: ColorToken;
info: ColorToken;
};
iconColor: {
primary: ColorToken;
secondary: ColorToken;
muted: ColorToken;
placeholder: ColorToken;
active: ColorToken;
feature: ColorToken;
ok: ColorToken;
error: ColorToken;
warning: ColorToken;
info: ColorToken;
};
editor: {
background: ColorToken;
indent_guide: ColorToken;
indent_guide_active: ColorToken;
line: {
active: ColorToken;
highlighted: ColorToken;
inserted: ColorToken;
deleted: ColorToken;
modified: ColorToken;
};
highlight: {
selection: ColorToken;
occurrence: ColorToken;
activeOccurrence: ColorToken;
matchingBracket: ColorToken;
match: ColorToken;
activeMatch: ColorToken;
related: ColorToken;
};
gutter: {
primary: ColorToken;
active: ColorToken;
};
}; };
};
syntax: Syntax, syntax: Syntax,
player: { player: {
1: Player; 1: Player;
2: Player; 2: Player;
3: Player; 3: Player;
4: Player; 4: Player;
5: Player; 5: Player;
6: Player; 6: Player;
7: Player; 7: Player;
8: Player; 8: Player;
}; };
shadowAlpha: NumberToken; shadowAlpha: NumberToken;
} }

View file

@ -36,7 +36,7 @@ export const fontSizes = {
xl: fontSize(20), xl: fontSize(20),
}; };
export type FontWeight = export type FontWeight =
| "thin" | "thin"
| "extra_light" | "extra_light"
| "light" | "light"

View file

@ -4,49 +4,49 @@ import { ColorToken } from "../tokens";
export type Color = string; export type Color = string;
export type ColorRampStep = { value: Color; type: "color"; step: number }; export type ColorRampStep = { value: Color; type: "color"; step: number };
export type ColorRamp = { export type ColorRamp = {
[index: number]: ColorRampStep; [index: number]: ColorRampStep;
}; };
export function colorRamp( export function colorRamp(
color: Color | [Color, Color], color: Color | [Color, Color],
options?: { steps?: number; increment?: number; } options?: { steps?: number; increment?: number; }
): ColorRamp { ): ColorRamp {
let scale: Scale; let scale: Scale;
if (Array.isArray(color)) { if (Array.isArray(color)) {
const [startColor, endColor] = color; const [startColor, endColor] = color;
scale = chroma.scale([startColor, endColor]); scale = chroma.scale([startColor, endColor]);
} else { } else {
let hue = Math.round(chroma(color).hsl()[0]); let hue = Math.round(chroma(color).hsl()[0]);
let startColor = chroma.hsl(hue, 0.88, 0.96); let startColor = chroma.hsl(hue, 0.88, 0.96);
let endColor = chroma.hsl(hue, 0.68, 0.12); let endColor = chroma.hsl(hue, 0.68, 0.12);
scale = chroma scale = chroma
.scale([startColor, color, endColor]) .scale([startColor, color, endColor])
.domain([0, 0.5, 1]) .domain([0, 0.5, 1])
.mode("hsl") .mode("hsl")
.gamma(1) .gamma(1)
// .correctLightness(true) // .correctLightness(true)
.padding([0, 0]); .padding([0, 0]);
} }
const ramp: ColorRamp = {}; const ramp: ColorRamp = {};
const steps = options?.steps || 10; const steps = options?.steps || 10;
const increment = options?.increment || 100; const increment = options?.increment || 100;
scale.colors(steps, "hex").forEach((color, ix) => { scale.colors(steps, "hex").forEach((color, ix) => {
const step = ix * increment; const step = ix * increment;
ramp[step] = { ramp[step] = {
value: color, value: color,
step, step,
type: "color", type: "color",
}; };
}); });
return ramp; return ramp;
} }
export function withOpacity(color: ColorToken, opacity: number): ColorToken { export function withOpacity(color: ColorToken, opacity: number): ColorToken {
return { return {
...color, ...color,
value: chroma(color.value).alpha(opacity).hex() value: chroma(color.value).alpha(opacity).hex()
}; };
} }

View file

@ -4,32 +4,32 @@ import { snakeCase } from "case-anything";
// Typescript magic to convert any string from camelCase to snake_case at compile time // Typescript magic to convert any string from camelCase to snake_case at compile time
type SnakeCase<S> = type SnakeCase<S> =
S extends string ? S extends string ?
S extends `${infer T}${infer U}` ? S extends `${infer T}${infer U}` ?
`${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` : `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${SnakeCase<U>}` :
S : S :
S; S;
type SnakeCased<Type> = { type SnakeCased<Type> = {
[Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]> [Property in keyof Type as SnakeCase<Property>]: SnakeCased<Type[Property]>
} }
export default function snakeCaseTree<T>(object: T): SnakeCased<T> { export default function snakeCaseTree<T>(object: T): SnakeCased<T> {
const snakeObject: any = {}; const snakeObject: any = {};
for (const key in object) { for (const key in object) {
snakeObject[snakeCase(key)] = snakeCaseValue(object[key]); snakeObject[snakeCase(key)] = snakeCaseValue(object[key]);
} }
return snakeObject; return snakeObject;
} }
function snakeCaseValue(value: any): any { function snakeCaseValue(value: any): any {
if (typeof value === "object") { if (typeof value === "object") {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(snakeCaseValue); return value.map(snakeCaseValue);
} else {
return snakeCaseTree(value);
}
} else { } else {
return value; return snakeCaseTree(value);
} }
} else {
return value;
}
} }