Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24215e8f66 | ||
|
|
137483807d |
Generated
+8
-1
@@ -95,15 +95,22 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "json-ls"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"lexopt",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lexopt"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "json-ls"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
edition = "2024"
|
||||
description = "A JSON/JSONC language server"
|
||||
license = "Apache-2.0"
|
||||
@@ -14,6 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
||||
crossbeam-channel = "0.5"
|
||||
lexopt = "0.3"
|
||||
regex = { version = "1", default-features = false, features = [
|
||||
"std",
|
||||
"perf",
|
||||
|
||||
@@ -32,8 +32,22 @@ Requires a recent stable Rust toolchain (edition 2024).
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary lands at `target/release/json-ls`. It takes no arguments and
|
||||
communicates over stdin/stdout, so point your editor's LSP client at it.
|
||||
The binary lands at `target/release/json-ls`. It communicates over
|
||||
stdin/stdout, so point your editor's LSP client at it.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
json-ls [OPTIONS]
|
||||
```
|
||||
|
||||
| Option | Effect |
|
||||
| ------------------- | ------------------------------------------------------------------------------- |
|
||||
| `--cache-dir <DIR>` | Directory for the on-disk schema cache. Defaults to the location below. |
|
||||
| `--offline` | Refuse every network fetch, including the `json-ls.downloadSchema` command. |
|
||||
| `--stdio` | Accepted for editor-client compatibility. Has no effect, since stdio is the only transport. |
|
||||
| `-h`, `--help` | Print help. |
|
||||
| `-V`, `--version` | Print version. |
|
||||
|
||||
## Schemas
|
||||
|
||||
|
||||
+76
-2
@@ -14,6 +14,7 @@ mod test_support;
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
@@ -34,14 +35,87 @@ enum Event {
|
||||
WorkerGone,
|
||||
}
|
||||
|
||||
struct Args {
|
||||
/// Overrides the on-disk schema cache location. `None` keeps the default.
|
||||
cache_dir: Option<PathBuf>,
|
||||
offline: bool,
|
||||
}
|
||||
|
||||
const HELP: &str = "\
|
||||
json-ls, a JSON/JSONC language server
|
||||
|
||||
Usage: json-ls [OPTIONS]
|
||||
|
||||
The server speaks LSP over stdin/stdout. Point your editor's LSP client at it.
|
||||
|
||||
Options:
|
||||
--cache-dir <DIR> Directory for the on-disk schema cache
|
||||
[default: $XDG_CACHE_HOME/json-ls/schemas]
|
||||
--offline Refuse every network fetch, including the
|
||||
json-ls.downloadSchema command
|
||||
--stdio Accepted for editor-client compatibility (no effect)
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
";
|
||||
|
||||
fn parse_args() -> Result<Args, lexopt::Error> {
|
||||
use lexopt::prelude::Long;
|
||||
use lexopt::prelude::Short;
|
||||
|
||||
let mut cache_dir = None;
|
||||
let mut offline = false;
|
||||
let mut parser = lexopt::Parser::from_env();
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Short('h') | Long("help") => {
|
||||
print!("{HELP}");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Short('V') | Long("version") => {
|
||||
println!("json-ls {}", env!("CARGO_PKG_VERSION"));
|
||||
std::process::exit(0);
|
||||
}
|
||||
Long("cache-dir") => {
|
||||
cache_dir = Some(PathBuf::from(parser.value()?));
|
||||
}
|
||||
Long("offline") => offline = true,
|
||||
// Editor clients often spawn the server with `--stdio`; accept and
|
||||
// ignore it since stdio is the only transport.
|
||||
Long("stdio") => {}
|
||||
_ => return Err(arg.unexpected()),
|
||||
}
|
||||
}
|
||||
Ok(Args { cache_dir, offline })
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = match parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
eprintln!("json-ls: {e}");
|
||||
eprintln!("Try 'json-ls --help' for more information.");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
let (incoming_tx, incoming_rx) = unbounded::<IncomingMessage>();
|
||||
let reader = thread::spawn(move || read_stdin(&incoming_tx));
|
||||
|
||||
let cache_dir = fetch::default_cache_dir();
|
||||
let cache_dir = match args.cache_dir {
|
||||
Some(dir) => {
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
eprintln!(
|
||||
"json-ls: cannot create cache dir {}: {e}",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
Some(dir)
|
||||
}
|
||||
None => fetch::default_cache_dir(),
|
||||
};
|
||||
let (fetch_tx, fetch_rx) = fetch::spawn_worker(cache_dir.clone());
|
||||
|
||||
let mut server = Server::new(fetch_tx, cache_dir);
|
||||
let mut server = Server::new(fetch_tx, cache_dir, args.offline);
|
||||
let stdout = io::stdout();
|
||||
let mut out = io::BufWriter::new(stdout.lock());
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ pub struct SchemaStore {
|
||||
/// still resolve to their own file.
|
||||
cache_dir: Option<PathBuf>,
|
||||
policy: FetchPolicy,
|
||||
/// When set, the `request_download` network entry point is refused, so no
|
||||
/// document ever reaches the network regardless of the fetch policy.
|
||||
offline: bool,
|
||||
fetch_tx: Sender<FetchRequest>,
|
||||
}
|
||||
|
||||
@@ -90,6 +93,7 @@ impl SchemaStore {
|
||||
workspace_roots: Vec::new(),
|
||||
cache_dir: None,
|
||||
policy: FetchPolicy::default(),
|
||||
offline: false,
|
||||
fetch_tx,
|
||||
}
|
||||
}
|
||||
@@ -102,6 +106,10 @@ impl SchemaStore {
|
||||
self.workspace_roots = roots;
|
||||
}
|
||||
|
||||
pub fn set_offline(&mut self, offline: bool) {
|
||||
self.offline = offline;
|
||||
}
|
||||
|
||||
pub fn set_fetch_policy(&mut self, policy: FetchPolicy) {
|
||||
self.policy = policy;
|
||||
}
|
||||
@@ -160,6 +168,9 @@ impl SchemaStore {
|
||||
/// The one entry point that allows a network fetch. Returns whether a
|
||||
/// download was started.
|
||||
pub fn request_download(&mut self, doc_uri: &str) -> bool {
|
||||
if self.offline {
|
||||
return false;
|
||||
}
|
||||
let Some(url) = self.doc_schema.get(doc_uri).cloned() else {
|
||||
return false;
|
||||
};
|
||||
@@ -452,6 +463,20 @@ mod tests {
|
||||
assert!(download.allow_network, "download must allow the network");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_refuses_the_download_entry_point() {
|
||||
let (tx, rx) = unbounded::<FetchRequest>();
|
||||
let mut store = SchemaStore::new(tx);
|
||||
store.set_offline(true);
|
||||
let d = doc(r#"{"$schema": "https://ex.com/s.json"}"#);
|
||||
store.bind_document(&d);
|
||||
let auto = rx.try_iter().next().unwrap();
|
||||
store.on_fetched(FetchResult::NotCached { url: auto.url });
|
||||
|
||||
assert!(!store.request_download("file:///dir/data.json"));
|
||||
assert_eq!(rx.try_iter().count(), 0, "offline must not enqueue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn download_during_cache_probe_escalates_on_miss() {
|
||||
let (tx, rx) = unbounded::<FetchRequest>();
|
||||
|
||||
@@ -115,9 +115,11 @@ impl Server {
|
||||
pub fn new(
|
||||
fetch_tx: Sender<FetchRequest>,
|
||||
cache_dir: Option<PathBuf>,
|
||||
offline: bool,
|
||||
) -> Self {
|
||||
let mut schema_store = SchemaStore::new(fetch_tx);
|
||||
schema_store.set_cache_dir(cache_dir);
|
||||
schema_store.set_offline(offline);
|
||||
Self {
|
||||
initialized: false,
|
||||
shutdown_requested: false,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ use crate::server::Server;
|
||||
/// fetch channel stays connected for the duration of the test.
|
||||
pub fn new_server() -> (Server, Receiver<FetchRequest>) {
|
||||
let (tx, rx) = unbounded::<FetchRequest>();
|
||||
(Server::new(tx, None), rx)
|
||||
(Server::new(tx, None, false), rx)
|
||||
}
|
||||
|
||||
pub fn request(id: i64, method: &str, params: Value) -> IncomingMessage {
|
||||
|
||||
Reference in New Issue
Block a user