Merge branch 'main' into guests

This commit is contained in:
Conrad Irwin 2023-10-17 09:51:35 -06:00
commit 9cc55f895c
159 changed files with 3535 additions and 11178 deletions

97
Cargo.lock generated
View File

@ -1501,6 +1501,7 @@ dependencies = [
"log",
"lsp",
"nanoid",
"node_runtime",
"parking_lot 0.11.2",
"pretty_assertions",
"project",
@ -2404,7 +2405,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
"pulldown-cmark",
"rand 0.8.5",
"rich_text",
"rpc",
@ -3989,6 +3989,7 @@ dependencies = [
"lsp",
"parking_lot 0.11.2",
"postage",
"pulldown-cmark",
"rand 0.8.5",
"regex",
"rpc",
@ -5518,6 +5519,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"
@ -5630,8 +5651,10 @@ dependencies = [
"lazy_static",
"log",
"lsp",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"prettier",
"pretty_assertions",
"rand 0.8.5",
"regex",
@ -6601,12 +6624,6 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustybuzz"
version = "0.3.0"
@ -6940,7 +6957,6 @@ dependencies = [
"unindent",
"util",
"workspace",
"zed",
]
[[package]]
@ -7660,28 +7676,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",
"fs",
"futures 0.3.28",
"gpui2",
"itertools 0.11.0",
"log",
"rust-embed",
"serde",
"settings",
"simplelog",
"strum",
"theme",
"ui",
"util",
]
[[package]]
name = "stringprep"
version = "0.1.4"
@ -7704,22 +7698,6 @@ name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.37",
]
[[package]]
name = "subtle"
@ -8815,6 +8793,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
@ -8886,21 +8873,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 = "unicase"
version = "2.7.0"
@ -9987,6 +9959,7 @@ dependencies = [
"lazy_static",
"log",
"menu",
"node_runtime",
"parking_lot 0.11.2",
"postage",
"project",
@ -10085,6 +10058,7 @@ name = "zed"
version = "0.109.0"
dependencies = [
"activity_indicator",
"ai",
"anyhow",
"assistant",
"async-compression",
@ -10197,6 +10171,7 @@ dependencies = [
"tree-sitter-svelte",
"tree-sitter-toml",
"tree-sitter-typescript",
"tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
"url",

View File

@ -52,6 +52,7 @@ members = [
"crates/plugin",
"crates/plugin_macros",
"crates/plugin_runtime",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
@ -65,13 +66,11 @@ members = [
"crates/sqlez_macros",
"crates/feature_flags",
"crates/rich_text",
"crates/storybook",
"crates/sum_tree",
"crates/terminal",
"crates/text",
"crates/theme",
"crates/theme_selector",
"crates/ui",
"crates/util",
"crates/semantic_index",
"crates/vim",
@ -150,7 +149,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }

View File

@ -50,6 +50,9 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// Whether to show wrap guides in the editor. Setting this to true will
// show a guide at the 'preferred_line_length' value if softwrap is set to
// 'preferred_line_length', and will show any additional guides as specified
@ -199,7 +202,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 +437,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

@ -140,12 +140,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(())
@ -868,7 +877,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();
@ -879,7 +888,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

@ -4,7 +4,9 @@ use lazy_static::lazy_static;
use parking_lot::Mutex;
use serde::Serialize;
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use tempfile::NamedTempFile;
use util::http::HttpClient;
use util::{channel::ReleaseChannel, TryFutureExt};
@ -166,8 +168,16 @@ impl Telemetry {
let this = self.clone();
cx.spawn(|mut cx| async move {
let mut system = System::new_all();
system.refresh_all();
// Avoiding calling `System::new_all()`, as there have been crashes related to it
let refresh_kind = RefreshKind::new()
.with_memory() // For memory usage
.with_processes(ProcessRefreshKind::everything()) // For process usage
.with_cpu(CpuRefreshKind::everything()); // For core count
let mut system = System::new_with_specifics(refresh_kind);
// Avoiding calling `refresh_all()`, just update what we need
system.refresh_specifics(refresh_kind);
loop {
// Waiting some amount of time before the first query is important to get a reasonable value
@ -175,8 +185,7 @@ impl Telemetry {
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
system.refresh_memory();
system.refresh_processes();
system.refresh_specifics(refresh_kind);
let current_process = Pid::from_u32(std::process::id());
let Some(process) = system.processes().get(&current_process) else {

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

@ -339,8 +339,22 @@ impl Database {
.filter(channel_message::Column::SenderId.eq(user_id))
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("no such message"))?;
if self
.check_user_is_channel_admin(channel_id, user_id, &*tx)
.await
.is_ok()
{
let result = channel_message::Entity::delete_by_id(message_id)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
Err(anyhow!("no such message"))?;
}
} else {
Err(anyhow!("operation could not be completed"))?;
}
}
Ok(participant_connection_ids)

View File

@ -225,6 +225,7 @@ impl Server {
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_project_request::<proto::GetCompletions>)
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_project_request::<proto::PrepareRename>)

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::{proto::ChannelRole, 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

@ -355,8 +355,12 @@ impl ChatPanel {
}
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let (message, is_continuation, is_last) = {
let (message, is_continuation, is_last, is_admin) = {
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
let is_admin = self
.channel_store
.read(cx)
.is_user_admin(active_chat.channel().id);
let last_message = active_chat.message(ix.saturating_sub(1));
let this_message = active_chat.message(ix);
let is_continuation = last_message.id != this_message.id
@ -366,6 +370,7 @@ impl ChatPanel {
active_chat.message(ix).clone(),
is_continuation,
active_chat.message_count() == ix + 1,
is_admin,
)
};
@ -386,12 +391,13 @@ impl ChatPanel {
};
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
let message_id_to_remove =
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
Some(id)
} else {
None
};
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
(message.id, belongs_to_user || is_admin)
{
Some(id)
} else {
None
};
enum MessageBackgroundHighlight {}
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {

View File

@ -38,6 +38,10 @@ impl DiagnosticIndicator {
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
project::Event::DiagnosticsUpdated { .. } => {
this.summary = project.read(cx).diagnostic_summary(cx);
cx.notify();
}
_ => {}
})
.detach();

View File

@ -57,7 +57,6 @@ log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
rand.workspace = true
schemars.workspace = true
serde.workspace = true

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub use_on_type_format: bool,
pub scrollbar: Scrollbar,
pub relative_line_numbers: bool,
@ -33,6 +34,7 @@ pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,

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 {
@ -5417,9 +5430,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit
"});
cx.simulate_keystroke(" ");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("s");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.assert_editor_state(indoc! {"
one.second_completion
@ -5481,12 +5494,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
});
cx.set_state("editorˇ");
cx.simulate_keystroke(".");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("c");
cx.simulate_keystroke("l");
cx.simulate_keystroke("o");
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _| e.context_menu.is_none()));
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions, cx);
});
@ -7775,7 +7788,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"]
@ -7788,7 +7801,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"]
@ -7804,7 +7817,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l");
cx.foreground().run_until_parked();
cx.update_editor(|editor, _| {
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"]
@ -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

@ -2428,7 +2428,7 @@ impl Element<Editor> for EditorElement {
}
let active = matches!(
editor.context_menu,
editor.context_menu.read().as_ref(),
Some(crate::ContextMenu::CodeActions(_))
);
@ -2439,9 +2439,13 @@ impl Element<Editor> for EditorElement {
}
let visible_rows = start_row..start_row + line_layouts.len() as u32;
let mut hover = editor
.hover_state
.render(&snapshot, &style, visible_rows, cx);
let mut hover = editor.hover_state.render(
&snapshot,
&style,
visible_rows,
editor.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
);
let mode = editor.mode;
let mut fold_indicators = editor.render_fold_indicators(

View File

@ -9,13 +9,15 @@ use gpui::{
actions,
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
};
use language::{
markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
};
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
use std::{ops::Range, sync::Arc, time::Duration};
use util::TryFutureExt;
use workspace::Workspace;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
this.hover_state.diagnostic_popover = None;
})?;
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
let blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
let hover_popover = InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
blocks: vec![inlay_hover.tooltip],
language: None,
rendered_content: None,
blocks,
parsed_content,
};
this.update(&mut cx, |this, cx| {
@ -288,35 +293,38 @@ fn show_hover(
});
})?;
// Construct new hover popover from hover request
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
if hover_result.is_empty() {
return None;
let hover_result = hover_request.await.ok().flatten();
let hover_popover = match hover_result {
Some(hover_result) if !hover_result.is_empty() => {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = if let Some(range) = hover_result.range {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.end);
start..end
} else {
anchor..anchor
};
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
Some(InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Text(range),
blocks,
parsed_content,
})
}
// Create symbol range of anchors for highlighting and filtering
// of future requests.
let range = if let Some(range) = hover_result.range {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.start);
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id.clone(), range.end);
start..end
} else {
anchor..anchor
};
Some(InfoPopover {
project: project.clone(),
symbol_range: RangeInEditor::Text(range),
blocks: hover_result.contents,
language: hover_result.language,
rendered_content: None,
})
});
_ => None,
};
this.update(&mut cx, |this, cx| {
if let Some(symbol_range) = hover_popover
@ -345,44 +353,56 @@ fn show_hover(
editor.hover_state.info_task = Some(task);
}
fn render_blocks(
async fn parse_blocks(
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>,
) -> RichText {
let mut data = RichText {
text: Default::default(),
highlights: Default::default(),
region_ranges: Default::default(),
regions: Default::default(),
};
language: Option<Arc<Language>>,
) -> markdown::ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
for block in blocks {
match &block.kind {
HoverBlockKind::PlainText => {
new_paragraph(&mut data.text, &mut Vec::new());
data.text.push_str(&block.text);
markdown::new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text);
}
HoverBlockKind::Markdown => {
render_markdown_mut(&block.text, language_registry, language, &mut data)
markdown::parse_markdown_block(
&block.text,
language_registry,
language.clone(),
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await
}
HoverBlockKind::Code { language } => {
if let Some(language) = language_registry
.language_for_name(language)
.now_or_never()
.and_then(Result::ok)
{
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
} else {
data.text.push_str(&block.text);
text.push_str(&block.text);
}
}
}
}
data.text = data.text.trim().to_string();
data
ParsedMarkdown {
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
}
}
#[derive(Default)]
@ -403,6 +423,7 @@ impl HoverState {
snapshot: &EditorSnapshot,
style: &EditorStyle,
visible_rows: Range<u32>,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
// If there is a diagnostic, position the popovers based on that.
@ -432,7 +453,7 @@ impl HoverState {
elements.push(diagnostic_popover.render(style, cx));
}
if let Some(info_popover) = self.info_popover.as_mut() {
elements.push(info_popover.render(style, cx));
elements.push(info_popover.render(style, workspace, cx));
}
Some((point, elements))
@ -444,32 +465,23 @@ pub struct InfoPopover {
pub project: ModelHandle<Project>,
symbol_range: RangeInEditor,
pub blocks: Vec<HoverBlock>,
language: Option<Arc<Language>>,
rendered_content: Option<RichText>,
parsed_content: ParsedMarkdown,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
workspace: Option<WeakViewHandle<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement<Editor> {
let rendered_content = self.rendered_content.get_or_insert_with(|| {
render_blocks(
&self.blocks,
self.project.read(cx).languages(),
self.language.as_ref(),
)
});
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
let code_span_background_color = style.document_highlight_read_background;
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
Flex::column()
.scrollable::<HoverBlock>(1, None, cx)
.with_child(rendered_content.element(
style.syntax.clone(),
style.text.clone(),
code_span_background_color,
.scrollable::<HoverBlock>(0, None, cx)
.with_child(crate::render_parsed_markdown::<HoverBlock>(
&self.parsed_content,
style,
workspace,
cx,
))
.contained()
@ -572,7 +584,6 @@ mod tests {
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use rich_text::Highlight;
use smol::stream::StreamExt;
use unindent::Unindent;
use util::test::marked_text_ranges;
@ -793,7 +804,7 @@ mod tests {
}],
);
let rendered = render_blocks(&blocks, &Default::default(), None);
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
assert_eq!(
rendered.text,
code_str.trim(),
@ -900,7 +911,7 @@ mod tests {
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](the-url) three".to_string(),
text: "one [two](https://the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
@ -921,7 +932,7 @@ mod tests {
- a
- b
* two
- [c](the-url)
- [c](https://the-url)
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
@ -985,7 +996,7 @@ mod tests {
expected_styles,
} in &rows[0..]
{
let rendered = render_blocks(&blocks, &Default::default(), None);
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges
@ -1001,11 +1012,8 @@ mod tests {
.highlights
.iter()
.filter_map(|(range, highlight)| {
let style = match highlight {
Highlight::Id(id) => id.style(&style.syntax)?,
Highlight::Highlight(style) => style.clone(),
};
Some((range.clone(), style))
let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight))
})
.collect();
@ -1258,11 +1266,7 @@ mod tests {
"Popover range should match the new type label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for new type hint")
.text,
popover.parsed_content.text,
format!("A tooltip for `{new_type_label}`"),
"Rendered text should not anyhow alter backticks"
);
@ -1316,11 +1320,7 @@ mod tests {
"Popover range should match the struct label part"
);
assert_eq!(
popover
.rendered_content
.as_ref()
.expect("should have label text for struct hint")
.text,
popover.parsed_content.text,
format!("A tooltip for {struct_label}"),
"Rendered markdown element should remove backticks from text"
);

View File

@ -498,77 +498,91 @@ impl MultiBuffer {
}
}
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|edit| edit.range.start);
self.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
let mut edits = edits.into_iter().peekable();
let mut insertions = Vec::new();
let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into();
while let Some(BufferEdit {
mut range,
new_text,
mut is_insertion,
original_indent_column,
}) = edits.next()
{
drop(cursor);
drop(snapshot);
// Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
fn tail(
this: &mut MultiBuffer,
buffer_edits: HashMap<u64, Vec<BufferEdit>>,
autoindent_mode: Option<AutoindentMode>,
edited_excerpt_ids: Vec<ExcerptId>,
cx: &mut ModelContext<MultiBuffer>,
) {
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|edit| edit.range.start);
this.buffers.borrow()[&buffer_id]
.buffer
.update(cx, |buffer, cx| {
let mut edits = edits.into_iter().peekable();
let mut insertions = Vec::new();
let mut original_indent_columns = Vec::new();
let mut deletions = Vec::new();
let empty_str: Arc<str> = "".into();
while let Some(BufferEdit {
range: next_range,
is_insertion: next_is_insertion,
..
}) = edits.peek()
mut range,
new_text,
mut is_insertion,
original_indent_column,
}) = edits.next()
{
if range.end >= next_range.start {
range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion;
edits.next();
} else {
break;
while let Some(BufferEdit {
range: next_range,
is_insertion: next_is_insertion,
..
}) = edits.peek()
{
if range.end >= next_range.start {
range.end = cmp::max(next_range.end, range.end);
is_insertion |= *next_is_insertion;
edits.next();
} else {
break;
}
}
if is_insertion {
original_indent_columns.push(original_indent_column);
insertions.push((
buffer.anchor_before(range.start)
..buffer.anchor_before(range.end),
new_text.clone(),
));
} else if !range.is_empty() {
deletions.push((
buffer.anchor_before(range.start)
..buffer.anchor_before(range.end),
empty_str.clone(),
));
}
}
if is_insertion {
original_indent_columns.push(original_indent_column);
insertions.push((
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
new_text.clone(),
));
} else if !range.is_empty() {
deletions.push((
buffer.anchor_before(range.start)..buffer.anchor_before(range.end),
empty_str.clone(),
));
}
}
let deletion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns: Default::default(),
})
} else {
None
};
let insertion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
})
} else {
None
};
let deletion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns: Default::default(),
})
} else {
None
};
let insertion_autoindent_mode =
if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
Some(AutoindentMode::Block {
original_indent_columns,
})
} else {
None
};
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
})
}
buffer.edit(deletions, deletion_autoindent_mode, cx);
buffer.edit(insertions, insertion_autoindent_mode, cx);
})
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
}
cx.emit(Event::ExcerptsEdited {
ids: edited_excerpt_ids,
});
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
}
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {

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

@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
use crate::{
json::{self, ToJson, Value},
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
ViewContext,
};
use pathfinder_geometry::{
rect::RectF,
@ -10,10 +11,10 @@ use pathfinder_geometry::{
};
use serde_json::json;
#[derive(Default)]
struct ScrollState {
scroll_to: Cell<Option<usize>>,
scroll_position: Cell<f32>,
type_tag: TypeTag,
}
pub struct Flex<V> {
@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
where
Tag: 'static,
{
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
scroll_state.read(cx).scroll_to.set(scroll_to);
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
element_id,
Rc::new(ScrollState {
scroll_to: Cell::new(scroll_to),
scroll_position: Default::default(),
type_tag: TypeTag::new::<Tag>(),
}),
);
self.scroll_state = Some((scroll_state, cx.handle().id()));
self
}
@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
if let Some((scroll_state, id)) = &self.scroll_state {
let scroll_state = scroll_state.read(cx).clone();
cx.scene().push_mouse_region(
crate::MouseRegion::new::<Self>(*id, 0, bounds)
.on_scroll({
let axis = self.axis;
move |e, _: &mut V, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
crate::MouseRegion::from_handlers(
scroll_state.type_tag,
*id,
0,
bounds,
Default::default(),
)
.on_scroll({
let axis = self.axis;
move |e, _: &mut V, cx| {
if remaining_space < 0. {
let scroll_delta = e.delta.raw();
let mut delta = match axis {
Axis::Horizontal => {
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
scroll_delta.y()
}
let mut delta = match axis {
Axis::Horizontal => {
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
scroll_delta.x()
} else {
scroll_delta.y()
}
Axis::Vertical => scroll_delta.y(),
};
if !e.delta.precise() {
delta *= 20.;
}
scroll_state
.scroll_position
.set(scroll_state.scroll_position.get() - delta);
cx.notify();
} else {
cx.propagate_event();
Axis::Vertical => scroll_delta.y(),
};
if !e.delta.precise() {
delta *= 20.;
}
scroll_state
.scroll_position
.set(scroll_state.scroll_position.get() - delta);
cx.notify();
} else {
cx.propagate_event();
}
})
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
}
})
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
)
}

View File

@ -45,6 +45,7 @@ lazy_static.workspace = true
log.workspace = true
parking_lot.workspace = true
postage.workspace = true
pulldown-cmark = { version = "0.9.2", default-features = false }
regex.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@ -1,11 +1,13 @@
pub use crate::{
diagnostic_set::DiagnosticSet,
highlight_map::{HighlightId, HighlightMap},
markdown::ParsedMarkdown,
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
};
use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
language_settings::{language_settings, LanguageSettings},
markdown::parse_markdown,
outline::OutlineItem,
syntax_map::{
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@ -143,11 +145,51 @@ pub struct Diagnostic {
pub is_unnecessary: bool,
}
pub async fn prepare_completion_documentation(
documentation: &lsp::Documentation,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> Documentation {
match documentation {
lsp::Documentation::String(text) => {
if text.lines().count() <= 1 {
Documentation::SingleLine(text.clone())
} else {
Documentation::MultiLinePlainText(text.clone())
}
}
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
lsp::MarkupKind::PlainText => {
if value.lines().count() <= 1 {
Documentation::SingleLine(value.clone())
} else {
Documentation::MultiLinePlainText(value.clone())
}
}
lsp::MarkupKind::Markdown => {
let parsed = parse_markdown(value, language_registry, language).await;
Documentation::MultiLineMarkdown(parsed)
}
},
}
}
#[derive(Clone, Debug)]
pub enum Documentation {
Undocumented,
SingleLine(String),
MultiLinePlainText(String),
MultiLineMarkdown(ParsedMarkdown),
}
#[derive(Clone, Debug)]
pub struct Completion {
pub old_range: Range<Anchor>,
pub new_text: String,
pub label: CodeLabel,
pub documentation: Option<Documentation>,
pub server_id: LanguageServerId,
pub lsp_completion: lsp::CompletionItem,
}
@ -1406,82 +1448,95 @@ impl Buffer {
return None;
}
self.start_transaction();
self.pending_autoindent.take();
let autoindent_request = autoindent_mode
.and_then(|mode| self.language.as_ref().map(|_| (self.snapshot(), mode)));
// Non-generic part hoisted out to reduce LLVM IR size.
fn tail(
this: &mut Buffer,
edits: Vec<(Range<usize>, Arc<str>)>,
autoindent_mode: Option<AutoindentMode>,
cx: &mut ModelContext<Buffer>,
) -> Option<clock::Lamport> {
this.start_transaction();
this.pending_autoindent.take();
let autoindent_request = autoindent_mode
.and_then(|mode| this.language.as_ref().map(|_| (this.snapshot(), mode)));
let edit_operation = self.text.edit(edits.iter().cloned());
let edit_id = edit_operation.timestamp();
let edit_operation = this.text.edit(edits.iter().cloned());
let edit_id = edit_operation.timestamp();
if let Some((before_edit, mode)) = autoindent_request {
let mut delta = 0isize;
let entries = edits
.into_iter()
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), new_text)| {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta += new_text_length as isize - (range.end as isize - range.start as isize);
if let Some((before_edit, mode)) = autoindent_request {
let mut delta = 0isize;
let entries = edits
.into_iter()
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
.map(|((ix, (range, _)), new_text)| {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
let new_start = (delta + range.start as isize) as usize;
delta +=
new_text_length as isize - (range.end as isize - range.start as isize);
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
let mut range_of_insertion_to_indent = 0..new_text_length;
let mut first_line_is_new = false;
let mut original_indent_column = None;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
if new_text.contains('\n')
&& old_start.column <= before_edit.indent_size_for_line(old_start.row).len
{
first_line_is_new = true;
}
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
// Avoid auto-indenting after the insertion.
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column =
Some(original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}));
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
// When inserting an entire line at the beginning of an existing line,
// treat the insertion as new.
if new_text.contains('\n')
&& old_start.column
<= before_edit.indent_size_for_line(old_start.row).len
{
first_line_is_new = true;
}
}
AutoindentRequestEntry {
first_line_is_new,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
..self.anchor_after(new_start + range_of_insertion_to_indent.end),
}
})
.collect();
// When inserting text starting with a newline, avoid auto-indenting the
// previous line.
if new_text.starts_with('\n') {
range_of_insertion_to_indent.start += 1;
first_line_is_new = true;
}
self.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
// Avoid auto-indenting after the insertion.
if let AutoindentMode::Block {
original_indent_columns,
} = &mode
{
original_indent_column = Some(
original_indent_columns.get(ix).copied().unwrap_or_else(|| {
indent_size_for_text(
new_text[range_of_insertion_to_indent.clone()].chars(),
)
.len
}),
);
if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') {
range_of_insertion_to_indent.end -= 1;
}
}
AutoindentRequestEntry {
first_line_is_new,
original_indent_column,
indent_size: before_edit.language_indent_size_at(range.start, cx),
range: this
.anchor_before(new_start + range_of_insertion_to_indent.start)
..this.anchor_after(new_start + range_of_insertion_to_indent.end),
}
})
.collect();
this.autoindent_requests.push(Arc::new(AutoindentRequest {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
}));
}
this.end_transaction(cx);
this.send_operation(Operation::Buffer(edit_operation), cx);
Some(edit_id)
}
self.end_transaction(cx);
self.send_operation(Operation::Buffer(edit_operation), cx);
Some(edit_id)
tail(self, edits, autoindent_mode, cx)
}
fn did_edit(

View File

@ -2,6 +2,7 @@ mod buffer;
mod diagnostic_set;
mod highlight_map;
pub mod language_settings;
pub mod markdown;
mod outline;
pub mod proto;
mod syntax_map;
@ -110,7 +111,6 @@ pub struct LanguageServerName(pub Arc<str>);
pub struct CachedLspAdapter {
pub name: LanguageServerName,
pub short_name: &'static str,
pub initialization_options: Option<Value>,
pub disk_based_diagnostic_sources: Vec<String>,
pub disk_based_diagnostics_progress_token: Option<String>,
pub language_ids: HashMap<String, String>,
@ -121,7 +121,6 @@ impl CachedLspAdapter {
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
let name = adapter.name().await;
let short_name = adapter.short_name();
let initialization_options = adapter.initialization_options().await;
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
let disk_based_diagnostics_progress_token =
adapter.disk_based_diagnostics_progress_token().await;
@ -130,7 +129,6 @@ impl CachedLspAdapter {
Arc::new(CachedLspAdapter {
name,
short_name,
initialization_options,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
language_ids,
@ -227,6 +225,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 +335,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 +496,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 +1759,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 +1816,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

@ -0,0 +1,301 @@
use std::sync::Arc;
use std::{ops::Range, path::PathBuf};
use crate::{HighlightId, Language, LanguageRegistry};
use gpui::fonts::{self, HighlightStyle, Weight};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
#[derive(Debug, Clone)]
pub struct ParsedMarkdown {
pub text: String,
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
pub region_ranges: Vec<Range<usize>>,
pub regions: Vec<ParsedRegion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarkdownHighlight {
Style(MarkdownHighlightStyle),
Code(HighlightId),
}
impl MarkdownHighlight {
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
match self {
MarkdownHighlight::Style(style) => {
let mut highlight = HighlightStyle::default();
if style.italic {
highlight.italic = Some(true);
}
if style.underline {
highlight.underline = Some(fonts::Underline {
thickness: 1.0.into(),
..Default::default()
});
}
if style.weight != fonts::Weight::default() {
highlight.weight = Some(style.weight);
}
Some(highlight)
}
MarkdownHighlight::Code(id) => id.style(theme),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MarkdownHighlightStyle {
pub italic: bool,
pub underline: bool,
pub weight: Weight,
}
#[derive(Debug, Clone)]
pub struct ParsedRegion {
pub code: bool,
pub link: Option<Link>,
}
#[derive(Debug, Clone)]
pub enum Link {
Web { url: String },
Path { path: PathBuf },
}
impl Link {
fn identify(text: String) -> Option<Link> {
if text.starts_with("http") {
return Some(Link::Web { url: text });
}
let path = PathBuf::from(text);
if path.is_absolute() {
return Some(Link::Path { path });
}
None
}
}
pub async fn parse_markdown(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
parse_markdown_block(
markdown,
language_registry,
language,
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await;
ParsedMarkdown {
text,
highlights,
region_ranges,
regions,
}
}
pub async fn parse_markdown_block(
markdown: &str,
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
region_ranges: &mut Vec<Range<usize>>,
regions: &mut Vec<ParsedRegion>,
) {
let mut bold_depth = 0;
let mut italic_depth = 0;
let mut link_url = None;
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(&markdown, Options::all()) {
let prev_len = text.len();
match event {
Event::Text(t) => {
if let Some(language) = &current_language {
highlight_code(text, highlights, t.as_ref(), language);
} else {
text.push_str(t.as_ref());
let mut style = MarkdownHighlightStyle::default();
if bold_depth > 0 {
style.weight = Weight::BOLD;
}
if italic_depth > 0 {
style.italic = true;
}
if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
region_ranges.push(prev_len..text.len());
regions.push(ParsedRegion {
code: false,
link: Some(link),
});
style.underline = true;
}
if style != MarkdownHighlightStyle::default() {
let mut new_highlight = true;
if let Some((last_range, MarkdownHighlight::Style(last_style))) =
highlights.last_mut()
{
if last_range.end == prev_len && last_style == &style {
last_range.end = text.len();
new_highlight = false;
}
}
if new_highlight {
let range = prev_len..text.len();
highlights.push((range, MarkdownHighlight::Style(style)));
}
}
}
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
let link = link_url.clone().and_then(|u| Link::identify(u));
if link.is_some() {
highlights.push((
prev_len..text.len(),
MarkdownHighlight::Style(MarkdownHighlightStyle {
underline: true,
..Default::default()
}),
));
}
regions.push(ParsedRegion { code: true, link });
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
Tag::CodeBlock(kind) => {
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.await
.ok()
} else {
language.clone()
}
}
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));
}
Tag::Item => {
let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false;
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
for _ in 0..len - 1 {
text.push_str(" ");
}
if let Some(number) = list_number {
text.push_str(&format!("{}. ", number));
*number += 1;
*has_content = false;
} else {
text.push_str("- ");
}
}
}
_ => {}
},
Event::End(tag) => match tag {
Tag::Heading(_, _, _) => bold_depth -= 1,
Tag::CodeBlock(_) => current_language = None,
Tag::Emphasis => italic_depth -= 1,
Tag::Strong => bold_depth -= 1,
Tag::Link(_, _, _) => link_url = None,
Tag::List(_) => drop(list_stack.pop()),
_ => {}
},
Event::HardBreak => text.push('\n'),
Event::SoftBreak => text.push(' '),
_ => {}
}
}
}
pub fn highlight_code(
text: &mut String,
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
content: &str,
language: &Arc<Language>,
) {
let prev_len = text.len();
text.push_str(content);
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
let highlight = MarkdownHighlight::Code(highlight_id);
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
}
}
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
let mut is_subsequent_paragraph_of_list = false;
if let Some((_, has_content)) = list_stack.last_mut() {
if *has_content {
is_subsequent_paragraph_of_list = true;
} else {
*has_content = true;
return;
}
}
if !text.is_empty() {
if !text.ends_with('\n') {
text.push('\n');
}
text.push('\n');
}
for _ in 0..list_stack.len().saturating_sub(1) {
text.push_str(" ");
}
if is_subsequent_paragraph_of_list {
text.push_str(" ");
}
}

View File

@ -482,6 +482,7 @@ pub async fn deserialize_completion(
lsp_completion.filter_text.as_deref(),
)
}),
documentation: None,
server_id: LanguageServerId(completion.server_id as usize),
lsp_completion,
})

View File

@ -685,6 +685,7 @@ impl View for LspLogToolbarItemView {
});
let server_selected = current_server.is_some();
enum LspLogScroll {}
enum Menu {}
let lsp_menu = Stack::new()
.with_child(Self::render_language_server_menu_header(
@ -697,7 +698,7 @@ impl View for LspLogToolbarItemView {
Overlay::new(
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
Flex::column()
.scrollable::<Self>(0, None, cx)
.scrollable::<LspLogScroll>(0, None, cx)
.with_children(menu_rows.into_iter().map(|row| {
Self::render_language_server_menu_item(
row.server_id,
@ -876,6 +877,7 @@ impl LspLogToolbarItemView {
) -> impl Element<Self> {
enum ActivateLog {}
enum ActivateRpcTrace {}
enum LanguageServerCheckbox {}
Flex::column()
.with_child({
@ -921,7 +923,7 @@ impl LspLogToolbarItemView {
.with_height(theme.toolbar_dropdown_menu.row_height),
)
.with_child(
ui::checkbox_with_label::<Self, _, Self, _>(
ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
Empty::new(),
&theme.welcome.checkbox,
rpc_trace_enabled,

View File

@ -466,7 +466,10 @@ impl LanguageServer {
completion_item: Some(CompletionItemCapability {
snippet_support: Some(true),
resolve_support: Some(CompletionItemCapabilityResolveSupport {
properties: vec!["additionalTextEdits".to_string()],
properties: vec![
"documentation".to_string(),
"additionalTextEdits".to_string(),
],
}),
..Default::default()
}),
@ -748,6 +751,15 @@ impl LanguageServer {
)
}
// some child of string literal (be it "" or ``) which is the child of an attribute
// <Foo className="bar" />
// <Foo className={`bar`} />
// <Foo className={something + "bar"} />
// <Foo className={something + "bar"} />
// const classes = "awesome ";
// <Foo className={classes} />
fn request_internal<T: request::Request>(
next_id: &AtomicUsize,
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,

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

@ -10,7 +10,7 @@ use futures::future;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
language_settings::{language_settings, InlayHintKind},
point_from_lsp, point_to_lsp,
point_from_lsp, point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions {
async fn response_from_lsp(
self,
completions: Option<lsp::CompletionResponse>,
_: ModelHandle<Project>,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext,
@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
}
}
} else {
Default::default()
Vec::new()
};
let completions = buffer.read_with(&cx, |buffer, _| {
let completions = buffer.read_with(&cx, |buffer, cx| {
let language_registry = project.read(cx).languages().clone();
let language = buffer.language().cloned();
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions {
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
if let Some(response_list) = &response_list {
if let Some(item_defaults) = &response_list.item_defaults {
if let Some(data) = &item_defaults.data {
lsp_completion.data = Some(data.clone());
}
}
}
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
// If the language server provides a range to overwrite, then
// check that the range is valid.
@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions {
}
};
let language = language.clone();
LineEnding::normalize(&mut new_text);
let language_registry = language_registry.clone();
let language = language.clone();
Some(async move {
let mut label = None;
if let Some(language) = language {
if let Some(language) = language.as_ref() {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
}
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
Some(
prepare_completion_documentation(
lsp_docs,
&language_registry,
language.clone(),
)
.await,
)
} else {
None
};
Completion {
old_range,
new_text,
@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
lsp_completion.filter_text.as_deref(),
)
}),
documentation,
server_id,
lsp_completion,
}

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 {
@ -580,6 +592,7 @@ impl Project {
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_on_type_formatting);
client.add_model_request_handler(Self::handle_inlay_hints);
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
client.add_model_request_handler(Self::handle_resolve_inlay_hint);
client.add_model_request_handler(Self::handle_refresh_inlay_hints);
client.add_model_request_handler(Self::handle_reload_buffers);
@ -605,6 +618,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 +674,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 +773,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 +813,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 +836,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 +919,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 +2672,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);
@ -2684,15 +2752,6 @@ impl Project {
let lsp = project_settings.lsp.get(&adapter.name.0);
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
let mut initialization_options = adapter.initialization_options.clone();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
}
(None, override_options) => initialization_options = override_options,
_ => {}
}
let server_id = pending_server.server_id;
let container_dir = pending_server.container_dir.clone();
let state = LanguageServerState::Starting({
@ -2704,7 +2763,7 @@ impl Project {
cx.spawn_weak(|this, mut cx| async move {
let result = Self::setup_and_insert_language_server(
this,
initialization_options,
override_options,
pending_server,
adapter.clone(),
language.clone(),
@ -2807,7 +2866,7 @@ impl Project {
async fn setup_and_insert_language_server(
this: WeakModelHandle<Self>,
initialization_options: Option<serde_json::Value>,
override_initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
@ -2817,7 +2876,7 @@ impl Project {
) -> Result<Option<Arc<LanguageServer>>> {
let setup = Self::setup_pending_language_server(
this,
initialization_options,
override_initialization_options,
pending_server,
adapter.clone(),
server_id,
@ -2849,7 +2908,7 @@ impl Project {
async fn setup_pending_language_server(
this: WeakModelHandle<Self>,
initialization_options: Option<serde_json::Value>,
override_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
server_id: LanguageServerId,
@ -2867,8 +2926,8 @@ impl Project {
move |mut params, mut cx| {
let this = this;
let adapter = adapter.clone();
adapter.process_diagnostics(&mut params);
if let Some(this) = this.upgrade(&cx) {
adapter.process_diagnostics(&mut params);
this.update(&mut cx, |this, cx| {
this.update_diagnostics(
server_id,
@ -2995,6 +3054,14 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options().await;
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
merge_json_value_into(override_options, initialization_options);
}
(None, override_options) => initialization_options = override_options,
_ => {}
}
let language_server = language_server.initialize(initialization_options).await?;
@ -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)?;
@ -7155,6 +7353,40 @@ impl Project {
})
}
async fn handle_resolve_completion_documentation(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::ResolveCompletionDocumentationResponse> {
let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
let completion = this
.read_with(&mut cx, |this, _| {
let id = LanguageServerId(envelope.payload.language_server_id as usize);
let Some(server) = this.language_server_for_id(id) else {
return Err(anyhow!("No language server {id}"));
};
Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
})?
.await?;
let mut is_markdown = false;
let text = match completion.documentation {
Some(lsp::Documentation::String(text)) => text,
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
is_markdown = kind == lsp::MarkupKind::Markdown;
value
}
_ => String::new(),
};
Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
}
async fn handle_apply_code_action(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::ApplyCodeAction>,
@ -8109,6 +8341,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

@ -89,89 +89,91 @@ message Envelope {
FormatBuffersResponse format_buffers_response = 70;
GetCompletions get_completions = 71;
GetCompletionsResponse get_completions_response = 72;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
GetCodeActions get_code_actions = 75;
GetCodeActionsResponse get_code_actions_response = 76;
GetHover get_hover = 77;
GetHoverResponse get_hover_response = 78;
ApplyCodeAction apply_code_action = 79;
ApplyCodeActionResponse apply_code_action_response = 80;
PrepareRename prepare_rename = 81;
PrepareRenameResponse prepare_rename_response = 82;
PerformRename perform_rename = 83;
PerformRenameResponse perform_rename_response = 84;
SearchProject search_project = 85;
SearchProjectResponse search_project_response = 86;
ResolveCompletionDocumentation resolve_completion_documentation = 73;
ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76;
GetCodeActions get_code_actions = 77;
GetCodeActionsResponse get_code_actions_response = 78;
GetHover get_hover = 79;
GetHoverResponse get_hover_response = 80;
ApplyCodeAction apply_code_action = 81;
ApplyCodeActionResponse apply_code_action_response = 82;
PrepareRename prepare_rename = 83;
PrepareRenameResponse prepare_rename_response = 84;
PerformRename perform_rename = 85;
PerformRenameResponse perform_rename_response = 86;
SearchProject search_project = 87;
SearchProjectResponse search_project_response = 88;
UpdateContacts update_contacts = 87;
UpdateInviteInfo update_invite_info = 88;
ShowContacts show_contacts = 89;
UpdateContacts update_contacts = 89;
UpdateInviteInfo update_invite_info = 90;
ShowContacts show_contacts = 91;
GetUsers get_users = 90;
FuzzySearchUsers fuzzy_search_users = 91;
UsersResponse users_response = 92;
RequestContact request_contact = 93;
RespondToContactRequest respond_to_contact_request = 94;
RemoveContact remove_contact = 95;
GetUsers get_users = 92;
FuzzySearchUsers fuzzy_search_users = 93;
UsersResponse users_response = 94;
RequestContact request_contact = 95;
RespondToContactRequest respond_to_contact_request = 96;
RemoveContact remove_contact = 97;
Follow follow = 96;
FollowResponse follow_response = 97;
UpdateFollowers update_followers = 98;
Unfollow unfollow = 99;
GetPrivateUserInfo get_private_user_info = 100;
GetPrivateUserInfoResponse get_private_user_info_response = 101;
UpdateDiffBase update_diff_base = 102;
Follow follow = 98;
FollowResponse follow_response = 99;
UpdateFollowers update_followers = 100;
Unfollow unfollow = 101;
GetPrivateUserInfo get_private_user_info = 102;
GetPrivateUserInfoResponse get_private_user_info_response = 103;
UpdateDiffBase update_diff_base = 104;
OnTypeFormatting on_type_formatting = 103;
OnTypeFormattingResponse on_type_formatting_response = 104;
OnTypeFormatting on_type_formatting = 105;
OnTypeFormattingResponse on_type_formatting_response = 106;
UpdateWorktreeSettings update_worktree_settings = 105;
UpdateWorktreeSettings update_worktree_settings = 107;
InlayHints inlay_hints = 106;
InlayHintsResponse inlay_hints_response = 107;
ResolveInlayHint resolve_inlay_hint = 108;
ResolveInlayHintResponse resolve_inlay_hint_response = 109;
RefreshInlayHints refresh_inlay_hints = 110;
InlayHints inlay_hints = 108;
InlayHintsResponse inlay_hints_response = 109;
ResolveInlayHint resolve_inlay_hint = 110;
ResolveInlayHintResponse resolve_inlay_hint_response = 111;
RefreshInlayHints refresh_inlay_hints = 112;
CreateChannel create_channel = 111;
CreateChannelResponse create_channel_response = 112;
InviteChannelMember invite_channel_member = 113;
RemoveChannelMember remove_channel_member = 114;
RespondToChannelInvite respond_to_channel_invite = 115;
UpdateChannels update_channels = 116;
JoinChannel join_channel = 117;
DeleteChannel delete_channel = 118;
GetChannelMembers get_channel_members = 119;
GetChannelMembersResponse get_channel_members_response = 120;
SetChannelMemberRole set_channel_member_role = 145;
RenameChannel rename_channel = 122;
RenameChannelResponse rename_channel_response = 123;
CreateChannel create_channel = 113;
CreateChannelResponse create_channel_response = 114;
InviteChannelMember invite_channel_member = 115;
RemoveChannelMember remove_channel_member = 116;
RespondToChannelInvite respond_to_channel_invite = 117;
UpdateChannels update_channels = 118;
JoinChannel join_channel = 119;
DeleteChannel delete_channel = 120;
GetChannelMembers get_channel_members = 121;
GetChannelMembersResponse get_channel_members_response = 122;
SetChannelMemberRole set_channel_member_role = 123;
RenameChannel rename_channel = 124;
RenameChannelResponse rename_channel_response = 125;
JoinChannelBuffer join_channel_buffer = 124;
JoinChannelBufferResponse join_channel_buffer_response = 125;
UpdateChannelBuffer update_channel_buffer = 126;
LeaveChannelBuffer leave_channel_buffer = 127;
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
RejoinChannelBuffers rejoin_channel_buffers = 129;
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
AckBufferOperation ack_buffer_operation = 143;
JoinChannelBuffer join_channel_buffer = 126;
JoinChannelBufferResponse join_channel_buffer_response = 127;
UpdateChannelBuffer update_channel_buffer = 128;
LeaveChannelBuffer leave_channel_buffer = 129;
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130;
RejoinChannelBuffers rejoin_channel_buffers = 131;
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132;
AckBufferOperation ack_buffer_operation = 145;
JoinChannelChat join_channel_chat = 131;
JoinChannelChatResponse join_channel_chat_response = 132;
LeaveChannelChat leave_channel_chat = 133;
SendChannelMessage send_channel_message = 134;
SendChannelMessageResponse send_channel_message_response = 135;
ChannelMessageSent channel_message_sent = 136;
GetChannelMessages get_channel_messages = 137;
GetChannelMessagesResponse get_channel_messages_response = 138;
RemoveChannelMessage remove_channel_message = 139;
AckChannelMessage ack_channel_message = 144;
JoinChannelChat join_channel_chat = 133;
JoinChannelChatResponse join_channel_chat_response = 134;
LeaveChannelChat leave_channel_chat = 135;
SendChannelMessage send_channel_message = 136;
SendChannelMessageResponse send_channel_message_response = 137;
ChannelMessageSent channel_message_sent = 138;
GetChannelMessages get_channel_messages = 139;
GetChannelMessagesResponse get_channel_messages_response = 140;
RemoveChannelMessage remove_channel_message = 141;
AckChannelMessage ack_channel_message = 146;
LinkChannel link_channel = 140;
UnlinkChannel unlink_channel = 141;
MoveChannel move_channel = 142;
SetChannelVisibility set_channel_visibility = 146; // current max: 146
LinkChannel link_channel = 142;
UnlinkChannel unlink_channel = 143;
MoveChannel move_channel = 144;
SetChannelVisibility set_channel_visibility = 147; // current max: 147
}
}
@ -833,6 +835,17 @@ message ResolveState {
}
}
message ResolveCompletionDocumentation {
uint64 project_id = 1;
uint64 language_server_id = 2;
bytes lsp_completion = 3;
}
message ResolveCompletionDocumentationResponse {
string text = 1;
bool is_markdown = 2;
}
message ResolveInlayHint {
uint64 project_id = 1;
uint64 buffer_id = 2;

View File

@ -205,6 +205,8 @@ messages!(
(OnTypeFormattingResponse, Background),
(InlayHints, Background),
(InlayHintsResponse, Background),
(ResolveCompletionDocumentation, Background),
(ResolveCompletionDocumentationResponse, Background),
(ResolveInlayHint, Background),
(ResolveInlayHintResponse, Background),
(RefreshInlayHints, Foreground),
@ -319,6 +321,10 @@ request_messages!(
(PrepareRename, PrepareRenameResponse),
(OnTypeFormatting, OnTypeFormattingResponse),
(InlayHints, InlayHintsResponse),
(
ResolveCompletionDocumentation,
ResolveCompletionDocumentationResponse
),
(ResolveInlayHint, ResolveInlayHintResponse),
(RefreshInlayHints, Ack),
(ReloadBuffers, ReloadBuffersResponse),
@ -383,6 +389,7 @@ entity_messages!(
PerformRename,
OnTypeFormatting,
InlayHints,
ResolveCompletionDocumentation,
ResolveInlayHint,
RefreshInlayHints,
PrepareRename,

View File

@ -6,4 +6,4 @@ pub use conn::Connection;
pub use peer::*;
mod macros;
pub const PROTOCOL_VERSION: u32 = 64;
pub const PROTOCOL_VERSION: u32 = 65;

View File

@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"]}
rust-embed = { version = "8.0", features = ["include-exclude"] }
client = { path = "../client" }
zed = { path = "../zed"}
node_runtime = { path = "../node_runtime"}
pretty_assertions.workspace = true
@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true
tree-sitter-lua.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-php.workspace = true
[[example]]
name = "eval"

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +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"
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,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

@ -150,11 +150,14 @@ impl TerminalView {
cx.notify();
cx.emit(Event::Wakeup);
}
Event::Bell => {
this.has_bell = true;
cx.emit(Event::Wakeup);
}
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
Event::TitleChanged => {
if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
let cwd = foreground_info.cwd.clone();
@ -171,6 +174,7 @@ impl TerminalView {
.detach();
}
}
Event::NewNavigationTarget(maybe_navigation_target) => {
this.can_navigate_to_selected_word = match maybe_navigation_target {
Some(MaybeNavigationTarget::Url(_)) => true,
@ -180,8 +184,10 @@ impl TerminalView {
None => false,
}
}
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
MaybeNavigationTarget::PathLike(maybe_path) => {
if !this.can_navigate_to_selected_word {
return;
@ -246,6 +252,7 @@ impl TerminalView {
}
}
},
_ => cx.emit(event.clone()),
})
.detach();

View File

@ -869,9 +869,13 @@ pub struct AutocompleteStyle {
pub selected_item: ContainerStyle,
pub hovered_item: ContainerStyle,
pub match_highlight: HighlightStyle,
pub server_name_container: ContainerStyle,
pub server_name_color: Color,
pub server_name_size_percent: f32,
pub completion_min_width: f32,
pub completion_max_width: f32,
pub inline_docs_container: ContainerStyle,
pub inline_docs_color: Color,
pub inline_docs_size_percent: f32,
pub alongside_docs_max_width: f32,
pub alongside_docs_container: ContainerStyle,
}
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]

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)
}))
}
}

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