Merge branch 'main' into add-installation-id-to-panic-events

This commit is contained in:
Joseph T. Lyons 2023-06-26 13:27:14 -04:00
commit 8d1cc8815b
125 changed files with 5487 additions and 2149 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

View File

@ -51,6 +51,7 @@ jobs:
rustup set profile minimal
rustup update stable
rustup target add wasm32-wasi
cargo install cargo-nextest
- name: Install Node
uses: actions/setup-node@v2
@ -70,7 +71,7 @@ jobs:
run: cargo check --workspace
- name: Run tests
run: cargo test --workspace --no-fail-fast
run: cargo nextest run --workspace --no-fail-fast
- name: Build collab
run: cargo build -p collab

2
.gitignore vendored
View File

@ -4,6 +4,8 @@
/plugins/bin
/script/node_modules
/styles/node_modules
/styles/src/types/zed.ts
/crates/theme/schemas/theme.json
/crates/collab/static/styles.css
/vendor/bin
/assets/themes/*.json

133
Cargo.lock generated
View File

@ -109,6 +109,8 @@ dependencies = [
"isahc",
"language",
"menu",
"project",
"regex",
"schemars",
"search",
"serde",
@ -190,6 +192,55 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal 0.4.7",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
[[package]]
name = "anstyle-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.71"
@ -1102,8 +1153,8 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"clap_derive 3.2.25",
"clap_lex 0.2.4",
"indexmap",
"once_cell",
"strsim",
@ -1111,6 +1162,30 @@ dependencies = [
"textwrap",
]
[[package]]
name = "clap"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
dependencies = [
"clap_builder",
"clap_derive 4.3.2",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"clap_lex 0.5.0",
"strsim",
]
[[package]]
name = "clap_derive"
version = "3.2.25"
@ -1124,6 +1199,18 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "clap_derive"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
@ -1133,12 +1220,18 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 3.2.25",
"core-foundation",
"core-services",
"dirs 3.0.2",
@ -1248,7 +1341,7 @@ dependencies = [
"axum-extra",
"base64 0.13.1",
"call",
"clap",
"clap 3.2.25",
"client",
"collections",
"ctor",
@ -1343,6 +1436,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "command_palette"
version = "0.1.0"
@ -6916,18 +7015,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "theme_testbench"
version = "0.1.0"
dependencies = [
"gpui",
"project",
"settings",
"smallvec",
"theme",
"workspace",
]
[[package]]
name = "thiserror"
version = "1.0.40"
@ -8780,6 +8867,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "xtask"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.3.5",
"schemars",
"serde_json",
"theme",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
@ -8809,7 +8907,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.92.0"
version = "0.93.0"
dependencies = [
"activity_indicator",
"ai",
@ -8888,7 +8986,6 @@ dependencies = [
"text",
"theme",
"theme_selector",
"theme_testbench",
"thiserror",
"tiny_http",
"toml",

View File

@ -61,11 +61,11 @@ members = [
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/theme_testbench",
"crates/util",
"crates/vim",
"crates/workspace",
"crates/welcome",
"crates/xtask",
"crates/zed",
]
default-members = ["crates/zed"]
@ -118,3 +118,4 @@ split-debuginfo = "unpacked"
[profile.release]
debug = true
lto = "thin"
codegen-units = 1

View File

@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.9 0.499976C13.9 0.279062 13.7209 0.0999756 13.5 0.0999756C13.2791 0.0999756 13.1 0.279062 13.1 0.499976V1.09998H12.5C12.2791 1.09998 12.1 1.27906 12.1 1.49998C12.1 1.72089 12.2791 1.89998 12.5 1.89998H13.1V2.49998C13.1 2.72089 13.2791 2.89998 13.5 2.89998C13.7209 2.89998 13.9 2.72089 13.9 2.49998V1.89998H14.5C14.7209 1.89998 14.9 1.72089 14.9 1.49998C14.9 1.27906 14.7209 1.09998 14.5 1.09998H13.9V0.499976ZM11.8536 3.14642C12.0488 3.34168 12.0488 3.65826 11.8536 3.85353L10.8536 4.85353C10.6583 5.04879 10.3417 5.04879 10.1465 4.85353C9.9512 4.65827 9.9512 4.34169 10.1465 4.14642L11.1464 3.14643C11.3417 2.95116 11.6583 2.95116 11.8536 3.14642ZM9.85357 5.14642C10.0488 5.34168 10.0488 5.65827 9.85357 5.85353L2.85355 12.8535C2.65829 13.0488 2.34171 13.0488 2.14645 12.8535C1.95118 12.6583 1.95118 12.3417 2.14645 12.1464L9.14646 5.14642C9.34172 4.95116 9.65831 4.95116 9.85357 5.14642ZM13.5 5.09998C13.7209 5.09998 13.9 5.27906 13.9 5.49998V6.09998H14.5C14.7209 6.09998 14.9 6.27906 14.9 6.49998C14.9 6.72089 14.7209 6.89998 14.5 6.89998H13.9V7.49998C13.9 7.72089 13.7209 7.89998 13.5 7.89998C13.2791 7.89998 13.1 7.72089 13.1 7.49998V6.89998H12.5C12.2791 6.89998 12.1 6.72089 12.1 6.49998C12.1 6.27906 12.2791 6.09998 12.5 6.09998H13.1V5.49998C13.1 5.27906 13.2791 5.09998 13.5 5.09998ZM8.90002 0.499976C8.90002 0.279062 8.72093 0.0999756 8.50002 0.0999756C8.2791 0.0999756 8.10002 0.279062 8.10002 0.499976V1.09998H7.50002C7.2791 1.09998 7.10002 1.27906 7.10002 1.49998C7.10002 1.72089 7.2791 1.89998 7.50002 1.89998H8.10002V2.49998C8.10002 2.72089 8.2791 2.89998 8.50002 2.89998C8.72093 2.89998 8.90002 2.72089 8.90002 2.49998V1.89998H9.50002C9.72093 1.89998 9.90002 1.72089 9.90002 1.49998C9.90002 1.27906 9.72093 1.09998 9.50002 1.09998H8.90002V0.499976Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
</svg>

After

Width:  |  Height:  |  Size: 552 B

View File

@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.42503 3.44136C10.0561 3.23654 10.7837 3.2402 11.3792 3.54623C12.7532 4.25224 13.3477 6.07191 12.7946 8C12.5465 8.8649 12.1102 9.70472 11.1861 10.5524C10.262 11.4 8.98034 11.9 8.38571 11.9C8.17269 11.9 8 11.7321 8 11.525C8 11.3179 8.17644 11.15 8.38571 11.15C9.06497 11.15 9.67189 10.7804 10.3906 10.236C10.9406 9.8193 11.3701 9.28633 11.608 8.82191C12.0628 7.93367 12.0782 6.68174 11.3433 6.34901C10.9904 6.73455 10.5295 6.95946 9.97725 6.95946C8.7773 6.95946 8.0701 5.99412 8.10051 5.12009C8.12957 4.28474 8.66032 3.68954 9.42503 3.44136ZM3.42503 3.44136C4.05614 3.23654 4.78366 3.2402 5.37923 3.54623C6.7532 4.25224 7.34766 6.07191 6.79462 8C6.54654 8.8649 6.11019 9.70472 5.1861 10.5524C4.26201 11.4 2.98034 11.9 2.38571 11.9C2.17269 11.9 2 11.7321 2 11.525C2 11.3179 2.17644 11.15 2.38571 11.15C3.06497 11.15 3.67189 10.7804 4.39058 10.236C4.94065 9.8193 5.37014 9.28633 5.60797 8.82191C6.06282 7.93367 6.07821 6.68174 5.3433 6.34901C4.99037 6.73455 4.52948 6.95946 3.97725 6.95946C2.7773 6.95946 2.0701 5.99412 2.10051 5.12009C2.12957 4.28474 2.66032 3.68954 3.42503 3.44136Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.81832 0.68179C7.64258 0.506054 7.35766 0.506054 7.18192 0.68179L5.18192 2.68179C5.00619 2.85753 5.00619 3.14245 5.18192 3.31819C5.35766 3.49392 5.64258 3.49392 5.81832 3.31819L7.05012 2.08638L7.05012 5.50023C7.05012 5.74876 7.25159 5.95023 7.50012 5.95023C7.74865 5.95023 7.95012 5.74876 7.95012 5.50023L7.95012 2.08638L9.18192 3.31819C9.35766 3.49392 9.64258 3.49392 9.81832 3.31819C9.99406 3.14245 9.99406 2.85753 9.81832 2.68179L7.81832 0.68179ZM7.95012 12.9136V9.50023C7.95012 9.2517 7.74865 9.05023 7.50012 9.05023C7.25159 9.05023 7.05012 9.2517 7.05012 9.50023V12.9136L5.81832 11.6818C5.64258 11.5061 5.35766 11.5061 5.18192 11.6818C5.00619 11.8575 5.00619 12.1424 5.18192 12.3182L7.18192 14.3182C7.26632 14.4026 7.38077 14.45 7.50012 14.45C7.61947 14.45 7.73393 14.4026 7.81832 14.3182L9.81832 12.3182C9.99406 12.1424 9.99406 11.8575 9.81832 11.6818C9.64258 11.5061 9.35766 11.5061 9.18192 11.6818L7.95012 12.9136ZM1.49994 7.00017C1.2238 7.00017 0.999939 7.22403 0.999939 7.50017C0.999939 7.77631 1.2238 8.00017 1.49994 8.00017L13.4999 8.00017C13.7761 8.00017 13.9999 7.77631 13.9999 7.50017C13.9999 7.22403 13.7761 7.00017 13.4999 7.00017L1.49994 7.00017Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -40,7 +40,8 @@
"cmd-o": "workspace::Open",
"alt-cmd-o": "projects::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"ctrl-`": "terminal_panel::ToggleFocus"
"ctrl-`": "terminal_panel::ToggleFocus",
"shift-escape": "workspace::ToggleZoom"
}
},
{
@ -197,9 +198,17 @@
}
},
{
"context": "AssistantEditor > Editor",
"context": "AssistantPanel",
"bindings": {
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch"
}
},
{
"context": "ConversationEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
@ -234,8 +243,7 @@
"cmd-shift-g": "search::SelectPrevMatch",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
"alt-cmd-r": "search::ToggleRegex",
"shift-escape": "workspace::ToggleZoom"
"alt-cmd-r": "search::ToggleRegex"
}
},
// Bindings from VS Code
@ -411,6 +419,7 @@
"ctrl-shift-k": "editor::DeleteLine",
"cmd-shift-d": "editor::DuplicateLine",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-j": "editor::JoinLines",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",

View File

@ -25,11 +25,15 @@
}
],
"h": "vim::Left",
"left": "vim::Left",
"backspace": "vim::Backspace",
"j": "vim::Down",
"down": "vim::Down",
"enter": "vim::NextLineStart",
"k": "vim::Up",
"up": "vim::Up",
"l": "vim::Right",
"right": "vim::Right",
"$": "vim::EndOfLine",
"shift-g": "vim::EndOfDocument",
"w": "vim::NextWordStart",
@ -90,6 +94,8 @@
}
}
],
"ctrl-o": "pane::GoBack",
"ctrl-]": "editor::GoToDefinition",
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
@ -131,7 +137,7 @@
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
@ -143,6 +149,7 @@
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "editor::JoinLines",
"y": [
"vim::PushOperator",
"Yank"
@ -184,7 +191,6 @@
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"ctrl-o": "pane::GoBack",
"/": [
"buffer_search::Deploy",
{
@ -214,7 +220,8 @@
"r": [
"vim::PushOperator",
"Replace"
]
],
"s": "vim::Substitute"
}
},
{
@ -301,6 +308,7 @@
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
"p": "vim::VisualPaste",
"s": "vim::Substitute",
"r": [
"vim::PushOperator",
"Replace"

View File

@ -57,37 +57,37 @@
"show_whitespaces": "selection",
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never"
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true
// When to show the scrollbar in the editor.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never"
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true
},
"project_panel": {
// Whether to show the git status in the project panel.
"git_status": true,
// Where to dock project panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the project panel.
"default_width": 240
// Whether to show the git status in the project panel.
"git_status": true,
// Where to dock project panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the project panel.
"default_width": 240
},
"assistant": {
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
"default_width": 450,
// Default height when the assistant is docked to the bottom.
"default_height": 320
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
"dock": "right",
// Default width when the assistant is docked to the left or right.
"default_width": 640,
// Default height when the assistant is docked to the bottom.
"default_height": 320
},
// Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true,

View File

@ -326,7 +326,7 @@ impl View for ActivityIndicator {
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
let style = if state.hovered() && on_click.is_some() {
theme.hover.as_ref().unwrap_or(&theme.default)
theme.hovered.as_ref().unwrap_or(&theme.default)
} else {
&theme.default
};

View File

@ -22,9 +22,10 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
anyhow.workspace = true
chrono = "0.4"
chrono = { version = "0.4", features = ["serde"] }
futures.workspace = true
isahc.workspace = true
regex.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
@ -33,3 +34,4 @@ tiktoken-rs = "0.4"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }

View File

@ -1,10 +1,22 @@
pub mod assistant;
mod assistant_settings;
use anyhow::Result;
pub use assistant::AssistantPanel;
use chrono::{DateTime, Local};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use gpui::AppContext;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
use std::{
cmp::Reverse,
fmt::{self, Display},
path::PathBuf,
sync::Arc,
};
use util::paths::CONVERSATIONS_DIR;
// Data types for chat completion requests
#[derive(Debug, Serialize)]
@ -14,6 +26,84 @@ struct OpenAIRequest {
stream: bool,
}
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
sent_at: DateTime<Local>,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
Pending,
Done,
Error(Arc<str>),
}
#[derive(Serialize, Deserialize)]
struct SavedMessage {
id: MessageId,
start: usize,
}
#[derive(Serialize, Deserialize)]
struct SavedConversation {
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
model: String,
}
impl SavedConversation {
const VERSION: &'static str = "0.1.0";
}
struct SavedConversationMetadata {
title: String,
path: PathBuf,
mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RequestMessage {
role: Role,

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@ impl View for UpdateNotification {
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -74,7 +74,7 @@ impl View for UpdateNotification {
),
)
.with_child({
let style = theme.action_message.style_for(state, false);
let style = theme.action_message.style_for(state);
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -83,7 +83,7 @@ impl View for Breadcrumbs {
}
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
let style = style.style_for(state);
crumbs.with_style(style.container)
})
.on_click(MouseButton::Left, |_, this, cx| {

View File

@ -299,7 +299,12 @@ impl CollabTitlebarItem {
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
let theme = theme::current(cx).clone();
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
let item_style = theme.context_menu.item.disabled_style().clone();
let item_style = theme
.context_menu
.item
.inactive_state()
.disabled_style()
.clone();
self.user_menu.update(cx, |user_menu, cx| {
let items = if let Some(user) = self.user_store.read(cx).current_user() {
vec![
@ -361,8 +366,20 @@ impl CollabTitlebarItem {
.contained()
.with_style(titlebar.toggle_contacts_badge)
.contained()
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
.with_margin_left(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.with_margin_top(
titlebar
.toggle_contacts_button
.inactive_state()
.default
.icon_width,
)
.aligned(),
)
};
@ -372,7 +389,8 @@ impl CollabTitlebarItem {
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
let style = titlebar
.toggle_contacts_button
.style_for(state, self.contacts_popover.is_some());
.in_state(self.contacts_popover.is_some())
.style_for(state);
Svg::new("icons/user_plus_16.svg")
.with_color(style.color)
.constrained()
@ -419,7 +437,7 @@ impl CollabTitlebarItem {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
let style = titlebar.call_control.style_for(state);
Svg::new(icon)
.with_color(style.color)
.constrained()
@ -473,7 +491,7 @@ impl CollabTitlebarItem {
.with_child(
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
//TODO: Ensure this button has consistent width for both text variations
let style = titlebar.share_button.style_for(state, false);
let style = titlebar.share_button.inactive_state().style_for(state);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@ -511,7 +529,7 @@ impl CollabTitlebarItem {
Stack::new()
.with_child(
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
let style = titlebar.call_control.style_for(state, false);
let style = titlebar.call_control.style_for(state);
Svg::new("icons/ellipsis_14.svg")
.with_color(style.color)
.constrained()
@ -549,7 +567,7 @@ impl CollabTitlebarItem {
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let titlebar = &theme.workspace.titlebar;
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
let style = titlebar.sign_in_prompt.style_for(state, false);
let style = titlebar.sign_in_prompt.inactive_state().style_for(state);
Label::new("Sign In", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate {
.contact_finder
.picker
.item
.style_for(mouse_state, selected);
.in_state(selected)
.style_for(mouse_state);
Flex::row()
.with_children(user.avatar.clone().map(|avatar| {
Image::from_data(avatar)

View File

@ -774,7 +774,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
.into_any()
}
@ -797,7 +798,7 @@ impl ContactList {
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.project_row.default;
let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
@ -810,8 +811,11 @@ impl ContactList {
};
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row()
.with_child(
@ -893,7 +897,7 @@ impl ContactList {
.width
.or(theme.contact_avatar.height)
.unwrap_or(0.);
let row = &theme.project_row.default;
let row = &theme.project_row.inactive_state().default;
let tree_branch = theme.tree_branch;
let line_height = row.name.text.line_height(font_cache);
let cap_height = row.name.text.cap_height(font_cache);
@ -904,8 +908,11 @@ impl ContactList {
peer_id.as_u64() as usize,
cx,
|mouse_state, _| {
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
let row = theme.project_row.style_for(mouse_state, is_selected);
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
let row = theme
.project_row
.in_state(is_selected)
.style_for(mouse_state);
Flex::row()
.with_child(
@ -989,7 +996,8 @@ impl ContactList {
let header_style = theme
.header_row
.style_for(&mut Default::default(), is_selected);
.in_state(is_selected)
.style_for(&mut Default::default());
let text = match section {
Section::ActiveCall => "Collaborators",
Section::Requests => "Contact Requests",
@ -999,7 +1007,7 @@ impl ContactList {
let leave_call = if section == Section::ActiveCall {
Some(
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
let style = theme.leave_call.style_for(state, false);
let style = theme.leave_call.style_for(state);
Label::new("Leave Call", style.text.clone())
.contained()
.with_style(style.container)
@ -1110,8 +1118,7 @@ impl ContactList {
contact.user.id as usize,
cx,
|mouse_state, _| {
let button_style =
theme.contact_button.style_for(mouse_state, false);
let button_style = theme.contact_button.style_for(mouse_state);
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
.flex_float()
@ -1146,7 +1153,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
})
.on_click(MouseButton::Left, move |_, this, cx| {
@ -1204,7 +1212,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
})
@ -1227,7 +1235,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/check_8.svg")
.aligned()
@ -1250,7 +1258,7 @@ impl ContactList {
let button_style = if is_contact_request_pending {
&theme.disabled_button
} else {
theme.contact_button.style_for(mouse_state, false)
theme.contact_button.style_for(mouse_state)
};
render_icon_button(button_style, "icons/x_mark_8.svg")
.aligned()
@ -1277,7 +1285,8 @@ impl ContactList {
.with_style(
*theme
.contact_row
.style_for(&mut Default::default(), is_selected),
.in_state(is_selected)
.style_for(&mut Default::default()),
)
.into_any()
}

View File

@ -53,7 +53,7 @@ where
)
.with_child(
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -93,7 +93,7 @@ where
.with_children(buttons.into_iter().enumerate().map(
|(ix, (message, handler))| {
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
let button = theme.button.style_for(state, false);
let button = theme.button.style_for(state);
Label::new(message, button.text.clone())
.contained()
.with_style(button.container)

View File

@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate {
let mat = &self.matches[ix];
let command = &self.actions[mat.candidate_id];
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let key_style = &theme.command_palette.key.in_state(selected);
let keystroke_spacing = theme.command_palette.keystroke_spacing;
Flex::row()

View File

@ -328,10 +328,8 @@ impl ContextMenu {
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { label, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(&mut Default::default());
match label {
ContextMenuItemLabel::String(label) => {
@ -363,10 +361,8 @@ impl ContextMenu {
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
match item {
ContextMenuItem::Item { action, .. } => {
let style = style.item.style_for(
&mut Default::default(),
Some(ix) == self.selected_index,
);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(&mut Default::default());
match action {
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
@ -412,8 +408,8 @@ impl ContextMenu {
let action = action.clone();
let view_id = self.parent_view_id;
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
let style =
style.item.style_for(state, Some(ix) == self.selected_index);
let style = style.item.in_state(self.selected_index == Some(ix));
let style = style.style_for(state);
let keystroke = match &action {
ContextMenuItemAction::Action(action) => Some(
KeystrokeLabel::new(

View File

@ -127,16 +127,16 @@ impl CopilotCodeVerification {
.with_child(
Label::new(
if copied { "Copied!" } else { "Copy" },
device_code_style.cta.style_for(state, false).text.clone(),
device_code_style.cta.style_for(state).text.clone(),
)
.aligned()
.contained()
.with_style(*device_code_style.right_container.style_for(state, false))
.with_style(*device_code_style.right_container.style_for(state))
.constrained()
.with_width(device_code_style.right),
)
.contained()
.with_style(device_code_style.cta.style_for(state, false).container)
.with_style(device_code_style.cta.style_for(state).container)
})
.on_click(gpui::platform::MouseButton::Left, {
let user_code = data.user_code.clone();

View File

@ -71,7 +71,8 @@ impl View for CopilotButton {
.status_bar
.panel_buttons
.button
.style_for(state, active);
.in_state(active)
.style_for(state);
Flex::row()
.with_child(
@ -255,7 +256,7 @@ impl CopilotButton {
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
Flex::row()
.with_child(Label::new("Copilot Settings", style.label.clone()))
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
.with_child(theme::ui::icon(icon_style.style_for(state)))
.align_children_center()
.into_any()
},

View File

@ -100,7 +100,7 @@ impl View for DiagnosticIndicator {
.workspace
.status_bar
.diagnostic_summary
.style_for(state, false);
.style_for(state);
let mut summary_row = Flex::row();
if self.summary.error_count > 0 {
@ -198,7 +198,7 @@ impl View for DiagnosticIndicator {
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
Label::new(
diagnostic.message.split('\n').next().unwrap().to_string(),
message_style.style_for(state, false).text.clone(),
message_style.style_for(state).text.clone(),
)
.aligned()
.contained()

View File

@ -206,6 +206,7 @@ actions!(
DuplicateLine,
MoveLineUp,
MoveLineDown,
JoinLines,
Transpose,
Cut,
Copy,
@ -321,6 +322,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::indent);
cx.add_action(Editor::outdent);
cx.add_action(Editor::delete_line);
cx.add_action(Editor::join_lines);
cx.add_action(Editor::delete_to_previous_word_start);
cx.add_action(Editor::delete_to_previous_subword_start);
cx.add_action(Editor::delete_to_next_word_end);
@ -3320,15 +3322,21 @@ impl Editor {
pub fn render_code_actions_indicator(
&self,
style: &EditorStyle,
active: bool,
is_active: bool,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
if self.available_code_actions.is_some() {
enum CodeActions {}
Some(
MouseEventHandler::<CodeActions, _>::new(0, cx, |state, _| {
Svg::new("icons/bolt_8.svg")
.with_color(style.code_actions.indicator.style_for(state, active).color)
Svg::new("icons/bolt_8.svg").with_color(
style
.code_actions
.indicator
.in_state(is_active)
.style_for(state)
.color,
)
})
.with_cursor_style(CursorStyle::PointingHand)
.with_padding(Padding::uniform(3.))
@ -3378,10 +3386,8 @@ impl Editor {
.with_color(
style
.indicator
.style_for(
mouse_state,
fold_status == FoldStatus::Folded,
)
.in_state(fold_status == FoldStatus::Folded)
.style_for(mouse_state)
.color,
)
.constrained()
@ -3952,6 +3958,60 @@ impl Editor {
});
}
pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
let mut row_ranges = Vec::<Range<u32>>::new();
for selection in self.selections.all::<Point>(cx) {
let start = selection.start.row;
let end = if selection.start.row == selection.end.row {
selection.start.row + 1
} else {
selection.end.row
};
if let Some(last_row_range) = row_ranges.last_mut() {
if start <= last_row_range.end {
last_row_range.end = end;
continue;
}
}
row_ranges.push(start..end);
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut cursor_positions = Vec::new();
for row_range in &row_ranges {
let anchor = snapshot.anchor_before(Point::new(
row_range.end - 1,
snapshot.line_len(row_range.end - 1),
));
cursor_positions.push(anchor.clone()..anchor);
}
self.transact(cx, |this, cx| {
for row_range in row_ranges.into_iter().rev() {
for row in row_range.rev() {
let end_of_line = Point::new(row, snapshot.line_len(row));
let indent = snapshot.indent_size_for_line(row + 1);
let start_of_next_line = Point::new(row + 1, indent.len);
let replace = if snapshot.line_len(row + 1) > indent.len {
" "
} else {
""
};
this.buffer.update(cx, |buffer, cx| {
buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
});
}
}
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(cursor_positions)
});
});
}
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;

View File

@ -1,7 +1,10 @@
use super::*;
use crate::test::{
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext, select_ranges,
use crate::{
test::{
assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
editor_test_context::EditorTestContext, select_ranges,
},
JoinLines,
};
use drag_and_drop::DragAndDrop;
use futures::StreamExt;
@ -2325,6 +2328,137 @@ fn test_delete_line(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
let mut editor = build_editor(buffer.clone(), cx);
let buffer = buffer.read(cx).as_singleton().unwrap();
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 0)..Point::new(0, 0)]
);
// When on single line, replace newline at end by space
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 3)..Point::new(0, 3)]
);
// When multiple lines are selected, remove newlines that are spanned by the selection
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 11)..Point::new(0, 11)]
);
// Undo should be transactional
editor.undo(&Undo, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
&[Point::new(0, 5)..Point::new(2, 2)]
);
// When joining an empty line don't insert a space
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// We can remove trailing newlines
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// We don't blow up on the last line
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[Point::new(2, 3)..Point::new(2, 3)]
);
// reset to test indentation
editor.buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(1, 0)..Point::new(1, 2), " "),
(Point::new(2, 0)..Point::new(2, 3), " \n\td"),
],
None,
cx,
)
});
// We remove any leading spaces
assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
// We don't insert a space for a line containing only spaces
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
// We ignore any leading tabs
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
editor
});
}
#[gpui::test]
fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
let mut editor = build_editor(buffer.clone(), cx);
let buffer = buffer.read(cx).as_singleton().unwrap();
editor.change_selections(None, cx, |s| {
s.select_ranges([
Point::new(0, 2)..Point::new(1, 1),
Point::new(1, 2)..Point::new(1, 2),
Point::new(3, 1)..Point::new(3, 2),
])
});
editor.join_lines(&JoinLines, cx);
assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
assert_eq!(
editor.selections.ranges::<Point>(cx),
[
Point::new(0, 7)..Point::new(0, 7),
Point::new(1, 3)..Point::new(1, 3)
]
);
editor
});
}
#[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@ -1529,7 +1529,7 @@ impl EditorElement {
enum JumpIcon {}
MouseEventHandler::<JumpIcon, _>::new((*id).into(), cx, |state, _| {
let style = style.jump_icon.style_for(state, false);
let style = style.jump_icon.style_for(state);
Svg::new("icons/arrow_up_right_8.svg")
.with_color(style.color)
.constrained()
@ -2094,7 +2094,7 @@ impl Element<Editor> for EditorElement {
.folds
.ellipses
.background
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
.color;
(id, fold, color)

View File

@ -41,7 +41,8 @@ impl View for DeployFeedbackButton {
.status_bar
.panel_buttons
.button
.style_for(state, active);
.in_state(active)
.style_for(state);
Svg::new("icons/feedback_16.svg")
.with_color(style.icon_color)

View File

@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton {
let theme = theme::current(cx).clone();
enum SubmitFeedbackButton {}
MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
let style = theme.feedback.submit_button.style_for(state, false);
let style = theme.feedback.submit_button.style_for(state);
Label::new("Submit as Markdown", style.text.clone())
.contained()
.with_style(style.container)

View File

@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate {
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match, cx, ix);
Flex::column()

View File

@ -6,15 +6,16 @@ use std::{
use crate::json::ToJson;
use pathfinder_color::{ColorF, ColorU};
use schemars::JsonSchema;
use serde::{
de::{self, Unexpected},
Deserialize, Deserializer,
};
use serde_json::json;
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)]
#[repr(transparent)]
pub struct Color(ColorU);
pub struct Color(#[schemars(with = "String")] ColorU);
impl Color {
pub fn transparent_black() -> Self {

View File

@ -41,13 +41,7 @@ use collections::HashMap;
use core::panic;
use json::ToJson;
use smallvec::SmallVec;
use std::{
any::Any,
borrow::Cow,
marker::PhantomData,
mem,
ops::{Deref, DerefMut, Range},
};
use std::{any::Any, borrow::Cow, mem, ops::Range};
pub trait Element<V: View>: 'static {
type LayoutState;
@ -567,90 +561,6 @@ impl<V: View> RootElement<V> {
}
}
pub trait Component<V: View>: 'static {
fn render(&self, view: &mut V, cx: &mut ViewContext<V>) -> AnyElement<V>;
}
pub struct ComponentHost<V: View, C: Component<V>> {
component: C,
view_type: PhantomData<V>,
}
impl<V: View, C: Component<V>> ComponentHost<V, C> {
pub fn new(c: C) -> Self {
Self {
component: c,
view_type: PhantomData,
}
}
}
impl<V: View, C: Component<V>> Deref for ComponentHost<V, C> {
type Target = C;
fn deref(&self) -> &Self::Target {
&self.component
}
}
impl<V: View, C: Component<V>> DerefMut for ComponentHost<V, C> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.component
}
}
impl<V: View, C: Component<V>> Element<V> for ComponentHost<V, C> {
type LayoutState = AnyElement<V>;
type PaintState = ();
fn layout(
&mut self,
constraint: SizeConstraint,
view: &mut V,
cx: &mut LayoutContext<V>,
) -> (Vector2F, AnyElement<V>) {
let mut element = self.component.render(view, cx);
let size = element.layout(constraint, view, cx);
(size, element)
}
fn paint(
&mut self,
scene: &mut SceneBuilder,
bounds: RectF,
visible_bounds: RectF,
element: &mut AnyElement<V>,
view: &mut V,
cx: &mut ViewContext<V>,
) {
element.paint(scene, bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: Range<usize>,
_: RectF,
_: RectF,
element: &AnyElement<V>,
_: &(),
view: &V,
cx: &ViewContext<V>,
) -> Option<RectF> {
element.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: RectF,
element: &AnyElement<V>,
_: &(),
view: &V,
cx: &ViewContext<V>,
) -> serde_json::Value {
element.debug(view, cx)
}
}
pub trait AnyRootElement {
fn layout(
&mut self,

View File

@ -12,10 +12,11 @@ use crate::{
scene::{self, Border, CursorRegion, Quad},
AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct ContainerStyle {
#[serde(default)]
pub margin: Margin,
@ -332,7 +333,7 @@ impl ToJson for ContainerStyle {
}
}
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Margin {
pub top: f32,
pub left: f32,
@ -359,7 +360,7 @@ impl ToJson for Margin {
}
}
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, JsonSchema)]
pub struct Padding {
pub top: f32,
pub left: f32,
@ -486,9 +487,10 @@ impl ToJson for Padding {
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct Shadow {
#[serde(default, deserialize_with = "deserialize_vec2f")]
#[schemars(with = "Vec::<f32>")]
offset: Vector2F,
#[serde(default)]
blur: f32,

View File

@ -8,6 +8,7 @@ use crate::{
scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View,
ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{ops::Range, sync::Arc};
@ -21,7 +22,7 @@ pub struct Image {
style: ImageStyle,
}
#[derive(Copy, Clone, Default, Deserialize)]
#[derive(Copy, Clone, Default, Deserialize, JsonSchema)]
pub struct ImageStyle {
#[serde(default)]
pub border: Border,

View File

@ -10,6 +10,7 @@ use crate::{
text_layout::{Line, RunStyle},
Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
use smallvec::{smallvec, SmallVec};
@ -20,7 +21,7 @@ pub struct Label {
highlight_indices: Vec<usize>,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
@ -164,6 +165,7 @@ impl<V: View> Element<V> for Label {
_: &mut V,
cx: &mut ViewContext<V>,
) -> Self::PaintState {
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
line.paint(
scene,
bounds.origin(),

View File

@ -1,7 +1,5 @@
use std::{borrow::Cow, ops::Range};
use serde_json::json;
use super::constrain_size_preserving_aspect_ratio;
use crate::json::ToJson;
use crate::{
color::Color,
geometry::{
@ -10,6 +8,10 @@ use crate::{
},
scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext,
};
use schemars::JsonSchema;
use serde_derive::Deserialize;
use serde_json::json;
use std::{borrow::Cow, ops::Range};
pub struct Svg {
path: Cow<'static, str>,
@ -24,6 +26,14 @@ impl Svg {
}
}
pub fn for_style<V: View>(style: SvgStyle) -> impl Element<V> {
Self::new(style.asset)
.with_color(style.color)
.constrained()
.with_width(style.dimensions.width)
.with_height(style.dimensions.height)
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
@ -105,9 +115,24 @@ impl<V: View> Element<V> for Svg {
}
}
use crate::json::ToJson;
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct SvgStyle {
pub color: Color,
pub asset: String,
pub dimensions: Dimensions,
}
use super::constrain_size_preserving_aspect_ratio;
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Dimensions {
pub width: f32,
pub height: f32,
}
impl Dimensions {
pub fn to_vec(&self) -> Vector2F {
vec2f(self.width, self.height)
}
}
fn from_usvg_rect(rect: usvg::Rect) -> RectF {
RectF::new(

View File

@ -9,6 +9,7 @@ use crate::{
Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View,
ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{
cell::{Cell, RefCell},
@ -33,7 +34,7 @@ struct TooltipState {
debounce: RefCell<Option<Task<()>>>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TooltipStyle {
#[serde(flatten)]
pub container: ContainerStyle,
@ -42,7 +43,7 @@ pub struct TooltipStyle {
pub max_text_width: Option<f32>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct KeystrokeStyle {
#[serde(flatten)]
container: ContainerStyle,

View File

@ -7,13 +7,14 @@ use crate::{
use anyhow::{anyhow, Result};
use ordered_float::OrderedFloat;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use schemars::JsonSchema;
use std::{
collections::HashMap,
ops::{Deref, DerefMut},
sync::Arc,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FamilyId(usize);
struct Family {

View File

@ -16,7 +16,7 @@ use serde::{de, Deserialize, Serialize};
use serde_json::Value;
use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)]
pub struct FontId(pub usize);
pub type GlyphId = u32;
@ -59,20 +59,44 @@ pub struct Features {
pub zero: Option<bool>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, JsonSchema)]
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
pub font_family_id: FamilyId,
pub font_id: FontId,
pub font_size: f32,
#[schemars(with = "PropertiesDef")]
pub font_properties: Properties,
pub underline: Underline,
}
#[derive(Copy, Clone, Debug, Default, PartialEq)]
#[derive(JsonSchema)]
#[serde(remote = "Properties")]
pub struct PropertiesDef {
/// The font style, as defined in CSS.
pub style: StyleDef,
/// The font weight, as defined in CSS.
pub weight: f32,
/// The font stretchiness, as defined in CSS.
pub stretch: f32,
}
#[derive(JsonSchema)]
#[schemars(remote = "Style")]
pub enum StyleDef {
/// A face that is neither italic not obliqued.
Normal,
/// A form that is generally cursive in nature.
Italic,
/// A typically-sloped version of the regular face.
Oblique,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)]
pub struct HighlightStyle {
pub color: Option<Color>,
#[schemars(with = "Option::<f32>")]
pub weight: Option<Weight>,
pub italic: Option<bool>,
pub underline: Option<Underline>,
@ -81,9 +105,10 @@ pub struct HighlightStyle {
impl Eq for HighlightStyle {}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)]
pub struct Underline {
pub color: Option<Color>,
#[schemars(with = "f32")]
pub thickness: OrderedFloat<f32>,
pub squiggly: bool,
}

View File

@ -26,7 +26,7 @@ pub mod color;
pub mod json;
pub mod keymap_matcher;
pub mod platform;
pub use gpui_macros::test;
pub use gpui_macros::{test, Element};
pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
pub use anyhow;

View File

@ -25,6 +25,7 @@ use anyhow::{anyhow, bail, Result};
use async_task::Runnable;
pub use event::*;
use postage::oneshot;
use schemars::JsonSchema;
use serde::Deserialize;
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
@ -282,7 +283,7 @@ pub enum PromptLevel {
Critical,
}
#[derive(Copy, Clone, Debug, Deserialize)]
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)]
pub enum CursorStyle {
Arrow,
ResizeLeftRight,

View File

@ -3,6 +3,7 @@ mod mouse_region;
#[cfg(debug_assertions)]
use collections::HashSet;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
use std::{borrow::Cow, sync::Arc};
@ -99,7 +100,7 @@ pub struct Icon {
pub color: Color,
}
#[derive(Clone, Copy, Default, Debug)]
#[derive(Clone, Copy, Default, Debug, JsonSchema)]
pub struct Border {
pub width: f32,
pub color: Color,

View File

@ -3,8 +3,8 @@ use proc_macro2::Ident;
use quote::{format_ident, quote};
use std::mem;
use syn::{
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta,
NestedMeta, Type,
parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg,
ItemFn, Lit, Meta, NestedMeta, Type,
};
#[proc_macro_attribute]
@ -275,3 +275,68 @@ fn parse_bool(literal: &Lit) -> Result<bool, TokenStream> {
result.map_err(|err| TokenStream::from(err.into_compile_error()))
}
#[proc_macro_derive(Element)]
pub fn element_derive(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
// The name of the struct/enum
let name = input.ident;
let expanded = quote! {
impl<V: gpui::View> gpui::elements::Element<V> for #name {
type LayoutState = gpui::elements::AnyElement<V>;
type PaintState = ();
fn layout(
&mut self,
constraint: gpui::SizeConstraint,
view: &mut V,
cx: &mut gpui::LayoutContext<V>,
) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement<V>) {
let mut element = self.render(view, cx);
let size = element.layout(constraint, view, cx);
(size, element)
}
fn paint(
&mut self,
scene: &mut gpui::SceneBuilder,
bounds: gpui::geometry::rect::RectF,
visible_bounds: gpui::geometry::rect::RectF,
element: &mut gpui::elements::AnyElement<V>,
view: &mut V,
cx: &mut gpui::ViewContext<V>,
) {
element.paint(scene, bounds.origin(), visible_bounds, view, cx);
}
fn rect_for_text_range(
&self,
range_utf16: std::ops::Range<usize>,
_: gpui::geometry::rect::RectF,
_: gpui::geometry::rect::RectF,
element: &gpui::elements::AnyElement<V>,
_: &(),
view: &V,
cx: &gpui::ViewContext<V>,
) -> Option<gpui::geometry::rect::RectF> {
element.rect_for_text_range(range_utf16, view, cx)
}
fn debug(
&self,
_: gpui::geometry::rect::RectF,
element: &gpui::elements::AnyElement<V>,
_: &(),
view: &V,
cx: &gpui::ViewContext<V>,
) -> serde_json::Value {
element.debug(view, cx)
}
}
};
// Return generated code
TokenStream::from(expanded)
}

View File

@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage {
MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar;
let style = theme.active_language.style_for(state, false);
let style = theme.active_language.style_for(state);
Label::new(active_language_text, style.text.clone())
.contained()
.with_style(style.container)

View File

@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let mat = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
let mut label = mat.string.clone();
if buffer_language_name.as_deref() == Some(mat.string.as_str()) {

View File

@ -681,7 +681,7 @@ impl LspLogToolbarItemView {
)
})
.unwrap_or_else(|| "No server selected".into());
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@ -722,7 +722,8 @@ impl LspLogToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, logs_selected);
.in_state(logs_selected)
.style_for(state);
Label::new(SERVER_LOGS, style.text.clone())
.contained()
.with_style(style.container)
@ -739,7 +740,8 @@ impl LspLogToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, rpc_trace_selected);
.in_state(rpc_trace_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(RPC_MESSAGES, style.text.clone())

View File

@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView {
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Flex::row()
.with_child(
Label::new(active_layer.language.name().to_string(), style.text.clone())
@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView {
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, is_selected);
.in_state(is_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(layer.language.name().to_string(), style.text.clone())

View File

@ -33,7 +33,7 @@ const JSON_RPC_VERSION: &str = "2.0";
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
pub struct LanguageServer {
@ -302,9 +302,9 @@ impl LanguageServer {
if let Some(error) = error {
handler(Err(error));
} else if let Some(result) = result {
handler(Ok(result.get()));
handler(Ok(result.get().into()));
} else {
handler(Ok("null"));
handler(Ok("null".into()));
}
}
} else {
@ -457,11 +457,13 @@ impl LanguageServer {
let response_handlers = self.response_handlers.clone();
let next_id = AtomicUsize::new(self.next_id.load(SeqCst));
let outbound_tx = self.outbound_tx.clone();
let executor = self.executor.clone();
let mut output_done = self.output_done_rx.lock().take().unwrap();
let shutdown_request = Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
&outbound_tx,
&executor,
(),
);
let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, ());
@ -658,6 +660,7 @@ impl LanguageServer {
&self.next_id,
&self.response_handlers,
&self.outbound_tx,
&self.executor,
params,
)
}
@ -666,6 +669,7 @@ impl LanguageServer {
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
outbound_tx: &channel::Sender<String>,
executor: &Arc<executor::Background>,
params: T::Params,
) -> impl 'static + Future<Output = Result<T::Result>>
where
@ -686,15 +690,20 @@ impl LanguageServer {
.as_mut()
.ok_or_else(|| anyhow!("server shut down"))
.map(|handlers| {
let executor = executor.clone();
handlers.insert(
id,
Box::new(move |result| {
let response = match result {
Ok(response) => serde_json::from_str(response)
.context("failed to deserialize response"),
Err(error) => Err(anyhow!("{}", error.message)),
};
let _ = tx.send(response);
executor
.spawn(async move {
let response = match result {
Ok(response) => serde_json::from_str(&response)
.context("failed to deserialize response"),
Err(error) => Err(anyhow!("{}", error.message)),
};
let _ = tx.send(response);
})
.detach();
}),
);
});

View File

@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate {
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];
let outline_item = &self.outline.items[string_match.candidate_id];

View File

@ -153,6 +153,7 @@ pub fn init(cx: &mut AppContext) {
);
}
#[derive(Debug)]
pub enum Event {
OpenedEntry {
entry_id: ProjectEntryId,
@ -1253,7 +1254,10 @@ impl ProjectPanel {
let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::<Self, _>::new(entry_id.to_usize(), cx, |state, cx| {
let mut style = entry_style.style_for(state, details.is_selected).clone();
let mut style = entry_style
.in_state(details.is_selected)
.style_for(state)
.clone();
if cx
.global::<DragAndDrop<Workspace>>()
@ -1264,7 +1268,7 @@ impl ProjectPanel {
.filter(|destination| details.path.starts_with(destination))
.is_some()
{
style = entry_style.active.clone().unwrap();
style = entry_style.active_state().default.clone();
}
let row_container_style = if show_editor {
@ -1405,9 +1409,11 @@ impl View for ProjectPanel {
let button_style = theme.open_project_button.clone();
let context_menu_item_style = theme::current(cx).context_menu.item.clone();
move |state, cx| {
let button_style = button_style.style_for(state, false).clone();
let context_menu_item =
context_menu_item_style.style_for(state, true).clone();
let button_style = button_style.style_for(state).clone();
let context_menu_item = context_menu_item_style
.active_state()
.style_for(state)
.clone();
theme::ui::keystroke_label(
"Open a project",

View File

@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = &theme.picker.item;
let current_style = style.style_for(mouse_state, selected);
let current_style = style.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];
let symbol = &self.symbols[string_match.candidate_id];
@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
.with_child(
// Avoid styling the path differently when it is selected, since
// the symbol's syntax highlighting doesn't change when selected.
Label::new(path.to_string(), style.default.label.clone()),
Label::new(
path.to_string(),
style.inactive_state().default.label.clone(),
),
)
.contained()
.with_style(current_style.container)

View File

@ -173,7 +173,7 @@ impl PickerDelegate for RecentProjectsDelegate {
cx: &gpui::AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let string_match = &self.matches[ix];

View File

@ -259,7 +259,11 @@ impl BufferSearchBar {
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
pub fn is_dismissed(&self) -> bool {
self.dismissed
}
pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.dismissed = true;
for searchable_item in self.seachable_items_with_matches.keys() {
if let Some(searchable_item) =
@ -275,7 +279,7 @@ impl BufferSearchBar {
cx.notify();
}
fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
} else {
@ -328,7 +332,11 @@ impl BufferSearchBar {
Some(
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -371,7 +379,7 @@ impl BufferSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false);
let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -403,7 +411,7 @@ impl BufferSearchBar {
enum CloseButton {}
MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -480,7 +488,7 @@ impl BufferSearchBar {
self.select_match(Direction::Prev, cx);
}
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self

View File

@ -896,7 +896,7 @@ impl ProjectSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, false);
let style = theme.search.option_button.inactive_state().style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)
@ -927,7 +927,11 @@ impl ProjectSearchBar {
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme.search.option_button.style_for(state, is_active);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Label::new(icon, style.text.clone())
.contained()
.with_style(style.container)

View File

@ -25,6 +25,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalPanel::new_terminal);
}
#[derive(Debug)]
pub enum Event {
Close,
DockPositionChanged,

View File

@ -4,15 +4,16 @@ pub mod ui;
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
fonts::{HighlightStyle, TextStyle},
platform, AppContext, AssetSource, Border, MouseState,
};
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use settings::SettingsStore;
use std::{collections::HashMap, sync::Arc};
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle};
pub use theme_registry::*;
pub use theme_settings::*;
@ -36,7 +37,7 @@ pub fn init(source: impl AssetSource, cx: &mut AppContext) {
.detach();
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct Theme {
#[serde(default)]
pub meta: ThemeMeta,
@ -67,7 +68,7 @@ pub struct Theme {
pub color_scheme: ColorScheme,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct ThemeMeta {
#[serde(skip_deserializing)]
pub id: usize,
@ -75,7 +76,7 @@ pub struct ThemeMeta {
pub is_light: bool,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct Workspace {
pub background: Color,
pub blank_pane: BlankPaneStyle,
@ -102,7 +103,7 @@ pub struct Workspace {
pub drop_target_overlay_color: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct BlankPaneStyle {
pub logo: SvgStyle,
pub logo_shadow: SvgStyle,
@ -112,7 +113,7 @@ pub struct BlankPaneStyle {
pub keyboard_hint_width: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Titlebar {
#[serde(flatten)]
pub container: ContainerStyle,
@ -128,16 +129,16 @@ pub struct Titlebar {
pub leader_avatar: AvatarStyle,
pub follower_avatar: AvatarStyle,
pub inactive_avatar_grayscale: bool,
pub sign_in_prompt: Interactive<ContainedText>,
pub sign_in_prompt: Toggleable<Interactive<ContainedText>>,
pub outdated_warning: ContainedText,
pub share_button: Interactive<ContainedText>,
pub share_button: Toggleable<Interactive<ContainedText>>,
pub call_control: Interactive<IconButton>,
pub toggle_contacts_button: Interactive<IconButton>,
pub user_menu_button: Interactive<IconButton>,
pub toggle_contacts_button: Toggleable<Interactive<IconButton>>,
pub user_menu_button: Toggleable<Interactive<IconButton>>,
pub toggle_contacts_badge: ContainerStyle,
}
#[derive(Copy, Clone, Deserialize, Default)]
#[derive(Copy, Clone, Deserialize, Default, JsonSchema)]
pub struct AvatarStyle {
#[serde(flatten)]
pub image: ImageStyle,
@ -145,14 +146,14 @@ pub struct AvatarStyle {
pub outer_corner_radius: f32,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct Copilot {
pub out_link_icon: Interactive<IconStyle>,
pub modal: ModalStyle,
pub auth: CopilotAuth,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuth {
pub content_width: f32,
pub prompting: CopilotAuthPrompting,
@ -162,14 +163,14 @@ pub struct CopilotAuth {
pub header: IconStyle,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthPrompting {
pub subheading: ContainedText,
pub hint: ContainedText,
pub device_code: DeviceCode,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct DeviceCode {
pub text: TextStyle,
pub cta: ButtonStyle,
@ -179,19 +180,19 @@ pub struct DeviceCode {
pub right_container: Interactive<ContainerStyle>,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthNotAuthorized {
pub subheading: ContainedText,
pub warning: ContainedText,
}
#[derive(Deserialize, Default, Clone)]
#[derive(Deserialize, Default, Clone, JsonSchema)]
pub struct CopilotAuthAuthorized {
pub subheading: ContainedText,
pub hint: ContainedText,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ContactsPopover {
#[serde(flatten)]
pub container: ContainerStyle,
@ -199,17 +200,17 @@ pub struct ContactsPopover {
pub width: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ContactList {
pub user_query_editor: FieldEditor,
pub user_query_editor_height: f32,
pub add_contact_button: IconButton,
pub header_row: Interactive<ContainedText>,
pub header_row: Toggleable<Interactive<ContainedText>>,
pub leave_call: Interactive<ContainedText>,
pub contact_row: Interactive<ContainerStyle>,
pub contact_row: Toggleable<Interactive<ContainerStyle>>,
pub row_height: f32,
pub project_row: Interactive<ProjectRow>,
pub tree_branch: Interactive<TreeBranch>,
pub project_row: Toggleable<Interactive<ProjectRow>>,
pub tree_branch: Toggleable<Interactive<TreeBranch>>,
pub contact_avatar: ImageStyle,
pub contact_status_free: ContainerStyle,
pub contact_status_busy: ContainerStyle,
@ -221,7 +222,7 @@ pub struct ContactList {
pub calling_indicator: ContainedText,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectRow {
#[serde(flatten)]
pub container: ContainerStyle,
@ -229,13 +230,13 @@ pub struct ProjectRow {
pub name: ContainedText,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[derive(Deserialize, Default, Clone, Copy, JsonSchema)]
pub struct TreeBranch {
pub width: f32,
pub color: Color,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ContactFinder {
pub picker: Picker,
pub row_height: f32,
@ -245,17 +246,17 @@ pub struct ContactFinder {
pub disabled_contact_button: IconButton,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct DropdownMenu {
#[serde(flatten)]
pub container: ContainerStyle,
pub header: Interactive<DropdownMenuItem>,
pub section_header: ContainedText,
pub item: Interactive<DropdownMenuItem>,
pub item: Toggleable<Interactive<DropdownMenuItem>>,
pub row_height: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct DropdownMenuItem {
#[serde(flatten)]
pub container: ContainerStyle,
@ -266,11 +267,11 @@ pub struct DropdownMenuItem {
pub secondary_text_spacing: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TabBar {
#[serde(flatten)]
pub container: ContainerStyle,
pub pane_button: Interactive<IconButton>,
pub pane_button: Toggleable<Interactive<IconButton>>,
pub pane_button_container: ContainerStyle,
pub active_pane: TabStyles,
pub inactive_pane: TabStyles,
@ -294,13 +295,13 @@ impl TabBar {
}
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TabStyles {
pub active_tab: Tab,
pub inactive_tab: Tab,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AvatarRibbon {
#[serde(flatten)]
pub container: ContainerStyle,
@ -308,7 +309,7 @@ pub struct AvatarRibbon {
pub height: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct OfflineIcon {
#[serde(flatten)]
pub container: ContainerStyle,
@ -316,7 +317,7 @@ pub struct OfflineIcon {
pub color: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Tab {
pub height: f32,
#[serde(flatten)]
@ -333,7 +334,7 @@ pub struct Tab {
pub icon_conflict: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Toolbar {
#[serde(flatten)]
pub container: ContainerStyle,
@ -342,14 +343,14 @@ pub struct Toolbar {
pub nav_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Notifications {
#[serde(flatten)]
pub container: ContainerStyle,
pub width: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Search {
#[serde(flatten)]
pub container: ContainerStyle,
@ -359,14 +360,14 @@ pub struct Search {
pub include_exclude_editor: FindEditor,
pub invalid_include_exclude_editor: ContainerStyle,
pub include_exclude_inputs: ContainedText,
pub option_button: Interactive<ContainedText>,
pub option_button: Toggleable<Interactive<ContainedText>>,
pub match_background: Color,
pub match_index: ContainedText,
pub results_status: TextStyle,
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FindEditor {
#[serde(flatten)]
pub input: FieldEditor,
@ -374,7 +375,7 @@ pub struct FindEditor {
pub max_width: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBar {
#[serde(flatten)]
pub container: ContainerStyle,
@ -390,15 +391,15 @@ pub struct StatusBar {
pub diagnostic_message: Interactive<ContainedText>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarPanelButtons {
pub group_left: ContainerStyle,
pub group_bottom: ContainerStyle,
pub group_right: ContainerStyle,
pub button: Interactive<PanelButton>,
pub button: Toggleable<Interactive<PanelButton>>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarDiagnosticSummary {
pub container_ok: ContainerStyle,
pub container_warning: ContainerStyle,
@ -413,7 +414,7 @@ pub struct StatusBarDiagnosticSummary {
pub summary_spacing: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct StatusBarLspStatus {
#[serde(flatten)]
pub container: ContainerStyle,
@ -424,14 +425,14 @@ pub struct StatusBarLspStatus {
pub message: TextStyle,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct Dock {
pub left: ContainerStyle,
pub bottom: ContainerStyle,
pub right: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct PanelButton {
#[serde(flatten)]
pub container: ContainerStyle,
@ -440,20 +441,20 @@ pub struct PanelButton {
pub label: ContainedText,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub entry: Interactive<ProjectPanelEntry>,
pub entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub dragged_entry: ProjectPanelEntry,
pub ignored_entry: Interactive<ProjectPanelEntry>,
pub cut_entry: Interactive<ProjectPanelEntry>,
pub ignored_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub cut_entry: Toggleable<Interactive<ProjectPanelEntry>>,
pub filename_editor: FieldEditor,
pub indent_width: f32,
pub open_project_button: Interactive<ContainedText>,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ProjectPanelEntry {
pub height: f32,
#[serde(flatten)]
@ -465,28 +466,28 @@ pub struct ProjectPanelEntry {
pub status: EntryStatus,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct EntryStatus {
pub git: GitProjectStatus,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct GitProjectStatus {
pub modified: Color,
pub inserted: Color,
pub conflict: Color,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContextMenu {
#[serde(flatten)]
pub container: ContainerStyle,
pub item: Interactive<ContextMenuItem>,
pub item: Toggleable<Interactive<ContextMenuItem>>,
pub keystroke_margin: f32,
pub separator: ContainerStyle,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContextMenuItem {
#[serde(flatten)]
pub container: ContainerStyle,
@ -496,13 +497,13 @@ pub struct ContextMenuItem {
pub icon_spacing: f32,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Deserialize, Default, JsonSchema)]
pub struct CommandPalette {
pub key: Interactive<ContainedLabel>,
pub key: Toggleable<ContainedLabel>,
pub keystroke_spacing: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct InviteLink {
#[serde(flatten)]
pub container: ContainerStyle,
@ -511,7 +512,7 @@ pub struct InviteLink {
pub icon: Icon,
}
#[derive(Deserialize, Clone, Copy, Default)]
#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
pub struct Icon {
#[serde(flatten)]
pub container: ContainerStyle,
@ -519,7 +520,7 @@ pub struct Icon {
pub width: f32,
}
#[derive(Deserialize, Clone, Copy, Default)]
#[derive(Deserialize, Clone, Copy, Default, JsonSchema)]
pub struct IconButton {
#[serde(flatten)]
pub container: ContainerStyle,
@ -528,7 +529,7 @@ pub struct IconButton {
pub button_width: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChatMessage {
#[serde(flatten)]
pub container: ContainerStyle,
@ -537,7 +538,7 @@ pub struct ChatMessage {
pub timestamp: ContainedText,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelSelect {
#[serde(flatten)]
pub container: ContainerStyle,
@ -549,7 +550,7 @@ pub struct ChannelSelect {
pub menu: ContainerStyle,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ChannelName {
#[serde(flatten)]
pub container: ContainerStyle,
@ -557,7 +558,7 @@ pub struct ChannelName {
pub name: TextStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Picker {
#[serde(flatten)]
pub container: ContainerStyle,
@ -565,10 +566,10 @@ pub struct Picker {
pub input_editor: FieldEditor,
pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel,
pub item: Interactive<ContainedLabel>,
pub item: Toggleable<Interactive<ContainedLabel>>,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContainedText {
#[serde(flatten)]
pub container: ContainerStyle,
@ -576,7 +577,7 @@ pub struct ContainedText {
pub text: TextStyle,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
pub struct ContainedLabel {
#[serde(flatten)]
pub container: ContainerStyle,
@ -584,7 +585,7 @@ pub struct ContainedLabel {
pub label: LabelStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ProjectDiagnostics {
#[serde(flatten)]
pub container: ContainerStyle,
@ -594,7 +595,7 @@ pub struct ProjectDiagnostics {
pub tab_summary_spacing: f32,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ContactNotification {
pub header_avatar: ImageStyle,
pub header_message: ContainedText,
@ -604,21 +605,21 @@ pub struct ContactNotification {
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct UpdateNotification {
pub message: ContainedText,
pub action_message: Interactive<ContainedText>,
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct MessageNotification {
pub message: ContainedText,
pub action_message: Interactive<ContainedText>,
pub dismiss_button: Interactive<IconButton>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct ProjectSharedNotification {
pub window_height: f32,
pub window_width: f32,
@ -635,7 +636,7 @@ pub struct ProjectSharedNotification {
pub dismiss_button: ContainedText,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, JsonSchema)]
pub struct IncomingCallNotification {
pub window_height: f32,
pub window_width: f32,
@ -652,7 +653,7 @@ pub struct IncomingCallNotification {
pub decline_button: ContainedText,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Editor {
pub text_color: Color,
#[serde(default)]
@ -693,7 +694,7 @@ pub struct Editor {
pub whitespace: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Scrollbar {
pub track: ContainerStyle,
pub thumb: ContainerStyle,
@ -702,14 +703,14 @@ pub struct Scrollbar {
pub git: GitDiffColors,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct GitDiffColors {
pub inserted: Color,
pub modified: Color,
pub deleted: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticPathHeader {
#[serde(flatten)]
pub container: ContainerStyle,
@ -718,7 +719,7 @@ pub struct DiagnosticPathHeader {
pub text_scale_factor: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticHeader {
#[serde(flatten)]
pub container: ContainerStyle,
@ -729,7 +730,7 @@ pub struct DiagnosticHeader {
pub icon_width_factor: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiagnosticStyle {
pub message: LabelStyle,
#[serde(default)]
@ -737,7 +738,7 @@ pub struct DiagnosticStyle {
pub text_scale_factor: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AutocompleteStyle {
#[serde(flatten)]
pub container: ContainerStyle,
@ -747,13 +748,13 @@ pub struct AutocompleteStyle {
pub match_highlight: HighlightStyle,
}
#[derive(Clone, Copy, Default, Deserialize)]
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
pub struct SelectionStyle {
pub cursor: Color,
pub selection: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FieldEditor {
#[serde(flatten)]
pub container: ContainerStyle,
@ -763,21 +764,21 @@ pub struct FieldEditor {
pub selection: SelectionStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct InteractiveColor {
pub color: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct CodeActions {
#[serde(default)]
pub indicator: Interactive<InteractiveColor>,
pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub vertical_scale: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Folds {
pub indicator: Interactive<InteractiveColor>,
pub indicator: Toggleable<Interactive<InteractiveColor>>,
pub ellipses: FoldEllipses,
pub fold_background: Color,
pub icon_margin_scale: f32,
@ -785,14 +786,14 @@ pub struct Folds {
pub foldable_icon: String,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FoldEllipses {
pub text_color: Color,
pub background: Interactive<InteractiveColor>,
pub corner_radius_factor: f32,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct DiffStyle {
pub inserted: Color,
pub modified: Color,
@ -802,41 +803,49 @@ pub struct DiffStyle {
pub corner_radius: f32,
}
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, JsonSchema)]
pub struct Interactive<T> {
pub default: T,
pub hover: Option<T>,
pub hover_and_active: Option<T>,
pub hovered: Option<T>,
pub clicked: Option<T>,
pub click_and_active: Option<T>,
pub active: Option<T>,
pub disabled: Option<T>,
}
impl<T> Interactive<T> {
pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T {
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
pub struct Toggleable<T> {
active: T,
inactive: T,
}
impl<T> Toggleable<T> {
pub fn new(active: T, inactive: T) -> Self {
Self { active, inactive }
}
pub fn in_state(&self, active: bool) -> &T {
if active {
if state.hovered() {
self.hover_and_active
.as_ref()
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some()
{
self.click_and_active
.as_ref()
.unwrap_or(self.active.as_ref().unwrap_or(&self.default))
} else {
self.active.as_ref().unwrap_or(&self.default)
}
} else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
&self.active
} else {
&self.inactive
}
}
pub fn active_state(&self) -> &T {
self.in_state(true)
}
pub fn inactive_state(&self) -> &T {
self.in_state(false)
}
}
impl<T> Interactive<T> {
pub fn style_for(&self, state: &mut MouseState) -> &T {
if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() {
self.clicked.as_ref().unwrap()
} else if state.hovered() {
self.hover.as_ref().unwrap_or(&self.default)
self.hovered.as_ref().unwrap_or(&self.default)
} else {
&self.default
}
}
pub fn disabled_style(&self) -> &T {
self.disabled.as_ref().unwrap_or(&self.default)
}
@ -849,13 +858,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
{
#[derive(Deserialize)]
struct Helper {
#[serde(flatten)]
default: Value,
hover: Option<Value>,
hover_and_active: Option<Value>,
hovered: Option<Value>,
clicked: Option<Value>,
click_and_active: Option<Value>,
active: Option<Value>,
disabled: Option<Value>,
}
@ -880,21 +885,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
}
};
let hover = deserialize_state(json.hover)?;
let hover_and_active = deserialize_state(json.hover_and_active)?;
let hovered = deserialize_state(json.hovered)?;
let clicked = deserialize_state(json.clicked)?;
let click_and_active = deserialize_state(json.click_and_active)?;
let active = deserialize_state(json.active)?;
let disabled = deserialize_state(json.disabled)?;
let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?;
Ok(Interactive {
default,
hover,
hover_and_active,
hovered,
clicked,
click_and_active,
active,
disabled,
})
}
@ -911,7 +910,7 @@ impl Editor {
}
}
#[derive(Default)]
#[derive(Default, JsonSchema)]
pub struct SyntaxTheme {
pub highlights: Vec<(String, HighlightStyle)>,
}
@ -945,7 +944,7 @@ impl<'de> Deserialize<'de> for SyntaxTheme {
}
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct HoverPopover {
pub container: ContainerStyle,
pub info_container: ContainerStyle,
@ -957,7 +956,7 @@ pub struct HoverPopover {
pub highlight: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct TerminalStyle {
pub black: Color,
pub red: Color,
@ -991,24 +990,39 @@ pub struct TerminalStyle {
pub dim_foreground: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct AssistantStyle {
pub container: ContainerStyle,
pub header: ContainerStyle,
pub hamburger_button: Interactive<IconStyle>,
pub split_button: Interactive<IconStyle>,
pub assist_button: Interactive<IconStyle>,
pub quote_button: Interactive<IconStyle>,
pub zoom_in_button: Interactive<IconStyle>,
pub zoom_out_button: Interactive<IconStyle>,
pub plus_button: Interactive<IconStyle>,
pub title: ContainedText,
pub message_header: ContainerStyle,
pub sent_at: ContainedText,
pub user_sender: Interactive<ContainedText>,
pub assistant_sender: Interactive<ContainedText>,
pub system_sender: Interactive<ContainedText>,
pub model_info_container: ContainerStyle,
pub model: Interactive<ContainedText>,
pub remaining_tokens: ContainedText,
pub no_remaining_tokens: ContainedText,
pub error_icon: Icon,
pub api_key_editor: FieldEditor,
pub api_key_prompt: ContainedText,
pub saved_conversation: SavedConversation,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct SavedConversation {
pub container: Interactive<ContainerStyle>,
pub saved_at: ContainedText,
pub title: ContainedText,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct FeedbackStyle {
pub submit_button: Interactive<ContainedText>,
pub button_margin: f32,
@ -1017,7 +1031,7 @@ pub struct FeedbackStyle {
pub link_text_hover: ContainedText,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct WelcomeStyle {
pub page_width: f32,
pub logo: SvgStyle,
@ -1031,7 +1045,7 @@ pub struct WelcomeStyle {
pub checkbox_group: ContainerStyle,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ColorScheme {
pub name: String,
pub is_light: bool,
@ -1046,13 +1060,13 @@ pub struct ColorScheme {
pub players: Vec<Player>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Player {
pub cursor: Color,
pub selection: Color,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct RampSet {
pub neutral: Vec<Color>,
pub red: Vec<Color>,
@ -1065,7 +1079,7 @@ pub struct RampSet {
pub magenta: Vec<Color>,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Layer {
pub base: StyleSet,
pub variant: StyleSet,
@ -1076,7 +1090,7 @@ pub struct Layer {
pub negative: StyleSet,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct StyleSet {
pub default: Style,
pub active: Style,
@ -1086,7 +1100,7 @@ pub struct StyleSet {
pub inverted: Style,
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct Style {
pub background: Color,
pub border: Color,

View File

@ -14,12 +14,13 @@ use util::ResultExt as _;
const MIN_FONT_SIZE: f32 = 6.0;
#[derive(Clone)]
#[derive(Clone, JsonSchema)]
pub struct ThemeSettings {
pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId,
pub(crate) buffer_font_size: f32,
#[serde(skip)]
pub theme: Arc<Theme>,
}

View File

@ -1,23 +1,23 @@
use std::borrow::Cow;
use gpui::{
color::Color,
elements::{
ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
MouseEventHandler, ParentElement, Stack, Svg,
ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label,
MouseEventHandler, ParentElement, Stack, Svg, SvgStyle,
},
fonts::TextStyle,
geometry::vector::{vec2f, Vector2F},
geometry::vector::Vector2F,
platform,
platform::MouseButton,
scene::MouseClick,
Action, Element, EventContext, MouseState, View, ViewContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use crate::{ContainedText, Interactive};
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct CheckboxStyle {
pub icon: SvgStyle,
pub label: ContainedText,
@ -93,25 +93,6 @@ where
.with_cursor_style(platform::CursorStyle::PointingHand)
}
#[derive(Clone, Deserialize, Default)]
pub struct SvgStyle {
pub color: Color,
pub asset: String,
pub dimensions: Dimensions,
}
#[derive(Clone, Deserialize, Default)]
pub struct Dimensions {
pub width: f32,
pub height: f32,
}
impl Dimensions {
pub fn to_vec(&self) -> Vector2F {
vec2f(self.width, self.height)
}
}
pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
Svg::new(style.asset.clone())
.with_color(style.color)
@ -120,10 +101,10 @@ pub fn svg<V: View>(style: &SvgStyle) -> ConstrainedBox<V> {
.with_height(style.dimensions.height)
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct IconStyle {
icon: SvgStyle,
container: ContainerStyle,
pub icon: SvgStyle,
pub container: ContainerStyle,
}
pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
@ -170,7 +151,7 @@ where
F: Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
{
MouseEventHandler::<Tag, V>::new(0, cx, |state, _| {
let style = style.style_for(state, false);
let style = style.style_for(state);
Label::new(label, style.text.to_owned())
.aligned()
.contained()
@ -182,7 +163,7 @@ where
.with_cursor_style(platform::CursorStyle::PointingHand)
}
#[derive(Clone, Deserialize, Default)]
#[derive(Clone, Deserialize, Default, JsonSchema)]
pub struct ModalStyle {
close_icon: Interactive<IconStyle>,
container: ContainerStyle,
@ -220,13 +201,13 @@ where
title,
style
.title_text
.style_for(&mut MouseState::default(), false)
.style_for(&mut MouseState::default())
.clone(),
))
.with_child(
// FIXME: Get a better tag type
MouseEventHandler::<Tag, V>::new(999999, cx, |state, _cx| {
let style = style.close_icon.style_for(state, false);
let style = style.close_icon.style_for(state);
icon(style)
})
.on_click(platform::MouseButton::Left, move |_, _, cx| {

View File

@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
cx: &AppContext,
) -> AnyElement<Picker<Self>> {
let theme = theme::current(cx);
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
let theme_match = &self.matches[ix];
Label::new(theme_match.string.clone(), style.label.clone())

View File

@ -1,19 +0,0 @@
[package]
name = "theme_testbench"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/theme_testbench.rs"
doctest = false
[dependencies]
gpui = { path = "../gpui" }
theme = { path = "../theme" }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
project = { path = "../project" }
smallvec.workspace = true

View File

@ -1,300 +0,0 @@
use gpui::{
actions,
color::Color,
elements::{
AnyElement, Canvas, Container, ContainerStyle, Flex, Label, Margin, MouseEventHandler,
Padding, ParentElement,
},
fonts::TextStyle,
AppContext, Border, Element, Entity, ModelHandle, Quad, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
};
use project::Project;
use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
actions!(theme, [DeployThemeTestbench]);
pub fn init(cx: &mut AppContext) {
cx.add_action(ThemeTestbench::deploy);
register_deserializable_item::<ThemeTestbench>(cx)
}
pub struct ThemeTestbench {}
impl ThemeTestbench {
pub fn deploy(
workspace: &mut Workspace,
_: &DeployThemeTestbench,
cx: &mut ViewContext<Workspace>,
) {
let view = cx.add_view(|_| ThemeTestbench {});
workspace.add_item(Box::new(view), cx);
}
fn render_ramps(color_scheme: &ColorScheme) -> Flex<Self> {
fn display_ramp(ramp: &Vec<Color>) -> AnyElement<ThemeTestbench> {
Flex::row()
.with_children(ramp.iter().cloned().map(|color| {
Canvas::new(move |scene, bounds, _, _, _| {
scene.push_quad(Quad {
bounds,
background: Some(color),
..Default::default()
});
})
.flex(1.0, false)
}))
.flex(1.0, false)
.into_any()
}
Flex::column()
.with_child(display_ramp(&color_scheme.ramps.neutral))
.with_child(display_ramp(&color_scheme.ramps.red))
.with_child(display_ramp(&color_scheme.ramps.orange))
.with_child(display_ramp(&color_scheme.ramps.yellow))
.with_child(display_ramp(&color_scheme.ramps.green))
.with_child(display_ramp(&color_scheme.ramps.cyan))
.with_child(display_ramp(&color_scheme.ramps.blue))
.with_child(display_ramp(&color_scheme.ramps.violet))
.with_child(display_ramp(&color_scheme.ramps.magenta))
}
fn render_layer(
layer_index: usize,
layer: &Layer,
cx: &mut ViewContext<Self>,
) -> Container<Self> {
Flex::column()
.with_child(
Self::render_button_set(0, layer_index, "base", &layer.base, cx).flex(1., false),
)
.with_child(
Self::render_button_set(1, layer_index, "variant", &layer.variant, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(2, layer_index, "on", &layer.on, cx).flex(1., false),
)
.with_child(
Self::render_button_set(3, layer_index, "accent", &layer.accent, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(4, layer_index, "positive", &layer.positive, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(5, layer_index, "warning", &layer.warning, cx)
.flex(1., false),
)
.with_child(
Self::render_button_set(6, layer_index, "negative", &layer.negative, cx)
.flex(1., false),
)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 10.,
bottom: 10.,
left: 10.,
right: 10.,
},
background_color: Some(layer.base.default.background),
..Default::default()
})
}
fn render_button_set(
set_index: usize,
layer_index: usize,
set_name: &'static str,
style_set: &StyleSet,
cx: &mut ViewContext<Self>,
) -> Flex<Self> {
Flex::row()
.with_child(Self::render_button(
set_index * 6,
layer_index,
set_name,
&style_set,
None,
cx,
))
.with_child(Self::render_button(
set_index * 6 + 1,
layer_index,
"hovered",
&style_set,
Some(|style_set| &style_set.hovered),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 2,
layer_index,
"pressed",
&style_set,
Some(|style_set| &style_set.pressed),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 3,
layer_index,
"active",
&style_set,
Some(|style_set| &style_set.active),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 4,
layer_index,
"disabled",
&style_set,
Some(|style_set| &style_set.disabled),
cx,
))
.with_child(Self::render_button(
set_index * 6 + 5,
layer_index,
"inverted",
&style_set,
Some(|style_set| &style_set.inverted),
cx,
))
}
fn render_button(
button_index: usize,
layer_index: usize,
text: &'static str,
style_set: &StyleSet,
style_override: Option<fn(&StyleSet) -> &Style>,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
enum TestBenchButton {}
MouseEventHandler::<TestBenchButton, _>::new(layer_index + button_index, cx, |state, cx| {
let style = if let Some(style_override) = style_override {
style_override(&style_set)
} else if state.clicked().is_some() {
&style_set.pressed
} else if state.hovered() {
&style_set.hovered
} else {
&style_set.default
};
Self::render_label(text.to_string(), style, cx)
.contained()
.with_style(ContainerStyle {
margin: Margin {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
padding: Padding {
top: 4.,
bottom: 4.,
left: 4.,
right: 4.,
},
background_color: Some(style.background),
border: Border {
width: 1.,
color: style.border,
overlay: false,
top: true,
bottom: true,
left: true,
right: true,
},
corner_radius: 2.,
..Default::default()
})
})
.flex(1., true)
.into_any()
}
fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
let settings = settings::get::<ThemeSettings>(cx);
let font_cache = cx.font_cache();
let family_id = settings.buffer_font_family;
let font_size = settings.buffer_font_size(cx);
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let text_style = TextStyle {
color: style.foreground,
font_family_id: family_id,
font_family_name: font_cache.family_name(family_id).unwrap(),
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
};
Label::new(text, text_style)
}
}
impl Entity for ThemeTestbench {
type Event = ();
}
impl View for ThemeTestbench {
fn ui_name() -> &'static str {
"ThemeTestbench"
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
let color_scheme = &theme::current(cx).clone().color_scheme;
Flex::row()
.with_child(
Self::render_ramps(color_scheme)
.contained()
.with_margin_right(10.)
.flex(0.1, false),
)
.with_child(
Flex::column()
.with_child(Self::render_layer(100, &color_scheme.lowest, cx).flex(1., true))
.with_child(Self::render_layer(200, &color_scheme.middle, cx).flex(1., true))
.with_child(Self::render_layer(300, &color_scheme.highest, cx).flex(1., true))
.flex(1., false),
)
.into_any()
}
}
impl Item for ThemeTestbench {
fn tab_content<T: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<T> {
Label::new("Theme Testbench", style.label.clone())
.aligned()
.contained()
.into_any()
}
fn serialized_item_kind() -> Option<&'static str> {
Some("ThemeTestBench")
}
fn deserialize(
_project: ModelHandle<Project>,
_workspace: WeakViewHandle<Workspace>,
_workspace_id: workspace::WorkspaceId,
_item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<gpui::anyhow::Result<ViewHandle<Self>>> {
Task::ready(Ok(cx.add_view(|_| Self {})))
}
}

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
lazy_static::lazy_static! {
pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations");
pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed");
pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");

View File

@ -209,8 +209,9 @@ impl Motion {
map: &DisplaySnapshot,
point: DisplayPoint,
goal: SelectionGoal,
times: usize,
maybe_times: Option<usize>,
) -> Option<(DisplayPoint, SelectionGoal)> {
let times = maybe_times.unwrap_or(1);
use Motion::*;
let infallible = self.infallible();
let (new_point, goal) = match self {
@ -236,7 +237,10 @@ impl Motion {
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (
end_of_document(map, point, maybe_times),
SelectionGoal::None,
),
Matching => (matching(map, point), SelectionGoal::None),
FindForward { before, text } => (
find_forward(map, point, *before, text.clone(), times),
@ -257,7 +261,7 @@ impl Motion {
&self,
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
times: Option<usize>,
expand_to_surrounding_newline: bool,
) -> bool {
if let Some((new_head, goal)) =
@ -473,14 +477,19 @@ fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) ->
map.clip_point(new_point, Bias::Left)
}
fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
let mut new_point = if line == 1 {
map.max_point()
fn end_of_document(
map: &DisplaySnapshot,
point: DisplayPoint,
line: Option<usize>,
) -> DisplayPoint {
let new_row = if let Some(line) = line {
(line - 1) as u32
} else {
Point::new((line - 1) as u32, 0).to_display_point(map)
map.max_buffer_row()
};
*new_point.column_mut() = point.column();
map.clip_point(new_point, Bias::Left)
let new_point = Point::new(new_row, point.column());
map.clip_point(new_point.to_display_point(map), Bias::Left)
}
fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {

View File

@ -1,5 +1,6 @@
mod change;
mod delete;
mod substitute;
mod yank;
use std::{borrow::Cow, cmp::Ordering, sync::Arc};
@ -25,6 +26,7 @@ use workspace::Workspace;
use self::{
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
substitute::substitute,
yank::{yank_motion, yank_object},
};
@ -45,6 +47,7 @@ actions!(
DeleteToEndOfLine,
Paste,
Yank,
Substitute,
]
);
@ -56,6 +59,12 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(insert_end_of_line);
cx.add_action(insert_line_above);
cx.add_action(insert_line_below);
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
substitute(vim, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
@ -93,7 +102,7 @@ pub fn init(cx: &mut AppContext) {
pub fn normal_motion(
motion: Motion,
operator: Option<Operator>,
times: usize,
times: Option<usize>,
cx: &mut WindowContext,
) {
Vim::update(cx, |vim, cx| {
@ -129,7 +138,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
})
}
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
@ -147,7 +156,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::Right.move_point(map, cursor, goal, 1)
Motion::Right.move_point(map, cursor, goal, None)
});
});
});
@ -164,7 +173,7 @@ fn insert_first_non_whitespace(
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
Motion::FirstNonWhitespace.move_point(map, cursor, goal, None)
});
});
});
@ -177,7 +186,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
Motion::EndOfLine.move_point(map, cursor, goal, None)
});
});
});
@ -237,7 +246,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
});
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal, 1)
Motion::EndOfLine.move_point(map, cursor, goal, None)
});
});
editor.edit_with_autoindent(edits, cx);

View File

@ -6,7 +6,7 @@ use editor::{
use gpui::WindowContext;
use language::Selection;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
@ -78,10 +78,10 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: usize,
times: Option<usize>,
ignore_punctuation: bool,
) -> bool {
if times == 1 {
if times.is_none() || times.unwrap() == 1 {
let in_word = map
.chars_at(selection.head())
.next()
@ -97,7 +97,8 @@ fn expand_changed_word_selection(
});
true
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
Motion::NextWordStart { ignore_punctuation }
.expand_selection(map, selection, None, false)
}
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)

View File

@ -3,7 +3,7 @@ use collections::{HashMap, HashSet};
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
use gpui::WindowContext;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);

View File

@ -0,0 +1,69 @@
use gpui::WindowContext;
use language::Point;
use crate::{motion::Motion, Mode, Vim};
pub fn substitute(vim: &mut Vim, count: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if selection.start == selection.end {
Motion::Right.expand_selection(map, selection, count, true);
}
})
});
editor.transact(cx, |editor, cx| {
let selections = editor.selections.all::<Point>(cx);
for selection in selections.into_iter().rev() {
editor.buffer().update(cx, |buffer, cx| {
buffer.edit([(selection.start..selection.end, "")], None, cx)
})
}
});
editor.set_clip_at_line_ends(true, cx);
});
vim.switch_mode(Mode::Insert, true, cx)
}
#[cfg(test)]
mod test {
use crate::{state::Mode, test::VimTestContext};
use indoc::indoc;
#[gpui::test]
async fn test_substitute(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// supports a single cursor
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
cx.simulate_keystrokes(["s", "x"]);
cx.assert_editor_state("xˇbc\n");
// supports a selection
cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false });
cx.assert_editor_state("a«bcˇ»\n");
cx.simulate_keystrokes(["s", "x"]);
cx.assert_editor_state("axˇ\n");
// supports counts
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
cx.simulate_keystrokes(["2", "s", "x"]);
cx.assert_editor_state("xˇc\n");
// supports multiple cursors
cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
cx.simulate_keystrokes(["2", "s", "x"]);
cx.assert_editor_state("axˇdexˇg\n");
// does not read beyond end of line
cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
cx.simulate_keystrokes(["5", "s", "x"]);
cx.assert_editor_state("\n");
// it handles multibyte characters
cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
cx.simulate_keystrokes(["4", "s", "x"]);
cx.assert_editor_state("\n");
}
}

View File

@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}
use collections::HashMap;
use gpui::WindowContext;
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) {
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);

View File

@ -98,3 +98,28 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
})
}
#[gpui::test]
async fn test_count_down(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal);
cx.simulate_keystrokes(["2", "down"]);
cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee");
cx.simulate_keystrokes(["9", "down"]);
cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe");
}
#[gpui::test]
async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// goes to end by default
cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal);
cx.simulate_keystrokes(["shift-g"]);
cx.assert_editor_state("aa\nbb\ncˇc");
// can go to line 1 (https://github.com/zed-industries/community/issues/710)
cx.simulate_keystrokes(["1", "shift-g"]);
cx.assert_editor_state("aˇa\nbb\ncc");
}

View File

@ -238,13 +238,12 @@ impl Vim {
popped_operator
}
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> usize {
let mut times = 1;
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
if let Some(Operator::Number(number)) = self.active_operator() {
times = number;
self.pop_operator(cx);
return Some(number);
}
times
None
}
fn clear_operator(&mut self, cx: &mut WindowContext) {

View File

@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(paste);
}
pub fn visual_motion(motion: Motion, times: usize, cx: &mut WindowContext) {
pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {

View File

@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
) -> gpui::AnyElement<Picker<Self>> {
let theme = &theme::current(cx);
let keymap_match = &self.matches[ix];
let style = theme.picker.item.style_for(mouse_state, selected);
let style = theme.picker.item.in_state(selected).style_for(mouse_state);
Label::new(keymap_match.string.clone(), style.label.clone())
.with_highlights(keymap_match.positions.clone())

View File

@ -249,7 +249,7 @@ impl Dock {
}
}
pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
pub(crate) fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
let subscriptions = [
cx.observe(&panel, |_, _, cx| cx.notify()),
cx.subscribe(&panel, |this, panel, event, cx| {
@ -498,7 +498,9 @@ impl View for PanelButtons {
Stack::new()
.with_child(
MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
let style = button_style.style_for(state, is_active);
let style = button_style.in_state(is_active);
let style = style.style_for(state);
Flex::row()
.with_child(
Svg::new(view.icon_path(cx))
@ -603,6 +605,7 @@ pub mod test {
use super::*;
use gpui::{ViewContext, WindowContext};
#[derive(Debug)]
pub enum TestPanelEvent {
PositionChanged,
Activated,

View File

@ -291,7 +291,7 @@ pub mod simple_message_notification {
)
.with_child(
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state, false);
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x_mark_8.svg")
.with_color(style.color)
.constrained()
@ -323,7 +323,7 @@ pub mod simple_message_notification {
0,
cx,
|state, _| {
let style = theme.action_message.style_for(state, false);
let style = theme.action_message.style_for(state);
Flex::row()
.with_child(

View File

@ -1,9 +1,10 @@
mod dragged_item_receiver;
use super::{ItemHandle, SplitDirection};
pub use crate::toolbar::Toolbar;
use crate::{
item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile,
NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
@ -250,7 +251,7 @@ impl Pane {
pane: handle.clone(),
next_timestamp,
}))),
toolbar: cx.add_view(|_| Toolbar::new(handle)),
toolbar: cx.add_view(|_| Toolbar::new(Some(handle))),
tab_bar_context_menu: TabBarContextMenu {
kind: TabBarContextMenuKind::New,
handle: context_menu,
@ -1112,7 +1113,7 @@ impl Pane {
.get(self.active_item_index)
.map(|item| item.as_ref());
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_active_pane_item(active_item, cx);
toolbar.set_active_item(active_item, cx);
});
}
@ -1410,7 +1411,7 @@ impl Pane {
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
index: usize,
icon: &'static str,
active: bool,
is_active: bool,
tooltip: Option<(String, Option<Box<dyn Action>>)>,
cx: &mut ViewContext<Pane>,
on_click: F,
@ -1420,7 +1421,7 @@ impl Pane {
let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
let style = theme.pane_button.style_for(mouse_state, active);
let style = theme.pane_button.in_state(is_active).style_for(mouse_state);
Svg::new(icon)
.with_color(style.color)
.constrained()
@ -1602,7 +1603,7 @@ impl View for Pane {
}
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(true, cx);
toolbar.focus_changed(true, cx);
});
if let Some(active_item) = self.active_item() {
@ -1631,7 +1632,7 @@ impl View for Pane {
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_focus = false;
self.toolbar.update(cx, |toolbar, cx| {
toolbar.pane_focus_update(false, cx);
toolbar.focus_changed(false, cx);
});
cx.notify();
}

View File

@ -38,7 +38,7 @@ trait ToolbarItemViewHandle {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
) -> ToolbarItemLocation;
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext);
fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext);
fn row_count(&self, cx: &WindowContext) -> usize;
}
@ -51,10 +51,10 @@ pub enum ToolbarItemLocation {
}
pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>,
active_item: Option<Box<dyn ItemHandle>>,
hidden: bool,
can_navigate: bool,
pane: WeakViewHandle<Pane>,
pane: Option<WeakViewHandle<Pane>>,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
@ -121,7 +121,7 @@ impl View for Toolbar {
let pane = self.pane.clone();
let mut enable_go_backward = false;
let mut enable_go_forward = false;
if let Some(pane) = pane.upgrade(cx) {
if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) {
let pane = pane.read(cx);
enable_go_backward = pane.can_navigate_backward();
enable_go_forward = pane.can_navigate_forward();
@ -143,19 +143,17 @@ impl View for Toolbar {
enable_go_backward,
spacing,
{
let pane = pane.clone();
move |toolbar, cx| {
if let Some(workspace) = toolbar
.pane
.upgrade(cx)
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
{
let pane = pane.clone();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
workspace.go_back(pane.clone(), cx).detach_and_log_err(cx);
});
})
if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
let pane = pane.downgrade();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
workspace.go_back(pane, cx).detach_and_log_err(cx);
});
})
}
}
}
},
@ -171,21 +169,17 @@ impl View for Toolbar {
enable_go_forward,
spacing,
{
let pane = pane.clone();
move |toolbar, cx| {
if let Some(workspace) = toolbar
.pane
.upgrade(cx)
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx))
{
let pane = pane.clone();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
workspace
.go_forward(pane.clone(), cx)
.detach_and_log_err(cx);
});
});
if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) {
let pane = pane.downgrade();
cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| {
workspace.go_forward(pane, cx).detach_and_log_err(cx);
});
})
}
}
}
},
@ -231,7 +225,7 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
) -> AnyElement<Toolbar> {
MouseEventHandler::<A, _>::new(0, cx, |state, _| {
let style = if enabled {
style.style_for(state, false)
style.style_for(state)
} else {
style.disabled_style()
};
@ -269,9 +263,9 @@ fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>
}
impl Toolbar {
pub fn new(pane: WeakViewHandle<Pane>) -> Self {
pub fn new(pane: Option<WeakViewHandle<Pane>>) -> Self {
Self {
active_pane_item: None,
active_item: None,
pane,
items: Default::default(),
hidden: false,
@ -288,7 +282,7 @@ impl Toolbar {
where
T: 'static + ToolbarItemView,
{
let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
let location = item.set_active_pane_item(self.active_item.as_deref(), cx);
cx.subscribe(&item, |this, item, event, cx| {
if let Some((_, current_location)) =
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
@ -307,20 +301,16 @@ impl Toolbar {
cx.notify();
}
pub fn set_active_pane_item(
&mut self,
pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
self.active_item = item.map(|item| item.boxed_clone());
self.hidden = self
.active_pane_item
.active_item
.as_ref()
.map(|item| !item.show_toolbar(cx))
.unwrap_or(false);
for (toolbar_item, current_location) in self.items.iter_mut() {
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
let new_location = toolbar_item.set_active_pane_item(item, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
@ -328,9 +318,9 @@ impl Toolbar {
}
}
pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext<Self>) {
pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext<Self>) {
for (toolbar_item, _) in self.items.iter_mut() {
toolbar_item.pane_focus_update(pane_focused, cx);
toolbar_item.focus_changed(focused, cx);
}
}
@ -364,7 +354,7 @@ impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
})
}
fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) {
fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) {
self.update(cx, |this, cx| {
this.pane_focus_update(pane_focused, cx);
cx.notify();

View File

@ -861,7 +861,10 @@ impl Workspace {
&self.right_dock
}
pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>) {
pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T::Event: std::fmt::Debug,
{
let dock = match panel.position(cx) {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
@ -904,10 +907,11 @@ impl Workspace {
});
} else if T::should_zoom_in_on_event(event) {
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
if panel.has_focus(cx) {
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx));
if !panel.has_focus(cx) {
cx.focus(&panel);
}
this.zoomed = Some(panel.downgrade().into_any());
this.zoomed_position = Some(panel.read(cx).position(cx));
} else if T::should_zoom_out_on_event(event) {
dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
if this.zoomed_position == Some(prev_position) {
@ -1702,6 +1706,11 @@ impl Workspace {
cx.notify();
}
#[cfg(any(test, feature = "test-support"))]
pub fn zoomed_view(&self, cx: &AppContext) -> Option<AnyViewHandle> {
self.zoomed.and_then(|view| view.upgrade(cx))
}
fn dismiss_zoomed_items_to_reveal(
&mut self,
dock_to_reveal: Option<DockPosition>,

13
crates/xtask/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0"
clap = {version = "4.0", features = ["derive"]}
theme = {path = "../theme"}
serde_json.workspace = true
schemars.workspace = true

23
crates/xtask/src/cli.rs Normal file
View File

@ -0,0 +1,23 @@
use clap::{Parser, Subcommand};
use std::path::PathBuf;
/// Common utilities for Zed developers.
// For more information, see [matklad's repository README](https://github.com/matklad/cargo-xtask/)
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
/// Command to run.
#[derive(Subcommand)]
pub enum Commands {
/// Builds theme types for interop with Typescript.
BuildThemeTypes {
#[clap(short, long, default_value = "schemas")]
out_dir: PathBuf,
#[clap(short, long, default_value = "theme.json")]
file_name: PathBuf,
},
}

29
crates/xtask/src/main.rs Normal file
View File

@ -0,0 +1,29 @@
mod cli;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use schemars::schema_for;
use theme::Theme;
fn build_themes(out_dir: PathBuf, file_name: PathBuf) -> Result<()> {
let theme = schema_for!(Theme);
let output = serde_json::to_string_pretty(&theme)?;
std::fs::create_dir(&out_dir)?;
let mut file_path = out_dir;
file_path.push(file_name);
std::fs::write(file_path, output)?;
Ok(())
}
fn main() -> Result<()> {
let args = cli::Cli::parse();
match args.command {
cli::Commands::BuildThemeTypes { out_dir, file_name } => build_themes(out_dir, file_name),
}
}

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.92.0"
version = "0.93.0"
publish = false
[lib]
@ -62,7 +62,6 @@ text = { path = "../text" }
terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
theme_testbench = { path = "../theme_testbench" }
util = { path = "../util" }
vim = { path = "../vim" }
workspace = { path = "../workspace" }

View File

@ -156,7 +156,6 @@ fn main() {
search::init(cx);
vim::init(cx);
terminal_view::init(cx);
theme_testbench::init(cx);
copilot::init(http.clone(), node_runtime, cx);
ai::init(cx);

1
styles/.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/
coverage/

20
styles/.zed/settings.json Normal file
View File

@ -0,0 +1,20 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
{
"languages": {
"TypeScript": {
"tab_size": 4
},
"TSX": {
"tab_size": 4
},
"JavaScript": {
"tab_size": 4
},
"JSON": {
"tab_size": 4
}
}
}

1642
styles/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,9 @@
"scripts": {
"build": "ts-node ./src/buildThemes.ts",
"build-licenses": "ts-node ./src/buildLicenses.ts",
"build-tokens": "ts-node ./src/buildTokens.ts"
"build-tokens": "ts-node ./src/buildTokens.ts",
"build-types": "cd ../crates/theme && cargo test && cd ../../styles && ts-node ./src/buildTypes.ts",
"test": "vitest"
},
"author": "",
"license": "ISC",
@ -19,13 +21,20 @@
"case-anything": "^2.1.10",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
"json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0",
"ts-node": "^10.9.1"
"ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
"utility-types": "^3.10.0",
"vitest": "^0.32.0"
},
"prettier": {
"semi": false,
"printWidth": 80,
"htmlWhitespaceSensitivity": "strict",
"tabWidth": 4
},
"devDependencies": {
"@vitest/coverage-v8": "^0.32.0"
}
}

View File

@ -1,13 +1,13 @@
import * as fs from "fs";
import * as path from "path";
import { ColorScheme, createColorScheme } from "./common";
import { themes } from "./themes";
import { slugify } from "./utils/slugify";
import { colorSchemeTokens } from "./theme/tokens/colorScheme";
import * as fs from "fs"
import * as path from "path"
import { ColorScheme, createColorScheme } from "./common"
import { themes } from "./themes"
import { slugify } from "./utils/slugify"
import { colorSchemeTokens } from "./theme/tokens/colorScheme"
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens");
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json");
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json");
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
function clearTokens(tokensDirectory: string) {
if (!fs.existsSync(tokensDirectory)) {
@ -22,64 +22,66 @@ function clearTokens(tokensDirectory: string) {
}
type TokenSet = {
id: string;
name: string;
selectedTokenSets: { [key: string]: "enabled" };
};
id: string
name: string
selectedTokenSets: { [key: string]: "enabled" }
}
function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } {
const tokenSetOrder: string[] = colorSchemes.map(
(scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_")
);
return { tokenSetOrder };
function buildTokenSetOrder(colorSchemes: ColorScheme[]): {
tokenSetOrder: string[]
} {
const tokenSetOrder: string[] = colorSchemes.map((scheme) =>
scheme.name.toLowerCase().replace(/\s+/g, "_")
)
return { tokenSetOrder }
}
function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
.toLowerCase()
.replace(/\s+/g, "_")}_${index}`;
const selectedTokenSets: { [key: string]: "enabled" } = {};
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_");
selectedTokenSets[tokenSet] = "enabled";
.replace(/\s+/g, "_")}_${index}`
const selectedTokenSets: { [key: string]: "enabled" } = {}
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_")
selectedTokenSets[tokenSet] = "enabled"
return {
id,
name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
selectedTokenSets,
};
});
}
})
return themesIndex;
return themesIndex
}
function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
clearTokens(tokensDirectory);
clearTokens(tokensDirectory)
for (const colorScheme of colorSchemes) {
const fileName = slugify(colorScheme.name) + ".json";
const tokens = colorSchemeTokens(colorScheme);
const tokensJSON = JSON.stringify(tokens, null, 2);
const outPath = path.join(tokensDirectory, fileName);
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 });
console.log(`- ${outPath} created`);
const fileName = slugify(colorScheme.name) + ".json"
const tokens = colorSchemeTokens(colorScheme)
const tokensJSON = JSON.stringify(tokens, null, 2)
const outPath = path.join(tokensDirectory, fileName)
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 })
console.log(`- ${outPath} created`)
}
const themeIndexData = buildThemesIndex(colorSchemes);
const themeIndexData = buildThemesIndex(colorSchemes)
const themesJSON = JSON.stringify(themeIndexData, null, 2);
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 });
console.log(`- ${TOKENS_FILE} created`);
const themesJSON = JSON.stringify(themeIndexData, null, 2)
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 })
console.log(`- ${TOKENS_FILE} created`)
const tokenSetOrderData = buildTokenSetOrder(colorSchemes);
const tokenSetOrderData = buildTokenSetOrder(colorSchemes)
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2);
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 });
console.log(`- ${METADATA_FILE} created`);
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2)
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 })
console.log(`- ${METADATA_FILE} created`)
}
const colorSchemes: ColorScheme[] = themes.map((theme) =>
createColorScheme(theme)
);
)
writeTokens(colorSchemes, TOKENS_DIRECTORY);
writeTokens(colorSchemes, TOKENS_DIRECTORY)

64
styles/src/buildTypes.ts Normal file
View File

@ -0,0 +1,64 @@
import * as fs from "fs/promises"
import * as fsSync from "fs"
import * as path from "path"
import { compile } from "json-schema-to-typescript"
const BANNER = `/*
* This file is autogenerated
*/\n\n`
const dirname = __dirname
async function main() {
let schemasPath = path.join(dirname, "../../", "crates/theme/schemas")
let schemaFiles = (await fs.readdir(schemasPath)).filter((x) =>
x.endsWith(".json")
)
let compiledTypes = new Set()
for (let filename of schemaFiles) {
let filePath = path.join(schemasPath, filename)
const fileContents = await fs.readFile(filePath)
let schema = JSON.parse(fileContents.toString())
let compiled = await compile(schema, schema.title, {
bannerComment: "",
})
let eachType = compiled.split("export")
for (let type of eachType) {
if (!type) {
continue
}
compiledTypes.add("export " + type.trim())
}
}
let output = BANNER + Array.from(compiledTypes).join("\n\n")
let outputPath = path.join(dirname, "../../styles/src/types/zed.ts")
try {
let existing = await fs.readFile(outputPath)
if (existing.toString() == output) {
// Skip writing if it hasn't changed
console.log("Schemas are up to date")
return
}
} catch (e) {
// It's fine if there's no output from a previous run.
// @ts-ignore
if (e.code !== "ENOENT") {
throw e
}
}
const typesDic = path.dirname(outputPath)
if (!fsSync.existsSync(typesDic)) {
await fs.mkdir(typesDic)
}
await fs.writeFile(outputPath, output)
console.log(`Wrote Typescript types to ${outputPath}`)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})

View File

@ -0,0 +1,4 @@
import { interactive } from "./interactive"
import { toggleable } from "./toggle"
export { interactive, toggleable }

View File

@ -0,0 +1,56 @@
import {
NOT_ENOUGH_STATES_ERROR,
NO_DEFAULT_OR_BASE_ERROR,
interactive,
} from "./interactive"
import { describe, it, expect } from "vitest"
describe("interactive", () => {
it("creates an Interactive<Element> with base properties and states", () => {
const result = interactive({
base: { fontSize: 10, color: "#FFFFFF" },
state: {
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("creates an Interactive<Element> with no base properties", () => {
const result = interactive({
state: {
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE" },
clicked: { color: "#CCCCCC" },
},
})
expect(result).toEqual({
default: { color: "#FFFFFF", fontSize: 10 },
hovered: { color: "#EEEEEE", fontSize: 10 },
clicked: { color: "#CCCCCC", fontSize: 10 },
})
})
it("throws error when both default and base are missing", () => {
const state = {
hovered: { color: "blue" },
}
expect(() => interactive({ state })).toThrow(NO_DEFAULT_OR_BASE_ERROR)
})
it("throws error when no other state besides default is present", () => {
const state = {
default: { fontSize: 10 },
}
expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
})
})

View File

@ -0,0 +1,97 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type InteractiveState =
| "default"
| "hovered"
| "clicked"
| "selected"
| "disabled"
type Interactive<T> = {
default: T
hovered?: T
clicked?: T
selected?: T
disabled?: T
}
export const NO_DEFAULT_OR_BASE_ERROR =
"An interactive object must have a default state, or a base property."
export const NOT_ENOUGH_STATES_ERROR =
"An interactive object must have a default and at least one other state."
interface InteractiveProps<T> {
base?: T
state: Partial<Record<InteractiveState, DeepPartial<T>>>
}
/**
* Helper function for creating Interactive<T> objects that works with Toggle<T>-like behavior.
* It takes a default object to be used as the value for `default` field and fills out other fields
* with fields from either `base` or from the `state` object which contains values for specific states.
* Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them.
*
* @param defaultObj Object to be used as the value for the `default` field.
* @param base Optional object containing base fields to be included in the resulting object.
* @param state Object containing optional modified fields to be included in the resulting object for each state.
* @returns Interactive<T> object with fields from `base` and `state`.
*/
export function interactive<T extends Object>({
base,
state,
}: InteractiveProps<T>): Interactive<T> {
if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
let defaultState: T
if (state.default && base) {
defaultState = merge(base, state.default) as T
} else {
defaultState = base ? base : (state.default as T)
}
let interactiveObj: Interactive<T> = {
default: defaultState,
}
let stateCount = 0
if (state.hovered !== undefined) {
interactiveObj.hovered = merge(
interactiveObj.default,
state.hovered
) as T
stateCount++
}
if (state.clicked !== undefined) {
interactiveObj.clicked = merge(
interactiveObj.default,
state.clicked
) as T
stateCount++
}
if (state.selected !== undefined) {
interactiveObj.selected = merge(
interactiveObj.default,
state.selected
) as T
stateCount++
}
if (state.disabled !== undefined) {
interactiveObj.disabled = merge(
interactiveObj.default,
state.disabled
) as T
stateCount++
}
if (stateCount < 1) {
throw new Error(NOT_ENOUGH_STATES_ERROR)
}
return interactiveObj
}

View File

@ -0,0 +1,52 @@
import {
NO_ACTIVE_ERROR,
NO_INACTIVE_OR_BASE_ERROR,
toggleable,
} from "./toggle"
import { describe, it, expect } from "vitest"
describe("toggleable", () => {
it("creates a Toggleable<Element> with base properties and states", () => {
const result = toggleable({
base: { background: "#000000", color: "#CCCCCC" },
state: {
active: { color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("creates a Toggleable<Element> with no base properties", () => {
const result = toggleable({
state: {
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
},
})
expect(result).toEqual({
inactive: { background: "#000000", color: "#CCCCCC" },
active: { background: "#000000", color: "#FFFFFF" },
})
})
it("throws error when both inactive and base are missing", () => {
const state = {
active: { background: "#000000", color: "#FFFFFF" },
}
expect(() => toggleable({ state })).toThrow(NO_INACTIVE_OR_BASE_ERROR)
})
it("throws error when no active state is present", () => {
const state = {
inactive: { background: "#000000", color: "#CCCCCC" },
}
expect(() => toggleable({ state })).toThrow(NO_ACTIVE_ERROR)
})
})

View File

@ -0,0 +1,47 @@
import merge from "ts-deepmerge"
import { DeepPartial } from "utility-types"
type ToggleState = "inactive" | "active"
type Toggleable<T> = Record<ToggleState, T>
export const NO_INACTIVE_OR_BASE_ERROR =
"A toggleable object must have an inactive state, or a base property."
export const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
interface ToggleableProps<T> {
base?: T
state: Partial<Record<ToggleState, DeepPartial<T>>>
}
/**
* Helper function for creating Toggleable objects.
* @template T The type of the object being toggled.
* @param props Object containing the base (inactive) state and state modifications to create the active state.
* @returns A Toggleable object containing both the inactive and active states.
* @example
* ```
* toggleable({
* base: { background: "#000000", text: "#CCCCCC" },
* state: { active: { text: "#CCCCCC" } },
* })
* ```
*/
export function toggleable<T extends object>(
props: ToggleableProps<T>
): Toggleable<T> {
const { base, state } = props
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
const inactiveState = base
? ((state.inactive ? merge(base, state.inactive) : base) as T)
: (state.inactive as T)
const toggleObj: Toggleable<T> = {
inactive: inactiveState,
active: merge(base ?? {}, state.active) as T,
}
return toggleObj
}

View File

@ -1,4 +1,3 @@
import { text } from "./components"
import contactFinder from "./contactFinder"
import contactsPopover from "./contactsPopover"
import commandPalette from "./commandPalette"

View File

@ -1,6 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { text, border, background, foreground } from "./components"
import editor from "./editor"
import { interactive } from "../element"
export default function assistant(colorScheme: ColorScheme) {
const layer = colorScheme.highest
@ -9,50 +10,244 @@ export default function assistant(colorScheme: ColorScheme) {
background: editor(colorScheme).background,
padding: { left: 12 },
},
header: {
messageHeader: {
border: border(layer, "default", { bottom: true, top: true }),
margin: { bottom: 6, top: 6 },
background: editor(colorScheme).background,
},
hamburgerButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/hamburger_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
margin: { left: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
splitButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/split_message_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
margin: { left: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
quoteButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/quote_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
margin: { left: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
assistButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/assist_15.svg",
dimensions: {
width: 15,
height: 15,
},
},
container: {
margin: { left: 12, right: 24 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
zoomInButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/maximize_8.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
margin: { right: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
zoomOutButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/minimize_8.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
margin: { right: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
plusButton: interactive({
base: {
icon: {
color: foreground(layer, "variant"),
asset: "icons/plus_12.svg",
dimensions: {
width: 12,
height: 12,
},
},
container: {
margin: { right: 12 },
}
},
state: {
hovered: {
icon: {
color: foreground(layer, "hovered")
}
}
}
}),
title: {
margin: { left: 12 },
...text(layer, "sans", "default", { size: "sm" })
},
savedConversation: {
container: interactive({
base: {
background: background(layer, "on"),
padding: { top: 4, bottom: 4 }
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
}
},
}),
savedAt: {
margin: { left: 8 },
...text(layer, "sans", "default", { size: "xs" }),
},
title: {
margin: { left: 16 },
...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
}
},
userSender: {
...text(layer, "sans", "default", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "default", {
size: "sm",
weight: "bold",
}),
},
},
assistantSender: {
...text(layer, "sans", "accent", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "accent", {
size: "sm",
weight: "bold",
}),
},
},
systemSender: {
...text(layer, "sans", "variant", { size: "sm", weight: "bold" }),
default: {
...text(layer, "sans", "variant", {
size: "sm",
weight: "bold",
}),
},
},
sentAt: {
margin: { top: 2, left: 8 },
...text(layer, "sans", "default", { size: "2xs" }),
},
modelInfoContainer: {
margin: { right: 16, top: 4 },
},
model: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
padding: 4,
cornerRadius: 4,
...text(layer, "sans", "default", { size: "xs" }),
hover: {
background: background(layer, "on", "hovered"),
model: interactive({
base: {
background: background(layer, "on"),
margin: { left: 12, right: 12, top: 12 },
padding: 4,
cornerRadius: 4,
...text(layer, "sans", "default", { size: "xs" }),
},
},
state: {
hovered: {
background: background(layer, "on", "hovered"),
border: border(layer, "on", { overlay: true }),
},
},
}),
remainingTokens: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
margin: { top: 12, right: 12 },
padding: 4,
margin: { left: 4 },
cornerRadius: 4,
...text(layer, "sans", "positive", { size: "xs" }),
},
noRemainingTokens: {
background: background(layer, "on"),
border: border(layer, "on", { overlay: true }),
margin: { top: 12, right: 12 },
padding: 4,
margin: { left: 4 },
cornerRadius: 4,
...text(layer, "sans", "negative", { size: "xs" }),
},

View File

@ -1,12 +1,13 @@
import { ColorScheme } from "../theme/colorScheme"
import { withOpacity } from "../theme/color"
import { text, background } from "./components"
import { toggleable } from "../element"
export default function commandPalette(colorScheme: ColorScheme) {
let layer = colorScheme.highest
return {
keystrokeSpacing: 8,
key: {
const key = toggleable({
base: {
text: text(layer, "mono", "variant", "default", { size: "xs" }),
cornerRadius: 2,
background: background(layer, "on"),
@ -21,10 +22,21 @@ export default function commandPalette(colorScheme: ColorScheme) {
bottom: 1,
left: 2,
},
},
state: {
active: {
text: text(layer, "mono", "on", "default", { size: "xs" }),
background: withOpacity(background(layer, "on"), 0.2),
},
},
})
return {
keystrokeSpacing: 8,
// TODO: This should be a Toggle<ContainedText> on the rust side so we don't have to do this
key: {
inactive: { ...key.inactive },
active: key.active,
},
}
}

View File

@ -85,7 +85,7 @@ export function foreground(
return getStyle(layer, styleSetOrStyles, style).foreground
}
interface Text {
interface Text extends Object {
family: keyof typeof fontFamilies
color: string
size: number

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