languages: Fix local path of JSON and YAML schemas (#44794)

Zhongqiu Zhao and Conrad Irwin created

Closes #30938

Release Notes:

- Fixed: Unable to load relative path JSON schema for YAML validation
(#30938)


This patch follows the vscode LSP client logic, see
[`jsonClient.ts`](https://github.com/microsoft/vscode/blob/cee904f80cc6d15f22c850482789e06b1f536e72/extensions/json-language-features/client/src/jsonClient.ts#L768-L770).
The `url` of the JSON schemas settings and the YAML schemas settings
should be resolved to an absolute path in the LSP client when it is
submitted to the server.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/language/src/language.rs          |  2 
crates/languages/src/json.rs             | 36 +++++++++++++++++++++++++
crates/languages/src/yaml.rs             | 34 +++++++++++++++++++++++
crates/project/src/debugger/dap_store.rs |  2 
crates/project/src/lsp_store.rs          |  6 ++--
crates/worktree/src/worktree.rs          |  2 
6 files changed, 74 insertions(+), 8 deletions(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -379,7 +379,7 @@ pub trait LspAdapterDelegate: Send + Sync {
     fn http_client(&self) -> Arc<dyn HttpClient>;
     fn worktree_id(&self) -> WorktreeId;
     fn worktree_root_path(&self) -> &Path;
-    fn resolve_executable_path(&self, path: PathBuf) -> PathBuf;
+    fn resolve_relative_path(&self, path: PathBuf) -> PathBuf;
     fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
     fn registered_lsp_adapters(&self) -> Vec<Arc<dyn LspAdapter>>;
     async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;

crates/languages/src/json.rs 🔗

@@ -296,7 +296,7 @@ impl LspAdapter for JsonLspAdapter {
         });
         let project_options = cx.update(|cx| {
             language_server_settings(delegate.as_ref(), &self.name(), cx)
-                .and_then(|s| s.settings.clone())
+                .and_then(|s| worktree_root(delegate, s.settings.clone()))
         });
 
         if let Some(override_options) = project_options {
@@ -320,6 +320,40 @@ impl LspAdapter for JsonLspAdapter {
     }
 }
 
+fn worktree_root(delegate: &Arc<dyn LspAdapterDelegate>, settings: Option<Value>) -> Option<Value> {
+    let Some(Value::Object(mut settings_map)) = settings else {
+        return settings;
+    };
+
+    let Some(Value::Object(json_config)) = settings_map.get_mut("json") else {
+        return Some(Value::Object(settings_map));
+    };
+
+    let Some(Value::Array(schemas)) = json_config.get_mut("schemas") else {
+        return Some(Value::Object(settings_map));
+    };
+
+    for schema in schemas.iter_mut() {
+        let Value::Object(schema_map) = schema else {
+            continue;
+        };
+        let Some(Value::String(url)) = schema_map.get_mut("url") else {
+            continue;
+        };
+
+        if !url.starts_with(".") && !url.starts_with("~") {
+            continue;
+        }
+
+        *url = delegate
+            .resolve_relative_path(url.clone().into())
+            .to_string_lossy()
+            .into_owned();
+    }
+
+    Some(Value::Object(settings_map))
+}
+
 async fn get_cached_server_binary(
     container_dir: PathBuf,
     node: &NodeRuntime,

crates/languages/src/yaml.rs 🔗

@@ -156,7 +156,7 @@ impl LspAdapter for YamlLspAdapter {
 
         let project_options = cx.update(|cx| {
             language_server_settings(delegate.as_ref(), &Self::SERVER_NAME, cx)
-                .and_then(|s| s.settings.clone())
+                .and_then(|s| worktree_root(delegate, s.settings.clone()))
         });
         if let Some(override_options) = project_options {
             merge_json_value_into(override_options, &mut options);
@@ -165,6 +165,38 @@ impl LspAdapter for YamlLspAdapter {
     }
 }
 
+fn worktree_root(delegate: &Arc<dyn LspAdapterDelegate>, settings: Option<Value>) -> Option<Value> {
+    let Some(Value::Object(mut settings_map)) = settings else {
+        return settings;
+    };
+
+    let Some(Value::Object(yaml_config)) = settings_map.get_mut("yaml") else {
+        return Some(Value::Object(settings_map));
+    };
+
+    let Some(Value::Object(schemas)) = yaml_config.remove("schemas") else {
+        return Some(Value::Object(settings_map));
+    };
+
+    let schemas = schemas
+        .into_iter()
+        .map(|(url, v)| {
+            if !url.starts_with(".") && !url.starts_with("~") {
+                (url, v)
+            } else {
+                let resolved_url = delegate
+                    .resolve_relative_path(url.into())
+                    .to_string_lossy()
+                    .into_owned();
+                (resolved_url, v)
+            }
+        })
+        .collect::<serde_json::Map<String, Value>>();
+
+    yaml_config.insert("schemas".into(), Value::Object(schemas));
+    Some(Value::Object(settings_map))
+}
+
 async fn get_cached_server_binary(
     container_dir: PathBuf,
     node: &NodeRuntime,

crates/project/src/debugger/dap_store.rs 🔗

@@ -265,7 +265,7 @@ impl DapStore {
                     DapBinary::Default => None,
                     DapBinary::Custom(binary) => {
                         let path = PathBuf::from(binary);
-                        Some(worktree.read(cx).resolve_executable_path(path))
+                        Some(worktree.read(cx).resolve_relative_path(path))
                     }
                 });
                 let user_args = dap_settings.and_then(|s| s.args.clone());

crates/project/src/lsp_store.rs 🔗

@@ -713,7 +713,7 @@ impl LocalLspStore {
                 env.extend(settings.env.unwrap_or_default());
 
                 Ok(LanguageServerBinary {
-                    path: delegate.resolve_executable_path(path),
+                    path: delegate.resolve_relative_path(path),
                     env: Some(env),
                     arguments: settings
                         .arguments
@@ -14069,8 +14069,8 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         self.worktree.abs_path().as_ref()
     }
 
-    fn resolve_executable_path(&self, path: PathBuf) -> PathBuf {
-        self.worktree.resolve_executable_path(path)
+    fn resolve_relative_path(&self, path: PathBuf) -> PathBuf {
+        self.worktree.resolve_relative_path(path)
     }
 
     async fn shell_env(&self) -> HashMap<String, String> {

crates/worktree/src/worktree.rs 🔗

@@ -2495,7 +2495,7 @@ impl Snapshot {
     ///
     /// Relative paths that do not exist in the worktree may
     /// still be found using the `PATH` environment variable.
-    pub fn resolve_executable_path(&self, path: PathBuf) -> PathBuf {
+    pub fn resolve_relative_path(&self, path: PathBuf) -> PathBuf {
         if let Some(path_str) = path.to_str() {
             if let Some(remaining_path) = path_str.strip_prefix("~/") {
                 return home_dir().join(remaining_path);