Commit Graph
100 Commits
Author SHA1 Message Date
oscar 7e9f82fcc4 docs(server): state why closing publishes empty diagnostics
The comment narrated the call below it. The non-obvious fact is that
the protocol has no clear operation, so an empty publish is the only
way to drop the client's diagnostics for a closed document.
2026-07-03 08:26:32 +02:00
oscar ffc521e5b5 test: share server test constructors and open helpers
The request, notification, and server construction helpers existed
byte-identical in both the server unit tests and the integration
tests, the didOpen notification blob was inlined seven times, the
redirect test rebuilt the download sequence download_request already
drives, and the request scan was duplicated at two sites. Move the
shared constructors into a test_support module and route the repeated
sequences through open, download_request, and find_request.

The initialize unit test asserted a strict subset of the integration
capability test plus the serverInfo name, so the name assertion moves
there and the unit test goes away.
2026-07-03 08:26:21 +02:00
oscar c8ea7e6caa refactor(server): extract request and response helpers
The id-allocation plus pending-registration scaffolding for
server-initiated requests was repeated across pull_configuration,
redirect_prompt, and code_lens_refresh, the serialize-and-respond
pair was inlined at seven handler sites next to the existing error
helper, and the parse-params-or-reject match was copied verbatim in
four handlers. Centralize each pattern in one helper so a change to
any of them has a single site.
2026-07-03 08:22:40 +02:00
oscar 2b39570a18 docs(todo): note bind_document allocations on the change path 2026-07-03 08:05:37 +02:00
oscar 7c659b15b9 feat(diagnostics): hold strict-JSON documents to strict JSON
The parser accepts comments and trailing commas unconditionally, which
also meant a plain .json file full of JSONC syntax validated silently.
The languageId the client sends at didOpen now picks the dialect:
documents opened as json get a warning for each comment and trailing
comma, jsonc documents keep accepting both. The dialect only affects
reporting, the parse itself is unchanged, and clients sending unknown
language ids stay on the lenient side.
2026-07-03 07:36:21 +02:00
oscar a8311ad3a8 perf(syntax): store token positions only and slice text from the source
Every token carried an owned String copy of its source slice, one heap
allocation per token on every reparse, and the copy could only ever
agree with the source it was cut from. Tokens now hold just a kind and
range, and consumers slice the document text through Token::text, so
the text lives in exactly one place and the tree becomes plain Copy
data. Accessors that read text (key_text, as_string, as_f64, the
formatter, the validator) take the source alongside the node.
2026-07-03 07:30:13 +02:00
oscar b78c51bcec perf(syntax/tree): borrow unescaped strings when no escapes are present
unescape_string built a fresh String for every call even though most
keys and values contain no escapes, and every key lookup funnels
through it on every reparse. key_text and as_string now return a Cow
that borrows the quote-stripped slice in the common case and only
allocates when an escape actually needs decoding.
2026-07-03 07:08:23 +02:00
oscar ad5c1381d6 perf(syntax/parser): move token text into the tree instead of cloning
eat_trivia and bump cloned each token, String and all, although every
token enters the tree at most once and the buffer is dropped right
after parsing. Taking the text out of the buffer slot avoids one heap
allocation per token per reparse.
2026-07-03 07:04:46 +02:00
oscar 96784e591c refactor(syntax/parser): share the separator handling between containers
The after-item comma-or-closer match in parse_object and parse_array
was the same four arms duplicated, each also repeating its loop's own
end-of-file close error.
2026-07-03 07:04:26 +02:00
oscar d614c0344b refactor(syntax): unify the lexer and parser error types
LexError and SyntaxError were the same range-plus-message pair, and
parse converted one into the other field by field just to merge the
lists. Both stages now report through one SyntaxError in tree.rs.
2026-07-03 07:04:02 +02:00
oscar dc3cc3a44b fix(syntax/lexer): validate numbers against the JSON grammar
Number validity was checked by f64::from_str, which accepts strings
that are not JSON numbers, so leading zeros like 05 and trailing dots
like 5. produced no diagnostic.
2026-07-03 07:02:18 +02:00
oscar 384a3201ee fix(syntax/tree): decode escaped UTF-16 surrogate pairs
char::from_u32 rejects surrogate code points, so both halves of an
escaped pair like \uD83D\uDE00 fell through silently and the astral
character vanished from the decoded string. That corrupted key
matching, enum and const comparison, and $schema detection for any
string spelling a non-BMP character as escapes. Pairs now combine into
the supplementary code point, and lone or mismatched surrogates decode
to the replacement character instead of disappearing.
2026-07-03 07:01:43 +02:00
oscar ca43a3cbef fix(syntax/lexer): stop strings at a backslash-escaped newline
The escape arm consumed whatever byte followed the backslash, so a
backslash at the end of a line swallowed the newline and the lexer ran
into the following lines until the next quote, restructuring the rest
of the document into one string token. The newline guard now applies
inside escapes too.
2026-07-03 07:00:40 +02:00
oscar 07c89a8ac9 perf(syntax/tree): parse unicode escapes without a temporary string
Each \u escape collected its hex digits into a heap String before
parsing. Slicing the digits straight out of the raw input parses the
same text without the per-escape allocation.
2026-07-03 02:13:57 +02:00
oscar 6410350b1f refactor(syntax/tree): make unescape_string private
Only key_text and as_string call it, so the public visibility was
unearned surface.
2026-07-03 02:13:42 +02:00
oscar 94d2a9f195 refactor(syntax/lexer): reuse the error helper and std char width
The unknown-keyword arm pushed a LexError by hand while every other
error path goes through the error helper, and advance_char re-derived
UTF-8 character width from continuation bytes that char::len_utf8
already encodes.
2026-07-03 02:10:04 +02:00
oscar 8c883cc56c fix(schema/store): unbind documents when they close
Nothing removed a closed document from the store, so doc_schema
entries and waiting-set URIs accumulated for the server's lifetime and
a completed fetch nominated long-closed documents for diagnostics.
2026-07-03 01:49:35 +02:00
oscar 14caccf290 perf(schema/store): decide auto-load eligibility once per schema
bind_document ran auto_loadable before consulting auto_attempted, so a
document whose schema never loads paid the workspace-containment
canonicalize() syscalls on every change. Recording the attempt first
also covers ineligible schemas, which previously never entered the set
and were re-probed forever.

A file schema outside one referring document's directory is no longer
retried when a second document in a different directory binds it while
no workspace root is known. That case keeps the explicit download lens.
2026-07-03 01:48:52 +02:00
oscar eb3ffeaafc fix(schema/matcher): percent-decode internal $ref fragments
A $ref fragment is percent-encoded per RFC 3986, so a def whose name
needs encoding, like #/$defs/foo%20bar, never resolved and its
constraints were silently dropped. Decode the fragment before the
pointer lookup, failing malformed escapes instead of passing them raw.
2026-07-03 01:48:28 +02:00
oscar 970e5d0529 refactor(schema): remove the unreachable redirect match arms
The redirect decision in fetch_one and the result bookkeeping in
on_fetched each split one decision across two control-flow constructs,
leaving a dead match arm that could only assert its own unreachability.
Deciding in a single match removes both arms and an extra url clone.
2026-07-03 01:42:53 +02:00
oscar 55a4514540 refactor(schema): deduplicate helpers and reuse existing ones
Share the root-object $schema member scan between URL detection and
the download lens range, the http(s) scheme test between the fetch
guards, the pointer-identity dedup push between navigation and
expansion, and the temp-dir, canned-response, and loopback-policy test
helpers between the fetch and store test modules. Also go through
Parse::value() like every other call site.
2026-07-03 01:42:22 +02:00
oscar 0088a99717 perf(schema/matcher): skip avoidable work in per-keystroke checks
Validation reruns on every change, so the walk paid for keywords that
were not even present: counting characters without a length constraint,
building the present-property set without a required list, cloning the
pattern string on every regex cache hit, and probing every oneOf branch
after two matches had already decided the outcome.
2026-07-03 01:40:32 +02:00
oscar 9a364bed84 fix(schema/fetch): avoid overflow when maxBytes is u64::MAX
schemaFetch.maxBytes is client-supplied and unclamped, so the over-read
probe's max_bytes + 1 could overflow: a panic that permanently kills
the fetch worker in debug builds, a wrap to take(0) in release.
2026-07-03 01:40:08 +02:00
oscar 2642f8cf83 fix(schema/matcher): let combinator probes pass on warnings
is_valid failed a probe on any finding, so an advisory format warning
inside an anyOf branch rejected values the schema accepts, made oneOf
undercount matches, inverted not, and steered if/then/else wrong. Only
error findings fail a probe now, matching how external ref warnings
are already kept out of probes via the side channel.
2026-07-03 01:39:55 +02:00
oscar 63b77fd39a docs: record schema matcher backlog items
Two known gaps in the schema engine worth more than a quick fix: the
duplicated property/item dispatch between validation and navigation,
and draft 2020-12 $ref sibling composition.
2026-07-03 01:36:43 +02:00
oscar 9d09ab942a perf(rpc): reuse the header line buffer across messages
read_message allocated a fresh String per message even though the
stdin reader calls it in a loop. The caller now owns the scratch
buffer, so one allocation serves the whole stream.
2026-07-03 01:24:33 +02:00
oscar fcfddae6d0 perf(rpc): buffer stdout and flush once per event batch
write_message flushed after every message over a bare line-buffered
StdoutLock, so an event producing a response, diagnostics, and a code
lens refresh paid the syscall cost per message. Stdout now goes through
a BufWriter and the dispatch loop flushes once per batch.
2026-07-03 01:23:36 +02:00
oscar 806729daa3 fix(rpc): validate Content-Length before trusting it
A malformed value was folded into the missing-header error by
parse().ok(), so the one stderr line the dying reader thread produced
pointed at the wrong cause. And the declared length sized the body
allocation with no bound, letting a corrupt or hostile header force a
giant zeroed allocation before read_exact could fail. Malformed and
oversized values now fail with errors naming the offending value.
2026-07-03 01:23:13 +02:00
oscar 5858895382 refactor(rpc): replace the jsonrpc string fields with a version marker
The &'static str fields threaded the same "2.0" literal through five
constructors and let a struct literal set a wrong version. A zero-sized
marker that serializes as "2.0" removes both.
2026-07-03 01:22:41 +02:00
oscar e31797002e perf(rpc): store outgoing method names as static strings
Every call site passes a string literal, so the owned String fields
heap-allocated a copy of a static name for each outgoing notification
and request.
2026-07-03 01:20:21 +02:00
oscar 30d82a7be8 refactor(rpc): remove the unused ResponseError data field
No response the server produces carries error data, so every
construction went through ResponseError::new hardcoding None. The
field can return when a response actually needs it.
2026-07-03 01:19:57 +02:00
oscar 29076d15a4 refactor(rpc): drop serde behavior the derives already provide
Missing fields deserialize to None for Option types without
#[serde(default)], and an untagged derive serializes each Outgoing
variant's payload directly, so the manual dispatch match duplicated
what serde generates.
2026-07-03 01:19:45 +02:00
oscar 03e5927fcf refactor(rpc): inline read_loop into the stdin reader
The wrapper had a single caller and forced the callback to fabricate a
BrokenPipe error just to stop reading, which the caller then had to
filter back out. The inline loop returns directly instead.
2026-07-03 01:18:25 +02:00
oscar f73fcd93aa refactor(lsp): make always-set response fields plain
Serialize-only types carried Option wrappers and skip_serializing_if
attributes on fields that every construction site sets, so the None
path never ran and callers paid for unwraps. Diagnostic.source also
becomes a static str because it is always the constant "json-ls"
and diagnostics are rebuilt on every edit. Option remains only where
a None is actually produced.
2026-07-03 00:51:02 +02:00
oscar f6aee4b2dc refactor(lsp): derive camelCase wire names at the container level
Per-field serde renames restate the snake-to-camel convention once
per field and invite a missed rename on future additions. A container
rename_all covers every field, while the LSP "type" fields keep their
explicit rename, which overrides the container attribute.
2026-07-03 00:49:11 +02:00
oscar b274df1462 refactor(lsp): deduplicate helpers and request params
The URI helper, the completion request params, and the client
capability descent each restated a definition that already existed
elsewhere, namely schema::resolve::file_url_to_path,
TextDocumentPositionParams, and the sibling capability accessor's
traversal. Collapse each onto the single definition and import
completion_item_tag instead of spelling the crate path inline.
2026-07-03 00:38:37 +02:00
oscar e2be17f997 perf(features/completion): defer key documentation to completionItem/resolve
Every candidate property rendered its markdown documentation during
the completion request, walking refs and enumerating combinator
variants per item on each keystroke while clients show one item at a
time. Key items now carry the document URI and schema path in their
data payload, and the new completionItem/resolve handler builds the
documentation for the single item the client asks about.
2026-07-02 21:08:02 +02:00
oscar 6b35268239 perf(line_index): resume utf-16 column walks from the previous conversion
Each conversion re-counted units from the line start, so publishing N
diagnostics on minified single-line JSON cost O(N x line length) per
change. Consecutive conversions on one line now continue from the last
offset, and the resume point resets with the index on every edit.
2026-07-02 21:05:04 +02:00
oscar d181646ed5 test(features): assert fact rendering where it is implemented
The key documentation tests in completion each rebuilt the same
document and schema setup to assert describe's rendering one layer
removed. Completion keeps a single wiring test and the rendering
assertions move next to the code in describe.
2026-07-02 20:58:24 +02:00
oscar 98f42c8608 refactor(features): share the offset-to-path descent between hover and completion
Hover and completion each walked the tree from an offset to a schema
path, with array branches that had already been copied verbatim and
object branches that had started to drift. The shared walker uses
completion's slot rule, so hovering between a colon and its value now
describes that member's schema instead of the enclosing object's.
2026-07-02 20:54:44 +02:00
oscar 8f8bae425d refactor(schema/matcher): share the type keyword extraction with describe
check_type and describe::type_names each turned the string and array
forms of type into a name list, and the copies could drift.
2026-07-02 20:52:51 +02:00
oscar e1391c2967 fix(features/completion): complete array gaps against the insertion index
A cursor between elements that touched neither fell through to the
element count, so positional schemas (prefixItems, tuple items) matched
the wrong index or none at all.
2026-07-02 20:51:53 +02:00
oscar a6ed40a0a8 fix(features/completion): propose literals for every member of a type union
value_items looked only at the first name in a type array, so
["string", "boolean"] proposed nothing and ordering decided whether
booleans or null showed up.
2026-07-02 20:51:29 +02:00
oscar 033838e242 fix(features/formatting): keep comments that sit between a key and its value
The parser attaches trivia around a member's colon to the member node,
but fmt_member only re-emitted the key, colon, and value, silently
deleting any comment there and breaking the comment-preserving
contract. Line comments push the value to the next line so it does not
end up commented out.
2026-07-02 20:51:10 +02:00
oscar 5e78e3d95e fix(features/completion): escape snippet metacharacters in key and const text
Keys like $schema and const strings containing a dollar sign were
embedded verbatim in snippet bodies, so client snippet engines parsed
them as variables and inserted the wrong text.
2026-07-02 20:50:43 +02:00
oscar c407647fdb fix(features/completion): do not re-insert a colon and value over an existing key
Completing on the key of a member that already has a value replaced
only the key range with the full key-colon-value snippet, turning
{"na": 1} into the invalid {"name": "$0": 1}. The full member
snippet now only applies when the member has neither colon nor value.
2026-07-02 20:50:18 +02:00
oscar 2ad5a80787 test(features/diagnostics): drop a duplicated no-diagnostics test
no_schema_means_no_validation_diagnostics ran the same input and
assertion as clean_document_has_no_diagnostics, adding no coverage.
2026-07-02 20:41:29 +02:00
oscar ad40be0aa0 docs(features): restructure two comments to read linearly 2026-07-02 20:41:15 +02:00
oscar d4b41f0105 perf(features): trim per-keystroke allocations in completion and diagnostics
Completion collected every ancestor object's keys on descent even when
the cursor lands in a value slot, filtered candidate properties with a
linear scan, and walked array elements twice to find the trailing
index. Duplicate-key detection cloned every key into the seen set even
though the text is only needed when a duplicate is found. All of this
runs on each keystroke, so the waste scales with document size.
2026-07-02 20:40:58 +02:00
oscar ae5856667e refactor(features/diagnostics): share diagnostic construction
Three sites each spelled out the full Diagnostic literal, repeating
the json-ls source string, so a constructor keeps them aligned.
2026-07-02 20:37:59 +02:00