Skip to content

Commit f8dc237

Browse files
feat(editor): command reference and inline docs in the console
The builder listed command names but said nothing about them. It now reads the command manifest, the one source the bindings are generated from, so the docs cannot drift. The selected command shows its signature, each field input is labeled with its type and carries a tooltip, and a Reference toggle opens a searchable list of every command and its signature that fills the builder when picked.
1 parent 071a2f0 commit f8dc237

2 files changed

Lines changed: 178 additions & 8 deletions

File tree

apps/editor/public/styles.css

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,70 @@ input[type="range"] {
14571457
color: var(--accent);
14581458
}
14591459

1460+
.console-header-actions {
1461+
display: flex;
1462+
gap: 6px;
1463+
}
1464+
1465+
.console-clear.console-clear-active {
1466+
border-color: var(--accent);
1467+
color: var(--accent);
1468+
}
1469+
1470+
.console-signature {
1471+
font-family: ui-monospace, "Cascadia Code", Menlo, monospace;
1472+
font-size: 11px;
1473+
color: var(--text);
1474+
opacity: 0.7;
1475+
padding: 1px 2px 4px;
1476+
word-break: break-word;
1477+
}
1478+
1479+
.console-field-type {
1480+
font-size: 9.5px;
1481+
font-weight: 400;
1482+
letter-spacing: 0;
1483+
text-transform: none;
1484+
opacity: 0.7;
1485+
}
1486+
1487+
.console-reference {
1488+
display: flex;
1489+
flex-direction: column;
1490+
gap: 6px;
1491+
padding: 10px 12px;
1492+
border-bottom: 1px solid var(--panel-border);
1493+
max-height: 42vh;
1494+
min-height: 0;
1495+
}
1496+
1497+
.console-reference-list {
1498+
overflow-y: auto;
1499+
display: flex;
1500+
flex-direction: column;
1501+
gap: 1px;
1502+
}
1503+
1504+
.console-reference-item {
1505+
text-align: left;
1506+
font-family: ui-monospace, "Cascadia Code", Menlo, monospace;
1507+
font-size: 11px;
1508+
padding: 4px 8px;
1509+
border-radius: 5px;
1510+
border: none;
1511+
background: transparent;
1512+
color: var(--text);
1513+
cursor: pointer;
1514+
white-space: nowrap;
1515+
overflow: hidden;
1516+
text-overflow: ellipsis;
1517+
}
1518+
1519+
.console-reference-item:hover {
1520+
background: color-mix(in srgb, var(--accent) 14%, transparent);
1521+
color: var(--accent);
1522+
}
1523+
14601524
.console-log {
14611525
flex: 1;
14621526
overflow-y: auto;

apps/editor/src/components/console.rs

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,44 @@ use leptos::prelude::*;
44
use nightshade_api::editor::protocol::{
55
ClientMessage, CommandLogEntry, CommandLogKind, EditorAction,
66
};
7+
use nightshade_api::prelude::CommandSpec;
78
use serde_json::{Value, json};
89

910
use crate::bridge::{Bridge, act, send};
1011
use crate::state::{EditorState, InspectorTab};
1112

13+
/// The command reference, keyed by name, from the one manifest the bindings are
14+
/// generated from, so the docs cannot drift from the commands.
15+
fn command_docs() -> HashMap<String, CommandSpec> {
16+
nightshade_api::prelude::command_manifest()
17+
.into_iter()
18+
.map(|spec| (spec.name.to_string(), spec))
19+
.collect()
20+
}
21+
22+
/// A command's one-line signature: name, fields with their types, and reply.
23+
fn signature(spec: &CommandSpec) -> String {
24+
let fields = spec
25+
.fields
26+
.iter()
27+
.map(|field| format!("{}: {}", field.name, field.type_name))
28+
.collect::<Vec<_>>()
29+
.join(", ");
30+
if spec.reply == "none" {
31+
format!("{}({fields})", spec.name)
32+
} else {
33+
format!("{}({fields}) \u{2192} {}", spec.name, spec.reply)
34+
}
35+
}
36+
37+
/// A command's field types, keyed by field name, for the per-field tooltips.
38+
fn field_types(spec: &CommandSpec) -> HashMap<String, String> {
39+
spec.fields
40+
.iter()
41+
.map(|field| (field.name.to_string(), field.type_name.to_string()))
42+
.collect()
43+
}
44+
1245
/// Every command variant the api exposes, as (name, arguments schema), read
1346
/// from the one source: `command_schema`. The builder lists all of them, so it
1447
/// always matches the `Command` enum instead of a hand-picked subset.
@@ -150,6 +183,7 @@ fn defaults_for(args: &Value, selected: Option<u32>) -> HashMap<String, String>
150183
fn field_input(
151184
name: String,
152185
schema: Value,
186+
type_name: String,
153187
fields: RwSignal<HashMap<String, String>>,
154188
state: EditorState,
155189
) -> AnyView {
@@ -171,7 +205,7 @@ fn field_input(
171205
};
172206
return view! {
173207
<div class="console-field">
174-
<label class="console-field-label">{name.clone()}</label>
208+
<label class="console-field-label" title=type_name.clone()>{name.clone()} <span class="console-field-type">{type_name.clone()}</span></label>
175209
<div class="console-field-row">
176210
<input
177211
class="console-input"
@@ -201,7 +235,7 @@ fn field_input(
201235
if let Some(options) = enum_options(&schema) {
202236
return view! {
203237
<div class="console-field">
204-
<label class="console-field-label">{name.clone()}</label>
238+
<label class="console-field-label" title=type_name.clone()>{name.clone()} <span class="console-field-type">{type_name.clone()}</span></label>
205239
<select
206240
class="console-select"
207241
prop:value=read
@@ -220,7 +254,7 @@ fn field_input(
220254
if schema.get("type").and_then(Value::as_str) == Some("boolean") {
221255
return view! {
222256
<div class="console-field console-field-inline">
223-
<label class="console-field-label">{name.clone()}</label>
257+
<label class="console-field-label" title=type_name.clone()>{name.clone()} <span class="console-field-type">{type_name.clone()}</span></label>
224258
<input
225259
type="checkbox"
226260
prop:checked=move || read() == "true"
@@ -242,7 +276,7 @@ fn field_input(
242276
.unwrap_or_default();
243277
view! {
244278
<div class="console-field">
245-
<label class="console-field-label">{name.clone()}</label>
279+
<label class="console-field-label" title=type_name.clone()>{name.clone()} <span class="console-field-type">{type_name.clone()}</span></label>
246280
<input
247281
class="console-input"
248282
r#type=if numeric { "number" } else { "text" }
@@ -263,6 +297,9 @@ pub fn Console(
263297
state: EditorState,
264298
) -> impl IntoView {
265299
let catalog = StoredValue::new(command_catalog());
300+
let docs = StoredValue::new(command_docs());
301+
let reference_open = RwSignal::new(false);
302+
let reference_filter = RwSignal::new(String::new());
266303
let names: Vec<String> =
267304
catalog.with_value(|c| c.iter().map(|(name, _)| name.clone()).collect());
268305
let first = names.first().cloned().unwrap_or_default();
@@ -328,10 +365,68 @@ pub fn Console(
328365
>
329366
<div class="console-header">
330367
<span class="console-title">"Commands and Events"</span>
331-
<button class="console-clear" on:click=move |_| state.command_log.set(Vec::new())>
332-
"Clear"
333-
</button>
368+
<div class="console-header-actions">
369+
<button
370+
class=move || {
371+
if reference_open.get() {
372+
"console-clear console-clear-active"
373+
} else {
374+
"console-clear"
375+
}
376+
}
377+
title="Browse every command and its signature"
378+
on:click=move |_| reference_open.update(|open| *open = !*open)
379+
>
380+
"Reference"
381+
</button>
382+
<button
383+
class="console-clear"
384+
on:click=move |_| state.command_log.set(Vec::new())
385+
>
386+
"Clear"
387+
</button>
388+
</div>
334389
</div>
390+
<Show when=move || reference_open.get() fallback=|| ()>
391+
<div class="console-reference">
392+
<input
393+
class="console-input"
394+
placeholder="filter commands"
395+
prop:value=move || reference_filter.get()
396+
on:input=move |event| reference_filter.set(event_target_value(&event))
397+
/>
398+
<div class="console-reference-list">
399+
{move || {
400+
let filter = reference_filter.get().to_lowercase();
401+
docs.with_value(|map| {
402+
let mut specs: Vec<CommandSpec> = map
403+
.values()
404+
.filter(|spec| spec.name.to_lowercase().contains(&filter))
405+
.cloned()
406+
.collect();
407+
specs.sort_by_key(|spec| spec.name);
408+
specs
409+
.into_iter()
410+
.map(|spec| {
411+
let name = spec.name.to_string();
412+
view! {
413+
<button
414+
class="console-reference-item"
415+
on:click=move |_| {
416+
choose(name.clone());
417+
reference_open.set(false);
418+
}
419+
>
420+
{signature(&spec)}
421+
</button>
422+
}
423+
})
424+
.collect_view()
425+
})
426+
}}
427+
</div>
428+
</div>
429+
</Show>
335430
<div class="console-log">
336431
<Show when=move || state.command_log.with(|log| log.is_empty()) fallback=|| ()>
337432
<div class="console-empty">
@@ -394,13 +489,24 @@ pub fn Console(
394489
.map(|name| view! { <option value=name.clone()>{name.clone()}</option> })
395490
.collect_view()}
396491
</select>
492+
<div class="console-signature">
493+
{move || {
494+
docs.with_value(|map| map.get(&selected.get()).map(signature)).unwrap_or_default()
495+
}}
496+
</div>
397497
<div class="console-fields">
398498
{move || {
399499
let name = selected.get();
500+
let types = docs
501+
.with_value(|map| map.get(&name).map(field_types))
502+
.unwrap_or_default();
400503
match args_for(&name) {
401504
Some(args) => variant_fields(&args)
402505
.into_iter()
403-
.map(|(field, schema)| field_input(field, schema, fields, state))
506+
.map(|(field, schema)| {
507+
let type_name = types.get(&field).cloned().unwrap_or_default();
508+
field_input(field, schema, type_name, fields, state)
509+
})
404510
.collect_view()
405511
.into_any(),
406512
None => ().into_any(),

0 commit comments

Comments
 (0)