docs/internal/settings-lsp-language-dialogs.md
Scope. End-user evaluation of the dialogs reached by: Open Settings → General → Languages → <lang> and Open Settings → General → Lsp → <lang> → <server>.
The base settings page (Open Settings, top level) is reasonably usable.
The nested dialogs are not. This review treats those nested dialogs as the
unit under test, from the perspective of a user who has never seen the
config schema and expects something close to "a web form".
Two independent walkthroughs, merged here.
Session A — launched the editor in tmux, ran Open Settings,
navigated keyboard‑only to:
hyprlang (language entry editor)python → pylsp (LSP map entry → server editor)astro → [+] Add new (brand‑new server)For each, tried focus cycling (Tab, ↑↓), entering and leaving edit mode on text/number/boolean/JSON/list fields, adding & removing list items, saving, cancelling, and using the on-screen help row.
Session B — separate user adding a second server
(pyright-langserver) under the existing python key:
Settings → search "python" → open python LSP value → Add new →
fill in Command/Name/Args/Root Markers → Save. Focus was on
keyboard model (Esc, Tab, Enter), focus traps in list rows, and
whether the dirty-state indicator and the collapsed LSP table row
reflect the saved state.
The two sessions agree on most findings; where they diverge the divergence is preserved (see F4 / F21 on commit semantics).
The dialogs work as data dumps of the underlying struct, not as forms a human is meant to fill in. Three issues compound to make multi-list editing actively hostile:
[+] Add new slot of a list traps the keyboard
(F19) — ↓ / Tab / → all do nothing from there; the only escape
paths are Ctrl+S (which saves and closes the whole dialog) or
Esc (which destroys all work).Together, (2) and (3) force a save → re-open → save → re-open workflow just to fill multiple list fields in one server config. Almost every other complaint below stacks on top of these three.
Severity tags below: 🟥 blocks the task · 🟧 forces guessing · 🟨 polish.
What the user sees after pressing Enter on the python row in the Lsp
list (matches the screenshot in the task):
╭ Edit Value ─────────────────────────────────────────╮
│ Key:python │
│ ─────────────────────────────────────────────────── │
│ ● Value: │
│> → pylsp [x] │
│ [+] Add new │
│ │
│ ... ~30 blank lines ... │
│ │
│ [ Save ] [ Delete ] [ Cancel ] │
╰─────────────────────────────────────────────────────╯
Problems a first-time user hits in the first 5 seconds:
→ pylsp is centred in the row. There is no column header,
no label "Server" / "Command". The → glyph is unexplained.python; the label adds zero info.Map<String, Vec<LspServer>>. Three nesting levels (map row → list → server) are
exposed even though 95% of languages have exactly one server. The user
has to drill down twice for what should be one form.Steps: navigate to Command : [pylsp], press Enter, type
test.
Observed: the text appears inside the field — but nothing about the field changed when Enter was pressed. No caret, no border colour change, no "editing" badge. The user can:
A field that is being focused for navigation and a field that is accepting characters are visually identical.
Reinforced by session B: typing pyright-langserver directly on the
Command field's first focus, with no prior Enter, immediately wrote
the characters into the field. The footer says Enter:Edit, implying
a modal text input — but the input is in fact modeless. Either the
footer is wrong or the controls are. A new user reading the legend
will press Enter expecting to enter edit mode and will instead trigger
"commit / add another row" on list controls (see F21).
Enabled : [ ✓ ACTIVE ]
Auto Start : [ ]
Name : [ ]
[ ] (unchecked) and [ ] (empty
text) differ only in width. [ ✓ ACTIVE ] is shouty and asymmetric
with the unchecked state. A first-time user does not know whether
[ ] is a checkbox they can toggle or a string they should type
into.
Suggestion: render as [ ] / [x], or ( ) Off / (•) On, or
Enabled: ☑ on. The label "ACTIVE" reads like the system state of the
field, not the value.
In the python‑pylsp dialog, with the cursor on Args: (empty list
header), pressing ↓ once jumps past the [+] Add new line into the
next top-level field. With the cursor on Root Markers: (populated
list), the same ↓ stops on each item. So the same key has different
"skip" semantics depending on whether the list is empty.
Result: after adding the first item to a list, the user's keyboard map silently changes.
[+] Add new is a hidden state machineSteps: cursor onto an [+] Add new line, press Enter (for primitive
lists like Args: / Root Markers:).
Observed: the line transforms in-place into
[ ] [+] — a draft input plus a separate confirm
button. There is no "Adding new item..." caption, no helper text, no
visible focus on the new input. The list of existing items above is
unchanged so the eye doesn't catch the shift. Esc collapses the row
back, but again silently.
For struct lists ([+] Add new under Lsp.python.Value), Enter instead
pops a brand‑new "Add Item" dialog on top of the open dialog. Same
verb, two different mechanisms, no preview.
The footer says Tab:Fields/Buttons. In practice, Tab moves focus to
the Save/Delete/Cancel row but those buttons never paint a focused
state. The > cursor disappears from the field list and nothing else
lights up. The user cannot tell if Tab "did" anything until they press
Enter and something happens.
Esc closes the current dialog level immediately. If the user typed into Command, toggled Enabled, added a Root Marker, then pressed Esc by reflex (e.g. to dismiss a popup that wasn't there), all of it is gone with no "Discard changes?" prompt.
Worse, session B showed that Esc on an in-progress list row jumps straight past the row to dismiss the enclosing "Edit Item" dialog and silently discards every change to every field — Command, Name, Args, Root Markers, all of it. A user's natural mental model is "Esc cancels the smallest current thing": the row I'm editing, not the whole form. Here Esc cancels the largest enclosing thing instead.
Expected: Esc on an in-progress text row commits-or-reverts only the row; Esc on a clean dialog closes the dialog; Esc on a dirty dialog prompts.
The selected field shows > at column 0. Modified fields show ●
at column 1. Both glyphs are the same width and similar weight, and
they sit next to each other unlabelled:
>● Command : [pylsp] ← focused AND modified
● Name : [ ] ← unfocused, modified
Enabled : [ ✓ ACTIVE ] ← unfocused, unmodified
> Enabled : [ ✓ ACTIVE ] ← focused, unmodified
There is no legend anywhere. A user cannot guess that ● means
"differs from default" — they will assume it's a bullet for an enabled
field, or a focus dot, or noise.
The "Edit Value" / "Edit Item" / "Add Item" dialogs render as smaller floating panels with the rest of the settings UI fully visible behind them — including the unchanged language list on the left. There is no dim/overlay, so the eye cannot tell at a glance which surface is active, and the panel borders fight the borders of the page underneath.
nullInitialization Options:
│null
Only Features:
│null
Except Features:
│null
Process Limits:
│{
│ "max_memory_percent": 50,
│ "max_cpu_percent": 90,
│ "enabled": true
│}
What the user has to do to set, say, a pylsp initialization option:
null means "no value set" (not "the JSON literal
null").{}, no way
to expand the area.In testing, typing { then } produced the broken display
│{ / │{null — the placeholder text "null" was treated as content
and the new characters were inserted alongside it. The user has no
indication this is a malformed state.
Process Limits should be three fieldsIt already has a known shape: max_memory_percent (int %),
max_cpu_percent (int %), enabled (bool). Exposing it as raw JSON
makes the user re-type JSON syntax to change a number.
Tab Size : [ 0 ] [-] [+]
The [-] and [+] buttons are small, not obviously clickable, not
documented in the footer, and redundant given the user can type the
number. Click targets in a TUI are also unreliable.
● Command : [pylsp]
● Name : [ ]
"Name" vs "Command" is ambiguous — both look like identifiers. There is no helper text describing that Name is a display label (or whatever it is). The user will either fill it in by guessing or leave it blank forever.
Half the fields in the LSP server editor (Env, Language Id Overrides,
Initialization Options, Only Features, Except Features, Process Limits)
are below an ── Advanced ── line. The line is a static separator —
the user cannot collapse it. So the dialog stays long and intimidating
even when nothing in Advanced is being changed.
→ glyph and unaligned columns inside list rowsIn the LSP map edit dialog the only data row is rendered as:
> → pylsp [x]
The leading whitespace is the width of an empty Name column that was
never drawn. The → is a separator between the (missing) name and the
command. The [x] to remove the row sits flush against the command.
None of this is labelled. The 30+ blank rows below it make the dialog
feel broken.
In the hyprlang language editor (matches what users will see for any
language), the panel renders inside the centre column and labels
collide with values at common widths:
Show Whitespace Tabs: [ ✓ ACTIVE ]
Tab Size : [ 0 ] [-] [+]
Textmate Grammar : [ ]
The dialog should expand to the available width like the main settings page does, or wrap labels onto two lines, instead of clipping.
The main settings page supports / to search across all settings —
this is great. Inside the language / LSP editor dialog there is no /
search, so a user looking for "tab size" has to scan ~25 fields by eye
even though half of them are unused defaults.
[ Save ] [ Delete ] [ Cancel ]
[ Cancel ] in the
outer footer — the red signal is overloaded.Modified fields are marked with ● but there's no per-field "reset to
default" action. The only Reset button lives in the outer settings
footer and resets the whole page.
Steps: in the "Add Item" / "Edit Item" dialog, open Args, type one
arg, press Enter to commit. Focus lands on the freshly-spawned empty
[ ] [+] row.
Observed: from that empty row,
So the empty add-new sentinel traps the keyboard. The user cannot move forward to Auto Start or any later field without losing data. The only ways out are:
This forces a save → re-open → save → re-open workflow to fill a multi-list form. It is the single most damaging finding alongside F6.
Suggestion: treat the trailing [+] Add new as a sentinel — ↓/Tab
from it must escape to the next form control, not absorb the key.
Observed in session B for both Args and Root Markers: after
committing one or more rows and moving focus away from the list,
previously-entered values rendered as if the list were empty (just
[+] Add new). Moving focus back into the list, or saving and
re-opening, showed the rows still there.
User impact: combined with F6, this turns "did my data actually save?" into a real question at every step. The user starts hitting Ctrl+S defensively (which then bites them via F22 below).
Suggestion: always render committed list rows. Use a row highlight to mark focus inside the list rather than hiding non-focused rows.
The user has no way to see whether a list row is "committed" or "still being typed". This is a different shape of the same problem as F1 (no edit-mode indicator), specialised to lists.
Suggestion: a clear visual cue for committed vs. pending rows (border colour, inline ✓), or auto-commit on focus change so the model is uniform across input types.
Session B: Ctrl+S in the inner Edit Item dialog, Ctrl+S in the outer Edit Value dialog, and the top-level Settings [User] • (modified) title still showed the dirty marker. A third Ctrl+S at the top level was required to flush to disk. Nothing in the UI told the user the outer save was needed, or which dialog level was currently dirty.
Suggestion: either flush each nested-dialog save to disk immediately, or surface a per-level "N unsaved changes" indicator so the save hierarchy is visible.
After adding pyright-langserver as a second server under python
and saving all the way out, the collapsed row in the Lsp table still
rendered just pylsp. The user reasonably concluded that the save
had failed and re-entered the dialog to check.
The schema supports multiple servers per language (Multi config); the table should too.
Suggestion: render name1, name2 (with +N more truncation when
long) so multi-server config is visible at a glance.
| Web form convention | Current dialog |
|---|---|
| Focused field has a coloured border / caret | No visible change |
| Checkbox looks like a checkbox | [ ✓ ACTIVE ] / [ ] |
| Save disabled until something changed | Always enabled |
| "Unsaved changes?" prompt on close | Esc silently discards |
| Field has a help tooltip / description | Almost no inline help |
| Modified-vs-default shown with "Reset" link per row | ● glyph with no legend, no reset |
| Sections collapse | "── Advanced ──" is a text divider |
| Delete is separated and confirms | Adjacent to Save, no confirm |
| List add shows the new row in place with a label | Hidden state, sometimes opens a sub‑dialog |
| Complex value (JSON) opens in a code editor | Single inline line, no validation |
> indicator).[ ] / [x] (or ☐ / ☑), with the
label not changing case. Drop "ACTIVE".[+] Add new slot moves focus to the
next form control. No focus traps.[+] Add new when focus is elsewhere.pylsp server for
Python? [Yes] [No]").python should drop the user
straight into the server form. Show a "+ Add another server"
affordance underneath for the rare multi-server case.null placeholder for JSON fields with
(not set — press Enter to add) and pop a full-size JSON editor
(the same editor users already know from .json buffers) when they
enter it. Validate on save.Enter:Edit
from the legend and document what Enter actually does on each
control type.● = "set, differs from
default; press Ctrl+R to reset".[reset] button or Ctrl+R shortcut
on focused row, with the description in the footer.[+] Add new. Never skip from a list header
to the next top-level field unless the list is collapsed.[+] Add new consistency. For primitive lists, open the same
"edit one row" sub-form modal that struct lists use; or for both,
add the new row inline with a clear "Editing new item — Enter to
save, Esc to cancel" caption.pylsp, pyright-langserver (truncate to +N more when long) so
the user can see at a glance that a second server saved.Process Limits into three labelled controls.[-]/[+]. Keep [ 0 ] as a typed input;
spec-allowed range can be shown in helper text./ search inside the dialog to filter by field name.→ glyph and the
empty padding column.● /
(Inherited) mixed signals.╭ Edit Value ───────────────────────────────╮
│ Key:python │
│ ───────────────────────────────────────── │
│ ● Value: │
│> → pylsp [x] │
│ [+] Add new │
│ │
│ [ Save ] [ Delete ] [ Cancel ] │
╰───────────────────────────────────────────╯
╭ Edit Item ─────────────────────────────────╮
│ ● Command : [pylsp ] │
│ Enabled : [ ✓ ACTIVE ] │
│ ● Name : [ ] │
│ Args: │
│ [+] Add new │
│ Auto Start : [ ] │
│ ● Root Markers: │
│ [pyproject.toml ] [x] │
│ [setup.py ] [x] │
│ [setup.cfg ] [x] │
│ [pyrightconfig.json ] [x] │
│ [.git ] [x] │
│ [+] Add new │
│ ── Advanced ── │
│ Env: │
│ [+] Add new │
│ Language Id Overrides: │
│ [+] Add new │
│ ● Initialization Options: │
│ │null │
│ ● Only Features: │
│ │null │
│ ● Except Features: │
│ │null │
│ Process Limits: │
│ │{ │
│ │ "max_memory_percent": 50, │
│ │ "max_cpu_percent": 90, │
│ │ "enabled": true │
│ │} │
│ [ Save ] [ Delete ] [ Cancel ] │
╰────────────────────────────────────────────╯
↑↓:Navigate Tab:Fields/Buttons Enter:Edit Ctrl+S:Save Esc:Cancel
The footer is the only place edit mode is mentioned, and it does not distinguish "Enter to start editing a text field" from "Enter to toggle a checkbox" from "Enter to open a sub-dialog". All three happen on the same key with no UI feedback.