Compare commits

...

5 Commits

Author SHA1 Message Date
Max Brunsfeld
d9006fa718 zed 0.93.1 2023-06-29 12:57:42 -07:00
Max Brunsfeld
dfa6e96c2d Avoid redundant FS scans when LSPs changed watched files (#2663)
Release Notes:

- Fixed a performance problem that could occur when a language server
requested to watch a set of files (preview only).
2023-06-29 12:57:13 -07:00
Nate Butler
69dfe70ea8 Re-add missing active state (#2664)
[[PR Description]]

Release Notes:

- Fixes project panel active state that was incorrectly removed.
2023-06-29 12:56:44 -07:00
Mikayla Maki
1f0cf01771 Fix project panel bug (#2656)
Release Notes:

* Fix a bug where project panel entries would not be styled correctly
(preview only)
2023-06-28 10:48:07 -07:00
Joseph T. Lyons
fbbf5a5c19 v0.93.x preview 2023-06-28 12:23:29 -04:00
18 changed files with 496 additions and 327 deletions

2
Cargo.lock generated
View File

@@ -8909,7 +8909,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.93.0"
version = "0.93.1"
dependencies = [
"activity_indicator",
"ai",

View File

@@ -388,6 +388,7 @@ struct FakeFsState {
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
events_paused: bool,
buffered_events: Vec<fsevent::Event>,
metadata_call_count: usize,
read_dir_call_count: usize,
}
@@ -538,6 +539,7 @@ impl FakeFs {
buffered_events: Vec::new(),
events_paused: false,
read_dir_call_count: 0,
metadata_call_count: 0,
}),
})
}
@@ -774,10 +776,16 @@ impl FakeFs {
result
}
/// How many `read_dir` calls have been issued.
pub fn read_dir_call_count(&self) -> usize {
self.state.lock().read_dir_call_count
}
/// How many `metadata` calls have been issued.
pub fn metadata_call_count(&self) -> usize {
self.state.lock().metadata_call_count
}
async fn simulate_random_delay(&self) {
self.executor
.upgrade()
@@ -1098,7 +1106,8 @@ impl Fs for FakeFs {
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
self.simulate_random_delay().await;
let path = normalize_path(path);
let state = self.state.lock();
let mut state = self.state.lock();
state.metadata_call_count += 1;
if let Some((mut entry, _)) = state.try_read_path(&path, false) {
let is_symlink = entry.lock().is_symlink();
if is_symlink {

View File

@@ -596,6 +596,8 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
);
});
let prev_read_dir_count = fs.read_dir_call_count();
// Keep track of the FS events reported to the language server.
let fake_server = fake_servers.next().await.unwrap();
let file_changes = Arc::new(Mutex::new(Vec::new()));
@@ -607,6 +609,12 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
register_options: serde_json::to_value(
lsp::DidChangeWatchedFilesRegistrationOptions {
watchers: vec![
lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String(
"/the-root/Cargo.toml".to_string(),
),
kind: None,
},
lsp::FileSystemWatcher {
glob_pattern: lsp::GlobPattern::String(
"/the-root/src/*.{rs,c}".to_string(),
@@ -638,6 +646,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.foreground().run_until_parked();
assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
// Now the language server has asked us to watch an ignored directory path,
// so we recursively load it.

View File

@@ -3071,17 +3071,20 @@ impl BackgroundScanner {
path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => {
let Ok(path_prefix) = path_prefix else { break };
log::trace!("adding path prefix {:?}", path_prefix);
self.forcibly_load_paths(&[path_prefix.clone()]).await;
let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await;
if did_scan {
let abs_path =
{
let mut state = self.state.lock();
state.path_prefixes_to_scan.insert(path_prefix.clone());
state.snapshot.abs_path.join(&path_prefix)
};
let abs_path =
{
let mut state = self.state.lock();
state.path_prefixes_to_scan.insert(path_prefix.clone());
state.snapshot.abs_path.join(path_prefix)
};
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
self.process_events(vec![abs_path]).await;
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
self.process_events(vec![abs_path]).await;
}
}
}
@@ -3097,10 +3100,13 @@ impl BackgroundScanner {
}
}
async fn process_scan_request(&self, request: ScanRequest, scanning: bool) -> bool {
async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
log::debug!("rescanning paths {:?}", request.relative_paths);
let root_path = self.forcibly_load_paths(&request.relative_paths).await;
request.relative_paths.sort_unstable();
self.forcibly_load_paths(&request.relative_paths).await;
let root_path = self.state.lock().snapshot.abs_path.clone();
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
Ok(path) => path,
Err(err) => {
@@ -3108,10 +3114,9 @@ impl BackgroundScanner {
return false;
}
};
let abs_paths = request
.relative_paths
.into_iter()
.iter()
.map(|path| {
if path.file_name().is_some() {
root_canonical_path.join(path)
@@ -3120,12 +3125,19 @@ impl BackgroundScanner {
}
})
.collect::<Vec<_>>();
self.reload_entries_for_paths(root_path, root_canonical_path, abs_paths, None)
.await;
self.reload_entries_for_paths(
root_path,
root_canonical_path,
&request.relative_paths,
abs_paths,
None,
)
.await;
self.send_status_update(scanning, Some(request.done))
}
async fn process_events(&mut self, abs_paths: Vec<PathBuf>) {
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
log::debug!("received fs events {:?}", abs_paths);
let root_path = self.state.lock().snapshot.abs_path.clone();
@@ -3137,25 +3149,61 @@ impl BackgroundScanner {
}
};
let (scan_job_tx, scan_job_rx) = channel::unbounded();
let paths = self
.reload_entries_for_paths(
let mut relative_paths = Vec::with_capacity(abs_paths.len());
let mut unloaded_relative_paths = Vec::new();
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| {
let snapshot = &self.state.lock().snapshot;
{
let relative_path: Arc<Path> =
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
path.into()
} else {
log::error!(
"ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
);
return false;
};
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
snapshot
.entry_for_path(parent)
.map_or(false, |entry| entry.kind == EntryKind::Dir)
});
if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory");
unloaded_relative_paths.push(relative_path);
return false;
}
relative_paths.push(relative_path);
true
}
});
if !relative_paths.is_empty() {
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.reload_entries_for_paths(
root_path,
root_canonical_path,
&relative_paths,
abs_paths,
Some(scan_job_tx.clone()),
)
.await;
drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await;
drop(scan_job_tx);
self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
let (scan_job_tx, scan_job_rx) = channel::unbounded();
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
}
{
let mut state = self.state.lock();
state.reload_repositories(&paths, self.fs.as_ref());
relative_paths.extend(unloaded_relative_paths);
state.reload_repositories(&relative_paths, self.fs.as_ref());
state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
state.scanned_dirs.remove(&entry_id);
@@ -3165,12 +3213,11 @@ impl BackgroundScanner {
self.send_status_update(false, None);
}
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> Arc<Path> {
let root_path;
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
let (scan_job_tx, mut scan_job_rx) = channel::unbounded();
{
let mut state = self.state.lock();
root_path = state.snapshot.abs_path.clone();
let root_path = state.snapshot.abs_path.clone();
for path in paths {
for ancestor in path.ancestors() {
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
@@ -3201,8 +3248,8 @@ impl BackgroundScanner {
while let Some(job) = scan_job_rx.next().await {
self.scan_dir(&job).await.log_err();
}
self.state.lock().paths_to_scan.clear();
root_path
mem::take(&mut self.state.lock().paths_to_scan).len() > 0
}
async fn scan_dirs(
@@ -3475,7 +3522,7 @@ impl BackgroundScanner {
.expect("channel is unbounded");
}
} else {
log::debug!("defer scanning directory {:?} {:?}", entry.path, entry.kind);
log::debug!("defer scanning directory {:?}", entry.path);
entry.kind = EntryKind::UnloadedDir;
}
}
@@ -3490,26 +3537,10 @@ impl BackgroundScanner {
&self,
root_abs_path: Arc<Path>,
root_canonical_path: PathBuf,
mut abs_paths: Vec<PathBuf>,
relative_paths: &[Arc<Path>],
abs_paths: Vec<PathBuf>,
scan_queue_tx: Option<Sender<ScanJob>>,
) -> Vec<Arc<Path>> {
let mut event_paths = Vec::<Arc<Path>>::with_capacity(abs_paths.len());
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| {
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
event_paths.push(path.into());
true
} else {
log::error!(
"unexpected event {:?} for root path {:?}",
abs_path,
root_canonical_path
);
false
}
});
) {
let metadata = futures::future::join_all(
abs_paths
.iter()
@@ -3538,30 +3569,15 @@ impl BackgroundScanner {
// Remove any entries for paths that no longer exist or are being recursively
// refreshed. Do this before adding any new entries, so that renames can be
// detected regardless of the order of the paths.
for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
if matches!(metadata, Ok(None)) || doing_recursive_update {
log::trace!("remove path {:?}", path);
state.remove_path(path);
}
}
for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
if let (Some(parent), true) = (path.parent(), doing_recursive_update) {
if state
.snapshot
.entry_for_path(parent)
.map_or(true, |entry| entry.kind != EntryKind::Dir)
{
log::debug!(
"ignoring event {path:?} within unloaded directory {:?}",
parent
);
continue;
}
}
for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
let abs_path: Arc<Path> = root_abs_path.join(&path).into();
match metadata {
Ok(Some((metadata, canonical_path))) => {
let ignore_stack = state
@@ -3624,12 +3640,10 @@ impl BackgroundScanner {
util::extend_sorted(
&mut state.changed_paths,
event_paths.iter().cloned(),
relative_paths.iter().cloned(),
usize::MAX,
Ord::cmp,
);
event_paths
}
fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> {
@@ -3760,25 +3774,22 @@ impl BackgroundScanner {
// Scan any directories that were previously ignored and weren't
// previously scanned.
if was_ignored
&& !entry.is_ignored
&& !entry.is_external
&& entry.kind == EntryKind::UnloadedDir
{
job.scan_queue
.try_send(ScanJob {
abs_path: abs_path.clone(),
path: entry.path.clone(),
ignore_stack: child_ignore_stack.clone(),
scan_queue: job.scan_queue.clone(),
ancestor_inodes: self
.state
.lock()
.snapshot
.ancestor_inodes_for_path(&entry.path),
is_external: false,
})
.unwrap();
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock();
if state.should_scan_directory(&entry) {
job.scan_queue
.try_send(ScanJob {
abs_path: abs_path.clone(),
path: entry.path.clone(),
ignore_stack: child_ignore_stack.clone(),
scan_queue: job.scan_queue.clone(),
ancestor_inodes: state
.snapshot
.ancestor_inodes_for_path(&entry.path),
is_external: false,
})
.unwrap();
}
}
job.ignore_queue

View File

@@ -454,6 +454,10 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
"b1.js": "b1",
"b2.js": "b2",
},
"c": {
"c1.js": "c1",
"c2.js": "c2",
}
},
},
"two": {
@@ -521,6 +525,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
(Path::new("one/node_modules/b"), true),
(Path::new("one/node_modules/b/b1.js"), true),
(Path::new("one/node_modules/b/b2.js"), true),
(Path::new("one/node_modules/c"), true),
(Path::new("two"), false),
(Path::new("two/x.js"), false),
(Path::new("two/y.js"), false),
@@ -564,6 +569,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
(Path::new("one/node_modules/b"), true),
(Path::new("one/node_modules/b/b1.js"), true),
(Path::new("one/node_modules/b/b2.js"), true),
(Path::new("one/node_modules/c"), true),
(Path::new("two"), false),
(Path::new("two/x.js"), false),
(Path::new("two/y.js"), false),
@@ -578,6 +584,17 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
// Only the newly-expanded directory is scanned.
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
});
// No work happens when files and directories change within an unloaded directory.
let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
fs.create_dir("/root/one/node_modules/c/lib".as_ref())
.await
.unwrap();
cx.foreground().run_until_parked();
assert_eq!(
fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
0
);
}
#[gpui::test]

View File

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.93.0"
version = "0.93.1"
publish = false
[lib]

View File

@@ -1 +1 @@
dev
preview

View File

@@ -1,25 +1,32 @@
import { ColorScheme } from "../common";
import { interactive, toggleable } from "../element";
import { background, foreground } from "../styleTree/components";
import { ColorScheme } from "../common"
import { interactive, toggleable } from "../element"
import { background, foreground } from "../styleTree/components"
export type Margin = {
top: number;
bottom: number;
left: number;
right: number;
top: number
bottom: number
left: number
right: number
}
interface IconButtonOptions {
layer?: ColorScheme['lowest'] | ColorScheme['middle'] | ColorScheme['highest'];
color?: keyof ColorScheme['lowest'];
margin?: Partial<Margin>;
layer?:
| ColorScheme["lowest"]
| ColorScheme["middle"]
| ColorScheme["highest"]
color?: keyof ColorScheme["lowest"]
margin?: Partial<Margin>
}
type ToggleableIconButtonOptions = IconButtonOptions & { active_color?: keyof ColorScheme['lowest'] };
type ToggleableIconButtonOptions = IconButtonOptions & {
active_color?: keyof ColorScheme["lowest"]
}
export function icon_button(theme: ColorScheme, { color, margin, layer }: IconButtonOptions) {
if (!color)
color = "base";
export function icon_button(
theme: ColorScheme,
{ color, margin, layer }: IconButtonOptions
) {
if (!color) color = "base"
const m = {
top: margin?.top ?? 0,
@@ -51,25 +58,29 @@ export function icon_button(theme: ColorScheme, { color, margin, layer }: IconBu
hovered: {
background: background(layer ?? theme.lowest, color, "hovered"),
color: foreground(layer ?? theme.lowest, color, "hovered"),
},
clicked: {
background: background(layer ?? theme.lowest, color, "pressed"),
color: foreground(layer ?? theme.lowest, color, "pressed"),
},
},
});
})
}
export function toggleable_icon_button(theme: ColorScheme, { color, active_color, margin }: ToggleableIconButtonOptions) {
if (!color)
color = "base";
export function toggleable_icon_button(
theme: ColorScheme,
{ color, active_color, margin }: ToggleableIconButtonOptions
) {
if (!color) color = "base"
return toggleable({
state: {
inactive: icon_button(theme, { color, margin }),
active: icon_button(theme, { color: active_color ? active_color : color, margin, layer: theme.middle }),
}
active: icon_button(theme, {
color: active_color ? active_color : color,
margin,
layer: theme.middle,
}),
},
})
}

View File

@@ -1,25 +1,37 @@
import { ColorScheme } from "../common";
import { interactive, toggleable } from "../element";
import { TextProperties, background, foreground, text } from "../styleTree/components";
import { Margin } from "./icon_button";
import { ColorScheme } from "../common"
import { interactive, toggleable } from "../element"
import {
TextProperties,
background,
foreground,
text,
} from "../styleTree/components"
import { Margin } from "./icon_button"
interface TextButtonOptions {
layer?: ColorScheme['lowest'] | ColorScheme['middle'] | ColorScheme['highest'];
color?: keyof ColorScheme['lowest'];
margin?: Partial<Margin>;
text_properties?: TextProperties;
layer?:
| ColorScheme["lowest"]
| ColorScheme["middle"]
| ColorScheme["highest"]
color?: keyof ColorScheme["lowest"]
margin?: Partial<Margin>
text_properties?: TextProperties
}
type ToggleableTextButtonOptions = TextButtonOptions & { active_color?: keyof ColorScheme['lowest'] };
type ToggleableTextButtonOptions = TextButtonOptions & {
active_color?: keyof ColorScheme["lowest"]
}
export function text_button(theme: ColorScheme, { color, layer, margin, text_properties }: TextButtonOptions) {
if (!color)
color = "base";
export function text_button(
theme: ColorScheme,
{ color, layer, margin, text_properties }: TextButtonOptions
) {
if (!color) color = "base"
const text_options: TextProperties = {
size: "xs",
weight: "normal",
...text_properties
...text_properties,
}
const m = {
@@ -40,7 +52,7 @@ export function text_button(theme: ColorScheme, { color, layer, margin, text_pro
},
margin: m,
button_height: 22,
...text(layer ?? theme.lowest, "sans", color, text_options)
...text(layer ?? theme.lowest, "sans", color, text_options),
},
state: {
default: {
@@ -50,25 +62,29 @@ export function text_button(theme: ColorScheme, { color, layer, margin, text_pro
hovered: {
background: background(layer ?? theme.lowest, color, "hovered"),
color: foreground(layer ?? theme.lowest, color, "hovered"),
},
clicked: {
background: background(layer ?? theme.lowest, color, "pressed"),
color: foreground(layer ?? theme.lowest, color, "pressed"),
},
},
});
})
}
export function toggleable_text_button(theme: ColorScheme, { color, active_color, margin }: ToggleableTextButtonOptions) {
if (!color)
color = "base";
export function toggleable_text_button(
theme: ColorScheme,
{ color, active_color, margin }: ToggleableTextButtonOptions
) {
if (!color) color = "base"
return toggleable({
state: {
inactive: text_button(theme, { color, margin }),
active: text_button(theme, { color: active_color ? active_color : color, margin, layer: theme.middle }),
}
active: text_button(theme, {
color: active_color ? active_color : color,
margin,
layer: theme.middle,
}),
},
})
}

View File

@@ -1,7 +1,7 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type InteractiveState =
export type InteractiveState =
| "default"
| "hovered"
| "clicked"

View File

@@ -26,15 +26,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 12, right: 8.5 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
splitButton: interactive({
base: {
@@ -48,15 +48,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 8.5, right: 8.5 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
quoteButton: interactive({
base: {
@@ -70,15 +70,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 8.5, right: 8.5 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
assistButton: interactive({
base: {
@@ -92,15 +92,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 8.5, right: 8.5 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
zoomInButton: interactive({
base: {
@@ -114,15 +114,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 10, right: 10 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
zoomOutButton: interactive({
base: {
@@ -136,15 +136,15 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 10, right: 10 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
plusButton: interactive({
base: {
@@ -158,29 +158,29 @@ export default function assistant(colorScheme: ColorScheme) {
},
container: {
padding: { left: 10, right: 10 },
}
},
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
color: foreground(layer, "hovered"),
},
},
},
}),
title: {
...text(layer, "sans", "default", { size: "sm" })
...text(layer, "sans", "default", { size: "sm" }),
},
savedConversation: {
container: interactive({
base: {
background: background(layer, "on"),
padding: { top: 4, bottom: 4 }
padding: { top: 4, bottom: 4 },
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
}
},
},
}),
savedAt: {
@@ -189,8 +189,11 @@ export default function assistant(colorScheme: ColorScheme) {
},
title: {
margin: { left: 16 },
...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
}
...text(layer, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
userSender: {
default: {

View File

@@ -93,6 +93,14 @@ interface Text extends Object {
underline?: boolean
}
export interface TextStyle extends Object {
family: keyof typeof fontFamilies
color: string
size: number
weight?: FontWeight
underline?: boolean
}
export interface TextProperties {
size?: keyof typeof fontSizes
weight?: FontWeight

View File

@@ -1,73 +1,125 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
import {
Border,
TextStyle,
background,
border,
foreground,
text,
} from "./components"
import { interactive, toggleable } from "../element"
import merge from "ts-deepmerge"
export default function projectPanel(colorScheme: ColorScheme) {
const { isLight } = colorScheme
let layer = colorScheme.middle
let baseEntry = {
height: 22,
iconColor: foreground(layer, "variant"),
iconSize: 7,
iconSpacing: 5,
type EntryStateProps = {
background?: string
border?: Border
text?: TextStyle
iconColor?: string
}
let status = {
git: {
modified: isLight
? colorScheme.ramps.yellow(0.6).hex()
: colorScheme.ramps.yellow(0.5).hex(),
inserted: isLight
? colorScheme.ramps.green(0.45).hex()
: colorScheme.ramps.green(0.5).hex(),
conflict: isLight
? colorScheme.ramps.red(0.6).hex()
: colorScheme.ramps.red(0.5).hex(),
},
type EntryState = {
default: EntryStateProps
hovered?: EntryStateProps
clicked?: EntryStateProps
}
const default_entry = interactive({
base: {
...baseEntry,
text: text(layer, "mono", "variant", { size: "sm" }),
status,
},
state: {
default: {
background: background(layer),
const entry = (unselected?: EntryState, selected?: EntryState) => {
const git_status = {
git: {
modified: isLight
? colorScheme.ramps.yellow(0.6).hex()
: colorScheme.ramps.yellow(0.5).hex(),
inserted: isLight
? colorScheme.ramps.green(0.45).hex()
: colorScheme.ramps.green(0.5).hex(),
conflict: isLight
? colorScheme.ramps.red(0.6).hex()
: colorScheme.ramps.red(0.5).hex(),
},
hovered: {
background: background(layer, "variant", "hovered"),
},
clicked: {
background: background(layer, "variant", "pressed"),
},
},
})
}
let entry = toggleable({
base: default_entry,
state: {
active: interactive({
base: {
...default_entry,
},
state: {
default: {
background: background(colorScheme.lowest),
const base_properties = {
height: 22,
background: background(layer),
iconColor: foreground(layer, "variant"),
iconSize: 7,
iconSpacing: 5,
text: text(layer, "sans", "variant", { size: "sm" }),
status: {
...git_status,
},
}
const selectedStyle: EntryState | undefined = selected
? selected
: unselected
const unselected_default_style = merge(
base_properties,
unselected?.default ?? {},
{}
)
const unselected_hovered_style = merge(
base_properties,
{ background: background(layer, "hovered") },
unselected?.hovered ?? {},
)
const unselected_clicked_style = merge(
base_properties,
{ background: background(layer, "pressed") },
unselected?.clicked ?? {},
)
const selected_default_style = merge(
base_properties,
{
background: background(colorScheme.lowest),
text: text(colorScheme.lowest, "sans", { size: "sm" }),
},
selectedStyle?.default ?? {},
)
const selected_hovered_style = merge(
base_properties,
{
background: background(colorScheme.lowest, "hovered"),
text: text(colorScheme.lowest, "sans", { size: "sm" }),
},
selectedStyle?.hovered ?? {},
)
const selected_clicked_style = merge(
base_properties,
{
background: background(colorScheme.lowest, "pressed"),
text: text(colorScheme.lowest, "sans", { size: "sm" }),
},
selectedStyle?.clicked ?? {},
)
return toggleable({
state: {
inactive: interactive({
state: {
default: unselected_default_style,
hovered: unselected_hovered_style,
clicked: unselected_clicked_style,
},
hovered: {
background: background(colorScheme.lowest, "hovered"),
}),
active: interactive({
state: {
default: selected_default_style,
hovered: selected_hovered_style,
clicked: selected_clicked_style,
},
clicked: {
background: background(colorScheme.lowest, "pressed"),
},
},
}),
},
})
}),
},
})
}
const defaultEntry = entry()
return {
openProjectButton: interactive({
@@ -104,38 +156,41 @@ export default function projectPanel(colorScheme: ColorScheme) {
background: background(layer),
padding: { left: 6, right: 6, top: 0, bottom: 6 },
indentWidth: 12,
entry,
entry: defaultEntry,
draggedEntry: {
...baseEntry,
text: text(layer, "mono", "on", { size: "sm" }),
...defaultEntry.inactive.default,
text: text(layer, "sans", "on", { size: "sm" }),
background: withOpacity(background(layer, "on"), 0.9),
border: border(layer),
status,
},
ignoredEntry: {
...entry,
iconColor: foreground(layer, "disabled"),
text: text(layer, "mono", "disabled"),
active: {
...entry.active,
iconColor: foreground(layer, "variant"),
},
},
cutEntry: {
...entry,
text: text(layer, "mono", "disabled"),
active: {
...entry.active,
ignoredEntry: entry(
{
default: {
...entry.active.default,
background: background(layer, "active"),
text: text(layer, "mono", "disabled", { size: "sm" }),
text: text(layer, "sans", "disabled"),
},
},
},
{
default: {
iconColor: foreground(layer, "variant"),
},
}
),
cutEntry: entry(
{
default: {
text: text(layer, "sans", "disabled"),
},
},
{
default: {
background: background(layer, "active"),
text: text(layer, "sans", "disabled", { size: "sm" }),
},
}
),
filenameEditor: {
background: background(layer, "on"),
text: text(layer, "mono", "on", { size: "sm" }),
text: text(layer, "sans", "on", { size: "sm" }),
selection: colorScheme.players[0],
},
}

View File

@@ -1,9 +1,9 @@
import { ColorScheme } from "../common";
import { ColorScheme } from "../common"
import { icon_button, toggleable_icon_button } from "../component/icon_button"
import { toggleable_text_button } from "../component/text_button"
import { interactive, toggleable } from "../element"
import { withOpacity } from "../theme/color";
import { background, border, foreground, text } from "./components";
import { withOpacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
const ITEM_SPACING = 8
const TITLEBAR_HEIGHT = 32
@@ -25,7 +25,7 @@ function build_spacing(
function call_controls(theme: ColorScheme) {
const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING);
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
const marginY = {
top: space.marginY,
bottom: space.marginY,
@@ -38,14 +38,14 @@ function call_controls(theme: ColorScheme) {
left: space.group,
right: space.half_item,
},
active_color: 'negative'
active_color: "negative",
}),
toggle_speakers_button: toggleable_icon_button(theme, {
margin: {
...marginY,
left: space.half_item,
right: space.half_item
right: space.half_item,
},
}),
@@ -53,9 +53,9 @@ function call_controls(theme: ColorScheme) {
margin: {
...marginY,
left: space.half_item,
right: space.group
right: space.group,
},
active_color: 'accent'
active_color: "accent",
}),
muted: foreground(theme.lowest, "negative"),
@@ -64,15 +64,15 @@ function call_controls(theme: ColorScheme) {
}
/**
* Opens the User Menu when toggled
*
* When logged in shows the user's avatar and a chevron,
* When logged out only shows a chevron.
*/
* Opens the User Menu when toggled
*
* When logged in shows the user's avatar and a chevron,
* When logged out only shows a chevron.
*/
function user_menu(theme: ColorScheme) {
const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING);
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
const build_button = ({ online }: { online: boolean }) => {
const button = toggleable({
@@ -124,8 +124,8 @@ function user_menu(theme: ColorScheme) {
background: background(theme.middle, "pressed"),
},
},
}
});
},
})
return {
user_menu: button,
@@ -134,7 +134,7 @@ function user_menu(theme: ColorScheme) {
icon_height: 16,
corner_radius: 4,
outer_width: 16,
outer_corner_radius: 16
outer_corner_radius: 16,
},
icon: {
margin: {
@@ -145,8 +145,8 @@ function user_menu(theme: ColorScheme) {
},
width: 11,
height: 11,
color: foreground(theme.lowest)
}
color: foreground(theme.lowest),
},
}
}
return {
@@ -240,7 +240,7 @@ export function titlebar(theme: ColorScheme) {
leave_call_button: icon_button(theme, {
margin: {
left: ITEM_SPACING / 2,
right: ITEM_SPACING
right: ITEM_SPACING,
},
}),
@@ -261,6 +261,6 @@ export function titlebar(theme: ColorScheme) {
background: foreground(theme.lowest, "accent"),
},
shareButton: toggleable_text_button(theme, {}),
user_menu: user_menu(theme)
user_menu: user_menu(theme),
}
}

View File

@@ -1,39 +1,39 @@
import { ThemeSyntax } from "../../common";
import { ThemeSyntax } from "../../common"
export const color = {
default: {
base: '#191724',
surface: '#1f1d2e',
overlay: '#26233a',
muted: '#6e6a86',
subtle: '#908caa',
text: '#e0def4',
love: '#eb6f92',
gold: '#f6c177',
rose: '#ebbcba',
pine: '#31748f',
foam: '#9ccfd8',
iris: '#c4a7e7',
highlightLow: '#21202e',
highlightMed: '#403d52',
highlightHigh: '#524f67',
base: "#191724",
surface: "#1f1d2e",
overlay: "#26233a",
muted: "#6e6a86",
subtle: "#908caa",
text: "#e0def4",
love: "#eb6f92",
gold: "#f6c177",
rose: "#ebbcba",
pine: "#31748f",
foam: "#9ccfd8",
iris: "#c4a7e7",
highlightLow: "#21202e",
highlightMed: "#403d52",
highlightHigh: "#524f67",
},
moon: {
base: '#232136',
surface: '#2a273f',
overlay: '#393552',
muted: '#6e6a86',
subtle: '#908caa',
text: '#e0def4',
love: '#eb6f92',
gold: '#f6c177',
rose: '#ea9a97',
pine: '#3e8fb0',
foam: '#9ccfd8',
iris: '#c4a7e7',
highlightLow: '#2a283e',
highlightMed: '#44415a',
highlightHigh: '#56526e',
base: "#232136",
surface: "#2a273f",
overlay: "#393552",
muted: "#6e6a86",
subtle: "#908caa",
text: "#e0def4",
love: "#eb6f92",
gold: "#f6c177",
rose: "#ea9a97",
pine: "#3e8fb0",
foam: "#9ccfd8",
iris: "#c4a7e7",
highlightLow: "#2a283e",
highlightMed: "#44415a",
highlightHigh: "#56526e",
},
dawn: {
base: "#faf4ed",
@@ -51,8 +51,8 @@ export const color = {
highlightLow: "#f4ede8",
highlightMed: "#dfdad9",
highlightHigh: "#cecacd",
}
};
},
}
export const syntax = (c: typeof color.default): Partial<ThemeSyntax> => {
return {

View File

@@ -6,12 +6,12 @@ import {
ThemeConfig,
} from "../../common"
import { color as c, syntax } from "./common";
import { color as c, syntax } from "./common"
const color = c.dawn
const green = chroma.mix(color.foam, "#10b981", 0.6, 'lab');
const magenta = chroma.mix(color.love, color.pine, 0.5, 'lab');
const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
export const theme: ThemeConfig = {
name: "Rosé Pine Dawn",
@@ -21,7 +21,19 @@ export const theme: ThemeConfig = {
licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
licenseFile: `${__dirname}/LICENSE`,
inputColor: {
neutral: chroma.scale([color.base, color.surface, color.highlightHigh, color.overlay, color.muted, color.subtle, color.text].reverse()).domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
neutral: chroma
.scale(
[
color.base,
color.surface,
color.highlightHigh,
color.overlay,
color.muted,
color.subtle,
color.text,
].reverse()
)
.domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]),
red: colorRamp(chroma(color.love)),
orange: colorRamp(chroma(color.iris)),
yellow: colorRamp(chroma(color.gold)),
@@ -32,6 +44,6 @@ export const theme: ThemeConfig = {
magenta: colorRamp(chroma(magenta)),
},
override: {
syntax: syntax(color)
}
syntax: syntax(color),
},
}

View File

@@ -6,12 +6,12 @@ import {
ThemeConfig,
} from "../../common"
import { color as c, syntax } from "./common";
import { color as c, syntax } from "./common"
const color = c.moon
const green = chroma.mix(color.foam, "#10b981", 0.6, 'lab');
const magenta = chroma.mix(color.love, color.pine, 0.5, 'lab');
const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
export const theme: ThemeConfig = {
name: "Rosé Pine Moon",
@@ -21,7 +21,17 @@ export const theme: ThemeConfig = {
licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
licenseFile: `${__dirname}/LICENSE`,
inputColor: {
neutral: chroma.scale([color.base, color.surface, color.highlightHigh, color.overlay, color.muted, color.subtle, color.text]).domain([0, 0.3, 0.55, 1]),
neutral: chroma
.scale([
color.base,
color.surface,
color.highlightHigh,
color.overlay,
color.muted,
color.subtle,
color.text,
])
.domain([0, 0.3, 0.55, 1]),
red: colorRamp(chroma(color.love)),
orange: colorRamp(chroma(color.iris)),
yellow: colorRamp(chroma(color.gold)),
@@ -32,6 +42,6 @@ export const theme: ThemeConfig = {
magenta: colorRamp(chroma(magenta)),
},
override: {
syntax: syntax(color)
}
syntax: syntax(color),
},
}

View File

@@ -5,12 +5,12 @@ import {
ThemeLicenseType,
ThemeConfig,
} from "../../common"
import { color as c, syntax } from "./common";
import { color as c, syntax } from "./common"
const color = c.default
const green = chroma.mix(color.foam, "#10b981", 0.6, 'lab');
const magenta = chroma.mix(color.love, color.pine, 0.5, 'lab');
const green = chroma.mix(color.foam, "#10b981", 0.6, "lab")
const magenta = chroma.mix(color.love, color.pine, 0.5, "lab")
export const theme: ThemeConfig = {
name: "Rosé Pine",
@@ -20,7 +20,15 @@ export const theme: ThemeConfig = {
licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme",
licenseFile: `${__dirname}/LICENSE`,
inputColor: {
neutral: chroma.scale([color.base, color.surface, color.highlightHigh, color.overlay, color.muted, color.subtle, color.text]),
neutral: chroma.scale([
color.base,
color.surface,
color.highlightHigh,
color.overlay,
color.muted,
color.subtle,
color.text,
]),
red: colorRamp(chroma(color.love)),
orange: colorRamp(chroma(color.iris)),
yellow: colorRamp(chroma(color.gold)),
@@ -31,6 +39,6 @@ export const theme: ThemeConfig = {
magenta: colorRamp(chroma(magenta)),
},
override: {
syntax: syntax(color)
}
syntax: syntax(color),
},
}