100 Commits
Author SHA1 Message Date
oscar 90297292e9 chore: remote TODO.md 2026-07-18 22:02:34 +02:00
oscar b6145f2e03 chore: release v1.3.0 2026-07-18 21:55:59 +02:00
oscar 0875c682a0 feat(schema): reload a changed schema on document open
An update made in one server instance reaches the others through the
shared disk cache the next time a document opens.
2026-07-18 21:55:40 +02:00
oscar 85f54c0c63 chore: release v1.2.0 2026-07-18 21:09:55 +02:00
oscar e7a7029a34 feat(schema): offer updating a loaded schema 2026-07-18 21:08:56 +02:00
oscar 0e8afc2273 docs: fix formatting 2026-07-04 04:00:04 +02:00
oscar 24215e8f66 chore: release v1.1.0 2026-07-04 03:54:48 +02:00
oscar 137483807d feat(cli): add command-line options
Parse arguments with lexopt: --cache-dir overrides the on-disk schema
cache location, --offline refuses every network fetch including the
json-ls.downloadSchema command, and --stdio is accepted as a no-op so
editor clients that always pass it keep working. Also wire up --help
and --version.

Offline is enforced at request_download, the sole network entry point,
so no document reaches the network regardless of the fetch policy.
2026-07-04 03:54:06 +02:00
oscar 1350f762e6 chore: release v1.0.0 2026-07-04 03:20:39 +02:00
oscar d62091c39e docs: clean up README 2026-07-04 03:26:09 +02:00
oscar 192b970436 chore: apply formatting 2026-07-04 03:15:20 +02:00
oscar 85445dca9f docs: restructure prose comments to drop semicolons 2026-07-04 03:06:42 +02:00
oscar e77c331d87 docs(schema): remove semicolons from two comments
The project prose style avoids semicolons. Split the joined clauses in the
file: URL authority comment and the redirect-budget comment into sentences.
2026-07-04 02:58:22 +02:00
oscar cd79629e9f refactor(schema): share the percent-escape decoder
resolve.rs and matcher.rs each walked a string decoding %XX escapes with
their own hex parsing. Extract decode_percent_escape as the shared
primitive. The callers keep their different policies for a malformed
escape: matcher fails the $ref lookup, while resolve passes a stray % in a
path through raw. This also drops the hand-rolled hex_value in favor of
from_str_radix, keeping the explicit hex-digit guard because from_str_radix
would otherwise accept a leading sign.
2026-07-04 02:56:16 +02:00
oscar c20e4b9d7f fix(schema): resolve authority-only base URIs
resolve_uri dropped RFC 3986 5.2.3's rule that a relative reference against
a base with an authority but no path becomes the whole path. A base like
https://ex.com with the relative reference foo produced https://ex.comfoo,
a malformed URL that then failed to fetch. This surfaces when a schema URL
has no path and a server returns a relative Location redirect.
2026-07-04 02:54:51 +02:00
oscar 945a2c18fa build: trim regex to the unicode features in use
Drop regex's default feature set for only the pieces schema pattern
validation relies on: unicode-perl for \d \s \w and word boundaries,
unicode-case for case-insensitive matching, and perf. This sheds the
larger Unicode tables (general categories, scripts, ages, segmentation)
that schema patterns rarely reference. A pattern that does use a dropped
class now fails to compile and falls back to the matcher's existing
permissive handling rather than being validated.
2026-07-04 02:03:11 +02:00
oscar 4ef1712009 refactor(schema): drop the url crate for hand-rolled URI handling
The url crate hard-depended on idna, which since idna 1.0 pulls the whole
ICU stack (icu_normalizer, icu_properties, zerovec, yoke, and five
proc-macro crates), around 24 crates in all, purely to validate
internationalized hostnames. A JSON language server that resolves a
handful of $schema URLs never needs that.

Hand-roll the small surface we actually used: file: URL <-> path
conversion, relative reference resolution, scheme detection, and the
same-origin check that scopes redirect consent. Move ureq from 2 to 3,
where url is an optional dependency rather than a mandatory one, and
re-express the private-network guard as ureq 3's Resolver trait. Vetting
still happens on the exact addresses ureq connects to, so a public DNS
answer cannot be swapped for a private one before the connection.

With both the direct dependency and ureq's transitive one gone, url,
idna, and the ICU stack leave the graph entirely.
2026-07-04 02:02:11 +02:00
oscar 34b1f06871 build: trim dependency features and use thin LTO
Drop ureq's default features and keep only tls, which removes the gzip
decode stack (flate2, miniz_oxide, crc32fast, adler2, simd-adler32) that
schema downloads never relied on.

Switch release LTO from fat to thin. On this workload the two produce an
identically sized binary, but thin roughly halves the release build
(measured 27s to 12s) with no meaningful runtime cost for an IO-bound
language server.
2026-07-04 01:26:01 +02:00
oscar 796a2bf89a fix(schema): stop asserting uri/iri format
The format keyword is annotation-only by default under JSON Schema, so
emitting a diagnostic for a malformed uri or iri was stricter than the
spec's default vocabulary. Asserting it also required parsing every value
as a URL, which is the only reason user-document validation reached for
full idna hostname handling. Treat uri/iri as annotations like every
other unrecognized format.
2026-07-04 01:26:01 +02:00
oscar 169ec2df13 feat(definition): resolve a data key to its schema declaration
Go-to-definition on a property key in a schema-bound document now jumps to
where the schema declares that property. This complements the internal
`$ref` navigation, which the two never overlap by cursor position.

The matcher gains a pointer-tracking navigator that mirrors the existing
value walk but yields JSON Pointers into the schema, expanding combinators
and following internal `$ref`s along the path while leaving the located
declaration unexpanded. The server turns each pointer into a range against
the schema's own source, which for a remote schema is its cache file so the
target is openable.

The schema source is parsed once and cached in the store, keyed like the
in-memory schema and dropped when the schema reloads, so repeated requests
into the same schema neither re-read nor re-parse it.
2026-07-03 23:58:01 +02:00
oscar 8a604710e3 feat(definition): add go-to-definition for internal $ref
Resolve a JSON Schema `$ref` under the cursor against the document's own
tree and point at the referenced node, so authoring a schema can navigate
its `#/...` pointers.

Only internal refs resolve, mirroring the validator: an external ref
returns nothing. Resolution reuses the matcher's percent-decoding and then
applies RFC 6901 token unescaping, so definition lands where validation
would. The feature reads the document tree alone and needs no bound schema.
2026-07-03 22:40:38 +02:00
oscar aafb0aad0b docs(todo): drop completed backlog items
The shared-dispatch refactor and the allocation-free `bind_document`
change have landed, so their backlog entries no longer describe work to
be done.
2026-07-03 22:26:25 +02:00
oscar 0af71c6ee2 fix(schema): compose $ref with its sibling keywords
The validator derefed a `$ref` and replaced the holder outright, so a
schema like `{"$ref": "#/$defs/base", "maxLength": 5}` silently dropped
`maxLength` and every other sibling keyword.

Validate the referenced target and the holder's remaining keywords
together, as draft 2020-12 requires. `deref` still backs navigation, so
only the validation path changes.
2026-07-03 22:26:16 +02:00
oscar 751427b64b perf(schema): avoid re-binding allocations on unchanged documents
`bind_document` runs on every change and unconditionally cloned `doc.uri`
and the detected URL to re-record a mapping that is almost always
identical, then cloned again to re-insert a `waiting` entry that was a
no-op after the first change.

Look entries up before inserting: skip the `doc_schema` write when the
detected URL is unchanged, and only add to `waiting` when the document is
not already recorded there. A keystroke on a bound document now does
lookups without allocating.
2026-07-03 22:24:44 +02:00
oscar 10222eb234 refactor(schema): share property and item dispatch
`child_schemas` re-implemented the same property and array-item dispatch
that `check_object` and `check_array` already encoded, and the copies had
drifted: navigation ignored a boolean `items` schema and applied both
`prefixItems` and `items` to a prefix index instead of letting the prefix
take precedence.

Extract `property_subschemas` and `item_subschemas` as the single source
of which subschemas apply to a given key or index, and have validation and
completion both consume them.
2026-07-03 22:24:34 +02:00
oscar 315bc242b3 fix(server): reject invalid params consistently across handlers
Document symbols, code lens, and code action swallowed a params parse
failure and answered success with an empty result, while formatting,
completion, and hover returned INVALID_PARAMS for the same failure.
All six now return the error, which is what JSON-RPC prescribes and
makes a client bug visible instead of looking like an empty
document.
2026-07-03 08:45:47 +02:00
oscar f4dd8e0e7d perf(server): skip republishing unchanged diagnostics
Every open and change published a diagnostics set even when it was
identical to the last one sent, waking the client's diagnostics
pipeline once per keystroke on a valid document. Remember the last
published set per document and publish only on a difference. The
close-time clearing publish stays unconditional and drops the entry,
so a reopened document always gets a fresh first publish.
2026-07-03 08:45:20 +02:00
oscar a628ffca5b perf(server): reuse the schema binding for diagnostics
bind_schema discarded the schema that bind_document returns, so the
open and change paths resolved the same schema again through
schema_for on every edit. Threading the binding into the diagnostics
computation leaves schema_for to the paths that have no fresh binding
in hand.
2026-07-03 08:43:48 +02:00
oscar 87cc704e25 refactor(document): apply LSP change events directly
The Change struct mirrored TextDocumentContentChangeEvent field for
field, and the only thing constructing it was the dispatcher copying
the wire type over verbatim. Passing the wire type through removes
the parallel definition and the copy loop.
2026-07-03 08:42:58 +02:00
oscar 5bb0547645 fix(server): drop pre-initialize notifications except exit
Requests were already gated on the handshake, but notifications were
routed unconditionally, so a didOpen arriving before initialize ran
under the not-yet-negotiated position encoding and published
diagnostics before the initialize response, both of which the
protocol forbids.
2026-07-03 08:42:11 +02:00
oscar 45e6ca0458 fix(line-index): treat a lone carriage return as a line break
LSP clients count \r, \n, and \r\n as line breaks when numbering
lines. The index only counted \n, so a document with bare \r line
endings read as one long line here while the client saw many, making
every exchanged position wrong and letting incremental edits resolve
to the wrong byte offsets.
2026-07-03 08:41:41 +02:00
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
oscar 0c33615ae3 refactor(features/completion): reuse is_scalar for the literal kind
push_literal spelled out the negation of the is_scalar predicate that
already lives in the same file, so the two could drift apart.
2026-07-02 20:37:01 +02:00
oscar e5a3e4dfc8 docs: add README
Cover features, building, the schema download model and its guards, the
supported JSON Schema and LSP subsets, configuration, and editor setup.
2026-07-02 20:00:03 +02:00
oscar b300a2538a build: drop the Makefile in favor of direct cargo commands
The targets were one-line wrappers around cargo, so the indirection cost
more than it saved. README.md and CLAUDE.md list the commands directly.
2026-07-02 20:00:38 +02:00
oscar ab1e23cde9 feat(server): apply runtime configuration from the client
Until now the fetch policy could only be set once, through
initializationOptions at startup. Settings now also arrive at runtime,
pushed via workspace/didChangeConfiguration or pulled through a
workspace/configuration request when the client advertises support.
Each update re-layers the runtime settings over the
initializationOptions base, so a setting the user removes reverts to
its startup value instead of sticking.
2026-07-02 03:31:01 +02:00
oscar 702d23ce26 feat(schema/fetch): follow schema redirects with cross-origin consent prompts
Redirected schema URLs previously failed with a confusing parse error
on the 3xx body, and silently following them would have stretched the
user's per-URL download approval to targets they never saw. The worker
now walks redirect chains itself (ureq never follows on its own) so
every hop can be vetted and put to the user.

Same-origin hops follow silently in every mode, since the approved
host serves both URLs. Cross-origin hops are governed by a new
schemaFetch.redirects mode: prompt (default) asks the user through
window/showMessageRequest, same-origin refuses with an actionable
error, and always follows silently. maxRedirects stays a pure chain
budget and now defaults to 3. Every hop still passes the scheme and
public-address guards, and a redirect off http(s) is refused so a
remote server cannot turn its redirect into a local file read.

The prompt is the server's first correlated client response: incoming
messages now carry result, and pending prompts are resumed or
abandoned by request id. A paused download stays pending so duplicate
lens clicks cannot start a second chain, and the schema remains keyed
under the URL the document declares wherever the chain leads.
2026-07-02 03:00:38 +02:00
oscar 864fe22898 feat(server): show an editor message when a schema download fails
Failures previously went only to stderr, so a user who clicked the
download lens saw nothing happen, and the guard messages that name the
setting to change never reached them. A failed fetch now produces a
window/showMessage error notification.

Only explicit downloads message the user. Automatic probes fail
routinely (nothing cached yet, an unreadable local file) and would
otherwise toast on every open or change, so they stay on stderr.
2026-07-02 02:28:35 +02:00
oscar 2ff3351c43 fix(schema/fetch): use a stable hash for schema cache filenames
DefaultHasher's algorithm is unspecified across Rust releases, so a
toolchain bump could silently orphan every cached schema. Hand-rolled
FNV-1a keeps the filenames stable, and a test pins the exact value so
any future change shows up as a deliberate test edit.

Existing cache entries are orphaned once by this change and will be
re-downloaded on the next explicit download.
2026-07-02 02:20:45 +02:00
oscar d47f2f2ab1 feat(schema/fetch): refuse downloads from non-public addresses by default
A $schema URL could point a user-approved download at loopback,
private, or link-local hosts. Resolution and vetting now happen in one
resolver step installed on the ureq agent, so the connection is pinned
to the addresses that were checked. A separate check-then-connect would
let DNS rebinding serve a public address to the check and a private one
to the connection. IPv4-mapped IPv6 addresses are judged by their IPv4
rules so they cannot bypass the range checks.

schemaFetch.allowPrivateNetwork opts out, for schema registries on an
intranet.
2026-07-02 02:20:05 +02:00
oscar 030b0515cb feat(schema/matcher): warn when an external $ref is left unresolved
Only internal #/... refs are resolved, so any subtree behind an
external $ref validated everything silently, which is the failure mode
users do not notice. Validation now emits one warning per distinct
external reference, anchored at the first node it applies to.

The refs are collected on the side and appended after the walk, because
a finding pushed inside a combinator probe would make is_valid fail
branches that are actually fine.
2026-07-02 02:18:11 +02:00
oscar 90aa5ec6bf fix(schema/matcher): apply additionalProperties only to uncovered keys in paths
check_object already gates additionalProperties behind named and
pattern properties, but child_schemas pushed its subschema
unconditionally. Completion and hover under a known property therefore
picked up the additionalProperties schema as well, diverging from the
validator's own logic. Mirror the validator's gating.
2026-07-02 02:17:28 +02:00
oscar 00702291fb fix(schema/matcher): cache compiled patterns and tolerate ECMA-only syntax
JSON Schema patterns use ECMA-262 syntax, and the regex crate rejects
some of it (lookaround, backreferences). A pattern that failed to
compile previously counted as unmatched, so a key covered only by such
a patternProperties entry was wrongly rejected when
additionalProperties is false. Unsupported patterns now conservatively
count as matching, their constraint is skipped, and each failure is
logged to stderr once.

Compiled regexes are also cached instead of being recompiled on every
node visit.
2026-07-02 02:17:08 +02:00
oscar 657631af97 docs: prune comments that restate the code in prose
A comment that describes behavior is a second implementation of that
behavior. Only the Rust copy is checked by the compiler and the tests,
so the prose copy drifts and starts lying. Keep a comment only when it
states something the code cannot: a deliberate non-goal, an invariant
the types do not capture, or the reason behind a surprising choice.

Also correct a stale dispatcher comment that claimed no
server-initiated requests are sent. Code lens refresh sends them
fire-and-forget.
2026-07-02 01:53:51 +02:00
oscar 2f4d05c7ac style(hover): stack each alternative's facts on their own lines
In the 'one of:' list, put the first fact on the bullet and the remaining
facts on indented lines beneath it, rather than comma-joining them all on
one line.
2026-07-02 01:04:04 +02:00
oscar 468e14d7e1 fix(hover): describe combinator schemas as alternatives, not a merge
Hover navigated with subschemas_for_path, which flattens every anyOf/oneOf
branch into one list, and describe then merged their keywords into a
single nonsensical fact block (a value that is a hex string OR "none" OR
an integer OR an object showed type, enum, pattern, min/max, and required
all at once).

Split navigation from expansion in the matcher: subschemas_for_path keeps
the flattened union for completion, while the new schemas_at_path returns
the leaf schema without expanding its combinators. Make describe
combinator-aware instead: it resolves internal $refs, merges allOf into
one shape, and fans anyOf/oneOf out into a 'one of:' list, each branch on
its own line. Bounded by depth and a variant cap against ref cycles. Key
completion docs go through the same renderer, so a property whose schema
is a union now documents its alternatives too.
2026-07-02 00:38:54 +02:00
oscar 6b14a37550 feat(completion): deprecation tag, and shared docs with hover
Extract the schema fact rendering into features/describe.rs and have both
completion and hover use it, so the hover popup now shows the same fact
list (title, type, assertions, annotations) as key completion instead of
its own smaller set. describe reads each keyword from the first applicable
subschema, so completion passes a single-element slice and hover the full
set from subschemas_for_path.

Mark properties whose subschema has deprecated: true with the LSP
Deprecated completion item tag (strikethrough), negotiated via the
client's completionItem.tagSupport capability, in addition to the existing
deprecated row. Group the completion capability flags into a small struct
to keep Server within the bool-count lint.
2026-07-02 00:26:16 +02:00
oscar 15b7de39cc feat(completion): surface title and status annotations in key docs
Read title, deprecated, readOnly, and writeOnly for the key documentation.
title renders as a bold heading above the description; the three boolean
annotations become fact rows shown only when set to true. These are
annotations, so they do not participate in schema validation.
2026-07-02 00:13:20 +02:00
oscar fece822fc7 feat(completion): surface all supported assertions in key docs
Extend the key documentation with every scalar assertion the matcher
validates: minimum, maximum, exclusiveMinimum, exclusiveMaximum,
multipleOf, minLength, maxLength, pattern, format, and required. format,
pattern, and required names render without JSON quotes since they name a
constraint rather than carry a value. Applicators are left out because
they describe subschemas, which this path does not resolve.
2026-07-02 00:08:22 +02:00
oscar e4166ffa02 feat(completion): show union types and examples in key docs
Render a `type` array as a union (each name in its own inline code span,
joined by |) instead of dropping all but the first entry, and add an
examples row alongside the existing type/enum/const/default facts.
2026-07-02 00:01:13 +02:00
oscar 3fc8e13e94 refactor(completion): render key doc facts as inline code lines
Replace the fenced json blocks with one fact per line, each value in an
inline code span: type, allowed (as a JSON array literal), and default.
This is more compact than a fenced block per value and keeps the quotes on
string values. Inline spans are not syntax-highlighted by token the way a
fenced json block is, trading that coloring for a tighter layout.
2026-07-01 23:30:50 +02:00