Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6145f2e03 | ||
|
|
0875c682a0 |
Generated
+1
-1
@@ -95,7 +95,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "json-ls"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"lexopt",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "json-ls"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
edition = "2024"
|
||||
description = "A JSON/JSONC language server"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -64,7 +64,9 @@ Relative and `file://` URLs resolve against the document. Workspace-local
|
||||
schema files load automatically. Remote schemas are served from the on-disk
|
||||
cache when present, and are otherwise offered as a code lens and code action.
|
||||
Once a schema is loaded, the same lens and action offer to update it, which
|
||||
re-fetches the schema and overwrites the cached copy. Opening or editing a
|
||||
re-fetches the schema and overwrites the cached copy. Opening a document
|
||||
whose schema's cached or local file changed on disk reloads it, so an
|
||||
update made by another server instance is picked up. Opening or editing a
|
||||
document never touches the network: only accepting a download or update
|
||||
offer does, via the `json-ls.downloadSchema` command.
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ mod tests {
|
||||
let mut store = store();
|
||||
store.bind_document(&doc);
|
||||
store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: "https://ex.com/s.json".to_owned(),
|
||||
root: serde_json::json!({}),
|
||||
});
|
||||
|
||||
+61
-52
@@ -20,11 +20,15 @@ const DOC_URI: &str = "file:///t.json";
|
||||
const SCHEMA_URL: &str = "https://example.test/schema.json";
|
||||
|
||||
fn open(server: &mut Server, text: &str) -> Vec<Outgoing> {
|
||||
open_at(server, DOC_URI, text)
|
||||
}
|
||||
|
||||
fn open_at(server: &mut Server, uri: &str, text: &str) -> Vec<Outgoing> {
|
||||
server.handle(notification(
|
||||
"textDocument/didOpen",
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": DOC_URI,
|
||||
"uri": uri,
|
||||
"languageId": "json",
|
||||
"version": 1,
|
||||
"text": text
|
||||
@@ -33,6 +37,17 @@ fn open(server: &mut Server, text: &str) -> Vec<Outgoing> {
|
||||
))
|
||||
}
|
||||
|
||||
fn download_schema(server: &mut Server, id: i64) -> Vec<Outgoing> {
|
||||
server.handle(request(
|
||||
id,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
fn find_request<'a>(outgoing: &'a [Outgoing], method: &str) -> &'a Request {
|
||||
outgoing
|
||||
.iter()
|
||||
@@ -96,6 +111,7 @@ fn ready_server(
|
||||
open(&mut server, doc_text);
|
||||
let fetch = rx.try_recv().expect("a schema fetch was requested");
|
||||
server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: fetch.url,
|
||||
root: schema,
|
||||
});
|
||||
@@ -157,18 +173,12 @@ fn schema_is_fetched_only_on_the_download_command() {
|
||||
json!("json-ls.downloadSchema")
|
||||
);
|
||||
|
||||
server.handle(request(
|
||||
3,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
));
|
||||
download_schema(&mut server, 3);
|
||||
let download = rx.try_recv().expect("the command started a download");
|
||||
assert!(download.allow_network, "the command must allow the network");
|
||||
|
||||
let out = server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: download.url,
|
||||
root: json!({"properties": {"name": {"type": "string"}}}),
|
||||
});
|
||||
@@ -187,6 +197,7 @@ fn update_command_refetches_a_loaded_schema() {
|
||||
open(&mut server, &doc_with_schema(r#""name": 42"#));
|
||||
let auto = rx.try_recv().expect("a cache-only load was requested");
|
||||
server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: auto.url,
|
||||
root: json!({}),
|
||||
});
|
||||
@@ -200,17 +211,11 @@ fn update_command_refetches_a_loaded_schema() {
|
||||
assert_eq!(lenses.len(), 1);
|
||||
assert_eq!(lenses[0]["command"]["title"], json!("Update schema"));
|
||||
|
||||
server.handle(request(
|
||||
3,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
));
|
||||
download_schema(&mut server, 3);
|
||||
let update = rx.try_recv().expect("the command started an update");
|
||||
assert!(update.allow_network, "the update must allow the network");
|
||||
let out = server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: update.url,
|
||||
root: json!({"properties": {"name": {"type": "string"}}}),
|
||||
});
|
||||
@@ -231,6 +236,7 @@ fn schema_fetch_produces_validation_diagnostics() {
|
||||
// inspect the diagnostics republished as a result.
|
||||
let fetch = rx.try_recv().expect("a schema fetch was requested");
|
||||
let out = server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: fetch.url,
|
||||
root: json!({"properties": {"name": {"type": "string"}}}),
|
||||
});
|
||||
@@ -255,14 +261,7 @@ fn failed_download_shows_an_error_message() {
|
||||
});
|
||||
assert!(show_messages(&out).is_empty());
|
||||
|
||||
server.handle(request(
|
||||
2,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
));
|
||||
download_schema(&mut server, 2);
|
||||
let download = rx.try_recv().expect("the command started a download");
|
||||
let out = server.handle_fetch(FetchResult::Err {
|
||||
url: download.url,
|
||||
@@ -318,6 +317,7 @@ fn approved_redirect_resumes_the_download() {
|
||||
// The schema fetched from the redirect target binds and validates under
|
||||
// the URL the document declares.
|
||||
let out = server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: resumed.url,
|
||||
root: json!({"properties": {"name": {"type": "string"}}}),
|
||||
});
|
||||
@@ -337,14 +337,7 @@ fn declined_redirect_leaves_the_download_retryable() {
|
||||
server.handle(response(&prompt_id, Value::Null));
|
||||
assert!(rx.try_recv().is_err(), "a decline must not fetch");
|
||||
|
||||
server.handle(request(
|
||||
3,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
));
|
||||
download_schema(&mut server, 3);
|
||||
let retried = rx.try_recv().expect("a fresh download after the decline");
|
||||
assert_eq!(retried.hops, 0);
|
||||
}
|
||||
@@ -374,6 +367,38 @@ fn completion_offers_enum_values() {
|
||||
assert!(labels.contains(&"\"green\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_from_one_document_refreshes_another() {
|
||||
let schema = json!({"properties": {"name": {"type": "string"}}});
|
||||
let text = doc_with_schema(r#""a": 1"#);
|
||||
let (mut server, rx) = ready_server(&text, schema);
|
||||
|
||||
let other_uri = "file:///u.json";
|
||||
open_at(&mut server, other_uri, &text);
|
||||
assert!(rx.try_recv().is_err(), "no fetch for a loaded schema");
|
||||
|
||||
download_schema(&mut server, 2);
|
||||
let update = rx.try_recv().expect("an update fetch");
|
||||
server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: update.url,
|
||||
root: json!({"properties": {"color": {"type": "string"}}}),
|
||||
});
|
||||
|
||||
let out = server.handle(request(
|
||||
3,
|
||||
"textDocument/completion",
|
||||
json!({
|
||||
"textDocument": {"uri": other_uri},
|
||||
"position": {"line": 0, "character": 1}
|
||||
}),
|
||||
));
|
||||
let items = response_result(&out).as_array().unwrap();
|
||||
let labels: Vec<&str> =
|
||||
items.iter().map(|i| i["label"].as_str().unwrap()).collect();
|
||||
assert!(labels.contains(&"color"), "labels were: {labels:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_documentation_arrives_via_resolve() {
|
||||
let schema = json!({
|
||||
@@ -489,21 +514,12 @@ fn definition_jumps_from_a_data_key_to_the_schema() {
|
||||
let (mut server, rx) = new_server();
|
||||
server.handle(request(1, "initialize", json!({})));
|
||||
let data_text = format!(r#"{{"$schema": "{schema_uri}", "name": "x"}}"#);
|
||||
server.handle(notification(
|
||||
"textDocument/didOpen",
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": data_uri,
|
||||
"languageId": "json",
|
||||
"version": 1,
|
||||
"text": data_text
|
||||
}
|
||||
}),
|
||||
));
|
||||
open_at(&mut server, &data_uri, &data_text);
|
||||
// The file schema auto-loads because it sits in the document's directory.
|
||||
// Satisfy it.
|
||||
let fetch = rx.try_recv().expect("a schema fetch was requested");
|
||||
server.handle_fetch(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: fetch.url,
|
||||
root: serde_json::from_str(schema_body).unwrap(),
|
||||
});
|
||||
@@ -600,14 +616,7 @@ fn download_request(
|
||||
open(server, text);
|
||||
let probe = rx.try_recv().expect("a cache-only load was requested");
|
||||
server.handle_fetch(FetchResult::NotCached { url: probe.url });
|
||||
server.handle(request(
|
||||
9,
|
||||
"workspace/executeCommand",
|
||||
json!({
|
||||
"command": "json-ls.downloadSchema",
|
||||
"arguments": [DOC_URI]
|
||||
}),
|
||||
));
|
||||
download_schema(server, 9);
|
||||
rx.try_recv().expect("the command started a download")
|
||||
}
|
||||
|
||||
|
||||
+57
-21
@@ -7,6 +7,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use crossbeam_channel::Sender;
|
||||
@@ -129,11 +130,12 @@ pub enum FetchResult {
|
||||
Ok {
|
||||
url: String,
|
||||
root: Value,
|
||||
/// Modification time of the backing file the content was read from,
|
||||
/// captured at read time so it never describes a newer copy.
|
||||
mtime: Option<SystemTime>,
|
||||
},
|
||||
/// A cache-only load found nothing, so the user must request a download.
|
||||
NotCached {
|
||||
url: String,
|
||||
},
|
||||
NotCached { url: String },
|
||||
/// The download hit a cross-origin redirect that needs the user's
|
||||
/// approval before it continues.
|
||||
Redirected {
|
||||
@@ -186,17 +188,32 @@ pub fn default_cache_dir() -> Option<PathBuf> {
|
||||
|
||||
fn fetch_one(req: &FetchRequest, cache_dir: Option<&Path>) -> FetchResult {
|
||||
let url = &req.url;
|
||||
let cache_file = cache_dir.map(|dir| cache_path(dir, url));
|
||||
let cache_file = if is_http_url(url) {
|
||||
cache_dir.map(|dir| cache_path(dir, url))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Mtimes are taken before their content is read, so a concurrent
|
||||
// replacement can only make a copy look older than it is, never newer.
|
||||
let local_mtime = resolve::file_url_to_path(url)
|
||||
.and_then(|path| fs::metadata(path).ok())
|
||||
.and_then(|meta| meta.modified().ok());
|
||||
|
||||
if !req.allow_network
|
||||
&& let Some(path) = &cache_file
|
||||
&& let Ok(bytes) = fs::read(path)
|
||||
&& let Ok(root) = serde_json::from_slice::<Value>(&bytes)
|
||||
{
|
||||
return FetchResult::Ok {
|
||||
url: url.clone(),
|
||||
root,
|
||||
};
|
||||
let mtime = fs::metadata(path)
|
||||
.ok()
|
||||
.and_then(|meta| meta.modified().ok());
|
||||
if let Ok(bytes) = fs::read(path)
|
||||
&& let Ok(root) = serde_json::from_slice::<Value>(&bytes)
|
||||
{
|
||||
return FetchResult::Ok {
|
||||
url: url.clone(),
|
||||
root,
|
||||
mtime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// A remote schema that is not cached needs an explicit download.
|
||||
@@ -259,12 +276,14 @@ fn fetch_one(req: &FetchRequest, cache_dir: Option<&Path>) -> FetchResult {
|
||||
|
||||
match serde_json::from_str::<Value>(&body) {
|
||||
Ok(root) => {
|
||||
if let Some(path) = &cache_file {
|
||||
write_cache(path, body.as_bytes());
|
||||
}
|
||||
let mtime = match &cache_file {
|
||||
Some(path) => write_cache(path, body.as_bytes()),
|
||||
None => local_mtime,
|
||||
};
|
||||
FetchResult::Ok {
|
||||
url: url.clone(),
|
||||
root,
|
||||
mtime,
|
||||
}
|
||||
}
|
||||
Err(e) => err(format!("invalid schema JSON: {e}")),
|
||||
@@ -430,19 +449,29 @@ pub(crate) fn cache_path(dir: &Path, url: &str) -> PathBuf {
|
||||
dir.join(format!("{hash:016x}.json"))
|
||||
}
|
||||
|
||||
fn write_cache(path: &Path, bytes: &[u8]) {
|
||||
fn write_cache(path: &Path, bytes: &[u8]) -> Option<SystemTime> {
|
||||
if let Some(parent) = path.parent()
|
||||
&& fs::create_dir_all(parent).is_err()
|
||||
{
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
// The rename swaps the copy in atomically, so a concurrent read never
|
||||
// sees a partial write and a failure leaves the previous copy intact.
|
||||
// The temp file is statted before the rename (which preserves mtime),
|
||||
// so the returned mtime belongs to exactly this content.
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
let written = fs::write(&tmp, bytes).and_then(|()| fs::rename(&tmp, path));
|
||||
if let Err(e) = written {
|
||||
drop(fs::remove_file(&tmp));
|
||||
eprintln!("json-ls: failed to write schema cache: {e}");
|
||||
let written = fs::write(&tmp, bytes).and_then(|()| {
|
||||
let mtime = fs::metadata(&tmp)?.modified()?;
|
||||
fs::rename(&tmp, path)?;
|
||||
Ok(mtime)
|
||||
});
|
||||
match written {
|
||||
Ok(mtime) => Some(mtime),
|
||||
Err(e) => {
|
||||
drop(fs::remove_file(&tmp));
|
||||
eprintln!("json-ls: failed to write schema cache: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,8 +581,9 @@ mod tests {
|
||||
|
||||
// A cache hit is returned even when the network is not allowed.
|
||||
match fetch(url, false, FetchPolicy::default(), Some(&dir)) {
|
||||
FetchResult::Ok { root, .. } => {
|
||||
FetchResult::Ok { root, mtime, .. } => {
|
||||
assert_eq!(root, json!({"type": "string"}));
|
||||
assert!(mtime.is_some(), "a cache hit must carry its mtime");
|
||||
}
|
||||
other => panic!("expected cache hit: {}", describe(&other)),
|
||||
}
|
||||
@@ -567,8 +597,14 @@ mod tests {
|
||||
fs::write(cache_path(&dir, &url), br#"{"type":"string"}"#).unwrap();
|
||||
|
||||
match fetch(&url, true, loopback_policy(), Some(&dir)) {
|
||||
FetchResult::Ok { root, .. } => {
|
||||
FetchResult::Ok { root, mtime, .. } => {
|
||||
assert_eq!(root, json!({"type": "number"}));
|
||||
let cached = cache_path(&dir, &url);
|
||||
assert_eq!(
|
||||
mtime,
|
||||
Some(fs::metadata(cached).unwrap().modified().unwrap()),
|
||||
"the mtime must match the written cache copy"
|
||||
);
|
||||
}
|
||||
other => panic!("expected a fresh fetch: {}", describe(&other)),
|
||||
}
|
||||
|
||||
+96
-5
@@ -3,6 +3,7 @@ use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use serde_json::Value;
|
||||
@@ -65,6 +66,10 @@ pub struct SchemaStore {
|
||||
/// editing does not repeat the cache probe or the workspace-containment
|
||||
/// filesystem check on every change.
|
||||
auto_attempted: HashSet<String>,
|
||||
/// Modification time of each loaded schema's backing file, so opening a
|
||||
/// document can spot a copy refreshed on disk by another server
|
||||
/// instance.
|
||||
source_mtime: HashMap<String, SystemTime>,
|
||||
waiting: HashMap<String, HashSet<String>>,
|
||||
doc_schema: HashMap<String, String>,
|
||||
workspace_roots: Vec<PathBuf>,
|
||||
@@ -88,6 +93,7 @@ impl SchemaStore {
|
||||
pending: HashSet::new(),
|
||||
want_network: HashSet::new(),
|
||||
auto_attempted: HashSet::new(),
|
||||
source_mtime: HashMap::new(),
|
||||
waiting: HashMap::new(),
|
||||
doc_schema: HashMap::new(),
|
||||
workspace_roots: Vec::new(),
|
||||
@@ -183,14 +189,18 @@ impl SchemaStore {
|
||||
let Some(url) = self.doc_schema.get(doc_uri).cloned() else {
|
||||
return false;
|
||||
};
|
||||
let docs = self.waiting.entry(url.clone()).or_default();
|
||||
self.enroll_bound_documents(&url);
|
||||
self.enqueue(url, true);
|
||||
true
|
||||
}
|
||||
|
||||
fn enroll_bound_documents(&mut self, url: &str) {
|
||||
let docs = self.waiting.entry(url.to_owned()).or_default();
|
||||
for (doc, bound) in &self.doc_schema {
|
||||
if *bound == url {
|
||||
if bound == url {
|
||||
docs.insert(doc.clone());
|
||||
}
|
||||
}
|
||||
self.enqueue(url, true);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn schema_for(&self, doc_uri: &str) -> Option<Arc<SchemaDocument>> {
|
||||
@@ -227,6 +237,10 @@ impl SchemaStore {
|
||||
if !self.memory.contains_key(url) {
|
||||
return None;
|
||||
}
|
||||
self.source_path_for_url(url)
|
||||
}
|
||||
|
||||
fn source_path_for_url(&self, url: &str) -> Option<PathBuf> {
|
||||
if let Some(path) = resolve::file_url_to_path(url) {
|
||||
return Some(path);
|
||||
}
|
||||
@@ -235,6 +249,30 @@ impl SchemaStore {
|
||||
.map(|dir| crate::schema::fetch::cache_path(dir, url))
|
||||
}
|
||||
|
||||
/// Reload the schema bound to `doc_uri` from disk when its backing file
|
||||
/// changed since it was loaded. Never fetches over the network.
|
||||
pub fn reload_if_changed(&mut self, doc_uri: &str) {
|
||||
let Some(url) = self.doc_schema.get(doc_uri) else {
|
||||
return;
|
||||
};
|
||||
if !self.memory.contains_key(url) {
|
||||
return;
|
||||
}
|
||||
let Some(mtime) = self
|
||||
.source_path_for_url(url)
|
||||
.and_then(|path| std::fs::metadata(path).ok())
|
||||
.and_then(|meta| meta.modified().ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if self.source_mtime.get(url) == Some(&mtime) {
|
||||
return;
|
||||
}
|
||||
let url = url.clone();
|
||||
self.enroll_bound_documents(&url);
|
||||
self.enqueue(url, false);
|
||||
}
|
||||
|
||||
pub fn is_loaded(&self, doc_uri: &str) -> bool {
|
||||
self.doc_schema
|
||||
.get(doc_uri)
|
||||
@@ -249,8 +287,16 @@ impl SchemaStore {
|
||||
// the user's decision arrives via `follow_redirect` or
|
||||
// `abandon_fetch`.
|
||||
FetchResult::Redirected { .. } => Vec::new(),
|
||||
FetchResult::Ok { url, root } => {
|
||||
FetchResult::Ok { url, root, mtime } => {
|
||||
self.pending.remove(&url);
|
||||
match mtime {
|
||||
Some(mtime) => {
|
||||
self.source_mtime.insert(url.clone(), mtime);
|
||||
}
|
||||
None => {
|
||||
self.source_mtime.remove(&url);
|
||||
}
|
||||
}
|
||||
// A download requested while this load was in flight still
|
||||
// needs the network: deliver this copy now, but keep the
|
||||
// documents waiting for the fresh one.
|
||||
@@ -264,6 +310,7 @@ impl SchemaStore {
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect();
|
||||
self.sources.remove(&url);
|
||||
self.memory.insert(
|
||||
url.clone(),
|
||||
Arc::new(SchemaDocument { url, root }),
|
||||
@@ -466,6 +513,7 @@ mod tests {
|
||||
store.bind_document(&d);
|
||||
|
||||
let waiting = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: "https://ex.com/s.json".to_owned(),
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
@@ -519,6 +567,7 @@ mod tests {
|
||||
store.bind_document(&other);
|
||||
let probe = rx.try_iter().next().unwrap();
|
||||
store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: probe.url,
|
||||
root: json!({}),
|
||||
});
|
||||
@@ -529,6 +578,7 @@ mod tests {
|
||||
assert!(update.allow_network);
|
||||
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: update.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
@@ -579,6 +629,7 @@ mod tests {
|
||||
assert_eq!(rx.try_iter().count(), 0);
|
||||
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: probe.url,
|
||||
root: json!({}),
|
||||
});
|
||||
@@ -587,6 +638,7 @@ mod tests {
|
||||
assert!(escalated.allow_network);
|
||||
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: escalated.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
@@ -601,6 +653,7 @@ mod tests {
|
||||
store.bind_document(&d);
|
||||
let probe = rx.try_iter().next().unwrap();
|
||||
store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: probe.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
@@ -608,6 +661,7 @@ mod tests {
|
||||
assert!(store.request_download("file:///dir/data.json"));
|
||||
let update = rx.try_iter().next().unwrap();
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: update.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
@@ -623,6 +677,7 @@ mod tests {
|
||||
store.bind_document(&d);
|
||||
let probe = rx.try_iter().next().unwrap();
|
||||
store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: probe.url,
|
||||
root: json!({}),
|
||||
});
|
||||
@@ -637,12 +692,47 @@ mod tests {
|
||||
);
|
||||
assert!(store.bind_document(&other).is_some());
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: update.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
assert!(refreshed.contains(&"file:///dir/other.json".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_reloads_a_schema_whose_cache_copy_changed() {
|
||||
let dir = temp_dir("recheck");
|
||||
let (tx, rx) = unbounded::<FetchRequest>();
|
||||
let mut store = SchemaStore::new(tx);
|
||||
store.set_cache_dir(Some(dir.clone()));
|
||||
let d = doc(r#"{"$schema": "https://ex.com/s.json"}"#);
|
||||
store.bind_document(&d);
|
||||
let probe = rx.try_iter().next().unwrap();
|
||||
let cache = crate::schema::fetch::cache_path(&dir, &probe.url);
|
||||
std::fs::write(&cache, b"{}").unwrap();
|
||||
let mtime = std::fs::metadata(&cache).unwrap().modified().unwrap();
|
||||
store.on_fetched(FetchResult::Ok {
|
||||
mtime: Some(mtime),
|
||||
url: probe.url.clone(),
|
||||
root: json!({}),
|
||||
});
|
||||
|
||||
store.reload_if_changed("file:///dir/data.json");
|
||||
assert_eq!(rx.try_iter().count(), 0);
|
||||
|
||||
// Another instance replaced the cached copy.
|
||||
store.source_mtime.insert(probe.url, std::time::UNIX_EPOCH);
|
||||
store.reload_if_changed("file:///dir/data.json");
|
||||
let reload = rx.try_iter().next().expect("a cache reload");
|
||||
assert!(!reload.allow_network, "a reload must not hit the network");
|
||||
let refreshed = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: reload.url,
|
||||
root: json!({"type": "object"}),
|
||||
});
|
||||
assert!(refreshed.contains(&"file:///dir/data.json".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redirected_download_stays_pending_until_decided() {
|
||||
let (tx, rx) = unbounded::<FetchRequest>();
|
||||
@@ -711,6 +801,7 @@ mod tests {
|
||||
store.unbind_document("file:///dir/data.json");
|
||||
|
||||
let waiting = store.on_fetched(FetchResult::Ok {
|
||||
mtime: None,
|
||||
url: "https://ex.com/s.json".to_owned(),
|
||||
root: json!({}),
|
||||
});
|
||||
|
||||
@@ -341,6 +341,7 @@ impl Server {
|
||||
self.documents
|
||||
.open(uri.clone(), doc.version, doc.text, dialect);
|
||||
let schema = self.bind_schema(&uri);
|
||||
self.schema_store.reload_if_changed(&uri);
|
||||
self.refresh_diagnostics_with(&uri, schema.as_deref())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user