Compare commits
33 Commits
gamma
...
add-layout
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6240740605 | ||
|
|
2306d9fd91 | ||
|
|
e7cf1305b8 | ||
|
|
f699083b5b | ||
|
|
956eb5b3b0 | ||
|
|
af237f81a3 | ||
|
|
d6b1949143 | ||
|
|
e5c738daaa | ||
|
|
eba035eb5b | ||
|
|
1c65253b0f | ||
|
|
5d4bc1e492 | ||
|
|
dc84dbb6e2 | ||
|
|
903343e608 | ||
|
|
0a28800049 | ||
|
|
31a6ee0229 | ||
|
|
1f974d074e | ||
|
|
72125949d9 | ||
|
|
d605d192af | ||
|
|
f92e6e9a95 | ||
|
|
ff4f67993b | ||
|
|
07821083df | ||
|
|
09c599385a | ||
|
|
01503511ad | ||
|
|
8bc5bcf0a6 | ||
|
|
983bb5c5fc | ||
|
|
653b2dc676 | ||
|
|
7142d3777f | ||
|
|
01e12c0d3c | ||
|
|
706c385c24 | ||
|
|
edb89d8d11 | ||
|
|
09675d43b3 | ||
|
|
187356ab9b | ||
|
|
435708b615 |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2815,6 +2815,7 @@ name = "context_servers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"futures 0.3.30",
|
||||
@@ -4211,6 +4212,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"client",
|
||||
"collections",
|
||||
"context_servers",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
@@ -13260,9 +13262,12 @@ name = "ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"collections",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"linkme",
|
||||
"menu",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
@@ -15352,6 +15357,7 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"context_servers",
|
||||
"copilot",
|
||||
"db",
|
||||
"diagnostics",
|
||||
|
||||
@@ -339,6 +339,7 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clickhouse = "0.11.6"
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
convert_case = "0.6.0"
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation-sys = "0.8.6"
|
||||
@@ -370,6 +371,7 @@ itertools = "0.13.0"
|
||||
jsonwebtoken = "9.3"
|
||||
libc = "0.2"
|
||||
linkify = "0.10.0"
|
||||
linkme = "0.3"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
nanoid = "0.4"
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"ctrl-right": "editor::AcceptPartialInlineCompletion"
|
||||
"alt-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"bindings": {
|
||||
"alt-]": "editor::NextInlineCompletion",
|
||||
"alt-[": "editor::PreviousInlineCompletion",
|
||||
"cmd-right": "editor::AcceptPartialInlineCompletion"
|
||||
"ctrl-right": "editor::AcceptPartialInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-s": "zed::OpenSettings",
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem"
|
||||
}
|
||||
@@ -43,6 +44,7 @@
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"ctrl-alt-z": "editor::RevertSelectedHunks",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"context": "VimControl && !menu",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
@@ -171,6 +172,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
@@ -224,6 +226,7 @@
|
||||
},
|
||||
{
|
||||
"context": "VimControl && VimCount",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0],
|
||||
":": "vim::CountCommand"
|
||||
@@ -231,6 +234,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == visual",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "vim::VisualCommand",
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
@@ -279,6 +283,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -304,6 +309,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"ctrl-p": "editor::ShowCompletions",
|
||||
"ctrl-n": "editor::ShowCompletions"
|
||||
@@ -311,6 +317,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == replace",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -328,6 +335,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == waiting",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
@@ -341,6 +349,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == operator",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
@@ -349,6 +358,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
@@ -376,6 +386,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename", // zed specific
|
||||
@@ -384,6 +395,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == d",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"],
|
||||
@@ -393,6 +405,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
@@ -400,6 +413,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gU",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
@@ -407,6 +421,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g~",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
@@ -414,6 +429,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gq",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"g q": "vim::CurrentLine",
|
||||
"q": "vim::CurrentLine",
|
||||
@@ -423,6 +439,7 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == y",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
@@ -430,30 +447,35 @@
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == ys",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == >",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == <",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gc",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == literal",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
|
||||
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
|
||||
@@ -497,6 +519,7 @@
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar && !in_replace",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
@@ -504,6 +527,7 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -554,6 +578,7 @@
|
||||
},
|
||||
{
|
||||
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"g /": "pane::DeploySearch"
|
||||
@@ -562,6 +587,7 @@
|
||||
{
|
||||
// netrw compatibility
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
":": "command_palette::Toggle",
|
||||
"%": "project_panel::NewFile",
|
||||
@@ -589,6 +615,7 @@
|
||||
},
|
||||
{
|
||||
"context": "OutlinePanel && not_editing",
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrev",
|
||||
|
||||
@@ -1084,7 +1084,21 @@ impl AssistantPanel {
|
||||
self.show_updated_summary(&context_editor, cx);
|
||||
cx.notify()
|
||||
}
|
||||
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
|
||||
EditorEvent::Edited { .. } => {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace
|
||||
.project()
|
||||
.update(cx, |project, _| project.is_via_ssh());
|
||||
|
||||
workspace
|
||||
.client()
|
||||
.telemetry()
|
||||
.log_edit_event("assistant panel", is_via_ssh);
|
||||
})
|
||||
.log_err();
|
||||
cx.emit(AssistantPanelEvent::ContextEdited)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1533,6 +1547,7 @@ impl ContextEditor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let completion_provider = SlashCommandCompletionProvider::new(
|
||||
context.read(cx).slash_commands.clone(),
|
||||
Some(cx.view().downgrade()),
|
||||
Some(workspace.clone()),
|
||||
);
|
||||
@@ -2760,7 +2775,7 @@ impl ContextEditor {
|
||||
.h_11()
|
||||
.w_full()
|
||||
.relative()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.child(sender)
|
||||
.children(match &message.cache {
|
||||
Some(cache) if cache.is_final_anchor => match cache.status {
|
||||
@@ -2774,7 +2789,7 @@ impl ContextEditor {
|
||||
)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Context cached",
|
||||
"Context Cached",
|
||||
None,
|
||||
"Large messages cached to optimize performance",
|
||||
cx,
|
||||
@@ -2802,16 +2817,9 @@ impl ContextEditor {
|
||||
.selected_icon_color(Color::Error)
|
||||
.icon(IconName::XCircle)
|
||||
.icon_color(Color::Error)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::Start)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Error interacting with language model",
|
||||
None,
|
||||
"Click for more details",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.tooltip(move |cx| Tooltip::text("View Details", cx))
|
||||
.on_click({
|
||||
let context = context.clone();
|
||||
let error = error.clone();
|
||||
@@ -2826,21 +2834,19 @@ impl ContextEditor {
|
||||
.into_any_element(),
|
||||
),
|
||||
MessageStatus::Canceled => Some(
|
||||
ButtonLike::new("canceled")
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Disabled))
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
Icon::new(IconName::XCircle)
|
||||
.color(Color::Disabled)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
Label::new("Canceled")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.tooltip(move |cx| {
|
||||
Tooltip::with_meta(
|
||||
"Canceled",
|
||||
None,
|
||||
"Interaction with the assistant was canceled",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
_ => None,
|
||||
@@ -4516,7 +4522,6 @@ impl Render for ContextEditorToolbarItem {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let left_side = h_flex()
|
||||
.group("chat-title-group")
|
||||
.pl_0p5()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.flex_grow()
|
||||
@@ -4607,6 +4612,7 @@ impl Render for ContextEditorToolbarItem {
|
||||
.children(self.render_remaining_tokens(cx));
|
||||
|
||||
h_flex()
|
||||
.px_0p5()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
|
||||
@@ -10,7 +10,7 @@ use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use context_servers::manager::{ContextServerManager, ContextServerSettings};
|
||||
use context_servers::CONTEXT_SERVERS_NAMESPACE;
|
||||
use context_servers::{ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -51,8 +51,8 @@ pub struct ContextStore {
|
||||
contexts: Vec<ContextHandle>,
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
context_server_manager: Model<ContextServerManager>,
|
||||
context_server_slash_command_ids: HashMap<String, Vec<SlashCommandId>>,
|
||||
context_server_tool_ids: HashMap<String, Vec<ToolId>>,
|
||||
context_server_slash_command_ids: HashMap<Arc<str>, Vec<SlashCommandId>>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
host_contexts: Vec<RemoteContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
@@ -148,6 +148,47 @@ impl ContextStore {
|
||||
this.handle_project_changed(project, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
|
||||
// TODO: At the time when we construct the `ContextStore` we may not have yet initialized the extensions.
|
||||
// In order to register the context servers when the extension is loaded, we're periodically looping to
|
||||
// see if there are context servers to register.
|
||||
//
|
||||
// I tried doing this in a subscription on the `ExtensionStore`, but it never seemed to fire.
|
||||
//
|
||||
// We should find a more elegant way to do this.
|
||||
let context_server_factory_registry =
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
cx.spawn(|context_store, mut cx| async move {
|
||||
loop {
|
||||
let mut servers_to_register = Vec::new();
|
||||
for (_id, factory) in
|
||||
context_server_factory_registry.context_server_factories()
|
||||
{
|
||||
if let Some(server) = factory(&cx).await.log_err() {
|
||||
servers_to_register.push(server);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(_) = context_store
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.context_server_manager.update(cx, |this, cx| {
|
||||
for server in servers_to_register {
|
||||
this.add_server(server, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.log_err()
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
smol::Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
@@ -819,7 +860,7 @@ impl ContextStore {
|
||||
|context_server_manager, cx| {
|
||||
for server in context_server_manager.servers() {
|
||||
context_server_manager
|
||||
.restart_server(&server.id, cx)
|
||||
.restart_server(&server.id(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
},
|
||||
@@ -850,7 +891,7 @@ impl ContextStore {
|
||||
let server = server.clone();
|
||||
let server_id = server_id.clone();
|
||||
|this, mut cx| async move {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
let Some(protocol) = server.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -889,7 +930,7 @@ impl ContextStore {
|
||||
tool_working_set.insert(
|
||||
Arc::new(tools::context_server_tool::ContextServerTool::new(
|
||||
context_server_manager.clone(),
|
||||
server.id.clone(),
|
||||
server.id(),
|
||||
tool,
|
||||
)),
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ pub struct InlineAssistant {
|
||||
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ impl InlineAssistant {
|
||||
confirmed_assists: HashMap::default(),
|
||||
prompt_history: VecDeque::default(),
|
||||
prompt_builder,
|
||||
telemetry: Some(telemetry),
|
||||
telemetry,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
@@ -243,19 +243,17 @@ impl InlineAssistant {
|
||||
codegen_ranges.push(start..end);
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(telemetry) = self.telemetry.as_ref() {
|
||||
telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||
});
|
||||
}
|
||||
self.telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,7 +816,7 @@ impl InlineAssistant {
|
||||
error_message: None,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
self.telemetry.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
@@ -1759,6 +1757,20 @@ impl PromptEditor {
|
||||
) {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace
|
||||
.project()
|
||||
.update(cx, |project, _| project.is_via_ssh());
|
||||
|
||||
workspace
|
||||
.client()
|
||||
.telemetry()
|
||||
.log_edit_event("inline assist", is_via_ssh);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
let prompt = self.editor.read(cx).text(cx);
|
||||
if self
|
||||
.prompt_history_ix
|
||||
@@ -2337,7 +2349,7 @@ pub struct Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
is_insertion: bool,
|
||||
}
|
||||
@@ -2347,7 +2359,7 @@ impl Codegen {
|
||||
buffer: Model<MultiBuffer>,
|
||||
range: Range<Anchor>,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
telemetry: Option<Arc<Telemetry>>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
builder: Arc<PromptBuilder>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
@@ -2356,7 +2368,7 @@ impl Codegen {
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
false,
|
||||
telemetry.clone(),
|
||||
Some(telemetry.clone()),
|
||||
builder.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -2447,7 +2459,7 @@ impl Codegen {
|
||||
self.buffer.clone(),
|
||||
self.range.clone(),
|
||||
false,
|
||||
self.telemetry.clone(),
|
||||
Some(self.telemetry.clone()),
|
||||
self.builder.clone(),
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use feature_flags::ZedPro;
|
||||
use gpui::Action;
|
||||
use gpui::DismissEvent;
|
||||
|
||||
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
|
||||
use proto::Plan;
|
||||
use workspace::ShowConfiguration;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::ListItemSpacing;
|
||||
|
||||
use crate::assistant_settings::AssistantSettings;
|
||||
use fs::Fs;
|
||||
use gpui::SharedString;
|
||||
use gpui::Task;
|
||||
use gpui::{Action, AnyElement, DismissEvent, SharedString, Task};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
|
||||
|
||||
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
|
||||
|
||||
@@ -85,14 +81,36 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
|
||||
let llm_registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let configured_models: Vec<_> = llm_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.map(|provider| provider.id())
|
||||
.collect();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let filtered_models = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
if query.is_empty() {
|
||||
let displayed_models = if configured_models.is_empty() {
|
||||
all_models
|
||||
} else {
|
||||
all_models
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
configured_models.contains(&model_info.model.provider_id())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if query.is_empty() {
|
||||
displayed_models
|
||||
} else {
|
||||
displayed_models
|
||||
.into_iter()
|
||||
.filter(|model_info| {
|
||||
model_info
|
||||
@@ -141,6 +159,29 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
|
||||
let configured_models_count = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.count();
|
||||
|
||||
if configured_models_count > 0 {
|
||||
Some(
|
||||
Label::new("Configured Models")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mt_1()
|
||||
.mb_0p5()
|
||||
.ml_3()
|
||||
.into_any_element(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
@@ -148,9 +189,10 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
let model_info = self.filtered_models.get(ix)?;
|
||||
let show_badges = cx.has_flag::<ZedPro>();
|
||||
let provider_name: String = model_info.model.provider_name().0.into();
|
||||
|
||||
let model_info = self.filtered_models.get(ix)?;
|
||||
let provider_name: String = model_info.model.provider_name().0.clone().into();
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -165,27 +207,32 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex().w_full().justify_between().min_w(px(200.)).child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Label::new(model_info.model.name().0.clone()))
|
||||
.child(
|
||||
Label::new(provider_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(match model_info.availability {
|
||||
LanguageModelAvailability::Public => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
|
||||
show_badges.then(|| {
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.min_w(px(200.))
|
||||
.child(Label::new(model_info.model.name().0.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(provider_name)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.children(match model_info.availability {
|
||||
LanguageModelAvailability::Public => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
|
||||
show_badges.then(|| {
|
||||
Label::new("Pro")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.end_slot(div().when(model_info.is_selected, |this| {
|
||||
this.child(
|
||||
@@ -213,7 +260,7 @@ impl PickerDelegate for ModelPickerDelegate {
|
||||
.justify_between()
|
||||
.when(cx.has_flag::<ZedPro>(), |this| {
|
||||
this.child(match plan {
|
||||
// Already a zed pro subscriber
|
||||
// Already a Zed Pro subscriber
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
|
||||
.icon(IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -254,6 +301,7 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
|
||||
let selected_provider = LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|m| m.id());
|
||||
|
||||
let selected_model = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|m| m.id());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::SlashCommandWorkingSet;
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssistant};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -522,7 +523,11 @@ impl PromptLibrary {
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Some(Box::new(
|
||||
SlashCommandCompletionProvider::new(None, None),
|
||||
SlashCommandCompletionProvider::new(
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
)));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use crate::SlashCommandWorkingSet;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::AfterCompletion;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
@@ -39,6 +40,7 @@ pub mod terminal_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
@@ -52,11 +54,13 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
slash_commands: Arc<SlashCommandWorkingSet>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
slash_commands,
|
||||
editor,
|
||||
workspace,
|
||||
}
|
||||
@@ -69,9 +73,9 @@ impl SlashCommandCompletionProvider {
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let candidates = commands
|
||||
.command_names()
|
||||
let slash_commands = self.slash_commands.clone();
|
||||
let candidates = slash_commands
|
||||
.command_names(cx)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, def)| StringMatchCandidate {
|
||||
@@ -98,7 +102,7 @@ impl SlashCommandCompletionProvider {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| {
|
||||
let command = commands.command(&mat.string)?;
|
||||
let command = slash_commands.command(&mat.string, cx)?;
|
||||
let mut new_text = mat.string.clone();
|
||||
let requires_argument = command.requires_argument();
|
||||
let accepts_arguments = command.accepts_arguments();
|
||||
|
||||
@@ -20,18 +20,18 @@ use crate::slash_command::create_label_for_command;
|
||||
|
||||
pub struct ContextServerSlashCommand {
|
||||
server_manager: Model<ContextServerManager>,
|
||||
server_id: String,
|
||||
server_id: Arc<str>,
|
||||
prompt: Prompt,
|
||||
}
|
||||
|
||||
impl ContextServerSlashCommand {
|
||||
pub fn new(
|
||||
server_manager: Model<ContextServerManager>,
|
||||
server: &Arc<ContextServer>,
|
||||
server: &Arc<dyn ContextServer>,
|
||||
prompt: Prompt,
|
||||
) -> Self {
|
||||
Self {
|
||||
server_id: server.id.clone(),
|
||||
server_id: server.id(),
|
||||
prompt,
|
||||
server_manager,
|
||||
}
|
||||
@@ -89,7 +89,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
|
||||
if let Some(server) = self.server_manager.read(cx).get_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
let Some(protocol) = server.client() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||
let manager = self.server_manager.read(cx);
|
||||
if let Some(server) = manager.get_server(&server_id) {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
let Some(protocol) = server.client() else {
|
||||
return Err(anyhow!("Context server not initialized"));
|
||||
};
|
||||
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::AppContext;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub struct SlashCommandId(usize);
|
||||
|
||||
/// A working set of slash commands for use in one instance of the Assistant Panel.
|
||||
@@ -16,7 +16,7 @@ pub struct SlashCommandWorkingSet {
|
||||
#[derive(Default)]
|
||||
struct WorkingSetState {
|
||||
context_server_commands_by_id: HashMap<SlashCommandId, Arc<dyn SlashCommand>>,
|
||||
context_server_commands_by_name: HashMap<String, Arc<dyn SlashCommand>>,
|
||||
context_server_commands_by_name: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
|
||||
next_command_id: SlashCommandId,
|
||||
}
|
||||
|
||||
@@ -30,6 +30,19 @@ impl SlashCommandWorkingSet {
|
||||
.or_else(|| SlashCommandRegistry::global(cx).command(name))
|
||||
}
|
||||
|
||||
pub fn command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
|
||||
let mut command_names = SlashCommandRegistry::global(cx).command_names();
|
||||
command_names.extend(
|
||||
self.state
|
||||
.lock()
|
||||
.context_server_commands_by_name
|
||||
.keys()
|
||||
.cloned(),
|
||||
);
|
||||
|
||||
command_names
|
||||
}
|
||||
|
||||
pub fn featured_command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
|
||||
SlashCommandRegistry::global(cx).featured_command_names()
|
||||
}
|
||||
@@ -60,7 +73,7 @@ impl WorkingSetState {
|
||||
self.context_server_commands_by_name.extend(
|
||||
self.context_server_commands_by_id
|
||||
.values()
|
||||
.map(|command| (command.name(), command.clone())),
|
||||
.map(|command| (command.name().into(), command.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use assistant_tool::Tool;
|
||||
use context_servers::manager::ContextServerManager;
|
||||
@@ -6,14 +8,14 @@ use gpui::{Model, Task};
|
||||
|
||||
pub struct ContextServerTool {
|
||||
server_manager: Model<ContextServerManager>,
|
||||
server_id: String,
|
||||
server_id: Arc<str>,
|
||||
tool: types::Tool,
|
||||
}
|
||||
|
||||
impl ContextServerTool {
|
||||
pub fn new(
|
||||
server_manager: Model<ContextServerManager>,
|
||||
server_id: impl Into<String>,
|
||||
server_id: impl Into<Arc<str>>,
|
||||
tool: types::Tool,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -55,7 +57,7 @@ impl Tool for ContextServerTool {
|
||||
cx.foreground_executor().spawn({
|
||||
let tool_name = self.tool.name.clone();
|
||||
async move {
|
||||
let Some(protocol) = server.client.read().clone() else {
|
||||
let Some(protocol) = server.client() else {
|
||||
bail!("Context server not initialized");
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ path = "src/context_servers.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
pub mod client;
|
||||
pub mod manager;
|
||||
pub mod protocol;
|
||||
mod registry;
|
||||
pub mod types;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use gpui::{actions, AppContext};
|
||||
use settings::Settings;
|
||||
|
||||
pub use crate::manager::ContextServer;
|
||||
use crate::manager::ContextServerSettings;
|
||||
|
||||
pub mod client;
|
||||
pub mod manager;
|
||||
pub mod protocol;
|
||||
pub mod types;
|
||||
pub use crate::registry::ContextServerFactoryRegistry;
|
||||
|
||||
actions!(context_servers, [Restart]);
|
||||
|
||||
@@ -16,6 +19,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
ContextServerSettings::register(cx);
|
||||
ContextServerFactoryRegistry::default_global(cx);
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
|
||||
|
||||
@@ -15,9 +15,13 @@
|
||||
//! and react to changes in settings.
|
||||
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{Future, FutureExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, ModelContext, Task};
|
||||
use log;
|
||||
use parking_lot::RwLock;
|
||||
@@ -56,51 +60,84 @@ impl Settings for ContextServerSettings {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContextServer {
|
||||
pub id: String,
|
||||
pub config: ServerConfig,
|
||||
#[async_trait(?Send)]
|
||||
pub trait ContextServer: Send + Sync + 'static {
|
||||
fn id(&self) -> Arc<str>;
|
||||
fn config(&self) -> Arc<ServerConfig>;
|
||||
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>>;
|
||||
fn start<'a>(
|
||||
self: Arc<Self>,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>>;
|
||||
fn stop(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct NativeContextServer {
|
||||
pub id: Arc<str>,
|
||||
pub config: Arc<ServerConfig>,
|
||||
pub client: RwLock<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
|
||||
}
|
||||
|
||||
impl ContextServer {
|
||||
fn new(config: ServerConfig) -> Self {
|
||||
impl NativeContextServer {
|
||||
pub fn new(config: Arc<ServerConfig>) -> Self {
|
||||
Self {
|
||||
id: config.id.clone(),
|
||||
id: config.id.clone().into(),
|
||||
config,
|
||||
client: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start(&self, cx: &AsyncAppContext) -> anyhow::Result<()> {
|
||||
log::info!("starting context server {}", self.config.id,);
|
||||
let client = Client::new(
|
||||
client::ContextServerId(self.config.id.clone()),
|
||||
client::ModelContextServerBinary {
|
||||
executable: Path::new(&self.config.executable).to_path_buf(),
|
||||
args: self.config.args.clone(),
|
||||
env: self.config.env.clone(),
|
||||
},
|
||||
cx.clone(),
|
||||
)?;
|
||||
|
||||
let protocol = crate::protocol::ModelContextProtocol::new(client);
|
||||
let client_info = types::Implementation {
|
||||
name: "Zed".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
let initialized_protocol = protocol.initialize(client_info).await?;
|
||||
|
||||
log::debug!(
|
||||
"context server {} initialized: {:?}",
|
||||
self.config.id,
|
||||
initialized_protocol.initialize,
|
||||
);
|
||||
|
||||
*self.client.write() = Some(Arc::new(initialized_protocol));
|
||||
Ok(())
|
||||
#[async_trait(?Send)]
|
||||
impl ContextServer for NativeContextServer {
|
||||
fn id(&self) -> Arc<str> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
async fn stop(&self) -> anyhow::Result<()> {
|
||||
fn config(&self) -> Arc<ServerConfig> {
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>> {
|
||||
self.client.read().clone()
|
||||
}
|
||||
|
||||
fn start<'a>(
|
||||
self: Arc<Self>,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
|
||||
async move {
|
||||
log::info!("starting context server {}", self.config.id,);
|
||||
let client = Client::new(
|
||||
client::ContextServerId(self.config.id.clone()),
|
||||
client::ModelContextServerBinary {
|
||||
executable: Path::new(&self.config.executable).to_path_buf(),
|
||||
args: self.config.args.clone(),
|
||||
env: self.config.env.clone(),
|
||||
},
|
||||
cx.clone(),
|
||||
)?;
|
||||
|
||||
let protocol = crate::protocol::ModelContextProtocol::new(client);
|
||||
let client_info = types::Implementation {
|
||||
name: "Zed".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
let initialized_protocol = protocol.initialize(client_info).await?;
|
||||
|
||||
log::debug!(
|
||||
"context server {} initialized: {:?}",
|
||||
self.config.id,
|
||||
initialized_protocol.initialize,
|
||||
);
|
||||
|
||||
*self.client.write() = Some(Arc::new(initialized_protocol));
|
||||
Ok(())
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<()> {
|
||||
let mut client = self.client.write();
|
||||
if let Some(protocol) = client.take() {
|
||||
drop(protocol);
|
||||
@@ -114,13 +151,13 @@ impl ContextServer {
|
||||
/// must go through the `GlobalContextServerManager` which holds
|
||||
/// a model to the ContextServerManager.
|
||||
pub struct ContextServerManager {
|
||||
servers: HashMap<String, Arc<ContextServer>>,
|
||||
pending_servers: HashSet<String>,
|
||||
servers: HashMap<Arc<str>, Arc<dyn ContextServer>>,
|
||||
pending_servers: HashSet<Arc<str>>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
ServerStarted { server_id: String },
|
||||
ServerStopped { server_id: String },
|
||||
ServerStarted { server_id: Arc<str> },
|
||||
ServerStopped { server_id: Arc<str> },
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ContextServerManager {}
|
||||
@@ -141,10 +178,10 @@ impl ContextServerManager {
|
||||
|
||||
pub fn add_server(
|
||||
&mut self,
|
||||
config: ServerConfig,
|
||||
server: Arc<dyn ContextServer>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let server_id = config.id.clone();
|
||||
let server_id = server.id();
|
||||
|
||||
if self.servers.contains_key(&server_id) || self.pending_servers.contains(&server_id) {
|
||||
return Task::ready(Ok(()));
|
||||
@@ -153,8 +190,7 @@ impl ContextServerManager {
|
||||
let task = {
|
||||
let server_id = server_id.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let server = Arc::new(ContextServer::new(config));
|
||||
server.start(&cx).await?;
|
||||
server.clone().start(&cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.servers.insert(server_id.clone(), server);
|
||||
this.pending_servers.remove(&server_id);
|
||||
@@ -170,18 +206,24 @@ impl ContextServerManager {
|
||||
task
|
||||
}
|
||||
|
||||
pub fn get_server(&self, id: &str) -> Option<Arc<ContextServer>> {
|
||||
pub fn get_server(&self, id: &str) -> Option<Arc<dyn ContextServer>> {
|
||||
self.servers.get(id).cloned()
|
||||
}
|
||||
|
||||
pub fn remove_server(&mut self, id: &str, cx: &ModelContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
let id = id.to_string();
|
||||
pub fn remove_server(
|
||||
&mut self,
|
||||
id: &Arc<str>,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let id = id.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
|
||||
server.stop().await?;
|
||||
if let Some(server) =
|
||||
this.update(&mut cx, |this, _cx| this.servers.remove(id.as_ref()))?
|
||||
{
|
||||
server.stop()?;
|
||||
}
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_servers.remove(&id);
|
||||
this.pending_servers.remove(id.as_ref());
|
||||
cx.emit(Event::ServerStopped {
|
||||
server_id: id.clone(),
|
||||
})
|
||||
@@ -192,16 +234,16 @@ impl ContextServerManager {
|
||||
|
||||
pub fn restart_server(
|
||||
&mut self,
|
||||
id: &str,
|
||||
id: &Arc<str>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let id = id.to_string();
|
||||
let id = id.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
|
||||
server.stop().await?;
|
||||
let config = server.config.clone();
|
||||
let new_server = Arc::new(ContextServer::new(config));
|
||||
new_server.start(&cx).await?;
|
||||
server.stop()?;
|
||||
let config = server.config();
|
||||
let new_server = Arc::new(NativeContextServer::new(config));
|
||||
new_server.clone().start(&cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.servers.insert(id.clone(), new_server);
|
||||
cx.emit(Event::ServerStopped {
|
||||
@@ -216,7 +258,7 @@ impl ContextServerManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn servers(&self) -> Vec<Arc<ContextServer>> {
|
||||
pub fn servers(&self) -> Vec<Arc<dyn ContextServer>> {
|
||||
self.servers.values().cloned().collect()
|
||||
}
|
||||
|
||||
@@ -224,7 +266,7 @@ impl ContextServerManager {
|
||||
let current_servers = self
|
||||
.servers()
|
||||
.into_iter()
|
||||
.map(|server| (server.id.clone(), server.config.clone()))
|
||||
.map(|server| (server.id(), server.config()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let new_servers = settings
|
||||
@@ -235,19 +277,20 @@ impl ContextServerManager {
|
||||
|
||||
let servers_to_add = new_servers
|
||||
.values()
|
||||
.filter(|config| !current_servers.contains_key(&config.id))
|
||||
.filter(|config| !current_servers.contains_key(config.id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let servers_to_remove = current_servers
|
||||
.keys()
|
||||
.filter(|id| !new_servers.contains_key(*id))
|
||||
.filter(|id| !new_servers.contains_key(id.as_ref()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
log::trace!("servers_to_add={:?}", servers_to_add);
|
||||
for config in servers_to_add {
|
||||
self.add_server(config, cx).detach_and_log_err(cx);
|
||||
let server = Arc::new(NativeContextServer::new(Arc::new(config)));
|
||||
self.add_server(server, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
for id in servers_to_remove {
|
||||
|
||||
72
crates/context_servers/src/registry.rs
Normal file
72
crates/context_servers/src/registry.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, ReadGlobal};
|
||||
use gpui::{Global, Task};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
use crate::ContextServer;
|
||||
|
||||
pub type ContextServerFactory =
|
||||
Arc<dyn Fn(&AsyncAppContext) -> Task<Result<Arc<dyn ContextServer>>> + Send + Sync + 'static>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalContextServerFactoryRegistry(Arc<ContextServerFactoryRegistry>);
|
||||
|
||||
impl Global for GlobalContextServerFactoryRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ContextServerFactoryRegistryState {
|
||||
context_servers: HashMap<Arc<str>, ContextServerFactory>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ContextServerFactoryRegistry {
|
||||
state: RwLock<ContextServerFactoryRegistryState>,
|
||||
}
|
||||
|
||||
impl ContextServerFactoryRegistry {
|
||||
/// Returns the global [`ContextServerFactoryRegistry`].
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
GlobalContextServerFactoryRegistry::global(cx).0.clone()
|
||||
}
|
||||
|
||||
/// Returns the global [`ContextServerFactoryRegistry`].
|
||||
///
|
||||
/// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist.
|
||||
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
|
||||
cx.default_global::<GlobalContextServerFactoryRegistry>()
|
||||
.0
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
state: RwLock::new(ContextServerFactoryRegistryState {
|
||||
context_servers: HashMap::default(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn context_server_factories(&self) -> Vec<(Arc<str>, ContextServerFactory)> {
|
||||
self.state
|
||||
.read()
|
||||
.context_servers
|
||||
.iter()
|
||||
.map(|(id, factory)| (id.clone(), factory.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Registers the provided [`ContextServerFactory`].
|
||||
pub fn register_server_factory(&self, id: Arc<str>, factory: ContextServerFactory) {
|
||||
let mut state = self.state.write();
|
||||
state.context_servers.insert(id, factory);
|
||||
}
|
||||
|
||||
/// Unregisters the [`ContextServerFactory`] for the server with the given ID.
|
||||
pub fn unregister_server_factory_by_id(&self, server_id: &str) {
|
||||
let mut state = self.state.write();
|
||||
state.context_servers.remove(server_id);
|
||||
}
|
||||
}
|
||||
@@ -1968,10 +1968,10 @@ impl EditorElement {
|
||||
|
||||
fn layout_lines(
|
||||
rows: Range<DisplayRow>,
|
||||
line_number_layouts: &[Option<ShapedLine>],
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
editor_width: Pixels,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<LineWithInvisibles> {
|
||||
if rows.start >= rows.end {
|
||||
@@ -2020,9 +2020,9 @@ impl EditorElement {
|
||||
&style.text,
|
||||
MAX_LINE_LEN,
|
||||
rows.len(),
|
||||
line_number_layouts,
|
||||
snapshot.mode,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -2071,6 +2071,7 @@ impl EditorElement {
|
||||
scroll_width: &mut Pixels,
|
||||
resized_blocks: &mut HashMap<CustomBlockId, u32>,
|
||||
selections: &[Selection<Point>],
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> (AnyElement, Size<Pixels>) {
|
||||
let mut element = match block {
|
||||
@@ -2083,8 +2084,15 @@ impl EditorElement {
|
||||
line_layouts[align_to.row().minus(rows.start) as usize]
|
||||
.x_for_index(align_to.column() as usize)
|
||||
} else {
|
||||
layout_line(align_to.row(), snapshot, &self.style, editor_width, cx)
|
||||
.x_for_index(align_to.column() as usize)
|
||||
layout_line(
|
||||
align_to.row(),
|
||||
snapshot,
|
||||
&self.style,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
)
|
||||
.x_for_index(align_to.column() as usize)
|
||||
};
|
||||
|
||||
let selected = selections
|
||||
@@ -2447,6 +2455,7 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
selections: &[Selection<Point>],
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
|
||||
let (fixed_blocks, non_fixed_blocks) = snapshot
|
||||
@@ -2484,6 +2493,7 @@ impl EditorElement {
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
);
|
||||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||||
@@ -2529,6 +2539,7 @@ impl EditorElement {
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -2575,6 +2586,7 @@ impl EditorElement {
|
||||
scroll_width,
|
||||
&mut resized_blocks,
|
||||
selections,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -4359,9 +4371,9 @@ impl LineWithInvisibles {
|
||||
text_style: &TextStyle,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
line_number_layouts: &[Option<ShapedLine>],
|
||||
editor_mode: EditorMode,
|
||||
text_width: Pixels,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Vec<Self> {
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
@@ -4489,12 +4501,9 @@ impl LineWithInvisibles {
|
||||
if editor_mode == EditorMode::Full {
|
||||
// Line wrap pads its contents with fake whitespaces,
|
||||
// avoid printing them
|
||||
let inside_wrapped_string = line_number_layouts
|
||||
.get(row)
|
||||
.and_then(|layout| layout.as_ref())
|
||||
.is_none();
|
||||
let is_soft_wrapped = is_row_soft_wrapped(row);
|
||||
if highlighted_chunk.is_tab {
|
||||
if non_whitespace_added || !inside_wrapped_string {
|
||||
if non_whitespace_added || !is_soft_wrapped {
|
||||
invisibles.push(Invisible::Tab {
|
||||
line_start_offset: line.len(),
|
||||
line_end_offset: line.len() + line_chunk.len(),
|
||||
@@ -4510,7 +4519,7 @@ impl LineWithInvisibles {
|
||||
(*line_byte as char).is_whitespace();
|
||||
non_whitespace_added |= !is_whitespace;
|
||||
is_whitespace
|
||||
&& (non_whitespace_added || !inside_wrapped_string)
|
||||
&& (non_whitespace_added || !is_soft_wrapped)
|
||||
})
|
||||
.map(|(whitespace_index, _)| Invisible::Whitespace {
|
||||
line_offset: line.len() + whitespace_index,
|
||||
@@ -4873,10 +4882,10 @@ impl Element for EditorElement {
|
||||
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
|
||||
let line = Self::layout_lines(
|
||||
DisplayRow(0)..DisplayRow(1),
|
||||
&[],
|
||||
&editor_snapshot,
|
||||
&style,
|
||||
px(f32::MAX),
|
||||
|_| false, // Single lines never soft wrap
|
||||
cx,
|
||||
)
|
||||
.pop()
|
||||
@@ -5085,6 +5094,8 @@ impl Element for EditorElement {
|
||||
.buffer_rows(start_row)
|
||||
.take((start_row..end_row).len())
|
||||
.collect::<Vec<_>>();
|
||||
let is_row_soft_wrapped =
|
||||
|row| buffer_rows.get(row).copied().flatten().is_none();
|
||||
|
||||
let start_anchor = if start_row == Default::default() {
|
||||
Anchor::min()
|
||||
@@ -5176,10 +5187,10 @@ impl Element for EditorElement {
|
||||
let mut max_visible_line_width = Pixels::ZERO;
|
||||
let mut line_layouts = Self::layout_lines(
|
||||
start_row..end_row,
|
||||
&line_numbers,
|
||||
&snapshot,
|
||||
&self.style,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
);
|
||||
for line_with_invisibles in &line_layouts {
|
||||
@@ -5188,9 +5199,15 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let longest_line_width =
|
||||
layout_line(snapshot.longest_row(), &snapshot, &style, editor_width, cx)
|
||||
.width;
|
||||
let longest_line_width = layout_line(
|
||||
snapshot.longest_row(),
|
||||
&snapshot,
|
||||
&style,
|
||||
editor_width,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
)
|
||||
.width;
|
||||
let mut scroll_width =
|
||||
longest_line_width.max(max_visible_line_width) + overscroll.width;
|
||||
|
||||
@@ -5208,6 +5225,7 @@ impl Element for EditorElement {
|
||||
line_height,
|
||||
&line_layouts,
|
||||
&local_selections,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -5966,6 +5984,7 @@ fn layout_line(
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
text_width: Pixels,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> LineWithInvisibles {
|
||||
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
|
||||
@@ -5974,9 +5993,9 @@ fn layout_line(
|
||||
&style.text,
|
||||
MAX_LINE_LEN,
|
||||
1,
|
||||
&[],
|
||||
snapshot.mode,
|
||||
text_width,
|
||||
is_row_soft_wrapped,
|
||||
cx,
|
||||
)
|
||||
.pop()
|
||||
@@ -6661,15 +6680,22 @@ mod tests {
|
||||
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
|
||||
);
|
||||
|
||||
init_test(cx, |s| {
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
|
||||
});
|
||||
for show_line_numbers in [true, false] {
|
||||
init_test(cx, |s| {
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
|
||||
});
|
||||
|
||||
let actual_invisibles =
|
||||
collect_invisibles_from_new_editor(cx, EditorMode::Full, input_text, px(500.0));
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorMode::Full,
|
||||
input_text,
|
||||
px(500.0),
|
||||
show_line_numbers,
|
||||
);
|
||||
|
||||
assert_eq!(expected_invisibles, actual_invisibles);
|
||||
assert_eq!(expected_invisibles, actual_invisibles);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6683,14 +6709,17 @@ mod tests {
|
||||
EditorMode::SingleLine { auto_width: false },
|
||||
EditorMode::AutoHeight { max_lines: 100 },
|
||||
] {
|
||||
let invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
editor_mode_without_invisibles,
|
||||
"\t\t\t| | a b",
|
||||
px(500.0),
|
||||
);
|
||||
assert!(invisibles.is_empty(),
|
||||
for show_line_numbers in [true, false] {
|
||||
let invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
editor_mode_without_invisibles,
|
||||
"\t\t\t| | a b",
|
||||
px(500.0),
|
||||
show_line_numbers,
|
||||
);
|
||||
assert!(invisibles.is_empty(),
|
||||
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6741,43 +6770,48 @@ mod tests {
|
||||
let resize_step = 10.0;
|
||||
let mut editor_width = 200.0;
|
||||
while editor_width <= 1000.0 {
|
||||
update_test_language_settings(cx, |s| {
|
||||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
s.defaults.preferred_line_length = Some(editor_width as u32);
|
||||
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
|
||||
});
|
||||
for show_line_numbers in [true, false] {
|
||||
update_test_language_settings(cx, |s| {
|
||||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
s.defaults.preferred_line_length = Some(editor_width as u32);
|
||||
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
|
||||
});
|
||||
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorMode::Full,
|
||||
&input_text,
|
||||
px(editor_width),
|
||||
);
|
||||
let actual_invisibles = collect_invisibles_from_new_editor(
|
||||
cx,
|
||||
EditorMode::Full,
|
||||
&input_text,
|
||||
px(editor_width),
|
||||
show_line_numbers,
|
||||
);
|
||||
|
||||
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
||||
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
||||
let mut i = 0;
|
||||
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
||||
i = actual_index;
|
||||
match expected_invisibles.get(i) {
|
||||
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
||||
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
||||
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
||||
_ => {
|
||||
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
|
||||
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
|
||||
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
|
||||
let mut i = 0;
|
||||
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
|
||||
i = actual_index;
|
||||
match expected_invisibles.get(i) {
|
||||
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
|
||||
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
|
||||
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
|
||||
_ => {
|
||||
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
|
||||
}
|
||||
},
|
||||
None => {
|
||||
panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
|
||||
}
|
||||
},
|
||||
None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
||||
assert!(
|
||||
missing_expected_invisibles.is_empty(),
|
||||
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
||||
);
|
||||
let missing_expected_invisibles = &expected_invisibles[i + 1..];
|
||||
assert!(
|
||||
missing_expected_invisibles.is_empty(),
|
||||
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
|
||||
);
|
||||
|
||||
editor_width += resize_step;
|
||||
editor_width += resize_step;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6786,6 +6820,7 @@ mod tests {
|
||||
editor_mode: EditorMode,
|
||||
input_text: &str,
|
||||
editor_width: Pixels,
|
||||
show_line_numbers: bool,
|
||||
) -> Vec<Invisible> {
|
||||
info!(
|
||||
"Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
|
||||
@@ -6797,11 +6832,13 @@ mod tests {
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let editor = window.root(cx).unwrap();
|
||||
|
||||
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
|
||||
window
|
||||
.update(cx, |editor, cx| {
|
||||
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
|
||||
editor.set_wrap_width(Some(editor_width), cx);
|
||||
editor.set_show_line_numbers(show_line_numbers, cx);
|
||||
})
|
||||
.unwrap();
|
||||
let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {
|
||||
|
||||
@@ -75,6 +75,8 @@ pub struct ExtensionManifest {
|
||||
#[serde(default)]
|
||||
pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub context_servers: BTreeMap<Arc<str>, ContextServerManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
|
||||
#[serde(default)]
|
||||
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
|
||||
@@ -134,6 +136,9 @@ impl LanguageServerManifestEntry {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct ContextServerManifestEntry {}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct SlashCommandManifestEntry {
|
||||
pub description: String,
|
||||
@@ -205,6 +210,7 @@ fn manifest_from_old_manifest(
|
||||
.map(|grammar_name| (grammar_name, Default::default()))
|
||||
.collect(),
|
||||
language_servers: Default::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
|
||||
@@ -129,6 +129,11 @@ pub trait Extension: Send + Sync {
|
||||
Err("`run_slash_command` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Returns the command used to start a context server.
|
||||
fn context_server_command(&mut self, _context_server_id: &ContextServerId) -> Result<Command> {
|
||||
Err("`context_server_command` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Returns a list of package names as suggestions to be included in the
|
||||
/// search results of the `/docs` slash command.
|
||||
///
|
||||
@@ -270,6 +275,11 @@ impl wit::Guest for Component {
|
||||
extension().run_slash_command(command, args, worktree)
|
||||
}
|
||||
|
||||
fn context_server_command(context_server_id: String) -> Result<wit::Command> {
|
||||
let context_server_id = ContextServerId(context_server_id);
|
||||
extension().context_server_command(&context_server_id)
|
||||
}
|
||||
|
||||
fn suggest_docs_packages(provider: String) -> Result<Vec<String>, String> {
|
||||
extension().suggest_docs_packages(provider)
|
||||
}
|
||||
@@ -299,6 +309,22 @@ impl fmt::Display for LanguageServerId {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of a context server.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct ContextServerId(String);
|
||||
|
||||
impl AsRef<str> for ContextServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContextServerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeLabelSpan {
|
||||
/// Returns a [`CodeLabelSpan::CodeRange`].
|
||||
pub fn code_range(range: impl Into<wit::Range>) -> Self {
|
||||
|
||||
@@ -135,6 +135,9 @@ world extension {
|
||||
/// Returns the output from running the provided slash command.
|
||||
export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
|
||||
|
||||
/// Returns the command used to start up a context server.
|
||||
export context-server-command: func(context-server-id: string) -> result<command, string>;
|
||||
|
||||
/// Returns a list of packages as suggestions to be included in the `/docs`
|
||||
/// search results.
|
||||
///
|
||||
|
||||
@@ -145,6 +145,14 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_context_server(
|
||||
&self,
|
||||
_id: Arc<str>,
|
||||
_extension: WasmExtension,
|
||||
_host: Arc<WasmHost>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_docs_provider(
|
||||
&self,
|
||||
_extension: WasmExtension,
|
||||
@@ -1267,6 +1275,14 @@ impl ExtensionStore {
|
||||
);
|
||||
}
|
||||
|
||||
for (id, _context_server_entry) in &manifest.context_servers {
|
||||
this.registration_hooks.register_context_server(
|
||||
id.clone(),
|
||||
wasm_extension.clone(),
|
||||
this.wasm_host.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
for (provider_id, _provider) in &manifest.indexed_docs_providers {
|
||||
this.registration_hooks.register_docs_provider(
|
||||
wasm_extension.clone(),
|
||||
|
||||
@@ -384,6 +384,24 @@ impl Extension {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_context_server_command(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
context_server_id: Arc<str>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V020(ext) => {
|
||||
ext.call_context_server_command(store, &context_server_id)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) | Extension::V010(_) => {
|
||||
Err(anyhow!(
|
||||
"`context_server_command` not available prior to v0.2.0"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_suggest_docs_packages(
|
||||
&self,
|
||||
store: &mut Store<WasmState>,
|
||||
|
||||
@@ -20,6 +20,7 @@ assistant_slash_command.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
context_servers.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension_host.workspace = true
|
||||
|
||||
80
crates/extensions_ui/src/extension_context_server.rs
Normal file
80
crates/extensions_ui/src/extension_context_server.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use context_servers::manager::{NativeContextServer, ServerConfig};
|
||||
use context_servers::protocol::InitializedContextServerProtocol;
|
||||
use context_servers::ContextServer;
|
||||
use extension_host::wasm_host::{WasmExtension, WasmHost};
|
||||
use futures::{Future, FutureExt};
|
||||
use gpui::AsyncAppContext;
|
||||
|
||||
pub struct ExtensionContextServer {
|
||||
#[allow(unused)]
|
||||
pub(crate) extension: WasmExtension,
|
||||
#[allow(unused)]
|
||||
pub(crate) host: Arc<WasmHost>,
|
||||
id: Arc<str>,
|
||||
context_server: Arc<NativeContextServer>,
|
||||
}
|
||||
|
||||
impl ExtensionContextServer {
|
||||
pub async fn new(extension: WasmExtension, host: Arc<WasmHost>, id: Arc<str>) -> Result<Self> {
|
||||
let command = extension
|
||||
.call({
|
||||
let id = id.clone();
|
||||
|extension, store| {
|
||||
async move {
|
||||
let command = extension
|
||||
.call_context_server_command(store, id.clone())
|
||||
.await?
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
anyhow::Ok(command)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let config = Arc::new(ServerConfig {
|
||||
id: id.to_string(),
|
||||
executable: command.command,
|
||||
args: command.args,
|
||||
env: Some(command.env.into_iter().collect()),
|
||||
});
|
||||
|
||||
anyhow::Ok(Self {
|
||||
extension,
|
||||
host,
|
||||
id,
|
||||
context_server: Arc::new(NativeContextServer::new(config)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ContextServer for ExtensionContextServer {
|
||||
fn id(&self) -> Arc<str> {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn config(&self) -> Arc<ServerConfig> {
|
||||
self.context_server.config()
|
||||
}
|
||||
|
||||
fn client(&self) -> Option<Arc<InitializedContextServerProtocol>> {
|
||||
self.context_server.client()
|
||||
}
|
||||
|
||||
fn start<'a>(
|
||||
self: Arc<Self>,
|
||||
cx: &'a AsyncAppContext,
|
||||
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
|
||||
self.context_server.clone().start(cx)
|
||||
}
|
||||
|
||||
fn stop(&self) -> Result<()> {
|
||||
self.context_server.stop()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
@@ -11,6 +12,7 @@ use snippet_provider::SnippetRegistry;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::SharedString;
|
||||
|
||||
use crate::extension_context_server::ExtensionContextServer;
|
||||
use crate::{extension_indexed_docs_provider, extension_slash_command::ExtensionSlashCommand};
|
||||
|
||||
pub struct ConcreteExtensionRegistrationHooks {
|
||||
@@ -19,6 +21,7 @@ pub struct ConcreteExtensionRegistrationHooks {
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
@@ -29,6 +32,7 @@ impl ConcreteExtensionRegistrationHooks {
|
||||
indexed_docs_registry: Arc<IndexedDocsRegistry>,
|
||||
snippet_registry: Arc<SnippetRegistry>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
|
||||
cx: &AppContext,
|
||||
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
|
||||
Arc::new(Self {
|
||||
@@ -37,6 +41,7 @@ impl ConcreteExtensionRegistrationHooks {
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry,
|
||||
context_server_factory_registry,
|
||||
executor: cx.background_executor().clone(),
|
||||
})
|
||||
}
|
||||
@@ -69,6 +74,31 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio
|
||||
)
|
||||
}
|
||||
|
||||
fn register_context_server(
|
||||
&self,
|
||||
id: Arc<str>,
|
||||
extension: wasm_host::WasmExtension,
|
||||
host: Arc<wasm_host::WasmHost>,
|
||||
) {
|
||||
self.context_server_factory_registry
|
||||
.register_server_factory(
|
||||
id.clone(),
|
||||
Arc::new({
|
||||
move |cx| {
|
||||
let id = id.clone();
|
||||
let extension = extension.clone();
|
||||
let host = host.clone();
|
||||
cx.spawn(|_cx| async move {
|
||||
let context_server =
|
||||
ExtensionContextServer::new(extension, host, id).await?;
|
||||
|
||||
anyhow::Ok(Arc::new(context_server) as _)
|
||||
})
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn register_docs_provider(
|
||||
&self,
|
||||
extension: wasm_host::WasmExtension,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use extension_host::ExtensionSettings;
|
||||
use extension_host::SchemaVersion;
|
||||
use extension_host::{
|
||||
@@ -161,6 +162,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
.into_iter()
|
||||
.collect(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
@@ -187,6 +189,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
@@ -264,6 +267,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::new();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
@@ -273,6 +277,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
indexed_docs_registry.clone(),
|
||||
snippet_registry.clone(),
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -356,6 +361,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
@@ -406,6 +412,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -500,6 +507,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
let slash_command_registry = SlashCommandRegistry::new();
|
||||
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
|
||||
let snippet_registry = Arc::new(SnippetRegistry::new());
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::new();
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let mut status_updates = language_registry.language_server_binary_statuses();
|
||||
@@ -596,6 +604,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
indexed_docs_registry,
|
||||
snippet_registry,
|
||||
language_registry.clone(),
|
||||
context_server_factory_registry.clone(),
|
||||
cx,
|
||||
);
|
||||
ExtensionStore::new(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod components;
|
||||
mod extension_context_server;
|
||||
mod extension_indexed_docs_provider;
|
||||
mod extension_registration_hooks;
|
||||
mod extension_slash_command;
|
||||
|
||||
@@ -80,7 +80,7 @@ gpui_macros.workspace = true
|
||||
http_client = { optional = true, workspace = true }
|
||||
image = "0.25.1"
|
||||
itertools.workspace = true
|
||||
linkme = "0.3"
|
||||
linkme.workspace = true
|
||||
log.workspace = true
|
||||
num_cpus = "1.13"
|
||||
parking = "2.0.0"
|
||||
|
||||
@@ -546,7 +546,6 @@ impl InputExample {
|
||||
|
||||
impl Render for InputExample {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let num_keystrokes = self.recent_keystrokes.len();
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
@@ -561,7 +560,7 @@ impl Render for InputExample {
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keystrokes: {}", num_keystrokes))
|
||||
.child(format!("Keyboard {}", cx.keyboard_layout()))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
@@ -607,6 +606,7 @@ fn main() {
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
]);
|
||||
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
@@ -642,6 +642,13 @@ fn main() {
|
||||
.unwrap();
|
||||
})
|
||||
.detach();
|
||||
cx.on_keyboard_layout_change({
|
||||
move |cx| {
|
||||
window.update(cx, |_, cx| cx.notify()).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
cx.focus_view(&view.text_input);
|
||||
|
||||
@@ -243,6 +243,7 @@ pub struct AppContext {
|
||||
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
|
||||
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
|
||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||
pub(crate) keyboard_layout: SharedString,
|
||||
pub(crate) global_action_listeners:
|
||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||
pending_effects: VecDeque<Effect>,
|
||||
@@ -252,6 +253,7 @@ pub struct AppContext {
|
||||
// TypeId is the type of the event that the listener callback expects
|
||||
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
|
||||
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
|
||||
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
|
||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||
@@ -279,6 +281,7 @@ impl AppContext {
|
||||
|
||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||
let entities = EntityMap::new();
|
||||
let keyboard_layout = SharedString::from(platform.keyboard_layout());
|
||||
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(AppContext {
|
||||
@@ -302,6 +305,7 @@ impl AppContext {
|
||||
window_handles: FxHashMap::default(),
|
||||
windows: SlotMap::with_key(),
|
||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||
keyboard_layout,
|
||||
global_action_listeners: FxHashMap::default(),
|
||||
pending_effects: VecDeque::new(),
|
||||
pending_notifications: FxHashSet::default(),
|
||||
@@ -310,6 +314,7 @@ impl AppContext {
|
||||
event_listeners: SubscriberSet::new(),
|
||||
release_listeners: SubscriberSet::new(),
|
||||
keystroke_observers: SubscriberSet::new(),
|
||||
keyboard_layout_observers: SubscriberSet::new(),
|
||||
global_observers: SubscriberSet::new(),
|
||||
quit_observers: SubscriberSet::new(),
|
||||
layout_id_buffer: Default::default(),
|
||||
@@ -323,6 +328,19 @@ impl AppContext {
|
||||
|
||||
init_app_menus(platform.as_ref(), &mut app.borrow_mut());
|
||||
|
||||
platform.on_keyboard_layout_change(Box::new({
|
||||
let app = Rc::downgrade(&app);
|
||||
move || {
|
||||
if let Some(app) = app.upgrade() {
|
||||
let cx = &mut app.borrow_mut();
|
||||
cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
|
||||
cx.keyboard_layout_observers
|
||||
.clone()
|
||||
.retain(&(), move |callback| (callback)(cx));
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_quit(Box::new({
|
||||
let cx = app.clone();
|
||||
move || {
|
||||
@@ -356,6 +374,27 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the id of the current keyboard layout
|
||||
pub fn keyboard_layout(&self) -> &SharedString {
|
||||
&self.keyboard_layout
|
||||
}
|
||||
|
||||
/// Invokes a handler when the current keyboard layout changes
|
||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||
where
|
||||
F: 'static + FnMut(&mut AppContext),
|
||||
{
|
||||
let (subscription, activate) = self.keyboard_layout_observers.insert(
|
||||
(),
|
||||
Box::new(move |cx| {
|
||||
callback(cx);
|
||||
true
|
||||
}),
|
||||
);
|
||||
activate();
|
||||
subscription
|
||||
}
|
||||
|
||||
/// Gracefully quit the application via the platform's standard routine.
|
||||
pub fn quit(&self) {
|
||||
self.platform.quit();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, KeyBindingContextPredicate, Keystroke};
|
||||
use anyhow::Result;
|
||||
use smallvec::SmallVec;
|
||||
@@ -22,22 +24,37 @@ impl Clone for KeyBinding {
|
||||
impl KeyBinding {
|
||||
/// Construct a new keybinding from the given data.
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
|
||||
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||
pub fn load(
|
||||
keystrokes: &str,
|
||||
action: Box<dyn Action>,
|
||||
context: Option<&str>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
) -> Result<Self> {
|
||||
let context = if let Some(context) = context {
|
||||
Some(KeyBindingContextPredicate::parse(context)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let keystrokes = keystrokes
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
.map(Keystroke::parse)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
if let Some(equivalents) = key_equivalents {
|
||||
for keystroke in keystrokes.iter_mut() {
|
||||
if keystroke.key.chars().count() == 1 {
|
||||
if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
|
||||
keystroke.key = key.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
keystrokes,
|
||||
action,
|
||||
|
||||
@@ -169,6 +169,7 @@ pub(crate) trait Platform: 'static {
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||
|
||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||
@@ -180,6 +181,7 @@ pub(crate) trait Platform: 'static {
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||
fn keyboard_layout(&self) -> String;
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
""
|
||||
|
||||
@@ -138,6 +138,14 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
self.with_common(|common| common.text_system.clone())
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"unknown".into()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
|
||||
// todo(linux)
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
|
||||
on_finish_launching();
|
||||
|
||||
|
||||
@@ -259,10 +259,7 @@ impl PlatformInput {
|
||||
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
use cocoa::appkit::*;
|
||||
|
||||
let mut chars_ignoring_modifiers = native_event
|
||||
.charactersIgnoringModifiers()
|
||||
.to_str()
|
||||
.to_string();
|
||||
let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
|
||||
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
|
||||
let modifiers = native_event.modifierFlags();
|
||||
|
||||
@@ -314,28 +311,41 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
Some(NSF18FunctionKey) => "f18".to_string(),
|
||||
Some(NSF19FunctionKey) => "f19".to_string(),
|
||||
_ => {
|
||||
let mut chars_ignoring_modifiers_and_shift =
|
||||
chars_for_modified_key(native_event.keyCode(), false, false);
|
||||
// Cases to test when modifying this:
|
||||
//
|
||||
// qwerty key | none | cmd | cmd-shift
|
||||
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
|
||||
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
|
||||
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
|
||||
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
|
||||
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
|
||||
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
|
||||
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
|
||||
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
|
||||
|
||||
// Honor ⌘ when Dvorak-QWERTY is used.
|
||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
|
||||
if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
|
||||
chars_ignoring_modifiers =
|
||||
chars_for_modified_key(native_event.keyCode(), true, shift);
|
||||
chars_ignoring_modifiers_and_shift = chars_with_cmd;
|
||||
// Handle Dvorak+QWERTY / Russian / Armeniam
|
||||
if command || always_use_command_layout() {
|
||||
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
|
||||
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
|
||||
|
||||
// We don't do this in the case that the shifted command key generates
|
||||
// the same character as the unshifted command key (Norwegian, e.g.)
|
||||
if chars_with_both != chars_with_cmd {
|
||||
chars_with_shift = chars_with_both;
|
||||
|
||||
// Handle edge-case where cmd-shift-s reports cmd-s instead of
|
||||
// cmd-shift-s (Ukrainian, etc.)
|
||||
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
|
||||
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
|
||||
}
|
||||
chars_ignoring_modifiers = chars_with_cmd;
|
||||
}
|
||||
|
||||
if shift {
|
||||
if chars_ignoring_modifiers_and_shift
|
||||
== chars_ignoring_modifiers.to_ascii_lowercase()
|
||||
{
|
||||
chars_ignoring_modifiers_and_shift
|
||||
} else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
|
||||
shift = false;
|
||||
chars_ignoring_modifiers
|
||||
} else {
|
||||
chars_ignoring_modifiers
|
||||
}
|
||||
if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
|
||||
chars_ignoring_modifiers
|
||||
} else if shift {
|
||||
shift = false;
|
||||
chars_with_shift
|
||||
} else {
|
||||
chars_ignoring_modifiers
|
||||
}
|
||||
@@ -355,6 +365,28 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||
}
|
||||
}
|
||||
|
||||
fn always_use_command_layout() -> bool {
|
||||
// look at the key to the right of "tab" ('a' in QWERTY)
|
||||
// if it produces a non-ASCII character, but with command held produces ASCII,
|
||||
// we default to the command layout for our keyboard system.
|
||||
let event = synthesize_keyboard_event(0);
|
||||
let without_cmd = unsafe {
|
||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||
event.characters().to_str().to_string()
|
||||
};
|
||||
if without_cmd.is_ascii() {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.set_flags(CGEventFlags::CGEventFlagCommand);
|
||||
let with_cmd = unsafe {
|
||||
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
|
||||
event.characters().to_str().to_string()
|
||||
};
|
||||
|
||||
with_cmd.is_ascii()
|
||||
}
|
||||
|
||||
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
|
||||
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
|
||||
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
|
||||
|
||||
@@ -13,7 +13,6 @@ use cocoa::{
|
||||
};
|
||||
use collections::HashMap;
|
||||
use core_foundation::base::TCFType;
|
||||
use core_graphics::{color_space::kCGColorSpaceSRGB, color_space::CGColorSpace};
|
||||
use foreign_types::ForeignType;
|
||||
use media::core_video::CVMetalTextureCache;
|
||||
use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
|
||||
@@ -122,12 +121,10 @@ impl MetalRenderer {
|
||||
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm_sRGB);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
unsafe {
|
||||
let color_space = CGColorSpace::create_with_name(kCGColorSpaceSRGB);
|
||||
let _: () = msg_send![&*layer, setColorspace: color_space];
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
let _: () = msg_send![
|
||||
@@ -180,7 +177,7 @@ impl MetalRenderer {
|
||||
"path_sprites",
|
||||
"path_sprite_vertex",
|
||||
"path_sprite_fragment",
|
||||
MTLPixelFormat::BGRA8Unorm_sRGB,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
);
|
||||
let shadows_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
@@ -188,7 +185,7 @@ impl MetalRenderer {
|
||||
"shadows",
|
||||
"shadow_vertex",
|
||||
"shadow_fragment",
|
||||
MTLPixelFormat::BGRA8Unorm_sRGB,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
);
|
||||
let quads_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
@@ -196,7 +193,7 @@ impl MetalRenderer {
|
||||
"quads",
|
||||
"quad_vertex",
|
||||
"quad_fragment",
|
||||
MTLPixelFormat::BGRA8Unorm_sRGB,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
);
|
||||
let underlines_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
@@ -204,7 +201,7 @@ impl MetalRenderer {
|
||||
"underlines",
|
||||
"underline_vertex",
|
||||
"underline_fragment",
|
||||
MTLPixelFormat::BGRA8Unorm_sRGB,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
);
|
||||
let monochrome_sprites_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
@@ -212,7 +209,7 @@ impl MetalRenderer {
|
||||
"monochrome_sprites",
|
||||
"monochrome_sprite_vertex",
|
||||
"monochrome_sprite_fragment",
|
||||
MTLPixelFormat::BGRA8Unorm_sRGB,
|
||||
MTLPixelFormat::BGRA8Unorm,
|
||||
);
|
||||
let polychrome_sprites_pipeline_state = build_pipeline_state(
|
||||
&device,
|
||||
|
||||
@@ -136,6 +136,11 @@ unsafe fn build_classes() {
|
||||
open_urls as extern "C" fn(&mut Object, Sel, id, id),
|
||||
);
|
||||
|
||||
decl.add_method(
|
||||
sel!(onKeyboardLayoutChange:),
|
||||
on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id),
|
||||
);
|
||||
|
||||
decl.register()
|
||||
}
|
||||
}
|
||||
@@ -152,6 +157,7 @@ pub(crate) struct MacPlatformState {
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
|
||||
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
@@ -196,6 +202,7 @@ impl MacPlatform {
|
||||
open_urls: None,
|
||||
finish_launching: None,
|
||||
dock_menu: None,
|
||||
on_keyboard_layout_change: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -785,6 +792,10 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().reopen = Some(callback);
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||
self.0.lock().on_keyboard_layout_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
|
||||
self.0.lock().menu_command = Some(callback);
|
||||
}
|
||||
@@ -797,6 +808,22 @@ impl Platform for MacPlatform {
|
||||
self.0.lock().validate_menu_command = Some(callback);
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
unsafe {
|
||||
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
|
||||
|
||||
let input_source_id: *mut Object = TISGetInputSourceProperty(
|
||||
current_keyboard,
|
||||
kTISPropertyInputSourceID as *const c_void,
|
||||
);
|
||||
let input_source_id: *const std::os::raw::c_char =
|
||||
msg_send![input_source_id, UTF8String];
|
||||
let input_source_id = CStr::from_ptr(input_source_id).to_str().unwrap();
|
||||
|
||||
input_source_id.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn app_path(&self) -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let bundle: id = NSBundle::mainBundle();
|
||||
@@ -1259,6 +1286,16 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
|
||||
let notification_center: *mut Object =
|
||||
msg_send![class!(NSNotificationCenter), defaultCenter];
|
||||
let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification");
|
||||
let _: () = msg_send![notification_center, addObserver: this as id
|
||||
selector: sel!(onKeyboardLayoutChange:)
|
||||
name: name
|
||||
object: nil
|
||||
];
|
||||
|
||||
let platform = get_mac_platform(this);
|
||||
let callback = platform.0.lock().finish_launching.take();
|
||||
if let Some(callback) = callback {
|
||||
@@ -1289,6 +1326,20 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||
let platform = unsafe { get_mac_platform(this) };
|
||||
let mut lock = platform.0.lock();
|
||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||
drop(lock);
|
||||
callback();
|
||||
platform
|
||||
.0
|
||||
.lock()
|
||||
.on_keyboard_layout_change
|
||||
.get_or_insert(callback);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
|
||||
let urls = unsafe {
|
||||
(0..urls.count())
|
||||
@@ -1395,6 +1446,17 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "Carbon", kind = "framework")]
|
||||
extern "C" {
|
||||
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
|
||||
fn TISGetInputSourceProperty(
|
||||
inputSource: *mut Object,
|
||||
propertyKey: *const c_void,
|
||||
) -> *mut Object;
|
||||
|
||||
pub static kTISPropertyInputSourceID: CFStringRef;
|
||||
}
|
||||
|
||||
mod security {
|
||||
#![allow(non_upper_case_globals)]
|
||||
use super::*;
|
||||
|
||||
@@ -526,12 +526,6 @@ fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]],
|
||||
return ycbcrToRGBTransform * ycbcr;
|
||||
}
|
||||
|
||||
float3 srgb_to_linear(float3 c) {
|
||||
float3 low = c / 12.92;
|
||||
float3 high = pow((c + 0.055) / 1.055, float3(2.4));
|
||||
return mix(low, high, step(0.04045, c));
|
||||
}
|
||||
|
||||
float4 hsla_to_rgba(Hsla hsla) {
|
||||
float h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
||||
float s = hsla.s;
|
||||
@@ -572,7 +566,12 @@ float4 hsla_to_rgba(Hsla hsla) {
|
||||
b = x;
|
||||
}
|
||||
|
||||
return float4(srgb_to_linear(float3(r, g, b) + m), a);
|
||||
float4 rgba;
|
||||
rgba.x = (r + m);
|
||||
rgba.y = (g + m);
|
||||
rgba.z = (b + m);
|
||||
rgba.w = a;
|
||||
return rgba;
|
||||
}
|
||||
|
||||
float4 to_device_position(float2 unit_vertex, Bounds_ScaledPixels bounds,
|
||||
|
||||
@@ -162,6 +162,12 @@ impl Platform for TestPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"zed.keyboard.example".to_string()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||
|
||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -197,6 +197,14 @@ impl Platform for WindowsPlatform {
|
||||
self.text_system.clone()
|
||||
}
|
||||
|
||||
fn keyboard_layout(&self) -> String {
|
||||
"unknown".into()
|
||||
}
|
||||
|
||||
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
|
||||
// todo(windows)
|
||||
}
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
on_finish_launching();
|
||||
let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };
|
||||
|
||||
@@ -912,7 +912,7 @@ impl Render for ConfigurationView {
|
||||
|
||||
let is_pro = plan == Some(proto::Plan::ZedPro);
|
||||
let subscription_text = Label::new(if is_pro {
|
||||
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google with faster speeds and higher limits through Zed Pro."
|
||||
"You have full access to Zed's hosted LLMs, which include models from Anthropic, OpenAI, and Google. They come with faster speeds and higher limits through Zed Pro."
|
||||
} else {
|
||||
"You have basic access to models from Anthropic through the Zed AI Free plan."
|
||||
});
|
||||
@@ -957,27 +957,14 @@ impl Render for ConfigurationView {
|
||||
})
|
||||
} else {
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(Label::new("Use the zed.dev to access language models."))
|
||||
.gap_2()
|
||||
.child(Label::new("Use Zed AI to access hosted language models."))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign in")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
),
|
||||
Button::new("sign_in", "Sign In")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ pub struct ProjectPanel {
|
||||
horizontal_scrollbar_state: ScrollbarState,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
max_width_item_index: Option<usize>,
|
||||
// We keep track of the mouse down state on entries so we don't flash the UI
|
||||
// in case a user clicks to open a file.
|
||||
mouse_down: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -212,7 +215,6 @@ pub enum Event {
|
||||
entry_id: ProjectEntryId,
|
||||
focus_opened_item: bool,
|
||||
allow_preview: bool,
|
||||
mark_selected: bool,
|
||||
},
|
||||
SplitEntry {
|
||||
entry_id: ProjectEntryId,
|
||||
@@ -339,6 +341,7 @@ impl ProjectPanel {
|
||||
.parent_view(cx.view()),
|
||||
max_width_item_index: None,
|
||||
scroll_handle,
|
||||
mouse_down: false,
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
|
||||
@@ -352,24 +355,12 @@ impl ProjectPanel {
|
||||
entry_id,
|
||||
focus_opened_item,
|
||||
allow_preview,
|
||||
mark_selected
|
||||
} => {
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
||||
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||
let file_path = entry.path.clone();
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
|
||||
project_panel.update(cx, |this, _| {
|
||||
if !mark_selected {
|
||||
this.marked_entries.clear();
|
||||
}
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id
|
||||
});
|
||||
}).ok();
|
||||
|
||||
let is_via_ssh = project.read(cx).is_via_ssh();
|
||||
|
||||
workspace
|
||||
@@ -399,12 +390,12 @@ impl ProjectPanel {
|
||||
});
|
||||
|
||||
if let Some(project_panel) = project_panel.upgrade() {
|
||||
// Always select the entry, regardless of whether it is opened or not.
|
||||
// Always select and mark the entry, regardless of whether it is opened or not.
|
||||
project_panel.update(cx, |project_panel, _| {
|
||||
project_panel.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id
|
||||
});
|
||||
let entry = SelectedEntry { worktree_id, entry_id };
|
||||
project_panel.marked_entries.clear();
|
||||
project_panel.marked_entries.insert(entry);
|
||||
project_panel.selection = Some(entry);
|
||||
});
|
||||
if !focus_opened_item {
|
||||
let focus_handle = project_panel.read(cx).focus_handle.clone();
|
||||
@@ -793,29 +784,22 @@ impl ProjectPanel {
|
||||
|
||||
fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
|
||||
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
|
||||
self.open_internal(false, true, !preview_tabs_enabled, cx);
|
||||
self.open_internal(true, !preview_tabs_enabled, cx);
|
||||
}
|
||||
|
||||
fn open_permanent(&mut self, _: &OpenPermanent, cx: &mut ViewContext<Self>) {
|
||||
self.open_internal(true, false, true, cx);
|
||||
self.open_internal(false, true, cx);
|
||||
}
|
||||
|
||||
fn open_internal(
|
||||
&mut self,
|
||||
mark_selected: bool,
|
||||
allow_preview: bool,
|
||||
focus_opened_item: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some((_, entry)) = self.selected_entry(cx) {
|
||||
if entry.is_file() {
|
||||
self.open_entry(
|
||||
entry.id,
|
||||
mark_selected,
|
||||
focus_opened_item,
|
||||
allow_preview,
|
||||
cx,
|
||||
);
|
||||
self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
|
||||
} else {
|
||||
self.toggle_expanded(entry.id, cx);
|
||||
}
|
||||
@@ -897,7 +881,7 @@ impl ProjectPanel {
|
||||
}
|
||||
project_panel.update_visible_entries(None, cx);
|
||||
if is_new_entry && !is_dir {
|
||||
project_panel.open_entry(new_entry.id, false, true, false, cx);
|
||||
project_panel.open_entry(new_entry.id, true, false, cx);
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
@@ -955,7 +939,6 @@ impl ProjectPanel {
|
||||
fn open_entry(
|
||||
&mut self,
|
||||
entry_id: ProjectEntryId,
|
||||
mark_selected: bool,
|
||||
focus_opened_item: bool,
|
||||
allow_preview: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
@@ -964,7 +947,6 @@ impl ProjectPanel {
|
||||
entry_id,
|
||||
focus_opened_item,
|
||||
allow_preview,
|
||||
mark_selected,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2172,7 +2154,7 @@ impl ProjectPanel {
|
||||
let opened_entries = task.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if open_file_after_drop && !opened_entries.is_empty() {
|
||||
this.open_entry(opened_entries[0], true, true, false, cx);
|
||||
this.open_entry(opened_entries[0], true, false, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2604,71 +2586,70 @@ impl ProjectPanel {
|
||||
this.hover_scroll_task.take();
|
||||
this.drag_onto(selections, entry_id, kind.is_file(), cx);
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.mouse_down = true;
|
||||
cx.propagate();
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
|
||||
if event.down.button == MouseButton::Right || event.down.first_mouse {
|
||||
if event.down.button == MouseButton::Right || event.down.first_mouse || show_editor
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !show_editor {
|
||||
cx.stop_propagation();
|
||||
if event.down.button == MouseButton::Left {
|
||||
this.mouse_down = false;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
||||
if let Some(selection) = this.selection.filter(|_| event.down.modifiers.shift) {
|
||||
let current_selection = this.index_for_selection(selection);
|
||||
let target_selection = this.index_for_selection(SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id,
|
||||
});
|
||||
if let Some(((_, _, source_index), (_, _, target_index))) =
|
||||
current_selection.zip(target_selection)
|
||||
{
|
||||
let range_start = source_index.min(target_index);
|
||||
let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
|
||||
let mut new_selections = BTreeSet::new();
|
||||
this.for_each_visible_entry(
|
||||
range_start..range_end,
|
||||
cx,
|
||||
|entry_id, details, _| {
|
||||
new_selections.insert(SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id: details.worktree_id,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.marked_entries = this
|
||||
.marked_entries
|
||||
.union(&new_selections)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
this.selection = Some(SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id,
|
||||
});
|
||||
// Ensure that the current entry is selected.
|
||||
this.marked_entries.insert(SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id,
|
||||
});
|
||||
}
|
||||
} else if event.down.modifiers.secondary() {
|
||||
if event.down.click_count > 1 {
|
||||
this.split_entry(entry_id, cx);
|
||||
} else if !this.marked_entries.insert(selection) {
|
||||
this.marked_entries.remove(&selection);
|
||||
}
|
||||
} else if kind.is_dir() {
|
||||
this.toggle_expanded(entry_id, cx);
|
||||
} else {
|
||||
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
|
||||
let click_count = event.up.click_count;
|
||||
this.open_entry(
|
||||
entry_id,
|
||||
cx.modifiers().secondary(),
|
||||
!preview_tabs_enabled || click_count > 1,
|
||||
!preview_tabs_enabled && click_count == 1,
|
||||
if let Some(selection) = this.selection.filter(|_| event.down.modifiers.shift) {
|
||||
let current_selection = this.index_for_selection(selection);
|
||||
let clicked_entry = SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id,
|
||||
};
|
||||
let target_selection = this.index_for_selection(clicked_entry);
|
||||
if let Some(((_, _, source_index), (_, _, target_index))) =
|
||||
current_selection.zip(target_selection)
|
||||
{
|
||||
let range_start = source_index.min(target_index);
|
||||
let range_end = source_index.max(target_index) + 1; // Make the range inclusive.
|
||||
let mut new_selections = BTreeSet::new();
|
||||
this.for_each_visible_entry(
|
||||
range_start..range_end,
|
||||
cx,
|
||||
|entry_id, details, _| {
|
||||
new_selections.insert(SelectedEntry {
|
||||
entry_id,
|
||||
worktree_id: details.worktree_id,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.marked_entries = this
|
||||
.marked_entries
|
||||
.union(&new_selections)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
this.selection = Some(clicked_entry);
|
||||
this.marked_entries.insert(clicked_entry);
|
||||
}
|
||||
} else if event.down.modifiers.secondary() {
|
||||
if event.down.click_count > 1 {
|
||||
this.split_entry(entry_id, cx);
|
||||
} else if !this.marked_entries.insert(selection) {
|
||||
this.marked_entries.remove(&selection);
|
||||
}
|
||||
} else if kind.is_dir() {
|
||||
this.toggle_expanded(entry_id, cx);
|
||||
} else {
|
||||
let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
|
||||
let click_count = event.up.click_count;
|
||||
let focus_opened_item = !preview_tabs_enabled || click_count > 1;
|
||||
let allow_preview = preview_tabs_enabled && click_count == 1;
|
||||
this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
|
||||
}
|
||||
}))
|
||||
.cursor_pointer()
|
||||
@@ -2799,7 +2780,7 @@ impl ProjectPanel {
|
||||
.border_color(colors.ghost_element_selected)
|
||||
})
|
||||
.when(
|
||||
is_active && self.focus_handle.contains_focused(cx),
|
||||
!self.mouse_down && is_active && self.focus_handle.contains_focused(cx),
|
||||
|this| this.border_color(Color::Selected.color(cx)),
|
||||
)
|
||||
}
|
||||
@@ -2996,9 +2977,18 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
let worktree_id = worktree.id();
|
||||
self.marked_entries.clear();
|
||||
self.expand_entry(worktree_id, entry_id, cx);
|
||||
self.update_visible_entries(Some((worktree_id, entry_id)), cx);
|
||||
|
||||
if self.marked_entries.len() == 1
|
||||
&& self
|
||||
.marked_entries
|
||||
.first()
|
||||
.filter(|entry| entry.entry_id == entry_id)
|
||||
.is_none()
|
||||
{
|
||||
self.marked_entries.clear();
|
||||
}
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -3629,6 +3619,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_opening_file(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"test": {
|
||||
"first.rs": "// First Rust file",
|
||||
"second.rs": "// Second Rust file",
|
||||
"third.rs": "// Third Rust file",
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "src/test", cx);
|
||||
select_path(&panel, "src/test/first.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.open(&Open, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs <== selected <== marked",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
|
||||
|
||||
select_path(&panel, "src/test/second.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.open(&Open, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs",
|
||||
" second.rs <== selected <== marked",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -4853,7 +4897,7 @@ mod tests {
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" first.rs <== selected",
|
||||
" first.rs <== selected <== marked",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
]
|
||||
@@ -4881,7 +4925,7 @@ mod tests {
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" second.rs <== selected",
|
||||
" second.rs <== selected <== marked",
|
||||
" third.rs"
|
||||
]
|
||||
);
|
||||
|
||||
@@ -43,6 +43,16 @@ impl QuickActionBar {
|
||||
|
||||
let editor = self.active_editor()?;
|
||||
|
||||
let is_local_project = editor
|
||||
.read(cx)
|
||||
.workspace()
|
||||
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_local_project {
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_nonempty_selection = {
|
||||
editor.update(cx, |this, cx| {
|
||||
this.selections
|
||||
|
||||
@@ -10,7 +10,9 @@ use language::{BufferSnapshot, Language, LanguageName, Point};
|
||||
|
||||
use crate::repl_store::ReplStore;
|
||||
use crate::session::SessionEvent;
|
||||
use crate::{KernelSpecification, Session};
|
||||
use crate::{
|
||||
ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown,
|
||||
};
|
||||
|
||||
pub fn assign_kernelspec(
|
||||
kernel_specification: KernelSpecification,
|
||||
@@ -240,6 +242,60 @@ pub fn restart(editor: WeakView<Editor>, cx: &mut WindowContext) {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakView<Editor>) {
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &ClearOutputs, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::clear_outputs(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Interrupt, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::interrupt(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Shutdown, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::shutdown(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Restart, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::restart(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range<Point> {
|
||||
let mut snippet_end_row = end_row;
|
||||
while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row {
|
||||
|
||||
@@ -61,85 +61,45 @@ pub fn init(cx: &mut AppContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor_handle = cx.view().downgrade();
|
||||
cx.defer(|editor, cx| {
|
||||
let workspace = Workspace::for_window(cx);
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Run, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
let is_local_project = workspace
|
||||
.map(|workspace| workspace.read(cx).project().read(cx).is_local())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_local_project {
|
||||
return;
|
||||
}
|
||||
|
||||
let editor_handle = cx.view().downgrade();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Run, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::run(editor_handle.clone(), true, cx).log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
crate::run(editor_handle.clone(), true, cx).log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &RunInPlace, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &RunInPlace, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
crate::run(editor_handle.clone(), false, cx).log_err();
|
||||
}
|
||||
|
||||
crate::run(editor_handle.clone(), false, cx).log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &ClearOutputs, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::clear_outputs(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Interrupt, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::interrupt(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Shutdown, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::shutdown(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
editor
|
||||
.register_action({
|
||||
let editor_handle = editor_handle.clone();
|
||||
move |_: &Restart, cx| {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
crate::restart(editor_handle.clone(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::components::KernelListItem;
|
||||
use crate::setup_editor_session_actions;
|
||||
use crate::{
|
||||
kernels::{Kernel, KernelSpecification, RunningKernel},
|
||||
outputs::{ExecutionStatus, ExecutionView},
|
||||
@@ -207,6 +208,14 @@ impl Session {
|
||||
None => Subscription::new(|| {}),
|
||||
};
|
||||
|
||||
let editor_handle = editor.clone();
|
||||
|
||||
editor
|
||||
.update(cx, |editor, _cx| {
|
||||
setup_editor_session_actions(editor, editor_handle);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let mut session = Self {
|
||||
fs,
|
||||
editor,
|
||||
|
||||
185
crates/settings/src/key_equivalents.rs
Normal file
185
crates/settings/src/key_equivalents.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use collections::HashMap;
|
||||
|
||||
// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
|
||||
// without using option. This means that some of our built in keyboard shortcuts do not work
|
||||
// for those users.
|
||||
//
|
||||
// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
|
||||
// even if the mnemoic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
|
||||
//
|
||||
// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
|
||||
// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
|
||||
// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
|
||||
// as cmd-> on a QWERTY layout.
|
||||
//
|
||||
// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
|
||||
// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
|
||||
// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
|
||||
// specific key moves)
|
||||
//
|
||||
// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
|
||||
// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
|
||||
//
|
||||
// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
|
||||
// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
|
||||
// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
|
||||
// From there I used multi-cursor to produce this match statement.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_key_equivalents(layout: &str) -> Option<HashMap<char, char>> {
|
||||
let (from, to) = match layout {
|
||||
"com.apple.keylayout.Welsh" => ("#", "£"),
|
||||
"com.apple.keylayout.Turkmen" => ("qc]Q`|[XV\\^v~Cx}{", "äçöÄžŞňÜÝş№ýŽÇüÖŇ"),
|
||||
"com.apple.keylayout.Turkish-QWERTY-PC" => (
|
||||
"$\\|`'[}^=.#{*+:/~;)(@<,&]>\"",
|
||||
"+,;<ığÜ&.ç^Ğ(:Ş*>ş=)'Öö/üÇI",
|
||||
),
|
||||
"com.apple.keylayout.Sami-PC" => (
|
||||
"}*x\"w[~^/@`]{|<)>W(\\X=Qq&':;",
|
||||
"Æ(čŊšøŽ&´\"žæØĐ;=:Š)đČ`Áá/ŋÅå",
|
||||
),
|
||||
"com.apple.keylayout.LatinAmerican" => {
|
||||
("[^~>`(<\\@{;*&/):]|='}\"", "{&>:<);¿\"[ñ(/'=Ñ}¡*´]¨")
|
||||
}
|
||||
"com.apple.keylayout.IrishExtended" => ("#", "£"),
|
||||
"com.apple.keylayout.Icelandic" => ("[}=:/'){(*&;^|`\"\\>]<~@", "æ´*Ð'ö=Æ)(/ð&Þ<Öþ:´;>\""),
|
||||
"com.apple.keylayout.German-DIN-2137" => {
|
||||
("}~/<^>{`:\\)&=[]@|;#'\"(*", "Ä>ß;&:Ö<Ü#=/*öä\"'ü§´`)(")
|
||||
}
|
||||
"com.apple.keylayout.FinnishSami-PC" => {
|
||||
(")=*\"\\[@{:>';/<|~(]}^`&", "=`(ˆ@ö\"ÖÅ:¨å´;*>)äÄ&</")
|
||||
}
|
||||
"com.apple.keylayout.FinnishExtended" => {
|
||||
("];{`:'*<~=/}\\|&[\"($^)>@", "äåÖ<Ũ(;>`´Ä'*/öˆ)€&=:\"")
|
||||
}
|
||||
"com.apple.keylayout.Faroese" => ("}\";/$>^@~`:&[*){|]=(\\<'", "ÐØæ´€:&\"><Æ/å(=Å*ð`)';ø"),
|
||||
"com.apple.keylayout.Croatian-PC" => {
|
||||
("{@~;<=>(&*['|]\":/}^`)\\", "Š\">č;*:)/(šćŽđĆČ'Đ&<=ž")
|
||||
}
|
||||
"com.apple.keylayout.Croatian" => ("{@;<~=>(&*['|]\":}^)\\`", "Š\"č;>*:)'(šćŽđĆČĐ&=ž<"),
|
||||
"com.apple.keylayout.Azeri" => (":{W?./\"[}<]|,>';w", "IÖÜ,ş.ƏöĞÇğ/çŞəıü"),
|
||||
"com.apple.keylayout.Albanian" => ("\\'~;:|<>`\"@", "ë@>çÇË;:<'\""),
|
||||
"com.apple.keylayout.SwissFrench" => (
|
||||
":@&'~^)$;\"][\\/#={!|*+`<(>}",
|
||||
"ü\"/^>&=çè`àé$'*¨ö+£(!<;):ä",
|
||||
),
|
||||
"com.apple.keylayout.Swedish" => ("(]\\\"~$`^{|/>*:;<)&=[}'@", ")ä'^>€<&Ö*´:(Åå;=/`öĨ\""),
|
||||
"com.apple.keylayout.Swedish-Pro" => {
|
||||
("/^*`'{|)$>&<[\\;(~\"}@]:=", "´&(<¨Ö*=€:/;ö'å)>^Ä\"äÅ`")
|
||||
}
|
||||
"com.apple.keylayout.Spanish" => ("|!\\<{[:;@`/~].'>}\"^", "\"¡'¿Ññº´!<.>;ç`Ç:¨/"),
|
||||
"com.apple.keylayout.Spanish-ISO" => (
|
||||
"|~`]/:)(<&^>*;#}\"{.\\['@",
|
||||
"\"><;.º=)¿/&Ç(´·not found¨Ñç'ñ`\"",
|
||||
),
|
||||
"com.apple.keylayout.Portuguese" => (")`/'^\"<];>[:{@}(&*=~", "=<'´&`;~º:çªÇ\"^)/(*>"),
|
||||
"com.apple.keylayout.Italian" => (
|
||||
"*7};8:!5%(1&4]^\\6)32>.</0|$,'{[`\"~9#@",
|
||||
"8)*ò£!1ç59&7($6§è0'\"/:.,é°4;ù^ì<%>à32",
|
||||
),
|
||||
"com.apple.keylayout.Italian-Pro" => {
|
||||
("/:@[]'\\=){;|#<\"(*^&`}>~", "'é\"òàìù*=çè§£;^)(&/<°:>")
|
||||
}
|
||||
"com.apple.keylayout.Irish" => ("#", "£"),
|
||||
"com.apple.keylayout.German" => ("=`#'}:)/\"^&]*{;|[<(>~@\\", "*<§´ÄÜ=ß`&/ä(Öü'ö;):>\"#"),
|
||||
"com.apple.keylayout.French" => (
|
||||
"*}7;8:!5%(1&4]\\^6)32>.</0|${'[`\"~9#@",
|
||||
"8*è)!°1(59&7'$`6§0\"é/;.:à£4¨ù^<%>ç32",
|
||||
),
|
||||
"com.apple.keylayout.French-numerical" => (
|
||||
"|!52;][>&@\"%'{)<~7.1/^(}*8#0$9`6\\3:4",
|
||||
"£1(é)$^/72%5ù¨0.>è;&:69*8!3à4ç<§`\"°'",
|
||||
),
|
||||
"com.apple.keylayout.French-PC" => (
|
||||
"!&\"_$}/72>8]#:31)*<%4;6\\-{['@(0|5.`9~^",
|
||||
"17%°4£:èé/_$3§\"&08.5'!-*)¨^ù29àμ(;<ç>6",
|
||||
),
|
||||
"com.apple.keylayout.Finnish" => ("/^*`)'{|$>&<[\\~;(\"}@]:=", "´&(<=¨Ö*€:/;ö'>å)^Ä\"äÅ`"),
|
||||
"com.apple.keylayout.Danish" => ("=[;'`{}|>]*^(&@~)<\\/$\":", "`æå¨<ÆØ*:ø(&)/\">=;'´€^Å"),
|
||||
"com.apple.keylayout.Canadian-CSA" => ("\\?']/><[{}|~`\"", "àÉèçé\"'^¨ÇÀÙùÈ"),
|
||||
"com.apple.keylayout.British" => ("#", "£"),
|
||||
"com.apple.keylayout.Brazilian-ABNT2" => ("\"|~?`'/^\\", "`^\"Ç'´ç¨~"),
|
||||
"com.apple.keylayout.Belgian" => (
|
||||
"`3/*<\\8>7#&96@);024(|'1\":$[~5.%^}]{!",
|
||||
"<\":8.`!/è37ç§20)àé'9£ù&%°4^>(;56*$¨1",
|
||||
),
|
||||
"com.apple.keylayout.Austrian" => ("/^*`'{|)>&<[\\;(~\"}@]:=#", "ß&(<´Ö'=:/;ö#ü)>`Ä\"äÜ*§"),
|
||||
"com.apple.keylayout.Slovak-QWERTY" => (
|
||||
"):9;63'\"]^/+@~>`?<!#5&${2}%*18(704[",
|
||||
"0\"íôžš§!ä6'%2Ň:ňˇ?13ť74ÚľÄ58+á9ýéčú",
|
||||
),
|
||||
"com.apple.keylayout.Slovak" => (
|
||||
"!$`10&:#4^*~{%5')}6/\"[8]97?;<@23>(+",
|
||||
"14ň+é7\"3č68ŇÚ5ť§0Äž'!úáäíýˇô?2ľš:9%",
|
||||
),
|
||||
"com.apple.keylayout.Polish" => (
|
||||
"&)|?,%:;^}]_{!+#(*`/[~<\"$.>'@=\\",
|
||||
":\"$Ż.+Łł=)(ćź§]!/_<żó>śę?,ńą%[;",
|
||||
),
|
||||
"com.apple.keylayout.Lithuanian" => ("+#&=!%1*@73^584$26", "ŽĘŲžĄĮąŪČųęŠįūėĖčš"),
|
||||
"com.apple.keylayout.Hungarian" => (
|
||||
"}(*@\"{=/|;>'[`<~\\!$&0#:]^)+",
|
||||
"Ú)(\"ÁŐóüŰé:áőíÜÍű'!=ö+Éú/ÖÓ",
|
||||
),
|
||||
"com.apple.keylayout.Hungarian-QWERTY" => (
|
||||
"=]#>@/&<`0')~(\\!:*;$\"+^{|}[",
|
||||
"óú+:\"ü=ÜíöáÖÍ)ű'É(é!ÁÓ/ŐŰÚő",
|
||||
),
|
||||
"com.apple.keylayout.Czech-QWERTY" => (
|
||||
"9>0[2()\"}@]46%5;#8{*7^~+!3?&'<$/1`:",
|
||||
"í:éúě90!(2)čž5řů3áÚ8ý6`%1šˇ7§?4'+¨\"",
|
||||
),
|
||||
"com.apple.keylayout.Maltese" => ("[`}{#]~", "ġżĦĠ£ħŻ"),
|
||||
"com.apple.keylayout.Turkish" => (
|
||||
"|}(#>&^-/`$%@]~*,[\"<_.{:'\\)",
|
||||
"ÜI%\"Ç)/ş.<'(*ı>_öğ-ÖŞçĞ$,ü:",
|
||||
),
|
||||
"com.apple.keylayout.Turkish-Standard" => {
|
||||
("|}(#>=&^`@]~*,;[\"<.{:'\\)", "ÜI)^;*'&ö\"ıÖ(.çğŞ:,ĞÇşü=")
|
||||
}
|
||||
"com.apple.keylayout.NorwegianSami-PC" => {
|
||||
("\"}~<`&>':{@*^|\\)=([]/;", "ˆÆ>;</:¨ÅØ\"(&*@=`)øæ´å")
|
||||
}
|
||||
"com.apple.keylayout.Serbian-Latin" => {
|
||||
(";\\@>&'<]\"|(=}^)`[~:*{", "čž\":'ć;đĆŽ)*Đ&=<š>Č(Š")
|
||||
}
|
||||
"com.apple.keylayout.Slovenian" => ("]`^@)&\":'*=<{;}(~>\\|[", "đ<&\"='ĆČć(*;ŠčĐ)>:žŽš"),
|
||||
"com.apple.keylayout.SwedishSami-PC" => {
|
||||
("@=<^|`>){'&\"}]~[/:*\\(;", "\"`;&*<:=Ö¨/ˆÄä>ö´Å(@)å")
|
||||
}
|
||||
"com.apple.keylayout.SwissGerman" => (
|
||||
"={#:\\}!(+]/<\";$'`*[>&^~@)|",
|
||||
"¨é*è$à+)!ä';`üç^<(ö:/&>\"=£",
|
||||
),
|
||||
"com.apple.keylayout.Hawaiian" => ("'", "ʻ"),
|
||||
"com.apple.keylayout.NorthernSami" => (
|
||||
":/[<{X\"wQx\\(;~>W}`*@])'^|=q&",
|
||||
"Å´ø;ØČŊšÁčđ)åŽ:ŠÆž(\"æ=ŋ&Đ`á/",
|
||||
),
|
||||
"com.apple.keylayout.USInternational-PC" => ("^~", "ˆ˜"),
|
||||
"com.apple.keylayout.NorwegianExtended" => ("^~", "ˆ˜"),
|
||||
"com.apple.keylayout.Norwegian" => ("`'~\"\\*|=/@)[:}&><]{(^;", "<¨>^@(*`´\"=øÅÆ/:;æØ)&å"),
|
||||
"com.apple.keylayout.ABC-QWERTZ" => {
|
||||
("\"}~<`>'&#:{@*^|\\)=(]/;[", "`Ä>;<:´/§ÜÖ\"(&'#=*)äßüö")
|
||||
}
|
||||
"com.apple.keylayout.ABC-AZERTY" => (
|
||||
">[$61%@7|)&8\":}593(.4^<!{`2]\\#;~*/'0",
|
||||
"/^4§&52è£07!%°*(ç\"9;'6.1¨<é$`3)>8:ùà",
|
||||
),
|
||||
"com.apple.keylayout.Czech" => (
|
||||
"(7*#193620?/{)@~!$8+;:%4\">`^]&5}[<'",
|
||||
"9ý83+íšžěéˇ'Ú02`14á%ů\"5č!:¨6)7ř(ú?§",
|
||||
),
|
||||
"com.apple.keylayout.Brazilian-Pro" => ("^~", "ˆ˜"),
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
debug_assert!(from.chars().count() == to.chars().count());
|
||||
|
||||
Some(HashMap::from_iter(from.chars().zip(to.chars())))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn get_key_equivalents(_layout: &str) -> Option<HashMap<char, char>> {
|
||||
None
|
||||
}
|
||||
@@ -19,6 +19,8 @@ pub struct KeymapFile(Vec<KeymapBlock>);
|
||||
pub struct KeymapBlock {
|
||||
#[serde(default)]
|
||||
context: Option<String>,
|
||||
#[serde(default)]
|
||||
use_layout_keys: Option<bool>,
|
||||
bindings: BTreeMap<String, KeymapAction>,
|
||||
}
|
||||
|
||||
@@ -74,7 +76,14 @@ impl KeymapFile {
|
||||
}
|
||||
|
||||
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
|
||||
for KeymapBlock { context, bindings } in self.0 {
|
||||
let key_equivalents = crate::key_equivalents::get_key_equivalents(&cx.keyboard_layout());
|
||||
|
||||
for KeymapBlock {
|
||||
context,
|
||||
use_layout_keys,
|
||||
bindings,
|
||||
} in self.0
|
||||
{
|
||||
let bindings = bindings
|
||||
.into_iter()
|
||||
.filter_map(|(keystroke, action)| {
|
||||
@@ -110,7 +119,18 @@ impl KeymapFile {
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
||||
.map(|action| {
|
||||
KeyBinding::load(
|
||||
&keystroke,
|
||||
action,
|
||||
context.as_deref(),
|
||||
if use_layout_keys.unwrap_or_default() {
|
||||
None
|
||||
} else {
|
||||
key_equivalents.as_ref()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod editable_setting_control;
|
||||
mod json_schema;
|
||||
mod key_equivalents;
|
||||
mod keymap_file;
|
||||
mod settings_file;
|
||||
mod settings_store;
|
||||
|
||||
@@ -16,7 +16,6 @@ pub enum ComponentStory {
|
||||
AutoHeightEditor,
|
||||
Avatar,
|
||||
Button,
|
||||
Checkbox,
|
||||
CollabNotification,
|
||||
ContextMenu,
|
||||
Cursor,
|
||||
@@ -52,7 +51,6 @@ impl ComponentStory {
|
||||
Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
|
||||
Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
|
||||
Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
|
||||
Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(),
|
||||
Self::CollabNotification => cx
|
||||
.new_view(|_| collab_ui::notifications::CollabNotificationStory)
|
||||
.into(),
|
||||
|
||||
@@ -14,9 +14,12 @@ path = "src/ui.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools = { workspace = true, optional = true }
|
||||
linkme.workspace = true
|
||||
menu.workspace = true
|
||||
once_cell.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
57
crates/ui/src/component_registry.rs
Normal file
57
crates/ui/src/component_registry.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyElement, WindowContext};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub type ComponentPreviewFn = fn(&WindowContext) -> AnyElement;
|
||||
|
||||
static COMPONENTS: Lazy<Mutex<HashMap<&'static str, Vec<(&'static str, ComponentPreviewFn)>>>> =
|
||||
Lazy::new(|| Mutex::new(HashMap::default()));
|
||||
|
||||
pub fn register_component(scope: &'static str, name: &'static str, preview: ComponentPreviewFn) {
|
||||
let mut components = COMPONENTS.lock().unwrap();
|
||||
components
|
||||
.entry(scope)
|
||||
.or_insert_with(Vec::new)
|
||||
.push((name, preview));
|
||||
}
|
||||
|
||||
/// Initializes all components that have been registered
|
||||
/// in the UI component registry.
|
||||
pub fn init_component_registry() {
|
||||
for register in __COMPONENT_REGISTRATIONS {
|
||||
register();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a map of all registered components and their previews.
|
||||
pub fn get_all_component_previews() -> HashMap<&'static str, Vec<(&'static str, ComponentPreviewFn)>>
|
||||
{
|
||||
COMPONENTS.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[linkme::distributed_slice]
|
||||
pub static __COMPONENT_REGISTRATIONS: [fn()];
|
||||
|
||||
/// Defines components that should be registered in the component registry.
|
||||
///
|
||||
/// This allows components to be previewed, and eventually tracked for documentation
|
||||
/// purposes and to help the systems team to understand component usage across the codebase.
|
||||
#[macro_export]
|
||||
macro_rules! register_components {
|
||||
($scope:ident, [ $($component:ty),+ $(,)? ]) => {
|
||||
const _: () = {
|
||||
#[linkme::distributed_slice($crate::component_registry::__COMPONENT_REGISTRATIONS)]
|
||||
fn register() {
|
||||
$(
|
||||
$crate::component_registry::register_component(
|
||||
stringify!($scope),
|
||||
stringify!($component),
|
||||
|cx: &$crate::WindowContext| <$component>::render_component_previews(cx),
|
||||
);
|
||||
)+
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
mod avatar;
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod content_box;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
@@ -26,6 +27,7 @@ mod settings_group;
|
||||
mod stack;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod table;
|
||||
mod tool_strip;
|
||||
mod tooltip;
|
||||
|
||||
@@ -35,6 +37,7 @@ mod stories;
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use content_box::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
@@ -60,6 +63,7 @@ pub use settings_group::*;
|
||||
pub use stack::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use table::*;
|
||||
pub use tool_strip::*;
|
||||
pub use tooltip::*;
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::internal::prelude::*;
|
||||
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use crate::{prelude::*, ElevationIndex, IconPosition, KeyBinding, Spacing, TintColor};
|
||||
use crate::{
|
||||
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
|
||||
};
|
||||
use crate::{ElevationIndex, IconPosition, KeyBinding, Spacing, TintColor};
|
||||
|
||||
use super::button_icon::ButtonIcon;
|
||||
|
||||
register_components!(button, [Button]);
|
||||
|
||||
/// An element that creates a button with a label and an optional icon.
|
||||
///
|
||||
/// Common buttons:
|
||||
@@ -445,7 +449,7 @@ impl ComponentPreview for Button {
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Styles",
|
||||
vec![
|
||||
single_example("Default", Button::new("default", "Default")),
|
||||
@@ -463,7 +467,7 @@ impl ComponentPreview for Button {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Tinted",
|
||||
vec![
|
||||
single_example(
|
||||
@@ -488,7 +492,7 @@ impl ComponentPreview for Button {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"States",
|
||||
vec![
|
||||
single_example("Default", Button::new("default_state", "Default")),
|
||||
@@ -502,7 +506,7 @@ impl ComponentPreview for Button {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"With Icons",
|
||||
vec![
|
||||
single_example(
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#![allow(missing_docs)]
|
||||
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
|
||||
use crate::internal::prelude::*;
|
||||
use crate::{ElevationIndex, SelectableButton, Tooltip};
|
||||
use crate::{IconName, IconSize};
|
||||
use gpui::{AnyView, DefiniteLength};
|
||||
|
||||
use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
|
||||
use crate::{prelude::*, ElevationIndex, SelectableButton};
|
||||
use crate::{IconName, IconSize};
|
||||
|
||||
use super::button_icon::ButtonIcon;
|
||||
|
||||
register_components!(button, [IconButton]);
|
||||
|
||||
/// The shape of an [`IconButton`].
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum IconButtonShape {
|
||||
@@ -165,3 +167,76 @@ impl RenderOnce for IconButton {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for IconButton {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"An IconButton is a button that displays only an icon. It's used for actions that can be represented by a single icon."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Basic",
|
||||
vec![
|
||||
single_example("Default", IconButton::new("default", IconName::Check)),
|
||||
single_example(
|
||||
"Selected",
|
||||
IconButton::new("selected", IconName::Check).selected(true),
|
||||
),
|
||||
single_example(
|
||||
"Disabled",
|
||||
IconButton::new("disabled", IconName::Check).disabled(true),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Shapes",
|
||||
vec![
|
||||
single_example(
|
||||
"Square",
|
||||
IconButton::new("square", IconName::Check).shape(IconButtonShape::Square),
|
||||
),
|
||||
single_example(
|
||||
"Wide",
|
||||
IconButton::new("wide", IconName::Check).shape(IconButtonShape::Wide),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Sizes",
|
||||
vec![
|
||||
single_example(
|
||||
"XSmall",
|
||||
IconButton::new("xsmall", IconName::Check).icon_size(IconSize::XSmall),
|
||||
),
|
||||
single_example(
|
||||
"Small",
|
||||
IconButton::new("small", IconName::Check).icon_size(IconSize::Small),
|
||||
),
|
||||
single_example(
|
||||
"Medium",
|
||||
IconButton::new("medium", IconName::Check).icon_size(IconSize::Medium),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Icon Color",
|
||||
vec![
|
||||
single_example("Default", IconButton::new("default_color", IconName::Check)),
|
||||
single_example(
|
||||
"Custom",
|
||||
IconButton::new("custom_color", IconName::Check).icon_color(Color::Success),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"With Tooltip",
|
||||
vec![single_example(
|
||||
"Tooltip",
|
||||
IconButton::new("tooltip", IconName::Check)
|
||||
.tooltip(|cx| Tooltip::text("This is a tooltip", cx)),
|
||||
)],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#![allow(missing_docs)]
|
||||
mod checkbox_with_label;
|
||||
|
||||
pub use checkbox_with_label::*;
|
||||
|
||||
use crate::internal::prelude::*;
|
||||
use crate::{Color, Icon, IconName, Selection};
|
||||
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Color, Icon, IconName, Selection};
|
||||
register_components!(checkbox, [Checkbox, CheckboxWithLabel]);
|
||||
|
||||
/// # Checkbox
|
||||
///
|
||||
@@ -123,7 +121,7 @@ impl ComponentPreview for Checkbox {
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Default",
|
||||
vec![
|
||||
single_example(
|
||||
@@ -140,7 +138,7 @@ impl ComponentPreview for Checkbox {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Disabled",
|
||||
vec![
|
||||
single_example(
|
||||
@@ -163,3 +161,89 @@ impl ComponentPreview for Checkbox {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A [`Checkbox`] that has a [`Label`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct CheckboxWithLabel {
|
||||
id: ElementId,
|
||||
label: Label,
|
||||
checked: Selection,
|
||||
on_click: Arc<dyn Fn(&Selection, &mut WindowContext) + 'static>,
|
||||
}
|
||||
|
||||
impl CheckboxWithLabel {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
label: Label,
|
||||
checked: Selection,
|
||||
on_click: impl Fn(&Selection, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label,
|
||||
checked,
|
||||
on_click: Arc::new(on_click),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CheckboxWithLabel {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.child(Checkbox::new(self.id.clone(), self.checked).on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |checked, cx| {
|
||||
(on_click)(checked, cx);
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{}-label", self.id)))
|
||||
.on_click(move |_event, cx| {
|
||||
(self.on_click)(&self.checked.inverse(), cx);
|
||||
})
|
||||
.child(self.label),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for CheckboxWithLabel {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Unselected",
|
||||
CheckboxWithLabel::new(
|
||||
"checkbox_with_label_unselected",
|
||||
Label::new("Always save on quit"),
|
||||
Selection::Unselected,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"Indeterminate",
|
||||
CheckboxWithLabel::new(
|
||||
"checkbox_with_label_indeterminate",
|
||||
Label::new("Always save on quit"),
|
||||
Selection::Indeterminate,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"Selected",
|
||||
CheckboxWithLabel::new(
|
||||
"checkbox_with_label_selected",
|
||||
Label::new("Always save on quit"),
|
||||
Selection::Selected,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{Color, Icon, IconName, Selection};
|
||||
|
||||
/// # Checkbox
|
||||
///
|
||||
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
|
||||
/// Each checkbox works independently from other checkboxes in the list,
|
||||
/// therefore checking an additional box does not affect any other selections.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Checkbox {
|
||||
id: ElementId,
|
||||
checked: Selection,
|
||||
disabled: bool,
|
||||
on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
checked,
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Checkbox {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let group_id = format!("checkbox_group_{:?}", self.id);
|
||||
|
||||
let icon = match self.checked {
|
||||
Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
|
||||
if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
},
|
||||
)),
|
||||
Selection::Indeterminate => Some(
|
||||
Icon::new(IconName::Dash)
|
||||
.size(IconSize::Small)
|
||||
.color(if self.disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Selected
|
||||
}),
|
||||
),
|
||||
Selection::Unselected => None,
|
||||
};
|
||||
|
||||
let selected =
|
||||
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
||||
|
||||
let (bg_color, border_color) = match (self.disabled, selected) {
|
||||
(true, _) => (
|
||||
cx.theme().colors().ghost_element_disabled,
|
||||
cx.theme().colors().border_disabled,
|
||||
),
|
||||
(false, true) => (
|
||||
cx.theme().colors().element_selected,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
(false, false) => (
|
||||
cx.theme().colors().element_background,
|
||||
cx.theme().colors().border,
|
||||
),
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.size(crate::styles::custom_spacing(cx, 20.))
|
||||
.group(group_id.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.m(Spacing::Small.px(cx))
|
||||
.size(crate::styles::custom_spacing(cx, 16.))
|
||||
.rounded_sm()
|
||||
.bg(bg_color)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.when(!self.disabled, |this| {
|
||||
this.group_hover(group_id.clone(), |el| {
|
||||
el.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
})
|
||||
.children(icon),
|
||||
)
|
||||
.when_some(
|
||||
self.on_click.filter(|_| !self.disabled),
|
||||
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{prelude::*, Checkbox};
|
||||
|
||||
/// A [`Checkbox`] that has a [`Label`].
|
||||
#[derive(IntoElement)]
|
||||
pub struct CheckboxWithLabel {
|
||||
id: ElementId,
|
||||
label: Label,
|
||||
checked: Selection,
|
||||
on_click: Arc<dyn Fn(&Selection, &mut WindowContext) + 'static>,
|
||||
}
|
||||
|
||||
impl CheckboxWithLabel {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
label: Label,
|
||||
checked: Selection,
|
||||
on_click: impl Fn(&Selection, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label,
|
||||
checked,
|
||||
on_click: Arc::new(on_click),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CheckboxWithLabel {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.child(Checkbox::new(self.id.clone(), self.checked).on_click({
|
||||
let on_click = self.on_click.clone();
|
||||
move |checked, cx| {
|
||||
(on_click)(checked, cx);
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("{}-label", self.id)))
|
||||
.on_click(move |_event, cx| {
|
||||
(self.on_click)(&self.checked.inverse(), cx);
|
||||
})
|
||||
.child(self.label),
|
||||
)
|
||||
}
|
||||
}
|
||||
118
crates/ui/src/components/content_box.rs
Normal file
118
crates/ui/src/components/content_box.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::internal::prelude::*;
|
||||
use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
register_components!(layout, [ContentBox]);
|
||||
|
||||
/// A flexible container component that can hold other elements.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ContentBox {
|
||||
base: Div,
|
||||
border: bool,
|
||||
fill: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ContentBox {
|
||||
/// Creates a new [ContentBox].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
border: true,
|
||||
fill: true,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the border from the [ContentBox].
|
||||
pub fn borderless(mut self) -> Self {
|
||||
self.border = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes the background fill from the [ContentBox].
|
||||
pub fn unfilled(mut self) -> Self {
|
||||
self.fill = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ContentBox {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ContentBox {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ContentBox {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
// TODO:
|
||||
// Baked in padding will make scrollable views inside of content boxes awkward.
|
||||
//
|
||||
// Do we make the padding optional, or do we push to use a different component?
|
||||
|
||||
self.base
|
||||
.when(self.fill, |this| {
|
||||
this.bg(cx.theme().colors().text.opacity(0.05))
|
||||
})
|
||||
.when(self.border, |this| {
|
||||
this.border_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.rounded_md()
|
||||
.p_2()
|
||||
.children(self.children)
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for ContentBox {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"A flexible container component that can hold other elements. It can be customized with or without a border and background fill."
|
||||
}
|
||||
|
||||
fn example_label_side() -> ExampleLabelSide {
|
||||
ExampleLabelSide::Bottom
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example(
|
||||
"Default",
|
||||
ContentBox::new()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h_48()
|
||||
.child(Label::new("Default ContentBox")),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Without Border",
|
||||
ContentBox::new()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h_48()
|
||||
.borderless()
|
||||
.child(Label::new("Borderless ContentBox")),
|
||||
)
|
||||
.grow(),
|
||||
single_example(
|
||||
"Without Fill",
|
||||
ContentBox::new()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.h_48()
|
||||
.unfilled()
|
||||
.child(Label::new("Unfilled ContentBox")),
|
||||
)
|
||||
.grow(),
|
||||
])
|
||||
.grow()]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::internal::prelude::*;
|
||||
use gpui::{ClickEvent, CursorStyle};
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{ClickEvent, CursorStyle};
|
||||
use crate::{Color, IconButton, IconButtonShape, IconName, IconSize};
|
||||
|
||||
use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize};
|
||||
register_components!(disclosure, [Disclosure]);
|
||||
|
||||
// TODO: This should be DisclosureControl, not Disclosure
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disclosure {
|
||||
id: ElementId,
|
||||
@@ -71,3 +74,20 @@ impl RenderOnce for Disclosure {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for Disclosure {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"A Disclosure component is used to show or hide content. It's typically used in expandable/collapsible sections or tree-like structures."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![example_group(vec![
|
||||
single_example("Closed", Disclosure::new("closed", false)),
|
||||
single_example("Open", Disclosure::new("open", true)),
|
||||
single_example(
|
||||
"Open (Selected)",
|
||||
Disclosure::new("open", true).selected(true),
|
||||
),
|
||||
])]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::{prelude::*, Avatar};
|
||||
use crate::internal::prelude::*;
|
||||
use crate::Avatar;
|
||||
use gpui::{AnyElement, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
register_components!(facepile, [Facepile]);
|
||||
|
||||
/// A facepile is a collection of faces stacked horizontally–
|
||||
/// always with the leftmost face on top and descending in z-index
|
||||
///
|
||||
@@ -83,7 +86,7 @@ impl ComponentPreview for Facepile {
|
||||
"https://avatars.githubusercontent.com/u/1714999?s=60&v=4",
|
||||
];
|
||||
|
||||
vec![example_group(
|
||||
vec![example_group_with_title(
|
||||
"Examples",
|
||||
vec![
|
||||
single_example(
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
#![allow(missing_docs)]
|
||||
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
|
||||
use crate::internal::prelude::*;
|
||||
use crate::Indicator;
|
||||
|
||||
use gpui::{svg, AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString, IntoStaticStr};
|
||||
use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
|
||||
use ui_macros::DerivePathStr;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
traits::component_preview::{example_group, ComponentExample, ComponentPreview},
|
||||
Indicator,
|
||||
};
|
||||
register_components!(icon, [Icon, DecoratedIcon, IconWithIndicator]);
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum AnyIcon {
|
||||
@@ -501,24 +500,147 @@ impl RenderOnce for IconWithIndicator {
|
||||
}
|
||||
|
||||
impl ComponentPreview for Icon {
|
||||
fn examples() -> Vec<ComponentExampleGroup<Icon>> {
|
||||
let arrow_icons = vec![
|
||||
IconName::ArrowDown,
|
||||
IconName::ArrowLeft,
|
||||
IconName::ArrowRight,
|
||||
IconName::ArrowUp,
|
||||
IconName::ArrowCircle,
|
||||
];
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"Icons are visual symbols used to represent ideas, objects, or actions. They communicate messages at a glance, enhance aesthetic appeal, and are used in buttons, labels, and more."
|
||||
}
|
||||
|
||||
vec![example_group(
|
||||
"Arrow Icons",
|
||||
arrow_icons
|
||||
.into_iter()
|
||||
.map(|icon| {
|
||||
let name = format!("{:?}", icon).to_string();
|
||||
ComponentExample::new(name, Icon::new(icon))
|
||||
})
|
||||
.collect(),
|
||||
)]
|
||||
fn custom_example(cx: &WindowContext) -> impl Into<Option<AnyElement>> {
|
||||
let all_icons = IconName::iter().collect::<Vec<_>>();
|
||||
let chunk_size = 12;
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.children(all_icons.chunks(chunk_size).map(|chunk| {
|
||||
h_flex().gap_4().children(chunk.iter().map(|&icon| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.size_8()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Icon::new(icon))
|
||||
}))
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Sizes",
|
||||
vec![
|
||||
single_example("XSmall", Icon::new(IconName::Check).size(IconSize::XSmall)),
|
||||
single_example("Small", Icon::new(IconName::Check).size(IconSize::Small)),
|
||||
single_example("Medium", Icon::new(IconName::Check).size(IconSize::Medium)),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Colors",
|
||||
vec![
|
||||
single_example("Default", Icon::new(IconName::Check)),
|
||||
single_example("Accent", Icon::new(IconName::Check).color(Color::Accent)),
|
||||
single_example("Error", Icon::new(IconName::Check).color(Color::Error)),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for DecoratedIcon {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"DecoratedIcon adds visual enhancements to an icon, such as a strikethrough or an indicator dot."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Decorations",
|
||||
vec![
|
||||
single_example(
|
||||
"Strikethrough",
|
||||
DecoratedIcon::new(
|
||||
Icon::new(IconName::Bell),
|
||||
IconDecoration::Strikethrough,
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"IndicatorDot",
|
||||
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot),
|
||||
),
|
||||
single_example(
|
||||
"X",
|
||||
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::X),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Colors",
|
||||
vec![
|
||||
single_example(
|
||||
"Default",
|
||||
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot),
|
||||
),
|
||||
single_example(
|
||||
"Custom Color",
|
||||
DecoratedIcon::new(Icon::new(IconName::Bell), IconDecoration::IndicatorDot)
|
||||
.decoration_color(Color::Accent),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for IconWithIndicator {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"IconWithIndicator combines an icon with an indicator, useful for showing status or notifications."
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group_with_title(
|
||||
"Indicator Types",
|
||||
vec![
|
||||
single_example(
|
||||
"Dot",
|
||||
IconWithIndicator::new(Icon::new(IconName::Bell), Some(Indicator::dot())),
|
||||
),
|
||||
single_example(
|
||||
"Bar",
|
||||
IconWithIndicator::new(Icon::new(IconName::Bell), Some(Indicator::bar())),
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group_with_title(
|
||||
"Indicator Colors",
|
||||
vec![
|
||||
single_example(
|
||||
"Info",
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Bell),
|
||||
Some(Indicator::dot().color(Color::Info)),
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"Warning",
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Bell),
|
||||
Some(Indicator::dot().color(Color::Warning)),
|
||||
),
|
||||
),
|
||||
single_example(
|
||||
"Error",
|
||||
IconWithIndicator::new(
|
||||
Icon::new(IconName::Bell),
|
||||
Some(Indicator::dot().color(Color::Error)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::{prelude::*, AnyIcon};
|
||||
use crate::internal::prelude::*;
|
||||
use crate::AnyIcon;
|
||||
|
||||
register_components!(indicator, [Indicator]);
|
||||
|
||||
#[derive(Default)]
|
||||
enum IndicatorKind {
|
||||
@@ -91,7 +94,7 @@ impl ComponentPreview for Indicator {
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Types",
|
||||
vec![
|
||||
single_example("Dot", Indicator::dot().color(Color::Info)),
|
||||
@@ -102,7 +105,7 @@ impl ComponentPreview for Indicator {
|
||||
),
|
||||
],
|
||||
),
|
||||
example_group(
|
||||
example_group_with_title(
|
||||
"Examples",
|
||||
vec![
|
||||
single_example("Info", Indicator::dot().color(Color::Info)),
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#![allow(missing_docs)]
|
||||
mod avatar;
|
||||
mod button;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod icon;
|
||||
@@ -20,7 +19,6 @@ mod tool_strip;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use icon::*;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use gpui::{Render, ViewContext};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_flex, Checkbox};
|
||||
|
||||
pub struct CheckboxStory;
|
||||
|
||||
impl Render for CheckboxStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
Story::container()
|
||||
.child(Story::title_for::<Checkbox>())
|
||||
.child(Story::label("Default"))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-enabled", Selection::Unselected))
|
||||
.child(Checkbox::new(
|
||||
"checkbox-intermediate",
|
||||
Selection::Indeterminate,
|
||||
))
|
||||
.child(Checkbox::new("checkbox-selected", Selection::Selected)),
|
||||
)
|
||||
.child(Story::label("Disabled"))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
|
||||
.disabled(true),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("checkbox-disabled-selected", Selection::Selected)
|
||||
.disabled(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
241
crates/ui/src/components/table.rs
Normal file
241
crates/ui/src/components/table.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use crate::internal::prelude::*;
|
||||
|
||||
use crate::Indicator;
|
||||
use gpui::{div, AnyElement, FontWeight, IntoElement, Length};
|
||||
|
||||
/// A table component
|
||||
#[derive(IntoElement)]
|
||||
pub struct Table {
|
||||
column_headers: Vec<SharedString>,
|
||||
rows: Vec<Vec<TableCell>>,
|
||||
column_count: usize,
|
||||
striped: bool,
|
||||
width: Length,
|
||||
}
|
||||
|
||||
impl Table {
|
||||
/// Create a new table with a column count equal to the
|
||||
/// number of headers provided.
|
||||
pub fn new(headers: Vec<impl Into<SharedString>>) -> Self {
|
||||
let column_count = headers.len();
|
||||
|
||||
Table {
|
||||
column_headers: headers.into_iter().map(Into::into).collect(),
|
||||
column_count,
|
||||
rows: Vec::new(),
|
||||
striped: false,
|
||||
width: Length::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a row to the table.
|
||||
///
|
||||
/// The row must have the same number of columns as the table.
|
||||
pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self {
|
||||
if items.len() == self.column_count {
|
||||
self.rows.push(items.into_iter().map(Into::into).collect());
|
||||
} else {
|
||||
// TODO: Log error: Row length mismatch
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple rows to the table.
|
||||
///
|
||||
/// Each row must have the same number of columns as the table.
|
||||
/// Rows that don't match the column count are ignored.
|
||||
pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self {
|
||||
for row in rows {
|
||||
self = self.row(row);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn base_cell_style(cx: &WindowContext) -> Div {
|
||||
div()
|
||||
.px_1p5()
|
||||
.flex_1()
|
||||
.justify_start()
|
||||
.text_ui(cx)
|
||||
.whitespace_nowrap()
|
||||
.text_ellipsis()
|
||||
.overflow_hidden()
|
||||
}
|
||||
|
||||
/// Enables row striping.
|
||||
pub fn striped(mut self) -> Self {
|
||||
self.striped = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the table.
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Table {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let header = div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.column_headers.into_iter().map(|h| {
|
||||
Self::base_cell_style(cx)
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child(h)
|
||||
}));
|
||||
|
||||
let row_count = self.rows.len();
|
||||
let rows = self.rows.into_iter().enumerate().map(|(ix, row)| {
|
||||
let is_last = ix == row_count - 1;
|
||||
let bg = if ix % 2 == 1 && self.striped {
|
||||
Some(cx.theme().colors().text.opacity(0.05))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.when_some(bg, |row, bg| row.bg(bg))
|
||||
.when(!is_last, |row| {
|
||||
row.border_b_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.children(row.into_iter().map(|cell| match cell {
|
||||
TableCell::String(s) => Self::base_cell_style(cx).child(s),
|
||||
TableCell::Element(e) => Self::base_cell_style(cx).child(e),
|
||||
}))
|
||||
});
|
||||
|
||||
div()
|
||||
.w(self.width)
|
||||
.overflow_hidden()
|
||||
.child(header)
|
||||
.children(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a cell in a table.
|
||||
pub enum TableCell {
|
||||
/// A cell containing a string value.
|
||||
String(SharedString),
|
||||
/// A cell containing a UI element.
|
||||
Element(AnyElement),
|
||||
}
|
||||
|
||||
/// Creates a `TableCell` containing a string value.
|
||||
pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
|
||||
TableCell::String(s.into())
|
||||
}
|
||||
|
||||
/// Creates a `TableCell` containing an element.
|
||||
pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
|
||||
TableCell::Element(e.into())
|
||||
}
|
||||
|
||||
impl<E> From<E> for TableCell
|
||||
where
|
||||
E: Into<SharedString>,
|
||||
{
|
||||
fn from(e: E) -> Self {
|
||||
TableCell::String(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentPreview for Table {
|
||||
fn description() -> impl Into<Option<&'static str>> {
|
||||
"Used for showing tabular data. Tables may show both text and elements in their cells."
|
||||
}
|
||||
|
||||
fn example_label_side() -> ExampleLabelSide {
|
||||
ExampleLabelSide::Top
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>> {
|
||||
vec![
|
||||
example_group(vec![
|
||||
single_example(
|
||||
"Simple Table",
|
||||
Table::new(vec!["Name", "Age", "City"])
|
||||
.width(px(400.))
|
||||
.row(vec!["Alice", "28", "New York"])
|
||||
.row(vec!["Bob", "32", "San Francisco"])
|
||||
.row(vec!["Charlie", "25", "London"]),
|
||||
),
|
||||
single_example(
|
||||
"Two Column Table",
|
||||
Table::new(vec!["Category", "Value"])
|
||||
.width(px(300.))
|
||||
.row(vec!["Revenue", "$100,000"])
|
||||
.row(vec!["Expenses", "$75,000"])
|
||||
.row(vec!["Profit", "$25,000"]),
|
||||
),
|
||||
]),
|
||||
example_group(vec![single_example(
|
||||
"Striped Table",
|
||||
Table::new(vec!["Product", "Price", "Stock"])
|
||||
.width(px(600.))
|
||||
.striped()
|
||||
.row(vec!["Laptop", "$999", "In Stock"])
|
||||
.row(vec!["Phone", "$599", "Low Stock"])
|
||||
.row(vec!["Tablet", "$399", "Out of Stock"])
|
||||
.row(vec!["Headphones", "$199", "In Stock"]),
|
||||
)]),
|
||||
example_group_with_title(
|
||||
"Mixed Content Table",
|
||||
vec![single_example(
|
||||
"Table with Elements",
|
||||
Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
|
||||
.width(px(840.))
|
||||
.row(vec![
|
||||
element_cell(Indicator::dot().color(Color::Success).into_any_element()),
|
||||
string_cell("Project A"),
|
||||
string_cell("High"),
|
||||
string_cell("2023-12-31"),
|
||||
element_cell(
|
||||
Button::new("view_a", "View")
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.into_any_element(),
|
||||
),
|
||||
])
|
||||
.row(vec![
|
||||
element_cell(Indicator::dot().color(Color::Warning).into_any_element()),
|
||||
string_cell("Project B"),
|
||||
string_cell("Medium"),
|
||||
string_cell("2024-03-15"),
|
||||
element_cell(
|
||||
Button::new("view_b", "View")
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.into_any_element(),
|
||||
),
|
||||
])
|
||||
.row(vec![
|
||||
element_cell(Indicator::dot().color(Color::Error).into_any_element()),
|
||||
string_cell("Project C"),
|
||||
string_cell("Low"),
|
||||
string_cell("2024-06-30"),
|
||||
element_cell(
|
||||
Button::new("view_c", "View")
|
||||
.style(ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.into_any_element(),
|
||||
),
|
||||
]),
|
||||
)],
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ pub use gpui::{
|
||||
|
||||
pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle, StyledTypography, TextSize};
|
||||
pub use crate::traits::clickable::*;
|
||||
pub use crate::traits::component_preview::*;
|
||||
pub use crate::traits::disableable::*;
|
||||
pub use crate::traits::fixed::*;
|
||||
pub use crate::traits::selectable::*;
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
#![allow(missing_docs)]
|
||||
use crate::prelude::*;
|
||||
use gpui::{AnyElement, SharedString};
|
||||
|
||||
/// Which side of the preview to show labels on
|
||||
#[allow(unused)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ExampleLabelSide {
|
||||
/// Left side
|
||||
Left,
|
||||
/// Right side
|
||||
Right,
|
||||
#[default]
|
||||
/// Top side
|
||||
Top,
|
||||
/// Bottom side
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// Implement this trait to enable rich UI previews with metadata in the Theme Preview tool.
|
||||
pub trait ComponentPreview: IntoElement {
|
||||
fn title() -> &'static str {
|
||||
@@ -12,8 +26,16 @@ pub trait ComponentPreview: IntoElement {
|
||||
None
|
||||
}
|
||||
|
||||
fn example_label_side() -> ExampleLabelSide {
|
||||
ExampleLabelSide::default()
|
||||
}
|
||||
|
||||
fn examples() -> Vec<ComponentExampleGroup<Self>>;
|
||||
|
||||
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
fn component_previews() -> Vec<AnyElement> {
|
||||
Self::examples()
|
||||
.into_iter()
|
||||
@@ -29,7 +51,8 @@ pub trait ComponentPreview: IntoElement {
|
||||
let description = Self::description().into();
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
@@ -55,16 +78,23 @@ pub trait ComponentPreview: IntoElement {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(Self::custom_example(cx).into(), |this, custom_example| {
|
||||
this.child(custom_example)
|
||||
})
|
||||
.children(Self::component_previews())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(group.title).size(LabelSize::Small))
|
||||
.gap_6()
|
||||
.when(group.grow, |this| this.w_full().flex_1())
|
||||
.when_some(group.title, |this, title| {
|
||||
this.child(Label::new(title).size(LabelSize::Small))
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.children(group.examples.into_iter().map(Self::render_example))
|
||||
.into_any_element(),
|
||||
@@ -73,8 +103,17 @@ pub trait ComponentPreview: IntoElement {
|
||||
}
|
||||
|
||||
fn render_example(example: ComponentExample<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
let base = div().flex();
|
||||
|
||||
let base = match Self::example_label_side() {
|
||||
ExampleLabelSide::Right => base.flex_row(),
|
||||
ExampleLabelSide::Left => base.flex_row_reverse(),
|
||||
ExampleLabelSide::Bottom => base.flex_col(),
|
||||
ExampleLabelSide::Top => base.flex_col_reverse(),
|
||||
};
|
||||
|
||||
base.gap_1()
|
||||
.when(example.grow, |this| this.flex_1())
|
||||
.child(example.element)
|
||||
.child(
|
||||
Label::new(example.variant_name)
|
||||
@@ -89,6 +128,7 @@ pub trait ComponentPreview: IntoElement {
|
||||
pub struct ComponentExample<T> {
|
||||
variant_name: SharedString,
|
||||
element: T,
|
||||
grow: bool,
|
||||
}
|
||||
|
||||
impl<T> ComponentExample<T> {
|
||||
@@ -97,24 +137,48 @@ impl<T> ComponentExample<T> {
|
||||
Self {
|
||||
variant_name: variant_name.into(),
|
||||
element: example,
|
||||
grow: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the example to grow to fill the available horizontal space.
|
||||
pub fn grow(mut self) -> Self {
|
||||
self.grow = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of component examples.
|
||||
pub struct ComponentExampleGroup<T> {
|
||||
pub title: SharedString,
|
||||
pub title: Option<SharedString>,
|
||||
pub examples: Vec<ComponentExample<T>>,
|
||||
pub grow: bool,
|
||||
}
|
||||
|
||||
impl<T> ComponentExampleGroup<T> {
|
||||
/// Create a new group of examples with the given title.
|
||||
pub fn new(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
|
||||
pub fn new(examples: Vec<ComponentExample<T>>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
title: None,
|
||||
examples,
|
||||
grow: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new group of examples with the given title.
|
||||
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
|
||||
Self {
|
||||
title: Some(title.into()),
|
||||
examples,
|
||||
grow: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the group to grow to fill the available horizontal space.
|
||||
pub fn grow(mut self) -> Self {
|
||||
self.grow = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a single example
|
||||
@@ -122,10 +186,15 @@ pub fn single_example<T>(variant_name: impl Into<SharedString>, example: T) -> C
|
||||
ComponentExample::new(variant_name, example)
|
||||
}
|
||||
|
||||
/// Create a group of examples
|
||||
pub fn example_group<T>(
|
||||
/// Create a group of examples without a title
|
||||
pub fn example_group<T>(examples: Vec<ComponentExample<T>>) -> ComponentExampleGroup<T> {
|
||||
ComponentExampleGroup::new(examples)
|
||||
}
|
||||
|
||||
/// Create a group of examples with a title
|
||||
pub fn example_group_with_title<T>(
|
||||
title: impl Into<SharedString>,
|
||||
examples: Vec<ComponentExample<T>>,
|
||||
) -> ComponentExampleGroup<T> {
|
||||
ComponentExampleGroup::new(title, examples)
|
||||
ComponentExampleGroup::with_title(title, examples)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//! - [`ui_input`] - the single line input component
|
||||
//!
|
||||
|
||||
mod component_registry;
|
||||
mod components;
|
||||
pub mod prelude;
|
||||
mod styles;
|
||||
@@ -17,6 +18,17 @@ mod tests;
|
||||
mod traits;
|
||||
pub mod utils;
|
||||
|
||||
pub use component_registry::{get_all_component_previews, init_component_registry};
|
||||
pub use components::*;
|
||||
pub use prelude::*;
|
||||
pub use styles::*;
|
||||
|
||||
pub(crate) mod internal {
|
||||
/// A crate-internal extension of the prelude, used to expose the crate-specific
|
||||
/// needs like the component registry or component-preview types
|
||||
pub mod prelude {
|
||||
pub use crate::prelude::*;
|
||||
pub use crate::register_components;
|
||||
pub use crate::traits::component_preview::*;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#![allow(unused, dead_code)]
|
||||
use gpui::{actions, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla};
|
||||
use gpui::{actions, hsla, AnyElement, AppContext, EventEmitter, FocusHandle, FocusableView, Hsla};
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::all_theme_colors;
|
||||
use ui::{
|
||||
prelude::*, utils::calculate_contrast_ratio, AudioStatus, Availability, Avatar,
|
||||
AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, Checkbox, ElevationIndex,
|
||||
Facepile, Indicator, TintColor, Tooltip,
|
||||
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
|
||||
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
|
||||
Checkbox, CheckboxWithLabel, ElevationIndex, Facepile, Indicator, Table, TintColor, Tooltip,
|
||||
};
|
||||
|
||||
use crate::{Item, Workspace};
|
||||
@@ -502,20 +502,24 @@ impl ThemePreview {
|
||||
}
|
||||
|
||||
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
let layer = ElevationIndex::Surface;
|
||||
let all_previews = ui::get_all_component_previews();
|
||||
|
||||
v_flex()
|
||||
.id("theme-preview-components")
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.child(Checkbox::render_component_previews(cx))
|
||||
.child(Facepile::render_component_previews(cx))
|
||||
.child(Button::render_component_previews(cx))
|
||||
.child(Indicator::render_component_previews(cx))
|
||||
.child(Icon::render_component_previews(cx))
|
||||
.child(self.render_avatars(cx))
|
||||
.child(self.render_buttons(layer, cx))
|
||||
.gap_4()
|
||||
.children(all_previews.iter().map(|(scope, components)| {
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(Headline::new(*scope).size(HeadlineSize::Small))
|
||||
.children(components.iter().map(|(name, preview_fn)| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(*name).size(LabelSize::Small).color(Color::Muted))
|
||||
.child(preview_fn(cx))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_page_nav(&self, cx: &ViewContext<Self>) -> impl IntoElement {
|
||||
|
||||
@@ -4547,6 +4547,11 @@ impl Workspace {
|
||||
.children(leader_border),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
|
||||
let window = cx.window_handle().downcast::<Workspace>()?;
|
||||
cx.read_window(&window, |workspace, _| workspace).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn leader_border_for_pane(
|
||||
@@ -4730,6 +4735,7 @@ impl Render for Workspace {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let mut context = KeyContext::new_with_defaults();
|
||||
context.add("Workspace");
|
||||
context.set("keyboard_layout", cx.keyboard_layout().clone());
|
||||
let centered_layout = self.centered_layout
|
||||
&& self.center.panes().len() == 1
|
||||
&& self.active_item(cx).is_some();
|
||||
|
||||
@@ -35,6 +35,7 @@ collab_ui.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
context_servers.workspace = true
|
||||
copilot.workspace = true
|
||||
db.workspace = true
|
||||
diagnostics.workspace = true
|
||||
|
||||
@@ -13,6 +13,7 @@ use clap::{command, Parser};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use client::{parse_zed_link, Client, ProxySettings, UserStore};
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use context_servers::ContextServerFactoryRegistry;
|
||||
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
|
||||
use editor::Editor;
|
||||
use env_logger::Builder;
|
||||
@@ -411,6 +412,7 @@ fn main() {
|
||||
IndexedDocsRegistry::global(cx),
|
||||
SnippetRegistry::global(cx),
|
||||
app_state.languages.clone(),
|
||||
ContextServerFactoryRegistry::global(cx),
|
||||
cx,
|
||||
);
|
||||
extension_host::init(
|
||||
|
||||
@@ -92,6 +92,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
if ReleaseChannel::global(cx) == ReleaseChannel::Dev {
|
||||
cx.on_action(test_panic);
|
||||
}
|
||||
|
||||
ui::init_component_registry();
|
||||
}
|
||||
|
||||
pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) -> WindowOptions {
|
||||
@@ -808,6 +810,7 @@ pub fn handle_keymap_file_changes(
|
||||
VimModeSetting::register(cx);
|
||||
|
||||
let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
|
||||
let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
|
||||
let mut old_base_keymap = *BaseKeymap::get_global(cx);
|
||||
let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
@@ -822,6 +825,11 @@ pub fn handle_keymap_file_changes(
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_keyboard_layout_change(move |_| {
|
||||
keyboard_layout_tx.unbounded_send(()).ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
load_default_keymap(cx);
|
||||
|
||||
cx.spawn(move |cx| async move {
|
||||
@@ -829,6 +837,7 @@ pub fn handle_keymap_file_changes(
|
||||
loop {
|
||||
select_biased! {
|
||||
_ = base_keymap_rx.next() => {}
|
||||
_ = keyboard_layout_rx.next() => {}
|
||||
user_keymap_content = user_keymap_file_rx.next() => {
|
||||
if let Some(user_keymap_content) = user_keymap_content {
|
||||
match KeymapFile::parse(&user_keymap_content) {
|
||||
@@ -854,7 +863,7 @@ fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) {
|
||||
load_default_keymap(cx);
|
||||
keymap_content.clone().add_to_cx(cx).log_err();
|
||||
cx.set_menus(app_menus());
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)])
|
||||
cx.set_dock_menu(vec![MenuItem::action("New Window", workspace::NewWindow)]);
|
||||
}
|
||||
|
||||
pub fn load_default_keymap(cx: &mut AppContext) {
|
||||
|
||||
@@ -23,6 +23,8 @@ The file contains a JSON array of objects with `"bindings"`. If no `"context"` i
|
||||
|
||||
Within each binding section a [key sequence](#keybinding-syntax) is mapped to an [action](#actions). If conflicts are detected they are resolved as [described below](#precedence).
|
||||
|
||||
If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_layout_keys` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information.
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
@@ -58,7 +60,7 @@ Each key press is a sequence of modifiers followed by a key. The modifiers are:
|
||||
- `shift-` The shift key
|
||||
- `fn-` The function key
|
||||
|
||||
The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`), or any named key (`tab`, `f1`, `shift`, or `cmd`).
|
||||
The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`), or any named key (`tab`, `f1`, `shift`, or `cmd`). If you are using a non-Latin layout (e.g. Cyrillic), you can bind either to the cyrillic character, or the latin character that that key generates with `cmd` pressed.
|
||||
|
||||
A few examples:
|
||||
|
||||
@@ -89,7 +91,7 @@ For example:
|
||||
|
||||
```
|
||||
# in an editor, it might look like this:
|
||||
Workspace os=macos
|
||||
Workspace os=macos keyboard_layout=com.apple.keylayout.QWERTY
|
||||
Pane
|
||||
Editor mode=full extension=md inline_completion vim_mode=insert
|
||||
|
||||
@@ -130,6 +132,38 @@ The other kind of conflict that arises is when you have two bindings, one of whi
|
||||
|
||||
When this happens, and both bindings are active in the current context, Zed will wait for 1 second after you tupe `ctrl-w` to se if you're about to type `left`. If you don't type anything, or if you type a different key, then `DeleteToNextWordEnd` will be triggered. If you do, then `DeleteToEndOfLine` will be triggered.
|
||||
|
||||
### Non-QWERTY keyboards
|
||||
|
||||
As of Zed 0.162.0, Zed has some support for non-QWERTY keyboards on macOS. Better support for non-QWERTY keyboards on Linux is planned.
|
||||
|
||||
There are roughly three categories of keyboard to consider:
|
||||
|
||||
Keyboards that support full ASCII (QWERTY, DVORAK, COLEMAK, etc.). On these keyboards bindings are resolved based on the character that would be generated by the key. So to type `cmd-[`, find the key labelled `[` and press it with command.
|
||||
|
||||
Keyboards that are mostly non-ASCII, but support full ASCII when the command key is pressed. For example Cyrillic keyboards, Armenian, Hebrew, etc. On these keyboards bindings are resolved based on the character that would be generated by typing the key with command pressed. So to type `ctrl-a`, find the key that generates `cmd-a`. For these keyboards, keyboard shortcuts are displayed in the app using their ASCII equivalents. If the ASCII-equivalents are not printed on your keyboard, you can use the macOS keyboard viewer and holding down the `cmd` key to find things (though often the ASCII equivalents are in a QWERTY layout).
|
||||
|
||||
Finally keyboards that support extended Latin alphabets (usually ISO keyboards) require the most support. For example French AZERTY, German QWERTZ, etc. On these keyboards it is often not possible to type the entire ASCII range without option. To ensure that shortcuts _can_ be typed without option, keyboard shortcuts are mapped to "key equivalents" in the same way as [macOS](). This mapping is defined per layout, and is a compromise between leaving keyboard shortcuts triggered by the same character they are defined with, keeping shortcuts in the same place as a QWERTY layout, and moving shortcuts out of the way of system shortcuts.
|
||||
|
||||
For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key.
|
||||
|
||||
If you are defining shortcuts in your personal keymap, you can opt-out of the key equivalent mapping by setting `use_layout_keys` to `true` in your keymap:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl->": "editor::Indent" // parsed as ctrl-: when a German QWERTZ keyboard is active
|
||||
}
|
||||
},
|
||||
{
|
||||
"use_layout_keys": true,
|
||||
"bindings": {
|
||||
"ctrl->": "editor::Indent" // remains ctrl-> when a German QWERTZ keyboard is active
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Tips and tricks
|
||||
|
||||
### Disabling a binding
|
||||
|
||||
Reference in New Issue
Block a user