use crate::operation::LspCompletionItem;
use crate::schema::context::{CompletionMode, CursorContext};
use crate::schema::format;
use regex::Regex;
use serde_json::Value;
use std::path::Path;
pub fn completions_from_parsed_schema(
root: &Value,
lines: &[String],
trigger: crate::editor::position::Position,
path: Option<&Path>,
) -> Vec<LspCompletionItem> {
match format::detect_cursor_context(lines, &trigger, path) {
Some(ctx) => completions_for_context(root, &ctx),
None => legacy_heuristic(root, lines, &trigger),
}
}
pub fn completions_from_schema(
schema_json: &str,
lines: &[String],
trigger: crate::editor::position::Position,
path: Option<&Path>,
) -> Vec<LspCompletionItem> {
match serde_json::from_str::<Value>(schema_json) {
Ok(root) => completions_from_parsed_schema(&root, lines, trigger, path),
Err(_) => Vec::new(),
}
}
pub fn completions_for_context(root: &Value, ctx: &CursorContext) -> Vec<LspCompletionItem> {
match &ctx.mode {
CompletionMode::Key => {
if let Some(section_schema) = walk_section(root, &ctx.section_path) {
suggest_properties(root, section_schema)
} else {
Vec::new()
}
}
CompletionMode::Value { key } => {
if let Some(section_schema) = walk_section(root, &ctx.section_path)
&& let Some(props) = section_schema
.get("properties")
.and_then(|p| p.as_object())
&& let Some(key_schema) = props.get(key.as_str()) {
return suggest_values(root, key_schema);
}
Vec::new()
}
CompletionMode::InlineKey => {
if let Some(section_schema) = walk_section(root, &ctx.section_path) {
if let Some(add_props) = section_schema.get("additionalProperties") {
let resolved = resolve_concrete(root, add_props, 8);
return suggest_properties(root, resolved);
}
return suggest_properties(root, section_schema);
}
Vec::new()
}
}
}
fn walk_section<'a>(root: &'a Value, section_path: &[String]) -> Option<&'a Value> {
let mut cur = root;
for key in section_path {
let props = cur.get("properties")?.as_object()?;
let next = props.get(key.as_str())?;
cur = resolve_concrete(root, next, 8);
}
Some(cur)
}
pub fn resolve_ref<'a>(root: &'a Value, schema: &'a Value) -> &'a Value {
let mut cur = schema;
for _ in 0..16 {
if let Some(ref_str) = cur.get("$ref").and_then(|r| r.as_str())
&& let Some(def_path) = ref_str.strip_prefix("#/") {
let mut next = root;
let mut ok = true;
for part in def_path.split('/') {
match next.get(part) {
Some(n) => next = n,
None => { ok = false; break; }
}
}
if ok && !std::ptr::eq(cur, next) {
cur = next;
continue;
}
}
break;
}
cur
}
pub fn has_concrete_properties(root: &Value, schema: &Value, depth: u8) -> bool {
if depth == 0 {
return false;
}
let resolved = resolve_ref(root, schema);
if resolved.get("properties").is_some() {
return true;
}
if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
return any_of.iter().any(|opt| {
let r = resolve_ref(root, opt);
!is_null_schema(r) && has_concrete_properties(root, r, depth - 1)
});
}
false
}
pub fn resolve_concrete<'a>(root: &'a Value, schema: &'a Value, depth: u8) -> &'a Value {
if depth == 0 {
return schema;
}
let resolved = resolve_ref(root, schema);
if resolved.get("properties").is_some() {
return resolved;
}
if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
for option in any_of {
let r = resolve_ref(root, option);
if !is_null_schema(r) && has_concrete_properties(root, r, depth - 1) {
return resolve_concrete(root, r, depth - 1);
}
}
for option in any_of {
let r = resolve_ref(root, option);
if !is_null_schema(r) {
return resolve_concrete(root, r, depth - 1);
}
}
}
resolved
}
pub fn is_null_schema(schema: &Value) -> bool {
match schema.get("type") {
Some(Value::String(s)) => s == "null",
Some(Value::Array(arr)) => {
arr.len() == 1 && arr.first().and_then(|v| v.as_str()) == Some("null")
}
_ => false,
}
}
pub fn is_boolean_schema(root: &Value, schema: &Value, depth: u8) -> bool {
if depth == 0 {
return false;
}
let resolved = resolve_ref(root, schema);
let ty_is_bool = |v: &Value| match v {
Value::String(s) => s == "boolean" || s == "bool",
Value::Array(arr) => arr
.iter()
.any(|x| x.as_str() == Some("boolean") || x.as_str() == Some("bool")),
_ => false,
};
if let Some(t) = resolved.get("type")
&& ty_is_bool(t) {
return true;
}
if let Some(any_of) = resolved.get("anyOf").and_then(|a| a.as_array()) {
return any_of.iter().any(|opt| {
let r = resolve_ref(root, opt);
!is_null_schema(r) && is_boolean_schema(root, r, depth - 1)
});
}
false
}
pub fn suggest_properties(root: &Value, schema: &Value) -> Vec<LspCompletionItem> {
let mut out = Vec::new();
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
for (k, v) in props {
let resolved = resolve_concrete(root, v, 3);
let detail = resolved
.get("type")
.and_then(|t| t.as_str())
.map(String::from);
out.push(LspCompletionItem {
label: k.clone(),
kind: Some(10),
detail,
insert_text: None,
});
}
return out;
}
let resolved = resolve_concrete(root, schema, 6);
if let Some(props) = resolved.get("properties").and_then(|p| p.as_object()) {
for (k, v) in props {
let res = resolve_concrete(root, v, 3);
let detail = res
.get("type")
.and_then(|t| t.as_str())
.map(String::from);
out.push(LspCompletionItem {
label: k.clone(),
kind: Some(10),
detail,
insert_text: None,
});
}
}
out
}
pub fn suggest_values(root: &Value, schema: &Value) -> Vec<LspCompletionItem> {
let mut out = Vec::new();
if let Some(enum_vals) = schema.get("enum").and_then(|e| e.as_array()) {
for v in enum_vals {
let label = v.as_str().map(String::from).unwrap_or_else(|| v.to_string());
out.push(LspCompletionItem {
label: label.clone(),
kind: Some(12),
detail: None,
insert_text: Some(label),
});
}
return out;
}
if is_boolean_schema(root, schema, 8) {
out.push(LspCompletionItem {
label: "true".into(),
kind: Some(12),
detail: Some("boolean".into()),
insert_text: Some("true".into()),
});
out.push(LspCompletionItem {
label: "false".into(),
kind: Some(12),
detail: Some("boolean".into()),
insert_text: Some("false".into()),
});
}
out
}
fn legacy_heuristic(
schema_val: &Value,
lines: &[String],
trigger: &crate::editor::position::Position,
) -> Vec<LspCompletionItem> {
let mut out: Vec<LspCompletionItem> = Vec::new();
let mut text_up_to_cursor = String::new();
for (i, line) in lines.iter().enumerate() {
if i < trigger.line {
text_up_to_cursor.push_str(line);
text_up_to_cursor.push('\n');
} else if i == trigger.line {
let col = trigger.column.min(line.len());
text_up_to_cursor.push_str(&line[..col]);
break;
} else {
break;
}
}
let re_q = Regex::new("\"([^\"\\\\]+)\"\\s*:").unwrap();
let re_unq = Regex::new(r"(?m)^\s*([A-Za-z0-9_\-]+)\s*:").unwrap();
let mut keys: Vec<String> = Vec::new();
for caps in re_q.captures_iter(&text_up_to_cursor) {
if let Some(m) = caps.get(1) {
keys.push(m.as_str().to_string());
}
}
if keys.is_empty() {
for caps in re_unq.captures_iter(&text_up_to_cursor) {
if let Some(m) = caps.get(1) {
keys.push(m.as_str().to_string());
}
}
}
if keys.is_empty() {
if let Some(props) = schema_val.get("properties").and_then(|p| p.as_object()) {
for (k, v) in props.iter() {
let detail = v
.get("type")
.and_then(|t| t.as_str())
.map(|s| s.to_string());
out.push(LspCompletionItem {
label: k.clone(),
kind: Some(10),
detail,
insert_text: None,
});
}
}
return out;
}
let mut cur = schema_val;
for key in &keys {
if let Some(props) = cur.get("properties").and_then(|p| p.as_object()) {
if let Some(next) = props.get(key) {
cur = next;
continue;
} else {
break;
}
} else {
break;
}
}
if let Some(enum_vals) = cur.get("enum").and_then(|e| e.as_array()) {
for v in enum_vals {
let label = if let Some(s) = v.as_str() {
s.to_string()
} else {
v.to_string()
};
out.push(LspCompletionItem {
label: label.clone(),
kind: Some(12),
detail: None,
insert_text: Some(label),
});
}
return out;
}
if let Some(props) = cur.get("properties").and_then(|p| p.as_object()) {
for (k, v) in props.iter() {
let detail = v
.get("type")
.and_then(|t| t.as_str())
.map(|s| s.to_string());
out.push(LspCompletionItem {
label: k.clone(),
kind: Some(10),
detail,
insert_text: None,
});
}
return out;
}
if let Some(t) = cur.get("type").and_then(|t| t.as_str())
&& (t == "boolean" || t == "bool") {
out.push(LspCompletionItem {
label: "true".into(),
kind: Some(12),
detail: Some("boolean".into()),
insert_text: Some("true".into()),
});
out.push(LspCompletionItem {
label: "false".into(),
kind: Some(12),
detail: Some("boolean".into()),
insert_text: Some("false".into()),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::position::Position;
fn pos(line: usize, col: usize) -> Position {
Position { line, column: col }
}
#[test]
fn top_level_keys_suggested() {
let schema =
r#"{"type":"object","properties":{"foo":{"type":"string"},"bar":{"type":"number"}}}"#;
let lines = vec![String::new()];
let items = completions_from_schema(schema, &lines, pos(0, 0), None);
let labels: Vec<String> = items.into_iter().map(|i| i.label).collect();
assert!(labels.contains(&"foo".to_string()));
assert!(labels.contains(&"bar".to_string()));
}
#[test]
fn nested_enum_values_suggested() {
let schema = r#"{"type":"object","properties":{"parent":{"type":"object","properties":{"child":{"type":"string","enum":["a","b"]}}}}}"#;
let lines = vec![r#"{"parent": {"child":"#.to_string()];
let items = completions_from_schema(schema, &lines, pos(0, lines[0].len()), None);
let labels: Vec<String> = items.into_iter().map(|i| i.label).collect();
assert!(labels.contains(&"a".to_string()));
assert!(labels.contains(&"b".to_string()));
}
#[test]
fn resolve_ref_follows_defs() {
let schema: Value = serde_json::from_str(
r##"{"$defs":{"Foo":{"type":"object","properties":{"x":{"type":"string"}}}},"properties":{"a":{"$ref":"#/$defs/Foo"}}}"##
).unwrap();
let a = schema["properties"]["a"].clone();
let resolved = resolve_ref(&schema, &a);
assert!(resolved.get("properties").is_some());
}
#[test]
fn is_boolean_schema_detects_boolean_in_any_of() {
let root: Value = serde_json::from_str(
r#"{"$defs":{"BoolOrNull":{"anyOf":[{"type":"boolean"},{"type":"null"}]}}}"#,
)
.unwrap();
let schema: Value = serde_json::from_str(r##"{"$ref":"#/$defs/BoolOrNull"}"##).unwrap();
assert!(is_boolean_schema(&root, &schema, 5));
}
#[test]
fn suggest_values_returns_bool_literals() {
let root: Value = serde_json::from_str("{}").unwrap();
let schema: Value = serde_json::from_str(r#"{"type":"boolean"}"#).unwrap();
let items = suggest_values(&root, &schema);
let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"true"));
assert!(labels.contains(&"false"));
}
}