Merge branch 'main' into gpui2

This commit is contained in:
Marshall Bowers 2023-10-12 17:43:05 -04:00
commit ef18aaa66f
121 changed files with 1760 additions and 10632 deletions

62
Cargo.lock generated
View File

@ -1531,6 +1531,7 @@ dependencies = [
"log",
"lsp",
"nanoid",
"node_runtime",
"parking_lot 0.11.2",
"pretty_assertions",
"project",
@ -5645,6 +5646,26 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettier"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"fs",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
"node_runtime",
"serde",
"serde_derive",
"serde_json",
"util",
]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
@ -5757,8 +5778,10 @@ dependencies = [
"lazy_static",
"log",
"lsp",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"prettier",
"pretty_assertions",
"rand 0.8.5",
"regex",
@ -7787,29 +7810,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap 4.4.4",
"derive_more",
"fs",
"futures 0.3.28",
"gpui2",
"itertools 0.11.0",
"log",
"rust-embed",
"serde",
"settings",
"simplelog",
"strum",
"theme",
"ui",
"util",
]
[[package]]
name = "storybook2"
version = "0.1.0"
@ -9036,21 +9036,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ui"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"gpui2",
"rand 0.8.5",
"serde",
"settings",
"smallvec",
"strum",
"theme",
]
[[package]]
name = "ui2"
version = "0.1.0"
@ -10153,6 +10138,7 @@ dependencies = [
"lazy_static",
"log",
"menu",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"project",

View File

@ -54,6 +54,7 @@ members = [
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
@ -67,14 +68,12 @@ members = [
"crates/sqlez_macros",
"crates/feature_flags",
"crates/rich_text",
"crates/storybook",
"crates/storybook2",
"crates/sum_tree",
"crates/terminal",
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/ui",
"crates/ui2",
"crates/util",
"crates/semantic_index",

View File

@ -199,7 +199,12 @@
// "arguments": ["--stdin-filepath", "{buffer_path}"]
// }
// }
"formatter": "language_server",
// 3. Format code using Zed's Prettier integration:
// "formatter": "prettier"
// 4. Default. Format files using Zed's Prettier integration (if applicable),
// or falling back to formatting via language server:
// "formatter": "auto"
"formatter": "auto",
// How to soft-wrap long lines of text. This setting can take
// three values:
//
@ -429,6 +434,16 @@
"tab_size": 2
}
},
// Zed's Prettier integration settings.
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
// project has no other Prettier installed.
"prettier": {
// Use regular Prettier json configuration:
// "trailingComma": "es5",
// "tabWidth": 4,
// "semi": false,
// "singleQuote": true
},
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.

View File

@ -99,6 +99,10 @@ impl ChannelBuffer {
}))
}
pub fn remote_id(&self, cx: &AppContext) -> u64 {
self.buffer.read(cx).remote_id()
}
pub fn user_store(&self) -> &ModelHandle<UserStore> {
&self.user_store
}

View File

@ -114,12 +114,21 @@ impl ChannelStore {
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
while let Some(status) = connection_status.next().await {
let this = this.upgrade(&cx)?;
match status {
client::Status::Connected { .. } => {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
}
client::Status::SignedOut | client::Status::UpgradeRequired => {
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
}
_ => {
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
}
}
if status.is_connected() {
this.update(&mut cx, |this, cx| this.handle_connect(cx))
.await
.log_err()?;
} else {
this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
}
}
Some(())
@ -823,7 +832,7 @@ impl ChannelStore {
})
}
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
self.channel_index.clear();
self.channel_invitations.clear();
self.channel_participants.clear();
@ -834,7 +843,10 @@ impl ChannelStore {
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
cx.spawn_weak(|this, mut cx| async move {
cx.background().timer(RECONNECT_TIMEOUT).await;
if wait_for_reconnect {
cx.background().timer(RECONNECT_TIMEOUT).await;
}
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| {
for (_, buffer) in this.opened_buffers.drain() {

View File

@ -72,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
node_runtime = { path = "../node_runtime" }
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }

View File

@ -15,12 +15,14 @@ use gpui::{executor::Deterministic, test::EmptyView, AppContext, ModelHandle, Te
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
tree_sitter_rust, Anchor, BundledFormatter, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
use project::{
search::SearchQuery, DiagnosticSummary, FormatTrigger, HoverBlockKind, Project, ProjectPath,
};
use rand::prelude::*;
use serde_json::json;
use settings::SettingsStore;
@ -4407,8 +4409,6 @@ async fn test_formatting_buffer(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
use project::FormatTrigger;
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@ -4511,6 +4511,134 @@ async fn test_formatting_buffer(
);
}
#[gpui::test(iterations = 10)]
async fn test_prettier_formatting_buffer(
deterministic: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(&deterministic).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
// Set up a fake language server.
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let test_plugin = "test_plugin";
let mut fake_language_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
enabled_formatters: vec![BundledFormatter::Prettier {
parser_name: Some("test_parser"),
plugin_names: vec![test_plugin],
}],
..Default::default()
}))
.await;
let language = Arc::new(language);
client_a.language_registry().add(Arc::clone(&language));
// Here we insert a fake tree with a directory that exists on disk. This is needed
// because later we'll invoke a command, which requires passing a working directory
// that points to a valid location on disk.
let directory = env::current_dir().unwrap();
let buffer_text = "let one = \"two\"";
client_a
.fs()
.insert_tree(&directory, json!({ "a.rs": buffer_text }))
.await;
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
let prettier_format_suffix = project_a.update(cx_a, |project, _| {
let suffix = project.enable_test_prettier(&[test_plugin]);
project.languages().add(language);
suffix
});
let buffer_a = cx_a
.background()
.spawn(project_a.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
.await
.unwrap();
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let buffer_b = cx_b
.background()
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
.await
.unwrap();
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::Auto);
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(Formatter::LanguageServer);
});
});
});
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
panic!(
"Unexpected: prettier should be preferred since it's enabled and language supports it"
)
});
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
cx,
)
})
.await
.unwrap();
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after client's request"
);
project_a
.update(cx_a, |project, cx| {
project.format(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
cx,
)
})
.await
.unwrap();
cx_a.foreground().run_until_parked();
cx_b.foreground().run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after host's request"
);
}
#[gpui::test(iterations = 10)]
async fn test_definition(
deterministic: Arc<Deterministic>,

View File

@ -15,6 +15,7 @@ use fs::FakeFs;
use futures::{channel::oneshot, StreamExt as _};
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
use language::LanguageRegistry;
use node_runtime::FakeNodeRuntime;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use rpc::RECEIVE_TIMEOUT;
@ -218,6 +219,7 @@ impl TestServer {
build_window_options: |_, _, _| Default::default(),
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
background_actions: || &[],
node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
@ -567,6 +569,7 @@ impl TestClient {
cx.update(|cx| {
Project::local(
self.client().clone(),
self.app_state.node_runtime.clone(),
self.app_state.user_store.clone(),
self.app_state.languages.clone(),
self.app_state.fs.clone(),

View File

@ -24,7 +24,7 @@ use workspace::{
item::{FollowableItem, Item, ItemHandle},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
};
actions!(channel_view, [Deploy]);
@ -93,15 +93,36 @@ impl ChannelView {
}
pane.update(&mut cx, |pane, cx| {
pane.items_of_type::<Self>()
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
.unwrap_or_else(|| {
cx.add_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
})
})
let buffer_id = channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
return existing_view;
}
}
let view = cx.add_view(|cx| {
let mut this = Self::new(project, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
}
view
})
.ok_or_else(|| anyhow!("pane was dropped"))
})
@ -285,10 +306,14 @@ impl FollowableItem for ChannelView {
}
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
let channel = self.channel_buffer.read(cx).channel();
let channel_buffer = self.channel_buffer.read(cx);
if !channel_buffer.is_connected() {
return None;
}
Some(proto::view::Variant::ChannelView(
proto::view::ChannelView {
channel_id: channel.id,
channel_id: channel_buffer.channel().id,
editor: if let Some(proto::view::Variant::Editor(proto)) =
self.editor.read(cx).to_state_proto(cx)
{

View File

@ -19,8 +19,8 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
Override, Point,
BracketPairConfig, BundledFormatter, FakeLspAdapter, LanguageConfig, LanguageConfigOverride,
LanguageRegistry, Override, Point,
};
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
@ -5076,7 +5076,9 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
});
let mut language = Language::new(
LanguageConfig {
@ -5092,6 +5094,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
document_formatting_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
// Enable Prettier formatting for the same buffer, and ensure
// LSP is called instead of Prettier.
enabled_formatters: vec![BundledFormatter::Prettier {
parser_name: Some("test_parser"),
plugin_names: Vec::new(),
}],
..Default::default()
}))
.await;
@ -5100,7 +5108,10 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
project.update(cx, |project, _| {
project.enable_test_prettier(&[]);
project.languages().add(Arc::new(language));
});
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
@ -5218,7 +5229,9 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@ -7815,6 +7828,75 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
});
}
#[gpui::test]
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
});
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let test_plugin = "test_plugin";
let _ = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
enabled_formatters: vec![BundledFormatter::Prettier {
parser_name: Some("test_parser"),
plugin_names: vec![test_plugin],
}],
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let prettier_format_suffix = project.update(cx, |project, _| {
let suffix = project.enable_test_prettier(&[test_plugin]);
project.languages().add(Arc::new(language));
suffix
});
let buffer = project
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let buffer_text = "one\ntwo\nthree\n";
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
});
format.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string() + prettier_format_suffix,
"Test prettier formatting was not applied to the original buffer text",
);
update_test_language_settings(cx, |settings| {
settings.defaults.formatter = Some(language_settings::Formatter::Auto)
});
let format = editor.update(cx, |editor, cx| {
editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
});
format.await.unwrap();
assert_eq!(
editor.read_with(cx, |editor, cx| editor.text(cx)),
buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
"Autoformatting (via test prettier) was not applied to the original buffer text",
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point

View File

@ -85,7 +85,7 @@ pub struct RemoveOptions {
pub ignore_if_not_exists: bool,
}
#[derive(Clone, Debug)]
#[derive(Copy, Clone, Debug)]
pub struct Metadata {
pub inode: u64,
pub mtime: SystemTime,

View File

@ -227,6 +227,10 @@ impl CachedLspAdapter {
) -> Option<CodeLabel> {
self.adapter.label_for_symbol(name, kind, language).await
}
pub fn enabled_formatters(&self) -> Vec<BundledFormatter> {
self.adapter.enabled_formatters()
}
}
pub trait LspAdapterDelegate: Send + Sync {
@ -333,6 +337,33 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn language_ids(&self) -> HashMap<String, String> {
Default::default()
}
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
Vec::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BundledFormatter {
Prettier {
// See https://prettier.io/docs/en/options.html#parser for a list of valid values.
// Usually, every language has a single parser (standard or plugin-provided), hence `Some("parser_name")` can be used.
// There can not be multiple parsers for a single language, in case of a conflict, we would attempt to select the one with most plugins.
//
// But exceptions like Tailwind CSS exist, which uses standard parsers for CSS/JS/HTML/etc. but require an extra plugin to be installed.
// For those cases, `None` will install the plugin but apply other, regular parser defined for the language, and this would not be a conflict.
parser_name: Option<&'static str>,
plugin_names: Vec<&'static str>,
},
}
impl BundledFormatter {
pub fn prettier(parser_name: &'static str) -> Self {
Self::Prettier {
parser_name: Some(parser_name),
plugin_names: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -467,6 +498,7 @@ pub struct FakeLspAdapter {
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub disk_based_diagnostics_sources: Vec<String>,
pub enabled_formatters: Vec<BundledFormatter>,
}
#[derive(Clone, Debug, Default)]
@ -1729,6 +1761,7 @@ impl Default for FakeLspAdapter {
disk_based_diagnostics_progress_token: None,
initialization_options: None,
disk_based_diagnostics_sources: Vec::new(),
enabled_formatters: Vec::new(),
}
}
}
@ -1785,6 +1818,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
async fn initialization_options(&self) -> Option<Value> {
self.initialization_options.clone()
}
fn enabled_formatters(&self) -> Vec<BundledFormatter> {
self.enabled_formatters.clone()
}
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

View File

@ -50,6 +50,7 @@ pub struct LanguageSettings {
pub remove_trailing_whitespace_on_save: bool,
pub ensure_final_newline_on_save: bool,
pub formatter: Formatter,
pub prettier: HashMap<String, serde_json::Value>,
pub enable_language_server: bool,
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
#[serde(default)]
pub formatter: Option<Formatter>,
#[serde(default)]
pub prettier: Option<HashMap<String, serde_json::Value>>,
#[serde(default)]
pub enable_language_server: Option<bool>,
#[serde(default)]
pub show_copilot_suggestions: Option<bool>,
@ -149,10 +152,13 @@ pub enum ShowWhitespaceSetting {
All,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Formatter {
#[default]
Auto,
LanguageServer,
Prettier,
External {
command: Arc<str>,
arguments: Arc<[String]>,
@ -392,6 +398,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.preferred_line_length,
);
merge(&mut settings.formatter, src.formatter.clone());
merge(&mut settings.prettier, src.prettier.clone());
merge(&mut settings.format_on_save, src.format_on_save.clone());
merge(
&mut settings.remove_trailing_whitespace_on_save,

View File

@ -220,29 +220,129 @@ impl NodeRuntime for RealNodeRuntime {
}
}
pub struct FakeNodeRuntime;
pub struct FakeNodeRuntime(Option<PrettierSupport>);
struct PrettierSupport {
plugins: Vec<&'static str>,
}
impl FakeNodeRuntime {
pub fn new() -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime)
Arc::new(FakeNodeRuntime(None))
}
pub fn with_prettier_support(plugins: &[&'static str]) -> Arc<dyn NodeRuntime> {
Arc::new(FakeNodeRuntime(Some(PrettierSupport::new(plugins))))
}
}
#[async_trait::async_trait]
impl NodeRuntime for FakeNodeRuntime {
async fn binary_path(&self) -> Result<PathBuf> {
unreachable!()
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
if let Some(prettier_support) = &self.0 {
prettier_support.binary_path().await
} else {
unreachable!()
}
}
async fn run_npm_subcommand(
&self,
directory: Option<&Path>,
subcommand: &str,
args: &[&str],
) -> anyhow::Result<Output> {
if let Some(prettier_support) = &self.0 {
prettier_support
.run_npm_subcommand(directory, subcommand, args)
.await
} else {
unreachable!()
}
}
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
if let Some(prettier_support) = &self.0 {
prettier_support.npm_package_latest_version(name).await
} else {
unreachable!()
}
}
async fn npm_install_packages(
&self,
directory: &Path,
packages: &[(&str, &str)],
) -> anyhow::Result<()> {
if let Some(prettier_support) = &self.0 {
prettier_support
.npm_install_packages(directory, packages)
.await
} else {
unreachable!()
}
}
}
impl PrettierSupport {
const PACKAGE_VERSION: &str = "0.0.1";
fn new(plugins: &[&'static str]) -> Self {
Self {
plugins: plugins.to_vec(),
}
}
}
#[async_trait::async_trait]
impl NodeRuntime for PrettierSupport {
async fn binary_path(&self) -> anyhow::Result<PathBuf> {
Ok(PathBuf::from("prettier_fake_node"))
}
async fn run_npm_subcommand(&self, _: Option<&Path>, _: &str, _: &[&str]) -> Result<Output> {
unreachable!()
}
async fn npm_package_latest_version(&self, _: &str) -> Result<String> {
unreachable!()
async fn npm_package_latest_version(&self, name: &str) -> anyhow::Result<String> {
if name == "prettier" || self.plugins.contains(&name) {
Ok(Self::PACKAGE_VERSION.to_string())
} else {
panic!("Unexpected package name: {name}")
}
}
async fn npm_install_packages(&self, _: &Path, _: &[(&str, &str)]) -> Result<()> {
unreachable!()
async fn npm_install_packages(
&self,
_: &Path,
packages: &[(&str, &str)],
) -> anyhow::Result<()> {
assert_eq!(
packages.len(),
self.plugins.len() + 1,
"Unexpected packages length to install: {:?}, expected `prettier` + {:?}",
packages,
self.plugins
);
for (name, version) in packages {
assert!(
name == &"prettier" || self.plugins.contains(name),
"Unexpected package `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
name,
packages,
Self::PACKAGE_VERSION,
self.plugins
);
assert_eq!(
version,
&Self::PACKAGE_VERSION,
"Unexpected package version `{}` to install in packages {:?}, expected {} for `prettier` + {:?}",
version,
packages,
Self::PACKAGE_VERSION,
self.plugins
);
}
Ok(())
}
}

View File

@ -0,0 +1,34 @@
[package]
name = "prettier"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/prettier.rs"
doctest = false
[features]
test-support = []
[dependencies]
client = { path = "../client" }
collections = { path = "../collections"}
language = { path = "../language" }
gpui = { path = "../gpui" }
fs = { path = "../fs" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"}
util = { path = "../util" }
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
futures.workspace = true
[dev-dependencies]
language = { path = "../language", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }

View File

@ -0,0 +1,513 @@
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::Context;
use collections::{HashMap, HashSet};
use fs::Fs;
use gpui::{AsyncAppContext, ModelHandle};
use language::language_settings::language_settings;
use language::{Buffer, BundledFormatter, Diff};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use serde::{Deserialize, Serialize};
use util::paths::DEFAULT_PRETTIER_DIR;
pub enum Prettier {
Real(RealPrettier),
#[cfg(any(test, feature = "test-support"))]
Test(TestPrettier),
}
pub struct RealPrettier {
worktree_id: Option<usize>,
default: bool,
prettier_dir: PathBuf,
server: Arc<LanguageServer>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct TestPrettier {
worktree_id: Option<usize>,
prettier_dir: PathBuf,
default: bool,
}
#[derive(Debug)]
pub struct LocateStart {
pub worktree_root_path: Arc<Path>,
pub starting_path: Arc<Path>,
}
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
const PRETTIER_PACKAGE_NAME: &str = "prettier";
const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
impl Prettier {
pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
".prettierrc",
".prettierrc.json",
".prettierrc.json5",
".prettierrc.yaml",
".prettierrc.yml",
".prettierrc.toml",
".prettierrc.js",
".prettierrc.cjs",
"package.json",
"prettier.config.js",
"prettier.config.cjs",
".editorconfig",
];
#[cfg(any(test, feature = "test-support"))]
pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
pub async fn locate(
starting_path: Option<LocateStart>,
fs: Arc<dyn Fs>,
) -> anyhow::Result<PathBuf> {
let paths_to_check = match starting_path.as_ref() {
Some(starting_path) => {
let worktree_root = starting_path
.worktree_root_path
.components()
.into_iter()
.take_while(|path_component| {
path_component.as_os_str().to_string_lossy() != "node_modules"
})
.collect::<PathBuf>();
if worktree_root != starting_path.worktree_root_path.as_ref() {
vec![worktree_root]
} else {
let (worktree_root_metadata, start_path_metadata) = if starting_path
.starting_path
.as_ref()
== Path::new("")
{
let worktree_root_data =
fs.metadata(&worktree_root).await.with_context(|| {
format!(
"FS metadata fetch for worktree root path {worktree_root:?}",
)
})?;
(worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}), None)
} else {
let full_starting_path = worktree_root.join(&starting_path.starting_path);
let (worktree_root_data, start_path_data) = futures::try_join!(
fs.metadata(&worktree_root),
fs.metadata(&full_starting_path),
)
.with_context(|| {
format!("FS metadata fetch for starting path {full_starting_path:?}",)
})?;
(
worktree_root_data.unwrap_or_else(|| {
panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
}),
start_path_data,
)
};
match start_path_metadata {
Some(start_path_metadata) => {
anyhow::ensure!(worktree_root_metadata.is_dir,
"For non-empty start path, worktree root {starting_path:?} should be a directory");
anyhow::ensure!(
!start_path_metadata.is_dir,
"For non-empty start path, it should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!start_path_metadata.is_symlink,
"For non-empty start path, it should not be a symlink {starting_path:?}"
);
let file_to_format = starting_path.starting_path.as_ref();
let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
let mut current_path = worktree_root;
for path_component in file_to_format.components().into_iter() {
current_path = current_path.join(path_component);
paths_to_check.push_front(current_path.clone());
if path_component.as_os_str().to_string_lossy() == "node_modules" {
break;
}
}
paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
Vec::from(paths_to_check)
}
None => {
anyhow::ensure!(
!worktree_root_metadata.is_dir,
"For empty start path, worktree root should not be a directory {starting_path:?}"
);
anyhow::ensure!(
!worktree_root_metadata.is_symlink,
"For empty start path, worktree root should not be a symlink {starting_path:?}"
);
worktree_root
.parent()
.map(|path| vec![path.to_path_buf()])
.unwrap_or_default()
}
}
}
}
None => Vec::new(),
};
match find_closest_prettier_dir(paths_to_check, fs.as_ref())
.await
.with_context(|| format!("finding prettier starting with {starting_path:?}"))?
{
Some(prettier_dir) => Ok(prettier_dir),
None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn start(
worktree_id: Option<usize>,
_: LanguageServerId,
prettier_dir: PathBuf,
_: Arc<dyn NodeRuntime>,
_: AsyncAppContext,
) -> anyhow::Result<Self> {
Ok(
#[cfg(any(test, feature = "test-support"))]
Self::Test(TestPrettier {
worktree_id,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}),
)
}
#[cfg(not(any(test, feature = "test-support")))]
pub async fn start(
worktree_id: Option<usize>,
server_id: LanguageServerId,
prettier_dir: PathBuf,
node: Arc<dyn NodeRuntime>,
cx: AsyncAppContext,
) -> anyhow::Result<Self> {
use lsp::LanguageServerBinary;
let backgroud = cx.background();
anyhow::ensure!(
prettier_dir.is_dir(),
"Prettier dir {prettier_dir:?} is not a directory"
);
let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
anyhow::ensure!(
prettier_server.is_file(),
"no prettier server package found at {prettier_server:?}"
);
let node_path = backgroud
.spawn(async move { node.binary_path().await })
.await?;
let server = LanguageServer::new(
server_id,
LanguageServerBinary {
path: node_path,
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
},
Path::new("/"),
None,
cx,
)
.context("prettier server creation")?;
let server = backgroud
.spawn(server.initialize(None))
.await
.context("prettier server initialization")?;
Ok(Self::Real(RealPrettier {
worktree_id,
server,
default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
prettier_dir,
}))
}
pub async fn format(
&self,
buffer: &ModelHandle<Buffer>,
buffer_path: Option<PathBuf>,
cx: &AsyncAppContext,
) -> anyhow::Result<Diff> {
match self {
Self::Real(local) => {
let params = buffer.read_with(cx, |buffer, cx| {
let buffer_language = buffer.language();
let parsers_with_plugins = buffer_language
.into_iter()
.flat_map(|language| {
language
.lsp_adapters()
.iter()
.flat_map(|adapter| adapter.enabled_formatters())
.filter_map(|formatter| match formatter {
BundledFormatter::Prettier {
parser_name,
plugin_names,
} => Some((parser_name, plugin_names)),
})
})
.fold(
HashMap::default(),
|mut parsers_with_plugins, (parser_name, plugins)| {
match parser_name {
Some(parser_name) => parsers_with_plugins
.entry(parser_name)
.or_insert_with(HashSet::default)
.extend(plugins),
None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
existing_plugins.extend(plugins.iter());
}),
}
parsers_with_plugins
},
);
let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
if parsers_with_plugins.len() > 1 {
log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
}
let prettier_node_modules = self.prettier_dir().join("node_modules");
anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
let plugin_name_into_path = |plugin_name: &str| {
let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
for possible_plugin_path in [
prettier_plugin_dir.join("dist").join("index.mjs"),
prettier_plugin_dir.join("dist").join("index.js"),
prettier_plugin_dir.join("dist").join("plugin.js"),
prettier_plugin_dir.join("index.mjs"),
prettier_plugin_dir.join("index.js"),
prettier_plugin_dir.join("plugin.js"),
prettier_plugin_dir,
] {
if possible_plugin_path.is_file() {
return Some(possible_plugin_path);
}
}
None
};
let (parser, located_plugins) = match selected_parser_with_plugins {
Some((parser, plugins)) => {
// Tailwind plugin requires being added last
// https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
let mut add_tailwind_back = false;
let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
add_tailwind_back = true;
false
} else {
true
}
}).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
if add_tailwind_back {
plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
}
(Some(parser.to_string()), plugins)
},
None => (None, Vec::new()),
};
let prettier_options = if self.is_default() {
let language_settings = language_settings(buffer_language, buffer.file(), cx);
let mut options = language_settings.prettier.clone();
if !options.contains_key("tabWidth") {
options.insert(
"tabWidth".to_string(),
serde_json::Value::Number(serde_json::Number::from(
language_settings.tab_size.get(),
)),
);
}
if !options.contains_key("printWidth") {
options.insert(
"printWidth".to_string(),
serde_json::Value::Number(serde_json::Number::from(
language_settings.preferred_line_length,
)),
);
}
Some(options)
} else {
None
};
let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
match located_plugin_path {
Some(path) => Some(path),
None => {
log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
None},
}
}).collect();
log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
anyhow::Ok(FormatParams {
text: buffer.text(),
options: FormatOptions {
parser,
plugins,
path: buffer_path,
prettier_options,
},
})
}).context("prettier params calculation")?;
let response = local
.server
.request::<Format>(params)
.await
.context("prettier format request")?;
let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
Ok(diff_task.await)
}
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(buffer
.read_with(cx, |buffer, cx| {
let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
buffer.diff(formatted_text, cx)
})
.await),
}
}
pub async fn clear_cache(&self) -> anyhow::Result<()> {
match self {
Self::Real(local) => local
.server
.request::<ClearCache>(())
.await
.context("prettier clear cache"),
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => Ok(()),
}
}
pub fn server(&self) -> Option<&Arc<LanguageServer>> {
match self {
Self::Real(local) => Some(&local.server),
#[cfg(any(test, feature = "test-support"))]
Self::Test(_) => None,
}
}
pub fn is_default(&self) -> bool {
match self {
Self::Real(local) => local.default,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => test_prettier.default,
}
}
pub fn prettier_dir(&self) -> &Path {
match self {
Self::Real(local) => &local.prettier_dir,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => &test_prettier.prettier_dir,
}
}
pub fn worktree_id(&self) -> Option<usize> {
match self {
Self::Real(local) => local.worktree_id,
#[cfg(any(test, feature = "test-support"))]
Self::Test(test_prettier) => test_prettier.worktree_id,
}
}
}
async fn find_closest_prettier_dir(
paths_to_check: Vec<PathBuf>,
fs: &dyn Fs,
) -> anyhow::Result<Option<PathBuf>> {
for path in paths_to_check {
let possible_package_json = path.join("package.json");
if let Some(package_json_metadata) = fs
.metadata(&possible_package_json)
.await
.with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
{
if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
let package_json_contents = fs
.load(&possible_package_json)
.await
.with_context(|| format!("reading {possible_package_json:?} file contents"))?;
if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
&package_json_contents,
) {
if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
{
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return Ok(Some(path));
}
}
}
}
}
let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
if let Some(node_modules_location_metadata) = fs
.metadata(&possible_node_modules_location)
.await
.with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
{
if node_modules_location_metadata.is_dir {
return Ok(Some(path));
}
}
}
Ok(None)
}
enum Format {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatParams {
text: String,
options: FormatOptions,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatOptions {
plugins: Vec<PathBuf>,
parser: Option<String>,
#[serde(rename = "filepath")]
path: Option<PathBuf>,
prettier_options: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormatResult {
text: String,
}
impl lsp::request::Request for Format {
type Params = FormatParams;
type Result = FormatResult;
const METHOD: &'static str = "prettier/format";
}
enum ClearCache {}
impl lsp::request::Request for ClearCache {
type Params = ();
type Result = ();
const METHOD: &'static str = "prettier/clear_cache";
}

View File

@ -0,0 +1,217 @@
const { Buffer } = require('buffer');
const fs = require("fs");
const path = require("path");
const { once } = require('events');
const prettierContainerPath = process.argv[2];
if (prettierContainerPath == null || prettierContainerPath.length == 0) {
process.stderr.write(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`);
process.exit(1);
}
fs.stat(prettierContainerPath, (err, stats) => {
if (err) {
process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`);
process.exit(1);
}
if (!stats.isDirectory()) {
process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`);
process.exit(1);
}
});
const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier');
class Prettier {
constructor(path, prettier, config) {
this.path = path;
this.prettier = prettier;
this.config = config;
}
}
(async () => {
let prettier;
let config;
try {
prettier = await loadPrettier(prettierPath);
config = await prettier.resolveConfig(prettierPath) || {};
} catch (e) {
process.stderr.write(`Failed to load prettier: ${e}\n`);
process.exit(1);
}
process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`);
process.stdin.resume();
handleBuffer(new Prettier(prettierPath, prettier, config));
})()
async function handleBuffer(prettier) {
for await (const messageText of readStdin()) {
let message;
try {
message = JSON.parse(messageText);
} catch (e) {
sendResponse(makeError(`Failed to parse message '${messageText}': ${e}`));
continue;
}
// allow concurrent request handling by not `await`ing the message handling promise (async function)
handleMessage(message, prettier).catch(e => {
sendResponse({ id: message.id, ...makeError(`error during message handling: ${e}`) });
});
}
}
const headerSeparator = "\r\n";
const contentLengthHeaderName = 'Content-Length';
async function* readStdin() {
let buffer = Buffer.alloc(0);
let streamEnded = false;
process.stdin.on('end', () => {
streamEnded = true;
});
process.stdin.on('data', (data) => {
buffer = Buffer.concat([buffer, data]);
});
async function handleStreamEnded(errorMessage) {
sendResponse(makeError(errorMessage));
buffer = Buffer.alloc(0);
messageLength = null;
await once(process.stdin, 'readable');
streamEnded = false;
}
try {
let headersLength = null;
let messageLength = null;
main_loop: while (true) {
if (messageLength === null) {
while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) {
if (streamEnded) {
await handleStreamEnded('Unexpected end of stream: headers not found');
continue main_loop;
} else if (buffer.length > contentLengthHeaderName.length * 10) {
await handleStreamEnded(`Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`);
continue main_loop;
}
await once(process.stdin, 'readable');
}
const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString('ascii');
const contentLengthHeader = headers.split(headerSeparator)
.map(header => header.split(':'))
.filter(header => header[2] === undefined)
.filter(header => (header[1] || '').length > 0)
.find(header => (header[0] || '').trim() === contentLengthHeaderName);
const contentLength = (contentLengthHeader || [])[1];
if (contentLength === undefined) {
await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`);
continue main_loop;
}
headersLength = headers.length + headerSeparator.length * 2;
messageLength = parseInt(contentLength, 10);
}
while (buffer.length < (headersLength + messageLength)) {
if (streamEnded) {
await handleStreamEnded(
`Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`);
continue main_loop;
}
await once(process.stdin, 'readable');
}
const messageEnd = headersLength + messageLength;
const message = buffer.subarray(headersLength, messageEnd);
buffer = buffer.subarray(messageEnd);
headersLength = null;
messageLength = null;
yield message.toString('utf8');
}
} catch (e) {
sendResponse(makeError(`Error reading stdin: ${e}`));
} finally {
process.stdin.off('data', () => { });
}
}
async function handleMessage(message, prettier) {
const { method, id, params } = message;
if (method === undefined) {
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
}
if (id === undefined) {
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
}
if (method === 'prettier/format') {
if (params === undefined || params.text === undefined) {
throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`);
}
if (params.options === undefined) {
throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`);
}
let resolvedConfig = {};
if (params.options.filepath !== undefined) {
resolvedConfig = await prettier.prettier.resolveConfig(params.options.filepath) || {};
}
const options = {
...(params.options.prettierOptions || prettier.config),
...resolvedConfig,
parser: params.options.parser,
plugins: params.options.plugins,
path: params.options.filepath
};
process.stderr.write(`Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${params.options.filepath || ''}' with options: ${JSON.stringify(options)}\n`);
const formattedText = await prettier.prettier.format(params.text, options);
sendResponse({ id, result: { text: formattedText } });
} else if (method === 'prettier/clear_cache') {
prettier.prettier.clearConfigCache();
prettier.config = await prettier.prettier.resolveConfig(prettier.path) || {};
sendResponse({ id, result: null });
} else if (method === 'initialize') {
sendResponse({
id,
result: {
"capabilities": {}
}
});
} else {
throw new Error(`Unknown method: ${method}`);
}
}
function makeError(message) {
return {
error: {
"code": -32600, // invalid request code
message,
}
};
}
function sendResponse(response) {
const responsePayloadString = JSON.stringify({
jsonrpc: "2.0",
...response
});
const headers = `${contentLengthHeaderName}: ${Buffer.byteLength(responsePayloadString)}${headerSeparator}${headerSeparator}`;
process.stdout.write(headers + responsePayloadString);
}
function loadPrettier(prettierPath) {
return new Promise((resolve, reject) => {
fs.access(prettierPath, fs.constants.F_OK, (err) => {
if (err) {
reject(`Path '${prettierPath}' does not exist.Error: ${err}`);
} else {
try {
resolve(require(prettierPath));
} catch (err) {
reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`);
}
}
});
});
}

View File

@ -15,6 +15,7 @@ test-support = [
"language/test-support",
"settings/test-support",
"text/test-support",
"prettier/test-support",
]
[dependencies]
@ -31,6 +32,8 @@ git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime" }
prettier = { path = "../prettier" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
@ -73,6 +76,7 @@ gpui = { path = "../gpui", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
prettier = { path = "../prettier", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
git2.workspace = true

View File

@ -20,7 +20,7 @@ use futures::{
mpsc::{self, UnboundedReceiver},
oneshot,
},
future::{try_join_all, Shared},
future::{self, try_join_all, Shared},
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
@ -31,17 +31,19 @@ use gpui::{
};
use itertools::Itertools;
use language::{
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
language_settings::{
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
},
point_to_lsp,
proto::{
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
serialize_anchor, serialize_version, split_operations,
},
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
ToOffset, ToPointUtf16, Transaction, Unclipped,
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, BundledFormatter, CachedLspAdapter,
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
};
use log::error;
use lsp::{
@ -49,7 +51,9 @@ use lsp::{
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf,
};
use lsp_command::*;
use node_runtime::NodeRuntime;
use postage::watch;
use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS};
use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use search::SearchQuery;
@ -75,10 +79,13 @@ use std::{
time::{Duration, Instant},
};
use terminals::Terminals;
use text::Anchor;
use text::{Anchor, LineEnding, Rope};
use util::{
debug_panic, defer, http::HttpClient, merge_json_value_into,
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
debug_panic, defer,
http::HttpClient,
merge_json_value_into,
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
post_inc, ResultExt, TryFutureExt as _,
};
pub use fs::*;
@ -152,6 +159,11 @@ pub struct Project {
copilot_lsp_subscription: Option<gpui::Subscription>,
copilot_log_subscription: Option<lsp::Subscription>,
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
node: Option<Arc<dyn NodeRuntime>>,
prettier_instances: HashMap<
(Option<WorktreeId>, PathBuf),
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
>,
}
struct DelayedDebounced {
@ -605,6 +617,7 @@ impl Project {
pub fn local(
client: Arc<Client>,
node: Arc<dyn NodeRuntime>,
user_store: ModelHandle<UserStore>,
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
@ -660,6 +673,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
node: Some(node),
prettier_instances: HashMap::default(),
}
})
}
@ -757,6 +772,8 @@ impl Project {
copilot_lsp_subscription,
copilot_log_subscription: None,
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
node: None,
prettier_instances: HashMap::default(),
};
for worktree in worktrees {
let _ = this.add_worktree(&worktree, cx);
@ -795,8 +812,16 @@ impl Project {
let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
let project =
cx.update(|cx| Project::local(client, user_store, Arc::new(languages), fs, cx));
let project = cx.update(|cx| {
Project::local(
client,
node_runtime::FakeNodeRuntime::new(),
user_store,
Arc::new(languages),
fs,
cx,
)
});
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
@ -810,19 +835,37 @@ impl Project {
project
}
/// Enables a prettier mock that avoids interacting with node runtime, prettier LSP wrapper, or any real file changes.
/// Instead, if appends the suffix to every input, this suffix is returned by this method.
#[cfg(any(test, feature = "test-support"))]
pub fn enable_test_prettier(&mut self, plugins: &[&'static str]) -> &'static str {
self.node = Some(node_runtime::FakeNodeRuntime::with_prettier_support(
plugins,
));
Prettier::FORMAT_SUFFIX
}
fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
let mut language_servers_to_start = Vec::new();
let mut language_formatters_to_check = Vec::new();
for buffer in self.opened_buffers.values() {
if let Some(buffer) = buffer.upgrade(cx) {
let buffer = buffer.read(cx);
if let Some((file, language)) = buffer.file().zip(buffer.language()) {
let settings = language_settings(Some(language), Some(file), cx);
let buffer_file = File::from_dyn(buffer.file());
let buffer_language = buffer.language();
let settings = language_settings(buffer_language, buffer.file(), cx);
if let Some(language) = buffer_language {
if settings.enable_language_server {
if let Some(file) = File::from_dyn(Some(file)) {
if let Some(file) = buffer_file {
language_servers_to_start
.push((file.worktree.clone(), language.clone()));
.push((file.worktree.clone(), Arc::clone(language)));
}
}
language_formatters_to_check.push((
buffer_file.map(|f| f.worktree_id(cx)),
Arc::clone(language),
settings.clone(),
));
}
}
}
@ -875,6 +918,11 @@ impl Project {
.detach();
}
for (worktree, language, settings) in language_formatters_to_check {
self.install_default_formatters(worktree, &language, &settings, cx)
.detach_and_log_err(cx);
}
// Start all the newly-enabled language servers.
for (worktree, language) in language_servers_to_start {
let worktree_path = worktree.read(cx).abs_path();
@ -2623,7 +2671,26 @@ impl Project {
}
});
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
let buffer_file = buffer.read(cx).file().cloned();
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
let task_buffer = buffer.clone();
let prettier_installation_task =
self.install_default_formatters(worktree, &new_language, &settings, cx);
cx.spawn(|project, mut cx| async move {
prettier_installation_task.await?;
let _ = project
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(&task_buffer, cx)
})
.await;
anyhow::Ok(())
})
.detach_and_log_err(cx);
if let Some(file) = buffer_file {
let worktree = file.worktree.clone();
if let Some(tree) = worktree.read(cx).as_local() {
self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
@ -3949,7 +4016,7 @@ impl Project {
push_to_history: bool,
trigger: FormatTrigger,
cx: &mut ModelContext<Project>,
) -> Task<Result<ProjectTransaction>> {
) -> Task<anyhow::Result<ProjectTransaction>> {
if self.is_local() {
let mut buffers_with_paths_and_servers = buffers
.into_iter()
@ -4027,6 +4094,7 @@ impl Project {
enum FormatOperation {
Lsp(Vec<(Range<Anchor>, String)>),
External(Diff),
Prettier(Diff),
}
// Apply language-specific formatting using either a language server
@ -4062,8 +4130,8 @@ impl Project {
| (_, FormatOnSave::External { command, arguments }) => {
if let Some(buffer_abs_path) = buffer_abs_path {
format_operation = Self::format_via_external_command(
&buffer,
&buffer_abs_path,
buffer,
buffer_abs_path,
&command,
&arguments,
&mut cx,
@ -4076,6 +4144,69 @@ impl Project {
.map(FormatOperation::External);
}
}
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
if let Some(prettier_task) = this
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(buffer, cx)
}).await {
match prettier_task.await
{
Ok(prettier) => {
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
});
format_operation = Some(FormatOperation::Prettier(
prettier
.format(buffer, buffer_path, &cx)
.await
.context("formatting via prettier")?,
));
}
Err(e) => anyhow::bail!(
"Failed to create prettier instance for buffer during autoformatting: {e:#}"
),
}
} else if let Some((language_server, buffer_abs_path)) =
language_server.as_ref().zip(buffer_abs_path.as_ref())
{
format_operation = Some(FormatOperation::Lsp(
Self::format_via_lsp(
&this,
&buffer,
buffer_abs_path,
&language_server,
tab_size,
&mut cx,
)
.await
.context("failed to format via language server")?,
));
}
}
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
if let Some(prettier_task) = this
.update(&mut cx, |project, cx| {
project.prettier_instance_for_buffer(buffer, cx)
}).await {
match prettier_task.await
{
Ok(prettier) => {
let buffer_path = buffer.read_with(&cx, |buffer, cx| {
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
});
format_operation = Some(FormatOperation::Prettier(
prettier
.format(buffer, buffer_path, &cx)
.await
.context("formatting via prettier")?,
));
}
Err(e) => anyhow::bail!(
"Failed to create prettier instance for buffer during formatting: {e:#}"
),
}
}
}
};
buffer.update(&mut cx, |b, cx| {
@ -4100,6 +4231,9 @@ impl Project {
FormatOperation::External(diff) => {
b.apply_diff(diff, cx);
}
FormatOperation::Prettier(diff) => {
b.apply_diff(diff, cx);
}
}
if let Some(transaction_id) = whitespace_transaction_id {
@ -5873,6 +6007,7 @@ impl Project {
this.update_local_worktree_buffers(&worktree, changes, cx);
this.update_local_worktree_language_servers(&worktree, changes, cx);
this.update_local_worktree_settings(&worktree, changes, cx);
this.update_prettier_settings(&worktree, changes, cx);
cx.emit(Event::WorktreeUpdatedEntries(
worktree.read(cx).id(),
changes.clone(),
@ -6252,6 +6387,69 @@ impl Project {
.detach();
}
fn update_prettier_settings(
&self,
worktree: &ModelHandle<Worktree>,
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
cx: &mut ModelContext<'_, Project>,
) {
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
.iter()
.map(Path::new)
.collect::<HashSet<_>>();
let prettier_config_file_changed = changes
.iter()
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
.filter(|(path, _, _)| {
!path
.components()
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
})
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
let current_worktree_id = worktree.read(cx).id();
if let Some((config_path, _, _)) = prettier_config_file_changed {
log::info!(
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
);
let prettiers_to_reload = self
.prettier_instances
.iter()
.filter_map(|((worktree_id, prettier_path), prettier_task)| {
if worktree_id.is_none() || worktree_id == &Some(current_worktree_id) {
Some((*worktree_id, prettier_path.clone(), prettier_task.clone()))
} else {
None
}
})
.collect::<Vec<_>>();
cx.background()
.spawn(async move {
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
async move {
prettier_task.await?
.clear_cache()
.await
.with_context(|| {
format!(
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
)
})
.map_err(Arc::new)
}
}))
.await
{
if let Err(e) = task_result {
log::error!("Failed to clear cache for prettier: {e:#}");
}
}
})
.detach();
}
}
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
let new_active_entry = entry.and_then(|project_path| {
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@ -8109,6 +8307,236 @@ impl Project {
Vec::new()
}
}
fn prettier_instance_for_buffer(
&mut self,
buffer: &ModelHandle<Buffer>,
cx: &mut ModelContext<Self>,
) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
let buffer = buffer.read(cx);
let buffer_file = buffer.file();
let Some(buffer_language) = buffer.language() else {
return Task::ready(None);
};
if !buffer_language
.lsp_adapters()
.iter()
.flat_map(|adapter| adapter.enabled_formatters())
.any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
{
return Task::ready(None);
}
let buffer_file = File::from_dyn(buffer_file);
let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
let worktree_path = buffer_file
.as_ref()
.and_then(|file| Some(file.worktree.read(cx).abs_path()));
let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
let Some(node) = self.node.as_ref().map(Arc::clone) else {
return Task::ready(None);
};
cx.spawn(|this, mut cx| async move {
let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
let prettier_dir = match cx
.background()
.spawn(Prettier::locate(
worktree_path.zip(buffer_path).map(
|(worktree_root_path, starting_path)| LocateStart {
worktree_root_path,
starting_path,
},
),
fs,
))
.await
{
Ok(path) => path,
Err(e) => {
return Some(
Task::ready(Err(Arc::new(e.context(
"determining prettier path for worktree {worktree_path:?}",
))))
.shared(),
);
}
};
if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
project
.prettier_instances
.get(&(worktree_id, prettier_dir.clone()))
.cloned()
}) {
return Some(existing_prettier);
}
log::info!("Found prettier in {prettier_dir:?}, starting.");
let task_prettier_dir = prettier_dir.clone();
let weak_project = this.downgrade();
let new_server_id =
this.update(&mut cx, |this, _| this.languages.next_language_server_id());
let new_prettier_task = cx
.spawn(|mut cx| async move {
let prettier = Prettier::start(
worktree_id.map(|id| id.to_usize()),
new_server_id,
task_prettier_dir,
node,
cx.clone(),
)
.await
.context("prettier start")
.map_err(Arc::new)?;
log::info!("Started prettier in {:?}", prettier.prettier_dir());
if let Some((project, prettier_server)) =
weak_project.upgrade(&mut cx).zip(prettier.server())
{
project.update(&mut cx, |project, cx| {
let name = if prettier.is_default() {
LanguageServerName(Arc::from("prettier (default)"))
} else {
let prettier_dir = prettier.prettier_dir();
let worktree_path = prettier
.worktree_id()
.map(WorktreeId::from_usize)
.and_then(|id| project.worktree_for_id(id, cx))
.map(|worktree| worktree.read(cx).abs_path());
match worktree_path {
Some(worktree_path) => {
if worktree_path.as_ref() == prettier_dir {
LanguageServerName(Arc::from(format!(
"prettier ({})",
prettier_dir
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default()
)))
} else {
let dir_to_display = match prettier_dir
.strip_prefix(&worktree_path)
.ok()
{
Some(relative_path) => relative_path,
None => prettier_dir,
};
LanguageServerName(Arc::from(format!(
"prettier ({})",
dir_to_display.display(),
)))
}
}
None => LanguageServerName(Arc::from(format!(
"prettier ({})",
prettier_dir.display(),
))),
}
};
project
.supplementary_language_servers
.insert(new_server_id, (name, Arc::clone(prettier_server)));
cx.emit(Event::LanguageServerAdded(new_server_id));
});
}
Ok(Arc::new(prettier)).map_err(Arc::new)
})
.shared();
this.update(&mut cx, |project, _| {
project
.prettier_instances
.insert((worktree_id, prettier_dir), new_prettier_task.clone());
});
Some(new_prettier_task)
})
} else if self.remote_id().is_some() {
return Task::ready(None);
} else {
Task::ready(Some(
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
))
}
}
fn install_default_formatters(
&self,
worktree: Option<WorktreeId>,
new_language: &Language,
language_settings: &LanguageSettings,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
match &language_settings.formatter {
Formatter::Prettier { .. } | Formatter::Auto => {}
Formatter::LanguageServer | Formatter::External { .. } => return Task::ready(Ok(())),
};
let Some(node) = self.node.as_ref().cloned() else {
return Task::ready(Ok(()));
};
let mut prettier_plugins = None;
for formatter in new_language
.lsp_adapters()
.into_iter()
.flat_map(|adapter| adapter.enabled_formatters())
{
match formatter {
BundledFormatter::Prettier { plugin_names, .. } => prettier_plugins
.get_or_insert_with(|| HashSet::default())
.extend(plugin_names),
}
}
let Some(prettier_plugins) = prettier_plugins else {
return Task::ready(Ok(()));
};
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
let already_running_prettier = self
.prettier_instances
.get(&(worktree, default_prettier_dir.to_path_buf()))
.cloned();
let fs = Arc::clone(&self.fs);
cx.background()
.spawn(async move {
let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE);
// method creates parent directory if it doesn't exist
fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await
.with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?;
let packages_to_versions = future::try_join_all(
prettier_plugins
.iter()
.chain(Some(&"prettier"))
.map(|package_name| async {
let returned_package_name = package_name.to_string();
let latest_version = node.npm_package_latest_version(package_name)
.await
.with_context(|| {
format!("fetching latest npm version for package {returned_package_name}")
})?;
anyhow::Ok((returned_package_name, latest_version))
}),
)
.await
.context("fetching latest npm versions")?;
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
let borrowed_packages = packages_to_versions.iter().map(|(package, version)| {
(package.as_str(), version.as_str())
}).collect::<Vec<_>>();
node.npm_install_packages(default_prettier_dir, &borrowed_packages).await.context("fetching formatter packages")?;
if !prettier_plugins.is_empty() {
if let Some(prettier) = already_running_prettier {
prettier.await.map_err(|e| anyhow::anyhow!("Default prettier startup await failure: {e:#}"))?.clear_cache().await.context("clearing default prettier cache after plugins install")?;
}
}
anyhow::Ok(())
})
}
}
fn subscribe_for_copilot_events(

View File

@ -494,6 +494,7 @@ fn main() {
let project = cx.update(|cx| {
Project::local(
client.clone(),
node_runtime::FakeNodeRuntime::new(),
user_store.clone(),
languages.clone(),
fs.clone(),

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
[package]
name = "storybook"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "storybook"
path = "src/storybook.rs"
[dependencies]
anyhow.workspace = true
clap = { version = "4.4", features = ["derive", "string"] }
chrono = "0.4"
derive_more.workspace = true
fs = { path = "../fs" }
futures.workspace = true
gpui2 = { path = "../gpui2" }
itertools = "0.11.0"
log.workspace = true
rust-embed.workspace = true
serde.workspace = true
settings = { path = "../settings" }
simplelog = "0.9"
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" }
[dev-dependencies]
gpui2 = { path = "../gpui2", features = ["test-support"] }

View File

@ -1,5 +0,0 @@
fn main() {
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
// TODO: We shouldn't depend on WebRTC in editor
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
}

View File

@ -1,72 +0,0 @@
Much of element styling is now handled by an external engine.
How do I make an element hover.
There's a hover style.
Hoverable needs to wrap another element. That element can be styled.
```rs
struct Hoverable<E: Element> {
}
impl<V> Element<V> for Hoverable {
}
```
```rs
#[derive(Styled, Interactive)]
pub struct Div {
declared_style: StyleRefinement,
interactions: Interactions
}
pub trait Styled {
fn declared_style(&mut self) -> &mut StyleRefinement;
fn compute_style(&mut self) -> Style {
Style::default().refine(self.declared_style())
}
// All the tailwind classes, modifying self.declared_style()
}
impl Style {
pub fn paint_background<V>(layout: Layout, cx: &mut PaintContext<V>);
pub fn paint_foreground<V>(layout: Layout, cx: &mut PaintContext<V>);
}
pub trait Interactive<V> {
fn interactions(&mut self) -> &mut Interactions<V>;
fn on_click(self, )
}
struct Interactions<V> {
click: SmallVec<[<Rc<dyn Fn(&mut V, &dyn Any, )>; 1]>,
}
```
```rs
trait Stylable {
type Style;
fn with_style(self, style: Self::Style) -> Self;
}
```

View File

@ -1,3 +0,0 @@
pub mod components;
pub mod elements;
pub mod kitchen_sink;

View File

@ -1,22 +0,0 @@
pub mod assistant_panel;
pub mod breadcrumb;
pub mod buffer;
pub mod chat_panel;
pub mod collab_panel;
pub mod context_menu;
pub mod facepile;
pub mod keybinding;
pub mod language_selector;
pub mod multi_buffer;
pub mod palette;
pub mod panel;
pub mod project_panel;
pub mod recent_projects;
pub mod status_bar;
pub mod tab;
pub mod tab_bar;
pub mod terminal;
pub mod theme_selector;
pub mod title_bar;
pub mod toolbar;
pub mod traffic_lights;

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::AssistantPanel;
use crate::story::Story;
#[derive(Element, Default)]
pub struct AssistantPanelStory {}
impl AssistantPanelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, AssistantPanel<V>>(cx))
.child(Story::label(cx, "Default"))
.child(AssistantPanel::new())
}
}

View File

@ -1,45 +0,0 @@
use std::path::PathBuf;
use std::str::FromStr;
use ui::prelude::*;
use ui::{Breadcrumb, HighlightedText, Symbol};
use crate::story::Story;
#[derive(Element, Default)]
pub struct BreadcrumbStory {}
impl BreadcrumbStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
Story::container(cx)
.child(Story::title_for::<_, Breadcrumb>(cx))
.child(Story::label(cx, "Default"))
.child(Breadcrumb::new(
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
vec![
Symbol(vec![
HighlightedText {
text: "impl ".to_string(),
color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "BreadcrumbStory".to_string(),
color: HighlightColor::Function.hsla(&theme),
},
]),
Symbol(vec![
HighlightedText {
text: "fn ".to_string(),
color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "render".to_string(),
color: HighlightColor::Function.hsla(&theme),
},
]),
],
))
}
}

View File

@ -1,36 +0,0 @@
use gpui2::geometry::rems;
use ui::prelude::*;
use ui::{
empty_buffer_example, hello_world_rust_buffer_example,
hello_world_rust_buffer_with_status_example, Buffer,
};
use crate::story::Story;
#[derive(Element, Default)]
pub struct BufferStory {}
impl BufferStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
Story::container(cx)
.child(Story::title_for::<_, Buffer>(cx))
.child(Story::label(cx, "Default"))
.child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
.child(Story::label(cx, "Hello World (Rust)"))
.child(
div()
.w(rems(64.))
.h_96()
.child(hello_world_rust_buffer_example(&theme)),
)
.child(Story::label(cx, "Hello World (Rust) with Status"))
.child(
div()
.w(rems(64.))
.h_96()
.child(hello_world_rust_buffer_with_status_example(&theme)),
)
}
}

View File

@ -1,46 +0,0 @@
use chrono::DateTime;
use ui::prelude::*;
use ui::{ChatMessage, ChatPanel, Panel};
use crate::story::Story;
#[derive(Element, Default)]
pub struct ChatPanelStory {}
impl ChatPanelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, ChatPanel<V>>(cx))
.child(Story::label(cx, "Default"))
.child(Panel::new(
ScrollState::default(),
|_, _| vec![ChatPanel::new(ScrollState::default()).into_any()],
Box::new(()),
))
.child(Story::label(cx, "With Mesages"))
.child(Panel::new(
ScrollState::default(),
|_, _| {
vec![ChatPanel::new(ScrollState::default())
.with_messages(vec![
ChatMessage::new(
"osiewicz".to_string(),
"is this thing on?".to_string(),
DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
.unwrap()
.naive_local(),
),
ChatMessage::new(
"maxdeviant".to_string(),
"Reading you loud and clear!".to_string(),
DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
.unwrap()
.naive_local(),
),
])
.into_any()]
},
Box::new(()),
))
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::CollabPanel;
use crate::story::Story;
#[derive(Element, Default)]
pub struct CollabPanelStory {}
impl CollabPanelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, CollabPanel<V>>(cx))
.child(Story::label(cx, "Default"))
.child(CollabPanel::new(ScrollState::default()))
}
}

View File

@ -1,21 +0,0 @@
use ui::prelude::*;
use ui::{ContextMenu, ContextMenuItem, Label};
use crate::story::Story;
#[derive(Element, Default)]
pub struct ContextMenuStory {}
impl ContextMenuStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
//.fill(theme.middle.base.default.background)
.child(Story::title_for::<_, ContextMenu>(cx))
.child(Story::label(cx, "Default"))
.child(ContextMenu::new([
ContextMenuItem::header("Section header"),
ContextMenuItem::Separator,
ContextMenuItem::entry(Label::new("Some entry")),
]))
}
}

View File

@ -1,25 +0,0 @@
use ui::prelude::*;
use ui::{static_players, Facepile};
use crate::story::Story;
#[derive(Element, Default)]
pub struct FacepileStory {}
impl FacepileStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let players = static_players();
Story::container(cx)
.child(Story::title_for::<_, Facepile>(cx))
.child(Story::label(cx, "Default"))
.child(
div()
.flex()
.gap_3()
.child(Facepile::new(players.clone().into_iter().take(1)))
.child(Facepile::new(players.clone().into_iter().take(2)))
.child(Facepile::new(players.clone().into_iter().take(3))),
)
}
}

View File

@ -1,64 +0,0 @@
use itertools::Itertools;
use strum::IntoEnumIterator;
use ui::prelude::*;
use ui::{Keybinding, ModifierKey, ModifierKeys};
use crate::story::Story;
#[derive(Element, Default)]
pub struct KeybindingStory {}
impl KeybindingStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let all_modifier_permutations = ModifierKey::iter().permutations(2);
Story::container(cx)
.child(Story::title_for::<_, Keybinding>(cx))
.child(Story::label(cx, "Single Key"))
.child(Keybinding::new("Z".to_string(), ModifierKeys::new()))
.child(Story::label(cx, "Single Key with Modifier"))
.child(
div()
.flex()
.gap_3()
.children(ModifierKey::iter().map(|modifier| {
Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier))
})),
)
.child(Story::label(cx, "Single Key with Modifier (Permuted)"))
.child(
div().flex().flex_col().children(
all_modifier_permutations
.chunks(4)
.into_iter()
.map(|chunk| {
div()
.flex()
.gap_4()
.py_3()
.children(chunk.map(|permutation| {
let mut modifiers = ModifierKeys::new();
for modifier in permutation {
modifiers = modifiers.add(modifier);
}
Keybinding::new("X".to_string(), modifiers)
}))
}),
),
)
.child(Story::label(cx, "Single Key with All Modifiers"))
.child(Keybinding::new("Z".to_string(), ModifierKeys::all()))
.child(Story::label(cx, "Chord"))
.child(Keybinding::new_chord(
("A".to_string(), ModifierKeys::new()),
("Z".to_string(), ModifierKeys::new()),
))
.child(Story::label(cx, "Chord with Modifier"))
.child(Keybinding::new_chord(
("A".to_string(), ModifierKeys::new().control(true)),
("Z".to_string(), ModifierKeys::new().shift(true)),
))
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::LanguageSelector;
use crate::story::Story;
#[derive(Element, Default)]
pub struct LanguageSelectorStory {}
impl LanguageSelectorStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, LanguageSelector>(cx))
.child(Story::label(cx, "Default"))
.child(LanguageSelector::new())
}
}

View File

@ -1,24 +0,0 @@
use ui::prelude::*;
use ui::{hello_world_rust_buffer_example, MultiBuffer};
use crate::story::Story;
#[derive(Element, Default)]
pub struct MultiBufferStory {}
impl MultiBufferStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
Story::container(cx)
.child(Story::title_for::<_, MultiBuffer<V>>(cx))
.child(Story::label(cx, "Default"))
.child(MultiBuffer::new(vec![
hello_world_rust_buffer_example(&theme),
hello_world_rust_buffer_example(&theme),
hello_world_rust_buffer_example(&theme),
hello_world_rust_buffer_example(&theme),
hello_world_rust_buffer_example(&theme),
]))
}
}

View File

@ -1,53 +0,0 @@
use ui::prelude::*;
use ui::{Keybinding, ModifierKeys, Palette, PaletteItem};
use crate::story::Story;
#[derive(Element, Default)]
pub struct PaletteStory {}
impl PaletteStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Palette<V>>(cx))
.child(Story::label(cx, "Default"))
.child(Palette::new(ScrollState::default()))
.child(Story::label(cx, "With Items"))
.child(
Palette::new(ScrollState::default())
.placeholder("Execute a command...")
.items(vec![
PaletteItem::new("theme selector: toggle").keybinding(
Keybinding::new_chord(
("k".to_string(), ModifierKeys::new().command(true)),
("t".to_string(), ModifierKeys::new().command(true)),
),
),
PaletteItem::new("assistant: inline assist").keybinding(Keybinding::new(
"enter".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("assistant: quote selection").keybinding(Keybinding::new(
">".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("assistant: toggle focus").keybinding(Keybinding::new(
"?".to_string(),
ModifierKeys::new().command(true),
)),
PaletteItem::new("auto update: check"),
PaletteItem::new("auto update: view release notes"),
PaletteItem::new("branches: open recent").keybinding(Keybinding::new(
"b".to_string(),
ModifierKeys::new().command(true).alt(true),
)),
PaletteItem::new("chat panel: toggle focus"),
PaletteItem::new("cli: install"),
PaletteItem::new("client: sign in"),
PaletteItem::new("client: sign out"),
PaletteItem::new("editor: cancel")
.keybinding(Keybinding::new("escape".to_string(), ModifierKeys::new())),
]),
)
}
}

View File

@ -1,25 +0,0 @@
use ui::prelude::*;
use ui::{Label, Panel};
use crate::story::Story;
#[derive(Element, Default)]
pub struct PanelStory {}
impl PanelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Panel<V>>(cx))
.child(Story::label(cx, "Default"))
.child(Panel::new(
ScrollState::default(),
|_, _| {
vec![div()
.overflow_y_scroll(ScrollState::default())
.children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1))))
.into_any()]
},
Box::new(()),
))
}
}

View File

@ -1,20 +0,0 @@
use ui::prelude::*;
use ui::{Panel, ProjectPanel};
use crate::story::Story;
#[derive(Element, Default)]
pub struct ProjectPanelStory {}
impl ProjectPanelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, ProjectPanel<V>>(cx))
.child(Story::label(cx, "Default"))
.child(Panel::new(
ScrollState::default(),
|_, _| vec![ProjectPanel::new(ScrollState::default()).into_any()],
Box::new(()),
))
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::RecentProjects;
use crate::story::Story;
#[derive(Element, Default)]
pub struct RecentProjectsStory {}
impl RecentProjectsStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, RecentProjects>(cx))
.child(Story::label(cx, "Default"))
.child(RecentProjects::new())
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::StatusBar;
use crate::story::Story;
#[derive(Element, Default)]
pub struct StatusBarStory {}
impl StatusBarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, StatusBar<V>>(cx))
.child(Story::label(cx, "Default"))
.child(StatusBar::new())
}
}

View File

@ -1,91 +0,0 @@
use strum::IntoEnumIterator;
use ui::prelude::*;
use ui::{h_stack, v_stack, Tab};
use crate::story::Story;
#[derive(Element, Default)]
pub struct TabStory {}
impl TabStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let git_statuses = GitStatus::iter();
let fs_statuses = FileSystemStatus::iter();
Story::container(cx)
.child(Story::title_for::<_, Tab>(cx))
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Default"))
.child(Tab::new()),
),
)
.child(
h_stack().child(
v_stack().gap_2().child(Story::label(cx, "Current")).child(
h_stack()
.gap_4()
.child(Tab::new().title("Current".to_string()).current(true))
.child(Tab::new().title("Not Current".to_string()).current(false)),
),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Titled"))
.child(Tab::new().title("label".to_string())),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "With Icon"))
.child(
Tab::new()
.title("label".to_string())
.icon(Some(ui::Icon::Envelope)),
),
),
)
.child(
h_stack().child(
v_stack()
.gap_2()
.child(Story::label(cx, "Close Side"))
.child(
h_stack()
.gap_4()
.child(
Tab::new()
.title("Left".to_string())
.close_side(IconSide::Left),
)
.child(Tab::new().title("Right".to_string())),
),
),
)
.child(
v_stack()
.gap_2()
.child(Story::label(cx, "Git Status"))
.child(h_stack().gap_4().children(git_statuses.map(|git_status| {
Tab::new()
.title(git_status.to_string())
.git_status(git_status)
}))),
)
.child(
v_stack()
.gap_2()
.child(Story::label(cx, "File System Status"))
.child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
Tab::new().title(fs_status.to_string()).fs_status(fs_status)
}))),
)
}
}

View File

@ -1,46 +0,0 @@
use ui::prelude::*;
use ui::{Tab, TabBar};
use crate::story::Story;
#[derive(Element, Default)]
pub struct TabBarStory {}
impl TabBarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, TabBar<V>>(cx))
.child(Story::label(cx, "Default"))
.child(TabBar::new(vec![
Tab::new()
.title("Cargo.toml".to_string())
.current(false)
.git_status(GitStatus::Modified),
Tab::new()
.title("Channels Panel".to_string())
.current(false),
Tab::new()
.title("channels_panel.rs".to_string())
.current(true)
.git_status(GitStatus::Modified),
Tab::new()
.title("workspace.rs".to_string())
.current(false)
.git_status(GitStatus::Modified),
Tab::new()
.title("icon_button.rs".to_string())
.current(false),
Tab::new()
.title("storybook.rs".to_string())
.current(false)
.git_status(GitStatus::Created),
Tab::new().title("theme.rs".to_string()).current(false),
Tab::new()
.title("theme_registry.rs".to_string())
.current(false),
Tab::new()
.title("styleable_helpers.rs".to_string())
.current(false),
]))
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::Terminal;
use crate::story::Story;
#[derive(Element, Default)]
pub struct TerminalStory {}
impl TerminalStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Terminal>(cx))
.child(Story::label(cx, "Default"))
.child(Terminal::new())
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::ThemeSelector;
use crate::story::Story;
#[derive(Element, Default)]
pub struct ThemeSelectorStory {}
impl ThemeSelectorStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, ThemeSelector>(cx))
.child(Story::label(cx, "Default"))
.child(ThemeSelector::new())
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::TitleBar;
use crate::story::Story;
#[derive(Element, Default)]
pub struct TitleBarStory {}
impl TitleBarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, TitleBar<V>>(cx))
.child(Story::label(cx, "Default"))
.child(TitleBar::new(cx))
}
}

View File

@ -1,70 +0,0 @@
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use ui::prelude::*;
use ui::{theme, Breadcrumb, HighlightColor, HighlightedText, Icon, IconButton, Symbol, Toolbar};
use crate::story::Story;
#[derive(Element, Default)]
pub struct ToolbarStory {}
impl ToolbarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
struct LeftItemsPayload {
pub theme: Arc<Theme>,
}
Story::container(cx)
.child(Story::title_for::<_, Toolbar<V>>(cx))
.child(Story::label(cx, "Default"))
.child(Toolbar::new(
|_, payload| {
let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
let theme = payload.theme.clone();
vec![Breadcrumb::new(
PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
vec![
Symbol(vec![
HighlightedText {
text: "impl ".to_string(),
color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "ToolbarStory".to_string(),
color: HighlightColor::Function.hsla(&theme),
},
]),
Symbol(vec![
HighlightedText {
text: "fn ".to_string(),
color: HighlightColor::Keyword.hsla(&theme),
},
HighlightedText {
text: "render".to_string(),
color: HighlightColor::Function.hsla(&theme),
},
]),
],
)
.into_any()]
},
Box::new(LeftItemsPayload {
theme: theme.clone(),
}),
|_, _| {
vec![
IconButton::new(Icon::InlayHint).into_any(),
IconButton::new(Icon::MagnifyingGlass).into_any(),
IconButton::new(Icon::MagicWand).into_any(),
]
},
Box::new(()),
))
}
}

View File

@ -1,18 +0,0 @@
use ui::prelude::*;
use ui::TrafficLights;
use crate::story::Story;
#[derive(Element, Default)]
pub struct TrafficLightsStory {}
impl TrafficLightsStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, TrafficLights>(cx))
.child(Story::label(cx, "Default"))
.child(TrafficLights::new())
.child(Story::label(cx, "Unfocused"))
.child(TrafficLights::new().window_has_focus(false))
}
}

View File

@ -1,5 +0,0 @@
pub mod avatar;
pub mod button;
pub mod icon;
pub mod input;
pub mod label;

View File

@ -1,23 +0,0 @@
use ui::prelude::*;
use ui::Avatar;
use crate::story::Story;
#[derive(Element, Default)]
pub struct AvatarStory {}
impl AvatarStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Avatar>(cx))
.child(Story::label(cx, "Default"))
.child(Avatar::new(
"https://avatars.githubusercontent.com/u/1714999?v=4",
))
.child(Story::label(cx, "Rounded rectangle"))
.child(
Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle),
)
}
}

View File

@ -1,192 +0,0 @@
use gpui2::elements::div;
use gpui2::geometry::rems;
use gpui2::{Element, IntoElement, ViewContext};
use strum::IntoEnumIterator;
use ui::prelude::*;
use ui::{h_stack, v_stack, Button, Icon, IconPosition, Label};
use crate::story::Story;
#[derive(Element, Default)]
pub struct ButtonStory {}
impl ButtonStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let states = InteractionState::iter();
Story::container(cx)
.child(Story::title_for::<_, Button<V>>(cx))
.child(
div()
.flex()
.gap_8()
.child(
div()
.child(Story::label(cx, "Ghost (Default)"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.state(state),
)
})))
.child(Story::label(cx, "Ghost Left Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Left)
.state(state),
)
})))
.child(Story::label(cx, "Ghost Right Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
.icon(Icon::Plus)
.icon_position(IconPosition::Right)
.state(state),
)
}))),
)
.child(
div()
.child(Story::label(cx, "Filled"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.state(state),
)
})))
.child(Story::label(cx, "Filled Left Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Left)
.state(state),
)
})))
.child(Story::label(cx, "Filled Right Button"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.icon(Icon::Plus)
.icon_position(IconPosition::Right)
.state(state),
)
}))),
)
.child(
div()
.child(Story::label(cx, "Fixed With"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.state(state)
.width(Some(rems(6.).into())),
)
})))
.child(Story::label(cx, "Fixed With Left Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.state(state)
.icon(Icon::Plus)
.icon_position(IconPosition::Left)
.width(Some(rems(6.).into())),
)
})))
.child(Story::label(cx, "Fixed With Right Icon"))
.child(h_stack().gap_2().children(states.clone().map(|state| {
v_stack()
.gap_1()
.child(
Label::new(state.to_string())
.color(ui::LabelColor::Muted)
.size(ui::LabelSize::Small),
)
.child(
Button::new("Label")
.variant(ButtonVariant::Filled)
.state(state)
.icon(Icon::Plus)
.icon_position(IconPosition::Right)
.width(Some(rems(6.).into())),
)
}))),
),
)
.child(Story::label(cx, "Button with `on_click`"))
.child(
Button::new("Label")
.variant(ButtonVariant::Ghost)
// NOTE: There currently appears to be a bug in GPUI2 where only the last event handler will fire.
// So adding additional buttons with `on_click`s after this one will cause this `on_click` to not fire.
.on_click(|_view, _cx| println!("Button clicked.")),
)
}
}

View File

@ -1,19 +0,0 @@
use strum::IntoEnumIterator;
use ui::prelude::*;
use ui::{Icon, IconElement};
use crate::story::Story;
#[derive(Element, Default)]
pub struct IconStory {}
impl IconStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let icons = Icon::iter();
Story::container(cx)
.child(Story::title_for::<_, IconElement>(cx))
.child(Story::label(cx, "All Icons"))
.child(div().flex().gap_3().children(icons.map(IconElement::new)))
}
}

View File

@ -1,16 +0,0 @@
use ui::prelude::*;
use ui::Input;
use crate::story::Story;
#[derive(Element, Default)]
pub struct InputStory {}
impl InputStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Input>(cx))
.child(Story::label(cx, "Default"))
.child(div().flex().child(Input::new("Search")))
}
}

View File

@ -1,18 +0,0 @@
use ui::prelude::*;
use ui::Label;
use crate::story::Story;
#[derive(Element, Default)]
pub struct LabelStory {}
impl LabelStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
Story::container(cx)
.child(Story::title_for::<_, Label>(cx))
.child(Story::label(cx, "Default"))
.child(Label::new("Hello, world!"))
.child(Story::label(cx, "Highlighted"))
.child(Label::new("Hello, world!").with_highlights(vec![0, 1, 2, 7, 8, 12]))
}
}

View File

@ -1,26 +0,0 @@
use strum::IntoEnumIterator;
use ui::prelude::*;
use crate::story::Story;
use crate::story_selector::{ComponentStory, ElementStory};
#[derive(Element, Default)]
pub struct KitchenSinkStory {}
impl KitchenSinkStory {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let element_stories = ElementStory::iter().map(|selector| selector.story());
let component_stories = ComponentStory::iter().map(|selector| selector.story());
Story::container(cx)
.overflow_y_scroll(ScrollState::default())
.child(Story::title(cx, "Kitchen Sink"))
.child(Story::label(cx, "Elements"))
.child(div().flex().flex_col().children_any(element_stories))
.child(Story::label(cx, "Components"))
.child(div().flex().flex_col().children_any(component_stories))
// Add a bit of space at the bottom of the kitchen sink so elements
// don't end up squished right up against the bottom of the screen.
.child(div().p_4())
}
}

View File

@ -1,44 +0,0 @@
use gpui2::elements::div::Div;
use ui::prelude::*;
use ui::theme;
pub struct Story {}
impl Story {
pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
let theme = theme(cx);
div()
.size_full()
.flex()
.flex_col()
.pt_2()
.px_4()
.font("Zed Mono Extended")
.fill(theme.lowest.base.default.background)
}
pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Element<V> {
let theme = theme(cx);
div()
.text_xl()
.text_color(theme.lowest.base.default.foreground)
.child(title.to_owned())
}
pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Element<V> {
Self::title(cx, std::any::type_name::<T>())
}
pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Element<V> {
let theme = theme(cx);
div()
.mt_4()
.mb_2()
.text_xs()
.text_color(theme.lowest.base.default.foreground)
.child(label.to_owned())
}
}

View File

@ -1,178 +0,0 @@
use std::str::FromStr;
use std::sync::OnceLock;
use anyhow::{anyhow, Context};
use clap::builder::PossibleValue;
use clap::ValueEnum;
use gpui2::{AnyElement, Element};
use strum::{EnumIter, EnumString, IntoEnumIterator};
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ElementStory {
Avatar,
Button,
Icon,
Input,
Label,
}
impl ElementStory {
pub fn story<V: 'static>(&self) -> AnyElement<V> {
use crate::stories::elements;
match self {
Self::Avatar => elements::avatar::AvatarStory::default().into_any(),
Self::Button => elements::button::ButtonStory::default().into_any(),
Self::Icon => elements::icon::IconStory::default().into_any(),
Self::Input => elements::input::InputStory::default().into_any(),
Self::Label => elements::label::LabelStory::default().into_any(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ComponentStory {
AssistantPanel,
Breadcrumb,
Buffer,
ContextMenu,
ChatPanel,
CollabPanel,
Facepile,
Keybinding,
LanguageSelector,
MultiBuffer,
Palette,
Panel,
ProjectPanel,
RecentProjects,
StatusBar,
Tab,
TabBar,
Terminal,
ThemeSelector,
TitleBar,
Toolbar,
TrafficLights,
}
impl ComponentStory {
pub fn story<V: 'static>(&self) -> AnyElement<V> {
use crate::stories::components;
match self {
Self::AssistantPanel => {
components::assistant_panel::AssistantPanelStory::default().into_any()
}
Self::Breadcrumb => components::breadcrumb::BreadcrumbStory::default().into_any(),
Self::Buffer => components::buffer::BufferStory::default().into_any(),
Self::ContextMenu => components::context_menu::ContextMenuStory::default().into_any(),
Self::ChatPanel => components::chat_panel::ChatPanelStory::default().into_any(),
Self::CollabPanel => components::collab_panel::CollabPanelStory::default().into_any(),
Self::Facepile => components::facepile::FacepileStory::default().into_any(),
Self::Keybinding => components::keybinding::KeybindingStory::default().into_any(),
Self::LanguageSelector => {
components::language_selector::LanguageSelectorStory::default().into_any()
}
Self::MultiBuffer => components::multi_buffer::MultiBufferStory::default().into_any(),
Self::Palette => components::palette::PaletteStory::default().into_any(),
Self::Panel => components::panel::PanelStory::default().into_any(),
Self::ProjectPanel => {
components::project_panel::ProjectPanelStory::default().into_any()
}
Self::RecentProjects => {
components::recent_projects::RecentProjectsStory::default().into_any()
}
Self::StatusBar => components::status_bar::StatusBarStory::default().into_any(),
Self::Tab => components::tab::TabStory::default().into_any(),
Self::TabBar => components::tab_bar::TabBarStory::default().into_any(),
Self::Terminal => components::terminal::TerminalStory::default().into_any(),
Self::ThemeSelector => {
components::theme_selector::ThemeSelectorStory::default().into_any()
}
Self::TitleBar => components::title_bar::TitleBarStory::default().into_any(),
Self::Toolbar => components::toolbar::ToolbarStory::default().into_any(),
Self::TrafficLights => {
components::traffic_lights::TrafficLightsStory::default().into_any()
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum StorySelector {
Element(ElementStory),
Component(ComponentStory),
KitchenSink,
}
impl FromStr for StorySelector {
type Err = anyhow::Error;
fn from_str(raw_story_name: &str) -> std::result::Result<Self, Self::Err> {
let story = raw_story_name.to_ascii_lowercase();
if story == "kitchen_sink" {
return Ok(Self::KitchenSink);
}
if let Some((_, story)) = story.split_once("elements/") {
let element_story = ElementStory::from_str(story)
.with_context(|| format!("story not found for element '{story}'"))?;
return Ok(Self::Element(element_story));
}
if let Some((_, story)) = story.split_once("components/") {
let component_story = ComponentStory::from_str(story)
.with_context(|| format!("story not found for component '{story}'"))?;
return Ok(Self::Component(component_story));
}
Err(anyhow!("story not found for '{raw_story_name}'"))
}
}
impl StorySelector {
pub fn story<V: 'static>(&self) -> AnyElement<V> {
match self {
Self::Element(element_story) => element_story.story(),
Self::Component(component_story) => component_story.story(),
Self::KitchenSink => {
crate::stories::kitchen_sink::KitchenSinkStory::default().into_any()
}
}
}
}
/// The list of all stories available in the storybook.
static ALL_STORY_SELECTORS: OnceLock<Vec<StorySelector>> = OnceLock::new();
impl ValueEnum for StorySelector {
fn value_variants<'a>() -> &'a [Self] {
let stories = ALL_STORY_SELECTORS.get_or_init(|| {
let element_stories = ElementStory::iter().map(StorySelector::Element);
let component_stories = ComponentStory::iter().map(StorySelector::Component);
element_stories
.chain(component_stories)
.chain(std::iter::once(StorySelector::KitchenSink))
.collect::<Vec<_>>()
});
stories
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
let value = match self {
Self::Element(story) => format!("elements/{story}"),
Self::Component(story) => format!("components/{story}"),
Self::KitchenSink => "kitchen_sink".to_string(),
};
Some(PossibleValue::new(value))
}
}

View File

@ -1,198 +0,0 @@
#![allow(dead_code, unused_variables)]
mod stories;
mod story;
mod story_selector;
use std::{process::Command, sync::Arc};
use ::theme as legacy_theme;
use clap::Parser;
use gpui2::{
serde_json, vec2f, view, Element, IntoElement, ParentElement, RectF, ViewContext, WindowBounds,
};
use legacy_theme::{ThemeRegistry, ThemeSettings};
use log::LevelFilter;
use settings::{default_settings, SettingsStore};
use simplelog::SimpleLogger;
use ui::prelude::*;
use ui::{ElementExt, Theme, WorkspaceElement};
use crate::story_selector::StorySelector;
gpui2::actions! {
storybook,
[ToggleInspector]
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(value_enum)]
story: Option<StorySelector>,
/// The name of the theme to use in the storybook.
///
/// If not provided, the default theme will be used.
#[arg(long)]
theme: Option<String>,
}
async fn watch_zed_changes(fs: Arc<dyn fs::Fs>) -> Option<()> {
if std::env::var("ZED_HOT_RELOAD").is_err() {
return None;
}
use futures::StreamExt;
let mut events = fs
.watch(".".as_ref(), std::time::Duration::from_millis(100))
.await;
let mut current_child: Option<std::process::Child> = None;
while let Some(events) = events.next().await {
if !events.iter().any(|event| {
event
.path
.to_str()
.map(|path| path.contains("/crates/"))
.unwrap_or_default()
}) {
continue;
}
let child = current_child.take().map(|mut child| child.kill());
log::info!("Storybook changed, rebuilding...");
current_child = Some(
Command::new("cargo")
.args(["run", "-p", "storybook"])
.spawn()
.ok()?,
);
}
Some(())
}
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
let args = Args::parse();
let fs = Arc::new(fs::RealFs);
gpui2::App::new(Assets).unwrap().run(move |cx| {
let mut store = SettingsStore::default();
store
.set_default_settings(default_settings().as_ref(), cx)
.unwrap();
cx.set_global(store);
legacy_theme::init(Assets, cx);
// load_embedded_fonts(cx.platform().as_ref());
let theme_registry = cx.global::<Arc<ThemeRegistry>>();
let theme_override = args
.theme
.and_then(|theme| {
theme_registry
.list_names(true)
.find(|known_theme| theme == *known_theme)
})
.and_then(|theme_name| theme_registry.get(&theme_name).ok());
cx.spawn(|_| async move {
watch_zed_changes(fs).await;
})
.detach();
cx.add_window(
gpui2::WindowOptions {
bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), vec2f(1700., 980.))),
center: true,
..Default::default()
},
|cx| match args.story {
Some(selector) => view(move |cx| {
render_story(
&mut ViewContext::new(cx),
theme_override.clone(),
div().flex().flex_col().h_full().child_any(selector.story()),
)
}),
None => view(move |cx| {
render_story(
&mut ViewContext::new(cx),
theme_override.clone(),
WorkspaceElement::default(),
)
}),
},
);
cx.platform().activate(true);
});
}
fn render_story<V: 'static, S: IntoElement<V>>(
cx: &mut ViewContext<V>,
theme_override: Option<Arc<legacy_theme::Theme>>,
story: S,
) -> impl Element<V> {
let theme = current_theme(cx, theme_override);
story.into_element().themed(theme)
}
fn current_theme<V: 'static>(
cx: &mut ViewContext<V>,
theme_override: Option<Arc<legacy_theme::Theme>>,
) -> Theme {
let legacy_theme =
theme_override.unwrap_or_else(|| settings::get::<ThemeSettings>(cx).theme.clone());
let new_theme: Theme = serde_json::from_value(legacy_theme.base_theme.clone()).unwrap();
add_base_theme_to_legacy_theme(&legacy_theme, new_theme)
}
// Nathan: During the transition to gpui2, we will include the base theme on the legacy Theme struct.
fn add_base_theme_to_legacy_theme(legacy_theme: &legacy_theme::Theme, new_theme: Theme) -> Theme {
legacy_theme
.deserialized_base_theme
.lock()
.get_or_insert_with(|| Box::new(new_theme))
.downcast_ref::<Theme>()
.unwrap()
.clone()
}
use anyhow::{anyhow, Result};
use gpui2::AssetSource;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "themes/**/*"]
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[exclude = "*.DS_Store"]
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
Self::get(path)
.map(|f| f.data)
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
Self::iter().filter(|p| p.starts_with(path)).collect()
}
}
// fn load_embedded_fonts(platform: &dyn gpui2::Platform) {
// let font_paths = Assets.list("fonts");
// let mut embedded_fonts = Vec::new();
// for font_path in &font_paths {
// if font_path.ends_with(".ttf") {
// let font_path = &*font_path;
// let font_bytes = Assets.load(font_path).unwrap().to_vec();
// embedded_fonts.push(Arc::from(font_bytes));
// }
// }
// platform.fonts().add_fonts(&embedded_fonts).unwrap();
// }

View File

@ -1,16 +0,0 @@
[package]
name = "ui"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow.workspace = true
chrono = "0.4"
gpui2 = { path = "../gpui2" }
serde.workspace = true
settings = { path = "../settings" }
smallvec.workspace = true
strum = { version = "0.25.0", features = ["derive"] }
theme = { path = "../theme" }
rand = "0.8"

View File

@ -1,13 +0,0 @@
## Project Plan
- Port existing UI to GPUI2
- Update UI in places that GPUI1 was limiting us*
- Understand the needs &/|| struggles the engineers have been having with building UI in the past and address as many of those as possible as we go
- Ship a simple, straightforward system with documentation that is easy to use to build UI
## Component Classification
To simplify the understanding of components and minimize unnecessary cognitive load, let's categorize components into two types:
- An element refers to a standalone component that doesn't import any other 'ui' components.
- A component indicates a component that utilizes or imports other 'ui' components.

View File

@ -1,57 +0,0 @@
# Elevation
Elevation in Zed applies to all surfaces and components. Elevation is categorized into levels.
Elevation accomplishes the following:
- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
- Reflects spatial relationships, for instance, how a floating action buttons shadow intimates its disconnection from a collection of cards.
- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
Elevations are the initial elevation values assigned to components by default.
Components may transition to a higher elevation in some cases, like user interations.
On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
## Understanding Elevation
Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design Elevation](https://m3.material.io/styles/elevation/overview)
## Elevation Levels
Zed integrates six unique elevation levels in its design system. The elevation of a surface is expressed as a whole number ranging from 0 to 5, both numbers inclusive. A components elevation is ascertained by combining the components resting elevation with any dynamic elevation offsets.
The levels are detailed as follows:
0. App Background
1. UI Surface
2. Elevated Elements
3. Wash
4. Focused Element
5. Dragged Element
### 0. App Background
The app background constitutes the lowest elevation layer, appearing behind all other surfaces and components. It is predominantly used for the background color of the app.
### 1. UI Surface
The UI Surface is the standard elevation for components and is placed above the app background. It is generally used for the background color of the app bar, card, and sheet.
### 2. Elevated Elements
Elevated elements appear above the UI surface layer surfaces and components. Elevated elements are predominantly used for creating popovers, context menus, and tooltips.
### 3. Wash
Wash denotes a distinct elevation reserved to isolate app UI layers from high elevation components such as modals, notifications, and overlaid panels. The wash may not consistently be visible when these components are active. This layer is often referred to as a scrim or overlay and the background color of the wash is typically deployed in its design.
### 4. Focused Element
Focused elements obtain a higher elevation above surfaces and components at wash elevation. They are often used for modals, notifications, and overlaid panels and indicate that they are the sole element the user is interacting with at the moment.
### 5. Dragged Element
Dragged elements gain the highest elevation, thus appearing above surfaces and components at the elevation of focused elements. These are typically used for elements that are being dragged, following the cursor

View File

@ -1,7 +0,0 @@
use std::any::Any;
use gpui2::{AnyElement, ViewContext};
pub type HackyChildren<V> = fn(&mut ViewContext<V>, &dyn Any) -> Vec<AnyElement<V>>;
pub type HackyChildrenPayload = Box<dyn Any>;

View File

@ -1,163 +0,0 @@
mod assistant_panel;
mod breadcrumb;
mod buffer;
mod chat_panel;
mod collab_panel;
mod command_palette;
mod context_menu;
mod editor_pane;
mod facepile;
mod icon_button;
mod keybinding;
mod language_selector;
mod list;
mod multi_buffer;
mod palette;
mod panel;
mod panes;
mod player_stack;
mod project_panel;
mod recent_projects;
mod status_bar;
mod tab;
mod tab_bar;
mod terminal;
mod theme_selector;
mod title_bar;
mod toast;
mod toolbar;
mod traffic_lights;
mod workspace;
pub use assistant_panel::*;
pub use breadcrumb::*;
pub use buffer::*;
pub use chat_panel::*;
pub use collab_panel::*;
pub use command_palette::*;
pub use context_menu::*;
pub use editor_pane::*;
pub use facepile::*;
pub use icon_button::*;
pub use keybinding::*;
pub use language_selector::*;
pub use list::*;
pub use multi_buffer::*;
pub use palette::*;
pub use panel::*;
pub use panes::*;
pub use player_stack::*;
pub use project_panel::*;
pub use recent_projects::*;
pub use status_bar::*;
pub use tab::*;
pub use tab_bar::*;
pub use terminal::*;
pub use theme_selector::*;
pub use title_bar::*;
pub use toast::*;
pub use toolbar::*;
pub use traffic_lights::*;
pub use workspace::*;
// Nate: Commenting this out for now, unsure if we need it.
// use std::marker::PhantomData;
// use std::rc::Rc;
// use gpui2::elements::div;
// use gpui2::interactive::Interactive;
// use gpui2::platform::MouseButton;
// use gpui2::{ArcCow, Element, EventContext, IntoElement, ParentElement, ViewContext};
// struct ButtonHandlers<V, D> {
// click: Option<Rc<dyn Fn(&mut V, &D, &mut EventContext<V>)>>,
// }
// impl<V, D> Default for ButtonHandlers<V, D> {
// fn default() -> Self {
// Self { click: None }
// }
// }
// #[derive(Element)]
// pub struct Button<V: 'static, D: 'static> {
// handlers: ButtonHandlers<V, D>,
// label: Option<ArcCow<'static, str>>,
// icon: Option<ArcCow<'static, str>>,
// data: Rc<D>,
// view_type: PhantomData<V>,
// }
// // Impl block for buttons without data.
// // See below for an impl block for any button.
// impl<V: 'static> Button<V, ()> {
// fn new() -> Self {
// Self {
// handlers: ButtonHandlers::default(),
// label: None,
// icon: None,
// data: Rc::new(()),
// view_type: PhantomData,
// }
// }
// pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
// Button {
// handlers: ButtonHandlers::default(),
// label: self.label,
// icon: self.icon,
// data: Rc::new(data),
// view_type: PhantomData,
// }
// }
// }
// // Impl block for button regardless of its data type.
// impl<V: 'static, D: 'static> Button<V, D> {
// pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
// self.label = Some(label.into());
// self
// }
// pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
// self.icon = Some(icon.into());
// self
// }
// pub fn on_click(
// mut self,
// handler: impl Fn(&mut V, &D, &mut EventContext<V>) + 'static,
// ) -> Self {
// self.handlers.click = Some(Rc::new(handler));
// self
// }
// }
// pub fn button<V>() -> Button<V, ()> {
// Button::new()
// }
// impl<V: 'static, D: 'static> Button<V, D> {
// fn render(
// &mut self,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> impl IntoElement<V> + Interactive<V> {
// // let colors = &cx.theme::<Theme>().colors;
// let button = div()
// // .fill(colors.error(0.5))
// .h_4()
// .children(self.label.clone());
// if let Some(handler) = self.handlers.click.clone() {
// let data = self.data.clone();
// button.on_mouse_down(MouseButton::Left, move |view, event, cx| {
// handler(view, data.as_ref(), cx)
// })
// } else {
// button
// }
// }
// }

View File

@ -1,91 +0,0 @@
use std::marker::PhantomData;
use gpui2::geometry::rems;
use crate::prelude::*;
use crate::theme::theme;
use crate::{Icon, IconButton, Label, Panel, PanelSide};
#[derive(Element)]
pub struct AssistantPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
current_side: PanelSide,
}
impl<V: 'static> AssistantPanel<V> {
pub fn new() -> Self {
Self {
view_type: PhantomData,
scroll_state: ScrollState::default(),
current_side: PanelSide::default(),
}
}
pub fn side(mut self, side: PanelSide) -> Self {
self.current_side = side;
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
struct PanelPayload {
pub scroll_state: ScrollState,
}
Panel::new(
self.scroll_state.clone(),
|_, payload| {
let payload = payload.downcast_ref::<PanelPayload>().unwrap();
vec![div()
.flex()
.flex_col()
.h_full()
.px_2()
.gap_2()
// Header
.child(
div()
.flex()
.justify_between()
.gap_2()
.child(
div()
.flex()
.child(IconButton::new(Icon::Menu))
.child(Label::new("New Conversation")),
)
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new(Icon::SplitMessage))
.child(IconButton::new(Icon::Quote))
.child(IconButton::new(Icon::MagicWand))
.child(IconButton::new(Icon::Plus))
.child(IconButton::new(Icon::Maximize)),
),
)
// Chat Body
.child(
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.overflow_y_scroll(payload.scroll_state.clone())
.child(Label::new("Is this thing on?")),
)
.into_any()]
},
Box::new(PanelPayload {
scroll_state: self.scroll_state.clone(),
}),
)
.side(self.current_side)
.width(rems(32.))
}
}

View File

@ -1,71 +0,0 @@
use std::path::PathBuf;
use gpui2::elements::div::Div;
use crate::{h_stack, theme};
use crate::{prelude::*, HighlightedText};
#[derive(Clone)]
pub struct Symbol(pub Vec<HighlightedText>);
#[derive(Element)]
pub struct Breadcrumb {
path: PathBuf,
symbols: Vec<Symbol>,
}
impl Breadcrumb {
pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
Self { path, symbols }
}
fn render_separator<V: 'static>(&self, theme: &Theme) -> Div<V> {
div()
.child(" ")
.text_color(HighlightColor::Default.hsla(theme))
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let symbols_len = self.symbols.len();
h_stack()
.px_1()
// TODO: Read font from theme (or settings?).
.font("Zed Mono Extended")
.text_sm()
.text_color(theme.middle.base.default.foreground)
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
.child(self.path.clone().to_str().unwrap().to_string())
.child(if !self.symbols.is_empty() {
self.render_separator(&theme)
} else {
div()
})
.child(
div().flex().children(
self.symbols
.iter()
.enumerate()
// TODO: Could use something like `intersperse` here instead.
.flat_map(|(ix, symbol)| {
let mut items =
vec![div().flex().children(symbol.0.iter().map(|segment| {
div().child(segment.text.clone()).text_color(segment.color)
}))];
let is_last_segment = ix == symbols_len - 1;
if !is_last_segment {
items.push(self.render_separator(&theme));
}
items
})
.collect::<Vec<_>>(),
),
)
}
}

View File

@ -1,233 +0,0 @@
use gpui2::{Hsla, WindowContext};
use crate::prelude::*;
use crate::{h_stack, theme, v_stack, Icon, IconElement};
#[derive(Default, PartialEq, Copy, Clone)]
pub struct PlayerCursor {
color: Hsla,
index: usize,
}
#[derive(Default, PartialEq, Clone)]
pub struct HighlightedText {
pub text: String,
pub color: Hsla,
}
#[derive(Default, PartialEq, Clone)]
pub struct HighlightedLine {
pub highlighted_texts: Vec<HighlightedText>,
}
#[derive(Default, PartialEq, Clone)]
pub struct BufferRow {
pub line_number: usize,
pub code_action: bool,
pub current: bool,
pub line: Option<HighlightedLine>,
pub cursors: Option<Vec<PlayerCursor>>,
pub status: GitStatus,
pub show_line_number: bool,
}
#[derive(Clone)]
pub struct BufferRows {
pub show_line_numbers: bool,
pub rows: Vec<BufferRow>,
}
impl Default for BufferRows {
fn default() -> Self {
Self {
show_line_numbers: true,
rows: vec![BufferRow {
line_number: 1,
code_action: false,
current: true,
line: None,
cursors: None,
status: GitStatus::None,
show_line_number: true,
}],
}
}
}
impl BufferRow {
pub fn new(line_number: usize) -> Self {
Self {
line_number,
code_action: false,
current: false,
line: None,
cursors: None,
status: GitStatus::None,
show_line_number: true,
}
}
pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
self.line = line;
self
}
pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
self.cursors = cursors;
self
}
pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
if let Some(cursors) = &mut self.cursors {
cursors.push(cursor);
} else {
self.cursors = Some(vec![cursor]);
}
self
}
pub fn set_status(mut self, status: GitStatus) -> Self {
self.status = status;
self
}
pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
self.show_line_number = show_line_number;
self
}
pub fn set_code_action(mut self, code_action: bool) -> Self {
self.code_action = code_action;
self
}
pub fn set_current(mut self, current: bool) -> Self {
self.current = current;
self
}
}
#[derive(Element, Clone)]
pub struct Buffer {
scroll_state: ScrollState,
rows: Option<BufferRows>,
readonly: bool,
language: Option<String>,
title: Option<String>,
path: Option<String>,
}
impl Buffer {
pub fn new() -> Self {
Self {
scroll_state: ScrollState::default(),
rows: Some(BufferRows::default()),
readonly: false,
language: None,
title: Some("untitled".to_string()),
path: None,
}
}
pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
self.scroll_state = scroll_state;
}
pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
self.title = title.into();
self
}
pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
self.path = path.into();
self
}
pub fn set_readonly(mut self, readonly: bool) -> Self {
self.readonly = readonly;
self
}
pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
self.rows = rows.into();
self
}
pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
self.language = language.into();
self
}
fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl IntoElement<V> {
let theme = theme(cx);
let system_color = SystemColor::new();
let line_background = if row.current {
theme.middle.base.default.background
} else {
system_color.transparent
};
let line_number_color = if row.current {
HighlightColor::Default.hsla(&theme)
} else {
HighlightColor::Comment.hsla(&theme)
};
h_stack()
.fill(line_background)
.w_full()
.gap_2()
.px_1()
.child(
h_stack()
.w_4()
.h_full()
.px_0p5()
.when(row.code_action, |c| {
div().child(IconElement::new(Icon::Bolt))
}),
)
.when(row.show_line_number, |this| {
this.child(
h_stack().justify_end().px_0p5().w_3().child(
div()
.text_color(line_number_color)
.child(row.line_number.to_string()),
),
)
})
.child(div().mx_0p5().w_1().h_full().fill(row.status.hsla(cx)))
.children(row.line.map(|line| {
div()
.flex()
.children(line.highlighted_texts.iter().map(|highlighted_text| {
div()
.text_color(highlighted_text.color)
.child(highlighted_text.text.clone())
}))
}))
}
fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl IntoElement<V>> {
match &self.rows {
Some(rows) => rows
.rows
.iter()
.map(|row| Self::render_row(row.clone(), cx))
.collect(),
None => vec![],
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let rows = self.render_rows(cx);
v_stack()
.flex_1()
.w_full()
.h_full()
.fill(theme.highest.base.default.background)
.children(rows)
}
}

View File

@ -1,108 +0,0 @@
use std::marker::PhantomData;
use chrono::NaiveDateTime;
use crate::prelude::*;
use crate::theme::theme;
use crate::{Icon, IconButton, Input, Label, LabelColor};
#[derive(Element)]
pub struct ChatPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
messages: Vec<ChatMessage>,
}
impl<V: 'static> ChatPanel<V> {
pub fn new(scroll_state: ScrollState) -> Self {
Self {
view_type: PhantomData,
scroll_state,
messages: Vec::new(),
}
}
pub fn with_messages(mut self, messages: Vec<ChatMessage>) -> Self {
self.messages = messages;
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.flex()
.flex_col()
.justify_between()
.h_full()
.px_2()
.gap_2()
// Header
.child(
div()
.flex()
.justify_between()
.py_2()
.child(div().flex().child(Label::new("#design")))
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new(Icon::File))
.child(IconButton::new(Icon::AudioOn)),
),
)
.child(
div()
.flex()
.flex_col()
// Chat Body
.child(
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.overflow_y_scroll(self.scroll_state.clone())
.children(self.messages.clone()),
)
// Composer
.child(div().flex().my_2().child(Input::new("Message #design"))),
)
}
}
#[derive(Element, Clone)]
pub struct ChatMessage {
author: String,
text: String,
sent_at: NaiveDateTime,
}
impl ChatMessage {
pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
Self {
author,
text,
sent_at,
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div()
.flex()
.flex_col()
.child(
div()
.flex()
.gap_2()
.child(Label::new(self.author.clone()))
.child(
Label::new(self.sent_at.format("%m/%d/%Y").to_string())
.color(LabelColor::Muted),
),
)
.child(div().child(Label::new(self.text.clone())))
}
}

View File

@ -1,161 +0,0 @@
use std::marker::PhantomData;
use gpui2::elements::{img, svg};
use gpui2::ArcCow;
use crate::prelude::*;
use crate::theme::{theme, Theme};
use crate::{
static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List,
ListHeader, ToggleState,
};
#[derive(Element)]
pub struct CollabPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
impl<V: 'static> CollabPanel<V> {
pub fn new(scroll_state: ScrollState) -> Self {
Self {
view_type: PhantomData,
scroll_state,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
v_stack()
.w_64()
.h_full()
.fill(theme.middle.base.default.background)
.child(
v_stack()
.w_full()
.overflow_y_scroll(self.scroll_state.clone())
.child(
div()
.fill(theme.lowest.base.default.background)
.pb_1()
.border_color(theme.lowest.base.default.border)
.border_b()
.child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CRDB")
.left_icon(Icon::Hash.into())
.set_toggle(ToggleState::Toggled),
)
.set_toggle(ToggleState::Toggled),
),
)
.child(
v_stack().py_1().child(
List::new(static_collab_panel_channels())
.header(
ListHeader::new("CHANNELS").set_toggle(ToggleState::Toggled),
)
.empty_message("No channels yet. Add a channel to get started.")
.set_toggle(ToggleState::Toggled),
),
)
.child(
v_stack().py_1().child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS ONLINE")
.set_toggle(ToggleState::Toggled),
)
.set_toggle(ToggleState::Toggled),
),
)
.child(
v_stack().py_1().child(
List::new(static_collab_panel_current_call())
.header(
ListHeader::new("CONTACTS OFFLINE")
.set_toggle(ToggleState::NotToggled),
)
.set_toggle(ToggleState::NotToggled),
),
),
)
.child(
div()
.h_7()
.px_2()
.border_t()
.border_color(theme.middle.variant.default.border)
.flex()
.items_center()
.child(
div()
.text_sm()
.text_color(theme.middle.variant.default.foreground)
.child("Find..."),
),
)
}
fn list_section_header(
&self,
label: impl Into<ArcCow<'static, str>>,
expanded: bool,
theme: &Theme,
) -> impl Element<V> {
div()
.h_7()
.px_2()
.flex()
.justify_between()
.items_center()
.child(div().flex().gap_1().text_sm().child(label))
.child(
div().flex().h_full().gap_1().items_center().child(
svg()
.path(if expanded {
"icons/caret_down.svg"
} else {
"icons/caret_up.svg"
})
.w_3p5()
.h_3p5()
.fill(theme.middle.variant.default.foreground),
),
)
}
fn list_item(
&self,
avatar_uri: impl Into<ArcCow<'static, str>>,
label: impl Into<ArcCow<'static, str>>,
theme: &Theme,
) -> impl Element<V> {
div()
.h_7()
.px_2()
.flex()
.items_center()
.hover()
.fill(theme.lowest.variant.hovered.background)
.active()
.fill(theme.lowest.variant.pressed.background)
.child(
div()
.flex()
.items_center()
.gap_1()
.text_sm()
.child(
img()
.uri(avatar_uri)
.size_3p5()
.rounded_full()
.fill(theme.middle.positive.default.foreground),
)
.child(label),
)
}
}

View File

@ -1,29 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::{example_editor_actions, OrderMethod, Palette};
#[derive(Element)]
pub struct CommandPalette<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
impl<V: 'static> CommandPalette<V> {
pub fn new(scroll_state: ScrollState) -> Self {
Self {
view_type: PhantomData,
scroll_state,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div().child(
Palette::new(self.scroll_state.clone())
.items(example_editor_actions())
.placeholder("Execute a command...")
.empty_string("No items found.")
.default_order(OrderMethod::Ascending),
)
}
}

View File

@ -1,65 +0,0 @@
use crate::prelude::*;
use crate::theme::theme;
use crate::{
v_stack, Label, List, ListEntry, ListItem, ListItemVariant, ListSeparator, ListSubHeader,
};
#[derive(Clone)]
pub enum ContextMenuItem {
Header(&'static str),
Entry(Label),
Separator,
}
impl ContextMenuItem {
fn to_list_item(self) -> ListItem {
match self {
ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
ContextMenuItem::Entry(label) => {
ListEntry::new(label).variant(ListItemVariant::Inset).into()
}
ContextMenuItem::Separator => ListSeparator::new().into(),
}
}
pub fn header(label: &'static str) -> Self {
Self::Header(label)
}
pub fn separator() -> Self {
Self::Separator
}
pub fn entry(label: Label) -> Self {
Self::Entry(label)
}
}
#[derive(Element)]
pub struct ContextMenu {
items: Vec<ContextMenuItem>,
}
impl ContextMenu {
pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
Self {
items: items.into_iter().collect(),
}
}
fn render<V: 'static>(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
v_stack()
.flex()
.fill(theme.lowest.base.default.background)
.border()
.border_color(theme.lowest.base.default.border)
.child(
List::new(
self.items
.clone()
.into_iter()
.map(ContextMenuItem::to_list_item)
.collect(),
)
.set_toggle(ToggleState::Toggled),
)
//div().p_1().children(self.items.clone())
}
}

View File

@ -1,60 +0,0 @@
use std::marker::PhantomData;
use std::path::PathBuf;
use crate::prelude::*;
use crate::{v_stack, Breadcrumb, Buffer, Icon, IconButton, Symbol, Tab, TabBar, Toolbar};
pub struct Editor {
pub tabs: Vec<Tab>,
pub path: PathBuf,
pub symbols: Vec<Symbol>,
pub buffer: Buffer,
}
#[derive(Element)]
pub struct EditorPane<V: 'static> {
view_type: PhantomData<V>,
editor: Editor,
}
impl<V: 'static> EditorPane<V> {
pub fn new(editor: Editor) -> Self {
Self {
view_type: PhantomData,
editor,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
struct LeftItemsPayload {
path: PathBuf,
symbols: Vec<Symbol>,
}
v_stack()
.w_full()
.h_full()
.flex_1()
.child(TabBar::new(self.editor.tabs.clone()))
.child(Toolbar::new(
|_, payload| {
let payload = payload.downcast_ref::<LeftItemsPayload>().unwrap();
vec![Breadcrumb::new(payload.path.clone(), payload.symbols.clone()).into_any()]
},
Box::new(LeftItemsPayload {
path: self.editor.path.clone(),
symbols: self.editor.symbols.clone(),
}),
|_, _| {
vec![
IconButton::new(Icon::InlayHint).into_any(),
IconButton::new(Icon::MagnifyingGlass).into_any(),
IconButton::new(Icon::MagicWand).into_any(),
]
},
Box::new(()),
))
.child(self.editor.buffer.clone())
}
}

View File

@ -1,28 +0,0 @@
use crate::prelude::*;
use crate::{theme, Avatar, Player};
#[derive(Element)]
pub struct Facepile {
players: Vec<Player>,
}
impl Facepile {
pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
Self {
players: players.collect(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let player_count = self.players.len();
let player_list = self.players.iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1;
div()
.when(isnt_last, |div| div.neg_mr_1())
.child(Avatar::new(player.avatar_src().to_string()))
});
div().p_1().flex().items_center().children(player_list)
}
}

View File

@ -1,67 +0,0 @@
use crate::prelude::*;
use crate::{theme, Icon, IconColor, IconElement};
#[derive(Element)]
pub struct IconButton {
icon: Icon,
color: IconColor,
variant: ButtonVariant,
state: InteractionState,
}
impl IconButton {
pub fn new(icon: Icon) -> Self {
Self {
icon,
color: IconColor::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
}
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = icon;
self
}
pub fn color(mut self, color: IconColor) -> Self {
self.color = color;
self
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let icon_color = match (self.state, self.color) {
(InteractionState::Disabled, _) => IconColor::Disabled,
_ => self.color,
};
let mut div = div();
if self.variant == ButtonVariant::Filled {
div = div.fill(theme.highest.on.default.background);
}
div.w_7()
.h_6()
.flex()
.items_center()
.justify_center()
.rounded_md()
.hover()
.fill(theme.highest.base.hovered.background)
.active()
.fill(theme.highest.base.pressed.background)
.child(IconElement::new(self.icon).color(icon_color))
}
}

View File

@ -1,158 +0,0 @@
use std::collections::HashSet;
use strum::{EnumIter, IntoEnumIterator};
use crate::prelude::*;
use crate::theme;
#[derive(Element, Clone)]
pub struct Keybinding {
/// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord.
///
/// This should always contain at least one element.
keybinding: Vec<(String, ModifierKeys)>,
}
impl Keybinding {
pub fn new(key: String, modifiers: ModifierKeys) -> Self {
Self {
keybinding: vec![(key, modifiers)],
}
}
pub fn new_chord(
first_note: (String, ModifierKeys),
second_note: (String, ModifierKeys),
) -> Self {
Self {
keybinding: vec![first_note, second_note],
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div()
.flex()
.gap_2()
.children(self.keybinding.iter().map(|(key, modifiers)| {
div()
.flex()
.gap_1()
.children(ModifierKey::iter().filter_map(|modifier| {
if modifiers.0.contains(&modifier) {
Some(Key::new(modifier.glyph()))
} else {
None
}
}))
.child(Key::new(key.clone()))
}))
}
}
#[derive(Element)]
pub struct Key {
key: String,
}
impl Key {
pub fn new<K>(key: K) -> Self
where
K: Into<String>,
{
Self { key: key.into() }
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.px_2()
.py_0()
.rounded_md()
.text_sm()
.text_color(theme.lowest.on.default.foreground)
.fill(theme.lowest.on.default.background)
.child(self.key.clone())
}
}
// NOTE: The order the modifier keys appear in this enum impacts the order in
// which they are rendered in the UI.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum ModifierKey {
Control,
Alt,
Command,
Shift,
}
impl ModifierKey {
/// Returns the glyph for the [`ModifierKey`].
pub fn glyph(&self) -> char {
match self {
Self::Control => '^',
Self::Alt => '⌥',
Self::Command => '⌘',
Self::Shift => '⇧',
}
}
}
#[derive(Clone)]
pub struct ModifierKeys(HashSet<ModifierKey>);
impl ModifierKeys {
pub fn new() -> Self {
Self(HashSet::new())
}
pub fn all() -> Self {
Self(HashSet::from_iter(ModifierKey::iter()))
}
pub fn add(mut self, modifier: ModifierKey) -> Self {
self.0.insert(modifier);
self
}
pub fn control(mut self, control: bool) -> Self {
if control {
self.0.insert(ModifierKey::Control);
} else {
self.0.remove(&ModifierKey::Control);
}
self
}
pub fn alt(mut self, alt: bool) -> Self {
if alt {
self.0.insert(ModifierKey::Alt);
} else {
self.0.remove(&ModifierKey::Alt);
}
self
}
pub fn command(mut self, command: bool) -> Self {
if command {
self.0.insert(ModifierKey::Command);
} else {
self.0.remove(&ModifierKey::Command);
}
self
}
pub fn shift(mut self, shift: bool) -> Self {
if shift {
self.0.insert(ModifierKey::Shift);
} else {
self.0.remove(&ModifierKey::Shift);
}
self
}
}

View File

@ -1,36 +0,0 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Element)]
pub struct LanguageSelector {
scroll_state: ScrollState,
}
impl LanguageSelector {
pub fn new() -> Self {
Self {
scroll_state: ScrollState::default(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div().child(
Palette::new(self.scroll_state.clone())
.items(vec![
PaletteItem::new("C"),
PaletteItem::new("C++"),
PaletteItem::new("CSS"),
PaletteItem::new("Elixir"),
PaletteItem::new("Elm"),
PaletteItem::new("ERB"),
PaletteItem::new("Rust (current)"),
PaletteItem::new("Scheme"),
PaletteItem::new("TOML"),
PaletteItem::new("TypeScript"),
])
.placeholder("Select a language...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}

View File

@ -1,512 +0,0 @@
use gpui2::elements::div::Div;
use gpui2::{Hsla, WindowContext};
use crate::prelude::*;
use crate::{
h_stack, theme, token, v_stack, Avatar, DisclosureControlVisibility, Icon, IconColor,
IconElement, IconSize, InteractionState, Label, LabelColor, LabelSize, SystemColor,
ToggleState,
};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
/// The list item extends to the far left and right of the list.
#[default]
FullWidth,
Inset,
}
#[derive(Element, Clone, Copy)]
pub struct ListHeader {
label: &'static str,
left_icon: Option<Icon>,
variant: ListItemVariant,
state: InteractionState,
toggleable: Toggleable,
}
impl ListHeader {
pub fn new(label: &'static str) -> Self {
Self {
label,
left_icon: None,
variant: ListItemVariant::default(),
state: InteractionState::default(),
toggleable: Toggleable::default(),
}
}
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
self.toggleable = toggle.into();
self
}
pub fn set_toggleable(mut self, toggleable: Toggleable) -> Self {
self.toggleable = toggleable;
self
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
fn disclosure_control<V: 'static>(&self) -> Div<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = Toggleable::is_toggled(&self.toggleable);
match (is_toggleable, is_toggled) {
(false, _) => div(),
(_, true) => div().child(IconElement::new(Icon::ChevronRight).color(IconColor::Muted)),
(_, false) => div().child(IconElement::new(Icon::ChevronDown).size(IconSize::Small)),
}
}
fn background_color(&self, cx: &WindowContext) -> Hsla {
let theme = theme(cx);
let system_color = SystemColor::new();
match self.state {
InteractionState::Hovered => theme.lowest.base.hovered.background,
InteractionState::Active => theme.lowest.base.pressed.background,
InteractionState::Enabled => theme.lowest.on.default.background,
_ => system_color.transparent,
}
}
fn label_color(&self) -> LabelColor {
match self.state {
InteractionState::Disabled => LabelColor::Disabled,
_ => Default::default(),
}
}
fn icon_color(&self) -> IconColor {
match self.state {
InteractionState::Disabled => IconColor::Disabled,
_ => Default::default(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let system_color = SystemColor::new();
let background_color = self.background_color(cx);
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = Toggleable::is_toggled(&self.toggleable);
let disclosure_control = self.disclosure_control();
h_stack()
.flex_1()
.w_full()
.fill(background_color)
.when(self.state == InteractionState::Focused, |this| {
this.border()
.border_color(theme.lowest.accent.default.border)
})
.relative()
.py_1()
.child(
div()
.h_6()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.flex()
.flex_1()
.w_full()
.gap_1()
.items_center()
.justify_between()
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(IconColor::Muted)
.size(IconSize::Small)
}))
.child(
Label::new(self.label)
.color(LabelColor::Muted)
.size(LabelSize::Small),
),
)
.child(disclosure_control),
)
}
}
#[derive(Element, Clone, Copy)]
pub struct ListSubHeader {
label: &'static str,
left_icon: Option<Icon>,
variant: ListItemVariant,
}
impl ListSubHeader {
pub fn new(label: &'static str) -> Self {
Self {
label,
left_icon: None,
variant: ListItemVariant::default(),
}
}
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
self.left_icon = left_icon;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
h_stack().flex_1().w_full().relative().py_1().child(
div()
.h_6()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.flex()
.flex_1()
.w_full()
.gap_1()
.items_center()
.justify_between()
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(IconColor::Muted)
.size(IconSize::Small)
}))
.child(
Label::new(self.label)
.color(LabelColor::Muted)
.size(LabelSize::Small),
),
),
)
}
}
#[derive(Clone)]
pub enum LeftContent {
Icon(Icon),
Avatar(&'static str),
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum ListEntrySize {
#[default]
Small,
Medium,
}
#[derive(Clone, Element)]
pub enum ListItem {
Entry(ListEntry),
Separator(ListSeparator),
Header(ListSubHeader),
}
impl From<ListEntry> for ListItem {
fn from(entry: ListEntry) -> Self {
Self::Entry(entry)
}
}
impl From<ListSeparator> for ListItem {
fn from(entry: ListSeparator) -> Self {
Self::Separator(entry)
}
}
impl From<ListSubHeader> for ListItem {
fn from(entry: ListSubHeader) -> Self {
Self::Header(entry)
}
}
impl ListItem {
fn render<V: 'static>(&mut self, v: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
match self {
ListItem::Entry(entry) => div().child(entry.render(v, cx)),
ListItem::Separator(separator) => div().child(separator.render(v, cx)),
ListItem::Header(header) => div().child(header.render(v, cx)),
}
}
pub fn new(label: Label) -> Self {
Self::Entry(ListEntry::new(label))
}
pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
if let Self::Entry(entry) = self {
Some(entry)
} else {
None
}
}
}
#[derive(Element, Clone)]
pub struct ListEntry {
disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
label: Label,
left_content: Option<LeftContent>,
variant: ListItemVariant,
size: ListEntrySize,
state: InteractionState,
toggle: Option<ToggleState>,
}
impl ListEntry {
pub fn new(label: Label) -> Self {
Self {
disclosure_control_style: DisclosureControlVisibility::default(),
indent_level: 0,
label,
variant: ListItemVariant::default(),
left_content: None,
size: ListEntrySize::default(),
state: InteractionState::default(),
toggle: None,
}
}
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
}
pub fn indent_level(mut self, indent_level: u32) -> Self {
self.indent_level = indent_level;
self
}
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
self.toggle = Some(toggle);
self
}
pub fn left_content(mut self, left_content: LeftContent) -> Self {
self.left_content = Some(left_content);
self
}
pub fn left_icon(mut self, left_icon: Icon) -> Self {
self.left_content = Some(LeftContent::Icon(left_icon));
self
}
pub fn left_avatar(mut self, left_avatar: &'static str) -> Self {
self.left_content = Some(LeftContent::Avatar(left_avatar));
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn size(mut self, size: ListEntrySize) -> Self {
self.size = size;
self
}
pub fn disclosure_control_style(
mut self,
disclosure_control_style: DisclosureControlVisibility,
) -> Self {
self.disclosure_control_style = disclosure_control_style;
self
}
fn background_color(&self, cx: &WindowContext) -> Hsla {
let theme = theme(cx);
let system_color = SystemColor::new();
match self.state {
InteractionState::Hovered => theme.lowest.base.hovered.background,
InteractionState::Active => theme.lowest.base.pressed.background,
InteractionState::Enabled => theme.lowest.on.default.background,
_ => system_color.transparent,
}
}
fn label_color(&self) -> LabelColor {
match self.state {
InteractionState::Disabled => LabelColor::Disabled,
_ => Default::default(),
}
}
fn icon_color(&self) -> IconColor {
match self.state {
InteractionState::Disabled => IconColor::Disabled,
_ => Default::default(),
}
}
fn disclosure_control<V: 'static>(
&mut self,
cx: &mut ViewContext<V>,
) -> Option<impl IntoElement<V>> {
let theme = theme(cx);
let token = token();
let disclosure_control_icon = if let Some(ToggleState::Toggled) = self.toggle {
IconElement::new(Icon::ChevronDown)
} else {
IconElement::new(Icon::ChevronRight)
}
.color(IconColor::Muted)
.size(IconSize::Small);
match (self.toggle, self.disclosure_control_style) {
(Some(_), DisclosureControlVisibility::OnHover) => {
Some(div().absolute().neg_left_5().child(disclosure_control_icon))
}
(Some(_), DisclosureControlVisibility::Always) => {
Some(div().child(disclosure_control_icon))
}
(None, _) => None,
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let system_color = SystemColor::new();
let background_color = self.background_color(cx);
let left_content = match self.left_content {
Some(LeftContent::Icon(i)) => {
Some(h_stack().child(IconElement::new(i).size(IconSize::Small)))
}
Some(LeftContent::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
None => None,
};
let sized_item = match self.size {
ListEntrySize::Small => div().h_6(),
ListEntrySize::Medium => div().h_7(),
};
div()
.fill(background_color)
.when(self.state == InteractionState::Focused, |this| {
this.border()
.border_color(theme.lowest.accent.default.border)
})
.relative()
.py_1()
.child(
sized_item
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
// .ml(rems(0.75 * self.indent_level as f32))
.children((0..self.indent_level).map(|_| {
div()
.w(token.list_indent_depth)
.h_full()
.flex()
.justify_center()
.child(h_stack().child(div().w_px().h_full()).child(
div().w_px().h_full().fill(theme.middle.base.default.border),
))
}))
.flex()
.gap_1()
.items_center()
.relative()
.children(self.disclosure_control(cx))
.children(left_content)
.child(self.label.clone()),
)
}
}
#[derive(Clone, Default, Element)]
pub struct ListSeparator;
impl ListSeparator {
pub fn new() -> Self {
Self::default()
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div().h_px().w_full().fill(theme.lowest.base.default.border)
}
}
#[derive(Element)]
pub struct List {
items: Vec<ListItem>,
empty_message: &'static str,
header: Option<ListHeader>,
toggleable: Toggleable,
}
impl List {
pub fn new(items: Vec<ListItem>) -> Self {
Self {
items,
empty_message: "No items",
header: None,
toggleable: Toggleable::default(),
}
}
pub fn empty_message(mut self, empty_message: &'static str) -> Self {
self.empty_message = empty_message;
self
}
pub fn header(mut self, header: ListHeader) -> Self {
self.header = Some(header);
self
}
pub fn set_toggle(mut self, toggle: ToggleState) -> Self {
self.toggleable = toggle.into();
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = Toggleable::is_toggled(&self.toggleable);
let disclosure_control = if is_toggleable {
IconElement::new(Icon::ChevronRight)
} else {
IconElement::new(Icon::ChevronDown)
};
let list_content = match (self.items.is_empty(), is_toggled) {
(_, false) => div(),
(false, _) => div().children(self.items.iter().cloned()),
(true, _) => div().child(Label::new(self.empty_message).color(LabelColor::Muted)),
};
v_stack()
.py_1()
.children(
self.header
.clone()
.map(|header| header.set_toggleable(self.toggleable)),
)
.child(list_content)
}
}

View File

@ -1,42 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::{v_stack, Buffer, Icon, IconButton, Label, LabelSize};
#[derive(Element)]
pub struct MultiBuffer<V: 'static> {
view_type: PhantomData<V>,
buffers: Vec<Buffer>,
}
impl<V: 'static> MultiBuffer<V> {
pub fn new(buffers: Vec<Buffer>) -> Self {
Self {
view_type: PhantomData,
buffers,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
v_stack()
.w_full()
.h_full()
.flex_1()
.children(self.buffers.clone().into_iter().map(|buffer| {
v_stack()
.child(
div()
.flex()
.items_center()
.justify_between()
.p_4()
.fill(theme.lowest.base.default.background)
.child(Label::new("main.rs").size(LabelSize::Small))
.child(IconButton::new(Icon::ArrowUpRight)),
)
.child(buffer)
}))
}
}

View File

@ -1,152 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::theme::theme;
use crate::{h_stack, v_stack, Keybinding, Label, LabelColor};
#[derive(Element)]
pub struct Palette<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
input_placeholder: &'static str,
empty_string: &'static str,
items: Vec<PaletteItem>,
default_order: OrderMethod,
}
impl<V: 'static> Palette<V> {
pub fn new(scroll_state: ScrollState) -> Self {
Self {
view_type: PhantomData,
scroll_state,
input_placeholder: "Find something...",
empty_string: "No items found.",
items: vec![],
default_order: OrderMethod::default(),
}
}
pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
self.items = items;
self
}
pub fn placeholder(mut self, input_placeholder: &'static str) -> Self {
self.input_placeholder = input_placeholder;
self
}
pub fn empty_string(mut self, empty_string: &'static str) -> Self {
self.empty_string = empty_string;
self
}
// TODO: Hook up sort order
pub fn default_order(mut self, default_order: OrderMethod) -> Self {
self.default_order = default_order;
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
v_stack()
.w_96()
.rounded_lg()
.fill(theme.lowest.base.default.background)
.border()
.border_color(theme.lowest.base.default.border)
.child(
v_stack()
.gap_px()
.child(v_stack().py_0p5().px_1().child(
div().px_2().py_0p5().child(
Label::new(self.input_placeholder).color(LabelColor::Placeholder),
),
))
.child(div().h_px().w_full().fill(theme.lowest.base.default.border))
.child(
v_stack()
.py_0p5()
.px_1()
.grow()
.max_h_96()
.overflow_y_scroll(self.scroll_state.clone())
.children(
vec![if self.items.is_empty() {
Some(h_stack().justify_between().px_2().py_1().child(
Label::new(self.empty_string).color(LabelColor::Muted),
))
} else {
None
}]
.into_iter()
.flatten(),
)
.children(self.items.iter().map(|item| {
h_stack()
.justify_between()
.px_2()
.py_0p5()
.rounded_lg()
.hover()
.fill(theme.lowest.base.hovered.background)
.active()
.fill(theme.lowest.base.pressed.background)
.child(item.clone())
})),
),
)
}
}
#[derive(Element, Clone)]
pub struct PaletteItem {
pub label: &'static str,
pub sublabel: Option<&'static str>,
pub keybinding: Option<Keybinding>,
}
impl PaletteItem {
pub fn new(label: &'static str) -> Self {
Self {
label,
sublabel: None,
keybinding: None,
}
}
pub fn label(mut self, label: &'static str) -> Self {
self.label = label;
self
}
pub fn sublabel<L: Into<Option<&'static str>>>(mut self, sublabel: L) -> Self {
self.sublabel = sublabel.into();
self
}
pub fn keybinding<K>(mut self, keybinding: K) -> Self
where
K: Into<Option<Keybinding>>,
{
self.keybinding = keybinding.into();
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.flex()
.flex_row()
.grow()
.justify_between()
.child(
v_stack()
.child(Label::new(self.label))
.children(self.sublabel.map(|sublabel| Label::new(sublabel))),
)
.children(self.keybinding.clone())
}
}

View File

@ -1,142 +0,0 @@
use std::marker::PhantomData;
use gpui2::geometry::AbsoluteLength;
use crate::prelude::*;
use crate::{theme, token, v_stack};
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum PanelAllowedSides {
LeftOnly,
RightOnly,
BottomOnly,
#[default]
LeftAndRight,
All,
}
impl PanelAllowedSides {
/// Return a `HashSet` that contains the allowable `PanelSide`s.
pub fn allowed_sides(&self) -> HashSet<PanelSide> {
match self {
Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum PanelSide {
#[default]
Left,
Right,
Bottom,
}
use std::collections::HashSet;
#[derive(Element)]
pub struct Panel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
current_side: PanelSide,
/// Defaults to PanelAllowedSides::LeftAndRight
allowed_sides: PanelAllowedSides,
initial_width: AbsoluteLength,
width: Option<AbsoluteLength>,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
}
impl<V: 'static> Panel<V> {
pub fn new(
scroll_state: ScrollState,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
) -> Self {
let token = token();
Self {
view_type: PhantomData,
scroll_state,
current_side: PanelSide::default(),
allowed_sides: PanelAllowedSides::default(),
initial_width: token.default_panel_size,
width: None,
children,
payload,
}
}
pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
self.initial_width = initial_width;
self
}
pub fn width(mut self, width: AbsoluteLength) -> Self {
self.width = Some(width);
self
}
pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
self.allowed_sides = allowed_sides;
self
}
pub fn side(mut self, side: PanelSide) -> Self {
let allowed_sides = self.allowed_sides.allowed_sides();
if allowed_sides.contains(&side) {
self.current_side = side;
} else {
panic!(
"The panel side {:?} was not added as allowed before it was set.",
side
);
}
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let token = token();
let theme = theme(cx);
let panel_base;
let current_width = self.width.unwrap_or(self.initial_width);
match self.current_side {
PanelSide::Left => {
panel_base = v_stack()
.flex_initial()
.h_full()
.w(current_width)
.fill(theme.middle.base.default.background)
.border_r()
.border_color(theme.middle.base.default.border);
}
PanelSide::Right => {
panel_base = v_stack()
.flex_initial()
.h_full()
.w(current_width)
.fill(theme.middle.base.default.background)
.border_l()
.border_color(theme.middle.base.default.border);
}
PanelSide::Bottom => {
panel_base = v_stack()
.flex_initial()
.w_full()
.h(current_width)
.fill(theme.middle.base.default.background)
.border_t()
.border_color(theme.middle.base.default.border);
}
}
panel_base.children_any((self.children)(cx, self.payload.as_ref()))
}
}

View File

@ -1,132 +0,0 @@
use std::marker::PhantomData;
use gpui2::geometry::{Length, Size};
use gpui2::{hsla, Hsla};
use crate::prelude::*;
use crate::theme;
#[derive(Default, PartialEq)]
pub enum SplitDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Element)]
pub struct Pane<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
size: Size<Length>,
fill: Hsla,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
}
impl<V: 'static> Pane<V> {
pub fn new(
scroll_state: ScrollState,
size: Size<Length>,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
) -> Self {
// Fill is only here for debugging purposes, remove before release
let system_color = SystemColor::new();
Self {
view_type: PhantomData,
scroll_state,
size,
fill: hsla(0.3, 0.3, 0.3, 1.),
// fill: system_color.transparent,
children,
payload,
}
}
pub fn fill(mut self, fill: Hsla) -> Self {
self.fill = fill;
self
}
fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.flex()
.flex_initial()
.fill(self.fill)
.w(self.size.width)
.h(self.size.height)
.overflow_y_scroll(self.scroll_state.clone())
.children_any((self.children)(cx, self.payload.as_ref()))
}
}
#[derive(Element)]
pub struct PaneGroup<V: 'static> {
view_type: PhantomData<V>,
groups: Vec<PaneGroup<V>>,
panes: Vec<Pane<V>>,
split_direction: SplitDirection,
}
impl<V: 'static> PaneGroup<V> {
pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
Self {
view_type: PhantomData,
groups,
panes: Vec::new(),
split_direction,
}
}
pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
Self {
view_type: PhantomData,
groups: Vec::new(),
panes,
split_direction,
}
}
fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
if !self.panes.is_empty() {
let el = div()
.flex()
.flex_1()
.gap_px()
.w_full()
.h_full()
.fill(theme.lowest.base.default.background)
.children(self.panes.iter_mut().map(|pane| pane.render(view, cx)));
if self.split_direction == SplitDirection::Horizontal {
return el;
} else {
return el.flex_col();
}
}
if !self.groups.is_empty() {
let el = div()
.flex()
.flex_1()
.gap_px()
.w_full()
.h_full()
.fill(theme.lowest.base.default.background)
.children(self.groups.iter_mut().map(|group| group.render(view, cx)));
if self.split_direction == SplitDirection::Horizontal {
return el;
} else {
return el.flex_col();
}
}
unreachable!()
}
}

View File

@ -1,65 +0,0 @@
use crate::prelude::*;
use crate::{Avatar, Facepile, PlayerWithCallStatus};
#[derive(Element)]
pub struct PlayerStack {
player_with_call_status: PlayerWithCallStatus,
}
impl PlayerStack {
pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
Self {
player_with_call_status,
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let system_color = SystemColor::new();
let player = self.player_with_call_status.get_player();
self.player_with_call_status.get_call_status();
let followers = self
.player_with_call_status
.get_call_status()
.followers
.as_ref()
.map(|followers| followers.clone());
// if we have no followers return a slightly different element
// if mic_status == muted add a red ring to avatar
div()
.h_full()
.flex()
.flex_col()
.gap_px()
.justify_center()
.child(
div().flex().justify_center().w_full().child(
div()
.w_4()
.h_0p5()
.rounded_sm()
.fill(player.cursor_color(cx)),
),
)
.child(
div()
.flex()
.items_center()
.justify_center()
.h_6()
.pl_1()
.rounded_lg()
.fill(if followers.is_none() {
system_color.transparent
} else {
player.selection_color(cx)
})
.child(Avatar::new(player.avatar_src().to_string()))
.children(followers.map(|followers| {
div().neg_ml_2().child(Facepile::new(followers.into_iter()))
})),
)
}
}

View File

@ -1,58 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::{
static_project_panel_project_items, static_project_panel_single_items, theme, Input, List,
ListHeader,
};
#[derive(Element)]
pub struct ProjectPanel<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
}
impl<V: 'static> ProjectPanel<V> {
pub fn new(scroll_state: ScrollState) -> Self {
Self {
view_type: PhantomData,
scroll_state,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.flex()
.flex_col()
.w_full()
.h_full()
.px_2()
.fill(theme.middle.base.default.background)
.child(
div()
.w_56()
.flex()
.flex_col()
.overflow_y_scroll(ScrollState::default())
.child(
List::new(static_project_panel_single_items())
.header(ListHeader::new("FILES").set_toggle(ToggleState::Toggled))
.empty_message("No files in directory")
.set_toggle(ToggleState::Toggled),
)
.child(
List::new(static_project_panel_project_items())
.header(ListHeader::new("PROJECT").set_toggle(ToggleState::Toggled))
.empty_message("No folders in directory")
.set_toggle(ToggleState::Toggled),
),
)
.child(
Input::new("Find something...")
.value("buffe".to_string())
.state(InteractionState::Focused),
)
}
}

View File

@ -1,32 +0,0 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Element)]
pub struct RecentProjects {
scroll_state: ScrollState,
}
impl RecentProjects {
pub fn new() -> Self {
Self {
scroll_state: ScrollState::default(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div().child(
Palette::new(self.scroll_state.clone())
.items(vec![
PaletteItem::new("zed").sublabel("~/projects/zed"),
PaletteItem::new("saga").sublabel("~/projects/saga"),
PaletteItem::new("journal").sublabel("~/journal"),
PaletteItem::new("dotfiles").sublabel("~/dotfiles"),
PaletteItem::new("zed.dev").sublabel("~/projects/zed.dev"),
PaletteItem::new("laminar").sublabel("~/projects/laminar"),
])
.placeholder("Recent Projects...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}

View File

@ -1,144 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::theme::{theme, Theme};
use crate::{Button, Icon, IconButton, IconColor, ToolDivider};
#[derive(Default, PartialEq)]
pub enum Tool {
#[default]
ProjectPanel,
CollaborationPanel,
Terminal,
Assistant,
Feedback,
Diagnostics,
}
struct ToolGroup {
active_index: Option<usize>,
tools: Vec<Tool>,
}
impl Default for ToolGroup {
fn default() -> Self {
ToolGroup {
active_index: None,
tools: vec![],
}
}
}
#[derive(Element)]
pub struct StatusBar<V: 'static> {
view_type: PhantomData<V>,
left_tools: Option<ToolGroup>,
right_tools: Option<ToolGroup>,
bottom_tools: Option<ToolGroup>,
}
impl<V: 'static> StatusBar<V> {
pub fn new() -> Self {
Self {
view_type: PhantomData,
left_tools: None,
right_tools: None,
bottom_tools: None,
}
}
pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.left_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.right_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
self.bottom_tools = {
let mut tools = vec![tool];
tools.extend(self.left_tools.take().unwrap_or_default().tools);
Some(ToolGroup {
active_index,
tools,
})
};
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.py_0p5()
.px_1()
.flex()
.items_center()
.justify_between()
.w_full()
.fill(theme.lowest.base.default.background)
.child(self.left_tools(&theme))
.child(self.right_tools(&theme))
}
fn left_tools(&self, theme: &Theme) -> impl Element<V> {
div()
.flex()
.items_center()
.gap_1()
.child(IconButton::new(Icon::FileTree).color(IconColor::Accent))
.child(IconButton::new(Icon::Hash))
.child(ToolDivider::new())
.child(IconButton::new(Icon::XCircle))
}
fn right_tools(&self, theme: &Theme) -> impl Element<V> {
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.flex()
.items_center()
.gap_1()
.child(Button::new("116:25"))
.child(Button::new("Rust")),
)
.child(ToolDivider::new())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(IconButton::new(Icon::Copilot))
.child(IconButton::new(Icon::Envelope)),
)
.child(ToolDivider::new())
.child(
div()
.flex()
.items_center()
.gap_1()
.child(IconButton::new(Icon::Terminal))
.child(IconButton::new(Icon::MessageBubbles))
.child(IconButton::new(Icon::Ai)),
)
}
}

View File

@ -1,131 +0,0 @@
use crate::prelude::*;
use crate::{theme, Icon, IconColor, IconElement, Label, LabelColor};
#[derive(Element, Clone)]
pub struct Tab {
title: String,
icon: Option<Icon>,
current: bool,
dirty: bool,
fs_status: FileSystemStatus,
git_status: GitStatus,
diagnostic_status: DiagnosticStatus,
close_side: IconSide,
}
impl Tab {
pub fn new() -> Self {
Self {
title: "untitled".to_string(),
icon: None,
current: false,
dirty: false,
fs_status: FileSystemStatus::None,
git_status: GitStatus::None,
diagnostic_status: DiagnosticStatus::None,
close_side: IconSide::Right,
}
}
pub fn current(mut self, current: bool) -> Self {
self.current = current;
self
}
pub fn title(mut self, title: String) -> Self {
self.title = title;
self
}
pub fn icon<I>(mut self, icon: I) -> Self
where
I: Into<Option<Icon>>,
{
self.icon = icon.into();
self
}
pub fn dirty(mut self, dirty: bool) -> Self {
self.dirty = dirty;
self
}
pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
self.fs_status = fs_status;
self
}
pub fn git_status(mut self, git_status: GitStatus) -> Self {
self.git_status = git_status;
self
}
pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
self.diagnostic_status = diagnostic_status;
self
}
pub fn close_side(mut self, close_side: IconSide) -> Self {
self.close_side = close_side;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
let is_deleted = self.fs_status == FileSystemStatus::Deleted;
let label = match (self.git_status, is_deleted) {
(_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
.color(LabelColor::Hidden)
.set_strikethrough(true),
(GitStatus::None, false) => Label::new(self.title.clone()),
(GitStatus::Created, false) => {
Label::new(self.title.clone()).color(LabelColor::Created)
}
(GitStatus::Modified, false) => {
Label::new(self.title.clone()).color(LabelColor::Modified)
}
(GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
(GitStatus::Conflict, false) => Label::new(self.title.clone()),
};
let close_icon = IconElement::new(Icon::Close).color(IconColor::Muted);
div()
.px_2()
.py_0p5()
.flex()
.items_center()
.justify_center()
.fill(if self.current {
theme.highest.base.default.background
} else {
theme.middle.base.default.background
})
.child(
div()
.px_1()
.flex()
.items_center()
.gap_1()
.children(has_fs_conflict.then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(crate::IconSize::Small)
.color(IconColor::Warning)
}))
.children(self.icon.map(IconElement::new))
.children(if self.close_side == IconSide::Left {
Some(close_icon.clone())
} else {
None
})
.child(label)
.children(if self.close_side == IconSide::Right {
Some(close_icon)
} else {
None
}),
)
}
}

View File

@ -1,85 +0,0 @@
use std::marker::PhantomData;
use crate::prelude::*;
use crate::{theme, Icon, IconButton, Tab};
#[derive(Element)]
pub struct TabBar<V: 'static> {
view_type: PhantomData<V>,
scroll_state: ScrollState,
tabs: Vec<Tab>,
}
impl<V: 'static> TabBar<V> {
pub fn new(tabs: Vec<Tab>) -> Self {
Self {
view_type: PhantomData,
scroll_state: ScrollState::default(),
tabs,
}
}
pub fn bind_scroll_state(&mut self, scroll_state: ScrollState) {
self.scroll_state = scroll_state;
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let can_navigate_back = true;
let can_navigate_forward = false;
div()
.w_full()
.flex()
.fill(theme.middle.base.default.background)
// Left Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(
IconButton::new(Icon::ArrowLeft)
.state(InteractionState::Enabled.if_enabled(can_navigate_back)),
)
.child(
IconButton::new(Icon::ArrowRight).state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
),
),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.flex()
.overflow_x_scroll(self.scroll_state.clone())
.children(self.tabs.clone()),
),
)
// Right Side
.child(
div()
.px_1()
.flex()
.flex_none()
.gap_2()
// Nav Buttons
.child(
div()
.flex()
.items_center()
.gap_px()
.child(IconButton::new(Icon::Plus))
.child(IconButton::new(Icon::Split)),
),
)
}
}

View File

@ -1,84 +0,0 @@
use std::sync::Arc;
use gpui2::geometry::{relative, rems, Size};
use crate::prelude::*;
use crate::{theme, Icon, IconButton, Pane, Tab};
#[derive(Element)]
pub struct Terminal {}
impl Terminal {
pub fn new() -> Self {
Self {}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let can_navigate_back = true;
let can_navigate_forward = false;
div()
.flex()
.flex_col()
.w_full()
.child(
// Terminal Tabs.
div()
.w_full()
.flex()
.fill(theme.middle.base.default.background)
.child(
div().px_1().flex().flex_none().gap_2().child(
div()
.flex()
.items_center()
.gap_px()
.child(
IconButton::new(Icon::ArrowLeft).state(
InteractionState::Enabled.if_enabled(can_navigate_back),
),
)
.child(IconButton::new(Icon::ArrowRight).state(
InteractionState::Enabled.if_enabled(can_navigate_forward),
)),
),
)
.child(
div().w_0().flex_1().h_full().child(
div()
.flex()
.child(
Tab::new()
.title("zed — fish".to_string())
.icon(Icon::Terminal)
.close_side(IconSide::Right)
.current(true),
)
.child(
Tab::new()
.title("zed — fish".to_string())
.icon(Icon::Terminal)
.close_side(IconSide::Right)
.current(false),
),
),
),
)
// Terminal Pane.
.child(Pane::new(
ScrollState::default(),
Size {
width: relative(1.).into(),
height: rems(36.).into(),
},
|_, payload| {
let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
vec![crate::static_data::terminal_buffer(&theme).into_any()]
},
Box::new(theme),
))
}
}

View File

@ -1,37 +0,0 @@
use crate::prelude::*;
use crate::{OrderMethod, Palette, PaletteItem};
#[derive(Element)]
pub struct ThemeSelector {
scroll_state: ScrollState,
}
impl ThemeSelector {
pub fn new() -> Self {
Self {
scroll_state: ScrollState::default(),
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
div().child(
Palette::new(self.scroll_state.clone())
.items(vec![
PaletteItem::new("One Dark"),
PaletteItem::new("Rosé Pine"),
PaletteItem::new("Rosé Pine Moon"),
PaletteItem::new("Sandcastle"),
PaletteItem::new("Solarized Dark"),
PaletteItem::new("Summercamp"),
PaletteItem::new("Atelier Cave Light"),
PaletteItem::new("Atelier Dune Light"),
PaletteItem::new("Atelier Estuary Light"),
PaletteItem::new("Atelier Forest Light"),
PaletteItem::new("Atelier Heath Light"),
])
.placeholder("Select Theme...")
.empty_string("No matches")
.default_order(OrderMethod::Ascending),
)
}
}

View File

@ -1,117 +0,0 @@
use std::marker::PhantomData;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use crate::{prelude::*, PlayerWithCallStatus};
use crate::{
theme, Avatar, Button, Icon, IconButton, IconColor, PlayerStack, ToolDivider, TrafficLights,
};
#[derive(Clone)]
pub struct Livestream {
pub players: Vec<PlayerWithCallStatus>,
pub channel: Option<String>, // projects
// windows
}
#[derive(Element)]
pub struct TitleBar<V: 'static> {
view_type: PhantomData<V>,
/// If the window is active from the OS's perspective.
is_active: Arc<AtomicBool>,
livestream: Option<Livestream>,
}
impl<V: 'static> TitleBar<V> {
pub fn new(cx: &mut ViewContext<V>) -> Self {
let is_active = Arc::new(AtomicBool::new(true));
let active = is_active.clone();
cx.observe_window_activation(move |_, is_active, cx| {
active.store(is_active, std::sync::atomic::Ordering::SeqCst);
cx.notify();
})
.detach();
Self {
view_type: PhantomData,
is_active,
livestream: None,
}
}
pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
self.livestream = livestream;
self
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let has_focus = cx.window_is_active();
let player_list = if let Some(livestream) = &self.livestream {
livestream.players.clone().into_iter()
} else {
vec![].into_iter()
};
div()
.flex()
.items_center()
.justify_between()
.w_full()
.h_8()
.fill(theme.lowest.base.default.background)
.child(
div()
.flex()
.items_center()
.h_full()
.gap_4()
.px_2()
.child(TrafficLights::new().window_has_focus(has_focus))
// === Project Info === //
.child(
div()
.flex()
.items_center()
.gap_1()
.child(Button::new("zed"))
.child(Button::new("nate/gpui2-ui-components")),
)
.children(player_list.map(|p| PlayerStack::new(p)))
.child(IconButton::new(Icon::Plus)),
)
.child(
div()
.flex()
.items_center()
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(IconButton::new(Icon::FolderX))
.child(IconButton::new(Icon::Close)),
)
.child(ToolDivider::new())
.child(
div()
.px_2()
.flex()
.items_center()
.gap_1()
.child(IconButton::new(Icon::Mic))
.child(IconButton::new(Icon::AudioOn))
.child(IconButton::new(Icon::Screen).color(IconColor::Accent)),
)
.child(
div().px_2().flex().items_center().child(
Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
.shape(Shape::RoundedRectangle),
),
),
)
}
}

View File

@ -1,66 +0,0 @@
use crate::prelude::*;
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum ToastOrigin {
#[default]
Bottom,
BottomRight,
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub enum ToastVariant {
#[default]
Toast,
Status,
}
/// A toast is a small, temporary window that appears to show a message to the user
/// or indicate a required action.
///
/// Toasts should not persist on the screen for more than a few seconds unless
/// they are actively showing the a process in progress.
///
/// Only one toast may be visible at a time.
#[derive(Element)]
pub struct Toast<V: 'static> {
origin: ToastOrigin,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
}
impl<V: 'static> Toast<V> {
pub fn new(
origin: ToastOrigin,
children: HackyChildren<V>,
payload: HackyChildrenPayload,
) -> Self {
Self {
origin,
children,
payload,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let color = ThemeColor::new(cx);
let mut div = div();
if self.origin == ToastOrigin::Bottom {
div = div.right_1_2();
} else {
div = div.right_4();
}
div.absolute()
.bottom_4()
.flex()
.py_2()
.px_1p5()
.min_w_40()
.rounded_md()
.fill(color.elevated_surface)
.max_w_64()
.children_any((self.children)(cx, self.payload.as_ref()))
}
}

View File

@ -1,49 +0,0 @@
use crate::prelude::*;
use crate::theme;
#[derive(Clone)]
pub struct ToolbarItem {}
#[derive(Element)]
pub struct Toolbar<V: 'static> {
left_items: HackyChildren<V>,
left_items_payload: HackyChildrenPayload,
right_items: HackyChildren<V>,
right_items_payload: HackyChildrenPayload,
}
impl<V: 'static> Toolbar<V> {
pub fn new(
left_items: HackyChildren<V>,
left_items_payload: HackyChildrenPayload,
right_items: HackyChildren<V>,
right_items_payload: HackyChildrenPayload,
) -> Self {
Self {
left_items,
left_items_payload,
right_items,
right_items_payload,
}
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
.fill(theme.highest.base.default.background)
.p_2()
.flex()
.justify_between()
.child(
div()
.flex()
.children_any((self.left_items)(cx, self.left_items_payload.as_ref())),
)
.child(
div()
.flex()
.children_any((self.right_items)(cx, self.right_items_payload.as_ref())),
)
}
}

View File

@ -1,78 +0,0 @@
use crate::prelude::*;
use crate::{theme, token, SystemColor};
#[derive(Clone, Copy)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}
#[derive(Element)]
struct TrafficLight {
color: TrafficLightColor,
window_has_focus: bool,
}
impl TrafficLight {
fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
Self {
color,
window_has_focus,
}
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let system_color = SystemColor::new();
let fill = match (self.window_has_focus, self.color) {
(true, TrafficLightColor::Red) => system_color.mac_os_traffic_light_red,
(true, TrafficLightColor::Yellow) => system_color.mac_os_traffic_light_yellow,
(true, TrafficLightColor::Green) => system_color.mac_os_traffic_light_green,
(false, _) => theme.lowest.base.active.background,
};
div().w_3().h_3().rounded_full().fill(fill)
}
}
#[derive(Element)]
pub struct TrafficLights {
window_has_focus: bool,
}
impl TrafficLights {
pub fn new() -> Self {
Self {
window_has_focus: true,
}
}
pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
self.window_has_focus = window_has_focus;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let token = token();
div()
.flex()
.items_center()
.gap_2()
.child(TrafficLight::new(
TrafficLightColor::Red,
self.window_has_focus,
))
.child(TrafficLight::new(
TrafficLightColor::Yellow,
self.window_has_focus,
))
.child(TrafficLight::new(
TrafficLightColor::Green,
self.window_has_focus,
))
}
}

View File

@ -1,186 +0,0 @@
use std::sync::Arc;
use chrono::DateTime;
use gpui2::geometry::{relative, rems, Size};
use crate::{
hello_world_rust_editor_with_status_example, prelude::*, random_players_with_call_status,
Livestream,
};
use crate::{
theme, v_stack, ChatMessage, ChatPanel, EditorPane, Pane, PaneGroup, Panel, PanelAllowedSides,
PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
};
#[derive(Element, Default)]
pub struct WorkspaceElement {
left_panel_scroll_state: ScrollState,
right_panel_scroll_state: ScrollState,
tab_bar_scroll_state: ScrollState,
bottom_panel_scroll_state: ScrollState,
}
impl WorkspaceElement {
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx).clone();
let temp_size = rems(36.).into();
let root_group = PaneGroup::new_groups(
vec![
PaneGroup::new_panes(
vec![
Pane::new(
ScrollState::default(),
Size {
width: relative(1.).into(),
height: temp_size,
},
|_, payload| {
let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
vec![EditorPane::new(hello_world_rust_editor_with_status_example(
&theme,
))
.into_any()]
},
Box::new(theme.clone()),
),
Pane::new(
ScrollState::default(),
Size {
width: relative(1.).into(),
height: temp_size,
},
|_, _| vec![Terminal::new().into_any()],
Box::new(()),
),
],
SplitDirection::Vertical,
),
PaneGroup::new_panes(
vec![Pane::new(
ScrollState::default(),
Size {
width: relative(1.).into(),
height: relative(1.).into(),
},
|_, payload| {
let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
vec![EditorPane::new(hello_world_rust_editor_with_status_example(
&theme,
))
.into_any()]
},
Box::new(theme.clone()),
)],
SplitDirection::Vertical,
),
],
SplitDirection::Horizontal,
);
div()
.relative()
.size_full()
.flex()
.flex_col()
.font("Zed Sans Extended")
.gap_0()
.justify_start()
.items_start()
.text_color(theme.lowest.base.default.foreground)
.fill(theme.lowest.base.default.background)
.child(TitleBar::new(cx).set_livestream(Some(Livestream {
players: random_players_with_call_status(7),
channel: Some("gpui2-ui".to_string()),
})))
.child(
div()
.flex_1()
.w_full()
.flex()
.flex_row()
.overflow_hidden()
.border_t()
.border_b()
.border_color(theme.lowest.base.default.border)
.child(
Panel::new(
self.left_panel_scroll_state.clone(),
|_, payload| vec![ProjectPanel::new(ScrollState::default()).into_any()],
Box::new(()),
)
.side(PanelSide::Left),
)
.child(
v_stack()
.flex_1()
.h_full()
.child(
div()
.flex()
.flex_1()
// CSS Hack: Flex 1 has to have a set height to properly fill the space
// Or it will give you a height of 0
.h_px()
.child(root_group),
)
.child(
Panel::new(
self.bottom_panel_scroll_state.clone(),
|_, _| vec![Terminal::new().into_any()],
Box::new(()),
)
.allowed_sides(PanelAllowedSides::BottomOnly)
.side(PanelSide::Bottom),
),
)
.child(
Panel::new(
self.right_panel_scroll_state.clone(),
|_, payload| {
vec![ChatPanel::new(ScrollState::default())
.with_messages(vec![
ChatMessage::new(
"osiewicz".to_string(),
"is this thing on?".to_string(),
DateTime::parse_from_rfc3339(
"2023-09-27T15:40:52.707Z",
)
.unwrap()
.naive_local(),
),
ChatMessage::new(
"maxdeviant".to_string(),
"Reading you loud and clear!".to_string(),
DateTime::parse_from_rfc3339(
"2023-09-28T15:40:52.707Z",
)
.unwrap()
.naive_local(),
),
])
.into_any()]
},
Box::new(()),
)
.side(PanelSide::Right),
),
)
.child(StatusBar::new())
// An example of a toast is below
// Currently because of stacking order this gets obscured by other elements
// .child(Toast::new(
// ToastOrigin::Bottom,
// |_, payload| {
// let theme = payload.downcast_ref::<Arc<Theme>>().unwrap();
// vec![Label::new("label").into_any()]
// },
// Box::new(theme.clone()),
// ))
}
}

View File

@ -1,24 +0,0 @@
use std::marker::PhantomData;
use gpui2::Element;
use crate::theme::{Theme, Themed};
pub trait ElementExt<V: 'static>: Element<V> {
fn themed(self, theme: Theme) -> Themed<V, Self>
where
Self: Sized;
}
impl<V: 'static, E: Element<V>> ElementExt<V> for E {
fn themed(self, theme: Theme) -> Themed<V, Self>
where
Self: Sized,
{
Themed {
child: self,
theme,
view_type: PhantomData,
}
}
}

View File

@ -1,19 +0,0 @@
mod avatar;
mod button;
mod details;
mod icon;
mod input;
mod label;
mod player;
mod stack;
mod tool_divider;
pub use avatar::*;
pub use button::*;
pub use details::*;
pub use icon::*;
pub use input::*;
pub use label::*;
pub use player::*;
pub use stack::*;
pub use tool_divider::*;

View File

@ -1,41 +0,0 @@
use gpui2::elements::img;
use gpui2::ArcCow;
use crate::prelude::*;
use crate::theme;
#[derive(Element, Clone)]
pub struct Avatar {
src: ArcCow<'static, str>,
shape: Shape,
}
impl Avatar {
pub fn new(src: impl Into<ArcCow<'static, str>>) -> Self {
Self {
src: src.into(),
shape: Shape::Circle,
}
}
pub fn shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let mut img = img();
if self.shape == Shape::Circle {
img = img.rounded_full();
} else {
img = img.rounded_md();
}
img.uri(self.src.clone())
.size_4()
.fill(theme.middle.warning.default.foreground)
}
}

View File

@ -1,203 +0,0 @@
use std::rc::Rc;
use gpui2::geometry::DefiniteLength;
use gpui2::platform::MouseButton;
use gpui2::{EventContext, Hsla, Interactive, WindowContext};
use crate::prelude::*;
use crate::{h_stack, theme, Icon, IconColor, IconElement, Label, LabelColor, LabelSize};
#[derive(Default, PartialEq, Clone, Copy)]
pub enum IconPosition {
#[default]
Left,
Right,
}
#[derive(Default, Copy, Clone, PartialEq)]
pub enum ButtonVariant {
#[default]
Ghost,
Filled,
}
struct ButtonHandlers<V> {
click: Option<Rc<dyn Fn(&mut V, &mut EventContext<V>)>>,
}
impl<V> Default for ButtonHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Element)]
pub struct Button<V: 'static> {
label: String,
variant: ButtonVariant,
state: InteractionState,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
width: Option<DefiniteLength>,
handlers: ButtonHandlers<V>,
}
impl<V: 'static> Button<V> {
pub fn new<L>(label: L) -> Self
where
L: Into<String>,
{
Self {
label: label.into(),
variant: Default::default(),
state: Default::default(),
icon: None,
icon_position: None,
width: Default::default(),
handlers: ButtonHandlers::default(),
}
}
pub fn ghost<L>(label: L) -> Self
where
L: Into<String>,
{
Self::new(label).variant(ButtonVariant::Ghost)
}
pub fn variant(mut self, variant: ButtonVariant) -> Self {
self.variant = variant;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn icon_position(mut self, icon_position: IconPosition) -> Self {
if self.icon.is_none() {
panic!("An icon must be present if an icon_position is provided.");
}
self.icon_position = Some(icon_position);
self
}
pub fn width(mut self, width: Option<DefiniteLength>) -> Self {
self.width = width;
self
}
pub fn on_click(mut self, handler: impl Fn(&mut V, &mut EventContext<V>) + 'static) -> Self {
self.handlers.click = Some(Rc::new(handler));
self
}
fn background_color(&self, cx: &mut ViewContext<V>) -> Hsla {
let theme = theme(cx);
let system_color = SystemColor::new();
match (self.variant, self.state) {
(ButtonVariant::Ghost, InteractionState::Hovered) => {
theme.lowest.base.hovered.background
}
(ButtonVariant::Ghost, InteractionState::Active) => {
theme.lowest.base.pressed.background
}
(ButtonVariant::Filled, InteractionState::Enabled) => {
theme.lowest.on.default.background
}
(ButtonVariant::Filled, InteractionState::Hovered) => {
theme.lowest.on.hovered.background
}
(ButtonVariant::Filled, InteractionState::Active) => theme.lowest.on.pressed.background,
(ButtonVariant::Filled, InteractionState::Disabled) => {
theme.lowest.on.disabled.background
}
_ => system_color.transparent,
}
}
fn label_color(&self) -> LabelColor {
match self.state {
InteractionState::Disabled => LabelColor::Disabled,
_ => Default::default(),
}
}
fn icon_color(&self) -> IconColor {
match self.state {
InteractionState::Disabled => IconColor::Disabled,
_ => Default::default(),
}
}
fn border_color(&self, cx: &WindowContext) -> Hsla {
let theme = theme(cx);
let system_color = SystemColor::new();
match self.state {
InteractionState::Focused => theme.lowest.accent.default.border,
_ => system_color.transparent,
}
}
fn render_label(&self) -> Label {
Label::new(self.label.clone())
.size(LabelSize::Small)
.color(self.label_color())
}
fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
fn render(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let icon_color = self.icon_color();
let system_color = SystemColor::new();
let border_color = self.border_color(cx);
let mut el = h_stack()
.h_6()
.px_1()
.items_center()
.rounded_md()
.border()
.border_color(border_color)
.fill(self.background_color(cx));
match (self.icon, self.icon_position) {
(Some(_), Some(IconPosition::Left)) => {
el = el
.gap_1()
.child(self.render_label())
.children(self.render_icon(icon_color))
}
(Some(_), Some(IconPosition::Right)) => {
el = el
.gap_1()
.children(self.render_icon(icon_color))
.child(self.render_label())
}
(_, _) => el = el.child(self.render_label()),
}
if let Some(width) = self.width {
el = el.w(width).justify_center();
}
if let Some(click_handler) = self.handlers.click.clone() {
el = el.on_mouse_down(MouseButton::Left, move |view, event, cx| {
click_handler(view, cx);
});
}
el
}
}

View File

@ -1,33 +0,0 @@
use crate::prelude::*;
use crate::theme;
#[derive(Element, Clone)]
pub struct Details {
text: &'static str,
meta: Option<&'static str>,
}
impl Details {
pub fn new(text: &'static str) -> Self {
Self { text, meta: None }
}
pub fn meta_text(mut self, meta: &'static str) -> Self {
self.meta = Some(meta);
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
div()
// .flex()
// .w_full()
.p_1()
.gap_0p5()
.text_xs()
.text_color(theme.lowest.base.default.foreground)
.child(self.text)
.children(self.meta.map(|m| m))
}
}

View File

@ -1,185 +0,0 @@
use std::sync::Arc;
use gpui2::elements::svg;
use gpui2::Hsla;
use strum::EnumIter;
use crate::prelude::*;
use crate::theme::theme;
use crate::Theme;
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconSize {
Small,
#[default]
Large,
}
#[derive(Default, PartialEq, Copy, Clone)]
pub enum IconColor {
#[default]
Default,
Muted,
Disabled,
Placeholder,
Accent,
Error,
Warning,
Success,
Info,
}
impl IconColor {
pub fn color(self, theme: Arc<Theme>) -> Hsla {
match self {
IconColor::Default => theme.lowest.base.default.foreground,
IconColor::Muted => theme.lowest.variant.default.foreground,
IconColor::Disabled => theme.lowest.base.disabled.foreground,
IconColor::Placeholder => theme.lowest.base.disabled.foreground,
IconColor::Accent => theme.lowest.accent.default.foreground,
IconColor::Error => theme.lowest.negative.default.foreground,
IconColor::Warning => theme.lowest.warning.default.foreground,
IconColor::Success => theme.lowest.positive.default.foreground,
IconColor::Info => theme.lowest.accent.default.foreground,
}
}
}
#[derive(Default, PartialEq, Copy, Clone, EnumIter)]
pub enum Icon {
Ai,
ArrowLeft,
ArrowRight,
ArrowUpRight,
AudioOff,
AudioOn,
Bolt,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
Close,
ExclamationTriangle,
ExternalLink,
File,
FileGeneric,
FileDoc,
FileGit,
FileLock,
FileRust,
FileToml,
FileTree,
Folder,
FolderOpen,
FolderX,
#[default]
Hash,
InlayHint,
MagicWand,
MagnifyingGlass,
Maximize,
Menu,
MessageBubbles,
Mic,
MicMute,
Plus,
Quote,
Screen,
SelectAll,
Split,
SplitMessage,
Terminal,
XCircle,
Copilot,
Envelope,
}
impl Icon {
pub fn path(self) -> &'static str {
match self {
Icon::Ai => "icons/ai.svg",
Icon::ArrowLeft => "icons/arrow_left.svg",
Icon::ArrowRight => "icons/arrow_right.svg",
Icon::ArrowUpRight => "icons/arrow_up_right.svg",
Icon::AudioOff => "icons/speaker-off.svg",
Icon::AudioOn => "icons/speaker-loud.svg",
Icon::Bolt => "icons/bolt.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
Icon::ChevronRight => "icons/chevron_right.svg",
Icon::ChevronUp => "icons/chevron_up.svg",
Icon::Close => "icons/x.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
Icon::ExternalLink => "icons/external_link.svg",
Icon::File => "icons/file.svg",
Icon::FileGeneric => "icons/file_icons/file.svg",
Icon::FileDoc => "icons/file_icons/book.svg",
Icon::FileGit => "icons/file_icons/git.svg",
Icon::FileLock => "icons/file_icons/lock.svg",
Icon::FileRust => "icons/file_icons/rust.svg",
Icon::FileToml => "icons/file_icons/toml.svg",
Icon::FileTree => "icons/project.svg",
Icon::Folder => "icons/file_icons/folder.svg",
Icon::FolderOpen => "icons/file_icons/folder_open.svg",
Icon::FolderX => "icons/stop_sharing.svg",
Icon::Hash => "icons/hash.svg",
Icon::InlayHint => "icons/inlay_hint.svg",
Icon::MagicWand => "icons/magic-wand.svg",
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
Icon::Maximize => "icons/maximize.svg",
Icon::Menu => "icons/menu.svg",
Icon::MessageBubbles => "icons/conversations.svg",
Icon::Mic => "icons/mic.svg",
Icon::MicMute => "icons/mic-mute.svg",
Icon::Plus => "icons/plus.svg",
Icon::Quote => "icons/quote.svg",
Icon::Screen => "icons/desktop.svg",
Icon::SelectAll => "icons/select-all.svg",
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
Icon::XCircle => "icons/error.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::Envelope => "icons/feedback.svg",
}
}
}
#[derive(Element, Clone)]
pub struct IconElement {
icon: Icon,
color: IconColor,
size: IconSize,
}
impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
icon,
color: IconColor::default(),
size: IconSize::default(),
}
}
pub fn color(mut self, color: IconColor) -> Self {
self.color = color;
self
}
pub fn size(mut self, size: IconSize) -> Self {
self.size = size;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let fill = self.color.color(theme);
let sized_svg = match self.size {
IconSize::Small => svg().size_3p5(),
IconSize::Large => svg().size_4(),
};
sized_svg.flex_none().path(self.icon.path()).fill(fill)
}
}

View File

@ -1,106 +0,0 @@
use crate::prelude::*;
use crate::theme;
#[derive(Default, PartialEq)]
pub enum InputVariant {
#[default]
Ghost,
Filled,
}
#[derive(Element)]
pub struct Input {
placeholder: &'static str,
value: String,
state: InteractionState,
variant: InputVariant,
}
impl Input {
pub fn new(placeholder: &'static str) -> Self {
Self {
placeholder,
value: "".to_string(),
state: InteractionState::default(),
variant: InputVariant::default(),
}
}
pub fn value(mut self, value: String) -> Self {
self.value = value;
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
self
}
pub fn variant(mut self, variant: InputVariant) -> Self {
self.variant = variant;
self
}
fn render<V: 'static>(&mut self, _: &mut V, cx: &mut ViewContext<V>) -> impl IntoElement<V> {
let theme = theme(cx);
let text_el;
let text_color;
let background_color_default;
let background_color_active;
let mut border_color_default = theme.middle.base.default.border;
let mut border_color_hover = theme.middle.base.hovered.border;
let mut border_color_active = theme.middle.base.pressed.border;
let border_color_focus = theme.middle.base.pressed.background;
match self.variant {
InputVariant::Ghost => {
background_color_default = theme.middle.base.default.background;
background_color_active = theme.middle.base.active.background;
}
InputVariant::Filled => {
background_color_default = theme.middle.on.default.background;
background_color_active = theme.middle.on.active.background;
}
};
if self.state == InteractionState::Focused {
border_color_default = theme.players[0].cursor;
border_color_hover = theme.players[0].cursor;
border_color_active = theme.players[0].cursor;
}
if self.state == InteractionState::Focused || self.state == InteractionState::Active {
text_el = self.value.clone();
text_color = theme.lowest.base.default.foreground;
} else {
text_el = self.placeholder.to_string().clone();
text_color = theme.lowest.base.disabled.foreground;
}
div()
.h_7()
.w_full()
.px_2()
.border()
.border_color(border_color_default)
.fill(background_color_default)
.hover()
.border_color(border_color_hover)
.active()
.border_color(border_color_active)
.fill(background_color_active)
.flex()
.items_center()
.child(
div()
.flex()
.items_center()
.text_sm()
.text_color(text_color)
.child(text_el)
.child(div().text_color(theme.players[0].cursor).child("|")),
)
}
}

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