crates/laymesh-language/src/lib.rs 1//! Static, non-evaluating language services shared by native LSP and WebAssembly.2mod bindings;3mod colors;4mod index;5pub mod inspect;6pub mod lsp;7use index::{Symbol, Token, index, tokens};8use laymesh_core::{9 engine::{API, predefined_value, predefined_variables},10 parser::{self, Expr, ExprKind, Stmt, StmtKind},11};12use regex::Regex;13use serde_json::{Value as J, json};14use std::collections::{BTreeMap, BTreeSet};15use std::{16 cell::RefCell,17 hash::{Hash, Hasher},18};19#[derive(Default)]20pub struct LanguageService {21 pub documents: BTreeMap<String, String>,22 pub locale: String,23 indexes: RefCell<BTreeMap<String, (u64, bool, index::Index)>>,24 bindings: RefCell<Option<(u64, bindings::Graph)>>,25}26#[derive(Default)]27struct Context {28 name: String,29 receiver: String,30 parameter: String,31 used: Vec<String>,32 positional: usize,33 prefix: String,34}35pub(crate) fn resolve(uri: &str, relative: &str) -> String {36 if let Some(path) = uri.strip_prefix("file://") {37 // URI paths are independent of the host filesystem's separators.38 let (authority, path) = if path.starts_with('/') {39 ("", path)40 } else {41 path.split_once('/').unwrap_or((path, ""))42 };43 let relative = if cfg!(windows) {44 relative.replace('\\', "/")45 } else {46 relative.to_owned()47 };48 let mut encoded = String::new();49 for byte in relative.bytes() {50 if byte.is_ascii_alphanumeric() || b"-._~/:".contains(&byte) {51 encoded.push(byte as char);52 } else {53 encoded.push_str(&format!("%{byte:02X}"));54 }55 }56 let drive_path = relative.as_bytes().get(1) == Some(&b':')57 && relative58 .as_bytes()59 .first()60 .is_some_and(u8::is_ascii_alphabetic);61 let joined = if drive_path {62 format!("/{encoded}")63 } else if encoded.starts_with('/') {64 encoded65 } else {66 format!(67 "{}/{encoded}",68 path.rsplit_once('/')69 .map(|(parent, _)| parent)70 .unwrap_or("")71 )72 };73 let mut parts: Vec<&str> = Vec::new();74 for part in joined.split('/') {75 match part {76 "" | "." => {}77 ".." => {78 if !(parts.len() == 1 && parts[0].ends_with(':')) {79 parts.pop();80 }81 }82 _ => parts.push(part),83 }84 }85 format!("file://{authority}/{}", parts.join("/"))86 } else {87 laymesh_core::model::resolve(uri, relative)88 }89}90fn strs(j: &J) -> Vec<String> {91 j.as_array()92 .map(|a| {93 a.iter()94 .filter_map(|v| v.as_str().map(str::to_string))95 .collect()96 })97 .unwrap_or_default()98}99fn entries() -> &'static Vec<J> {100 API["api"].as_array().unwrap()101}102fn entry(name: &str) -> Option<J> {103 entries().iter().find(|a| a["name"] == name).cloned()104}105fn desc<'a>(p: &'a J, en: bool) -> &'a str {106 p[if en { "descriptionEn" } else { "description" }]107 .as_str()108 .unwrap_or("")109}110fn link(name: &str, en: bool) -> String {111 format!(112 "[{}](https://muxkin.github.io/LayMesh/{}docs/topics/interface-reference{}.html#{})",113 if en { "Documentation" } else { "文档" },114 if en { "en/" } else { "" },115 if en { "" } else { ".zh-CN" },116 name.replace('.', "-")117 )118}119fn parameter_doc(p: &J, en: bool, owner: &str) -> String {120 let mut out = vec![121 format!(122 "**{}** — `{}`",123 p["name"].as_str().unwrap_or(""),124 parameter_type(p)125 ),126 desc(p, en).into(),127 ];128 for value in strs(&p["values"]) {129 let explanation = p["valueDescriptions"][&value][if en { "en" } else { "zh" }]130 .as_str()131 .unwrap_or("");132 out.push(format!("`{value}`: {explanation}"));133 }134 if let Some(parent) = p["defaultFrom"].as_str() {135 out.push(format!(136 "{} `{parent}`",137 if en {138 "Default inherits"139 } else {140 "默认继承"141 }142 ));143 }144 if p["unit"] != "none" {145 out.push(format!(146 "{}: {}",147 if en { "Unit" } else { "单位" },148 if p["unit"] == "geometry" {149 "figure unit (default mm)"150 } else {151 p["unit"].as_str().unwrap_or("")152 }153 ))154 }155 if let Some(d) = p["default"].as_str() {156 out.push(format!("{}: `{d}`", if en { "Default" } else { "默认" }))157 }158 if let Some(e) = p["example"].as_str() {159 out.push(format!("```lay\n{e}\n```"))160 }161 if !owner.is_empty() {162 out.push(link(owner, en));163 }164 out.into_iter()165 .filter(|s| !s.is_empty())166 .collect::<Vec<_>>()167 .join("\n\n")168}169fn parameter_type(p: &J) -> String {170 let values = strs(&p["values"]);171 if values.is_empty() {172 return p["type"].as_str().unwrap_or("value").into();173 }174 let mut typ = values175 .iter()176 .map(|v| format!("\"{v}\""))177 .collect::<Vec<_>>()178 .join(" | ");179 if p["name"] == "anchor" && values.contains(&"top_left".into()) {180 typ.push_str(" | self selector");181 }182 if p["type"].as_str().is_some_and(|t| t.contains("boolean")) {183 typ.push_str(" | boolean");184 }185 if p["name"] == "position" {186 typ.push_str(" | (length, length)");187 }188 typ189}190fn variable_doc(name: &str, value: &str) -> String {191 format!("```lay\n{name}: string = {}\n```", json!(value))192}193fn builtin(name: &str, en: bool) -> Option<J> {194 let e = entry(name)?;195 let mut names = strs(&e["positional"]);196 for p in e["parameters"].as_array().unwrap() {197 let n = p["name"].as_str().unwrap().to_string();198 if !names.contains(&n) {199 names.push(n)200 }201 }202 let ps:Vec<J>=names.iter().map(|n|{let p=e["parameters"].as_array().unwrap().iter().find(|p|p["name"]==*n).cloned().or_else(||entries().iter().flat_map(|a|a["parameters"].as_array().unwrap()).find(|p|p["name"]==*n).cloned()).unwrap_or(json!({"name":n,"type":"material","description":"需要放置的素材。","descriptionEn":"Material to place."}));let positional=strs(&e["positional"]).contains(n);let named=e["parameters"].as_array().unwrap().iter().any(|p|p["name"]==*n);json!({"name":n,"type":parameter_type(&p),"description":desc(&p,en),"unit":p["unit"],"example":p["example"],"documentation":parameter_doc(&p,en,name),"default":p["default"],"required":strs(&e["required"]).contains(n),"kind":if positional{if named{"either"}else{"positional"}}else{"named"},"label":[0,0]})}).collect();203 Some(204 json!({"name":name,"summary":desc(&e,en),"returns":e["returns"][if en{"en"}else{"zh"}],"documentation":format!("{}\n\n{}",desc(&e,en),link(name,en)),"parameters":ps}),205 )206}207fn authored(s: &Symbol, name: &str, en: bool) -> J {208 let d = s.doc.select(en);209 let ps:Vec<J>=s.parameters.iter().map(|p|{let n=p["name"].as_str().unwrap();let info=d.parameters.get(n).cloned().unwrap_or(json!({}));let default=p["default"].clone();json!({"name":n,"type":info["type"].as_str().unwrap_or("value"),"description":info["description"].as_str().unwrap_or(""),"default":default,"required":default.is_null(),"kind":"either","documentation":format!("**{n}**\n\n{}{}",info["description"].as_str().unwrap_or(""),default.as_str().map(|s|format!("\n\n{}: `{s}`",if en{"Default"}else{"默认"})).unwrap_or_default()),"label":[0,0]})}).collect();210 json!({"name":name,"summary":d.body.split("\n\n").next().unwrap_or(""),"documentation":s.doc.markdown(en),"parameters":ps})211}212fn word(source: &str, offset: usize) -> (usize, usize, String) {213 let mut a = offset.min(source.len());214 while a > 0215 && source.as_bytes()[a - 1].is_ascii()216 && (source.as_bytes()[a - 1].is_ascii_alphanumeric()217 || b"_-".contains(&source.as_bytes()[a - 1]))218 {219 a -= 1220 }221 let mut b = offset.min(source.len());222 while b < source.len()223 && (source.as_bytes()[b].is_ascii_alphanumeric() || b"_-".contains(&source.as_bytes()[b]))224 {225 b += 1226 }227 (a, b, source[a..b].into())228}229fn receiver_before(source: &str, offset: usize) -> Option<String> {230 let ts: Vec<Token> = tokens(&source[..offset.min(source.len())])231 .into_iter()232 .filter(|t| t.kind != "comment")233 .collect();234 if ts.last()?.text != "." {235 return None;236 }237 let end = ts.last()?.from;238 let mut depth = 0;239 let mut start = end;240 for t in ts[..ts.len() - 1].iter().rev() {241 if depth == 0 && start < end && source[t.to..start].contains('\n') {242 break;243 }244 match t.text.as_str() {245 ")" | "]" | "}" => depth += 1,246 "(" | "[" | "{" if depth > 0 => depth -= 1,247 "(" | "[" | "{" => break,248 _ if depth == 0 && t.text != "." && t.kind != "name" => break,249 _ => {}250 }251 if depth == 0 && start < end && source[t.to..start].contains('\n') {252 break;253 }254 start = t.from;255 }256 (start < end).then(|| source[start..end].trim().to_string())257}258fn expression(source: &str) -> Option<Expr> {259 let stmts = parser::parse(&format!("__probe={source}"), "<completion>").ok()?;260 match stmts.into_iter().next()?.kind {261 StmtKind::Bind(_, e, _) => Some(e),262 _ => None,263 }264}265fn type_info(typ: &str) -> &'static J {266 let typ = if typ == "table" { "dict" } else { typ };267 if API["valueTypes"].get(typ).is_some() {268 &API["valueTypes"][typ]269 } else {270 &API["geometryTypes"][typ]271 }272}273fn geometry_member_type(typ: &str, name: &str) -> Option<String> {274 type_info(typ)["members"][name].as_str().map(str::to_string)275}276fn geometry_surface(name: &str) -> Option<String> {277 entries()278 .iter()279 .find(|e| {280 e["geometryMethod"] == true281 && e["name"].as_str().unwrap().rsplit('.').next() == Some(name)282 })283 .and_then(|e| e["name"].as_str().map(str::to_string))284}285fn expression_type(e: &Expr, lookup: &impl Fn(&str) -> Option<String>) -> Option<String> {286 match &e.kind {287 ExprKind::Number(_, _) => Some("number".into()),288 ExprKind::String(_, _, _) => Some("string".into()),289 ExprKind::Bool(_) => Some("bool".into()),290 ExprKind::Unary(op, value) => {291 if op == "not" {292 Some("bool".into())293 } else {294 expression_type(value, lookup)295 }296 }297 ExprKind::Binary(op, a, b) => {298 if ["==", "!=", "<", ">", "<=", ">=", "and", "or"].contains(&op.as_str()) {299 Some("bool".into())300 } else {301 let a = expression_type(a, lookup)?;302 let b = expression_type(b, lookup)?;303 (a == b).then_some(a)304 }305 }306 ExprKind::Dict(_) => Some("dict".into()),307 ExprKind::List(_) => Some("list".into()),308 ExprKind::Ref(ps) => {309 let mut typ = if ps[0] == "self" {310 "instance".into()311 } else {312 lookup(&ps[0])?313 };314 for member in &ps[1..] {315 typ = geometry_member_type(&typ, member)?;316 }317 Some(typ)318 }319 ExprKind::Member(receiver, name) => {320 geometry_member_type(&expression_type(receiver, lookup)?, name)321 }322 ExprKind::Index(receiver, _) => type_info(&expression_type(receiver, lookup)?)["index"]323 .as_str()324 .map(str::to_string),325 ExprKind::Method(receiver, name, _) => {326 type_info(&expression_type(receiver, lookup)?)["methods"][name]327 .as_str()328 .map(str::to_string)329 }330 ExprKind::Call(ps, _) => {331 if ps.len() == 1 {332 return Some(333 entry(&ps[0])334 .and_then(|a| a["returnType"].as_str().map(str::to_string))335 .unwrap_or_else(|| ps[0].clone()),336 );337 }338 let mut typ = if ps[0] == "self" {339 "instance".into()340 } else {341 lookup(&ps[0])?342 };343 for member in &ps[1..ps.len() - 1] {344 typ = geometry_member_type(&typ, member)?;345 }346 let method = ps.last()?;347 if method == "add" || method == "fuse" {348 return Some("instance".into());349 }350 type_info(&typ)["methods"][method]351 .as_str()352 .map(str::to_string)353 .or_else(|| matches!(method.as_str(), "data" | "axis").then(|| "anchor".into()))354 }355 }356}357fn context(source: &str, offset: usize) -> Option<Context> {358 let ts: Vec<Token> = tokens(&source[..offset.min(source.len())])359 .into_iter()360 .filter(|t| t.kind != "comment")361 .collect();362 let mut stack = vec![];363 for (i, t) in ts.iter().enumerate() {364 if ["(", "[", "{"].contains(&t.text.as_str()) {365 stack.push(i)366 } else if [")", "]", "}"].contains(&t.text.as_str()) {367 let open = match t.text.as_str() {368 ")" => "(",369 "]" => "[",370 _ => "{",371 };372 if let Some(at) = stack.iter().rposition(|&j| ts[j].text == open) {373 stack.truncate(at)374 }375 }376 }377 let &at = stack.iter().rev().find(|&&i| {378 ts[i].text == "("379 && i > 0380 && ts[i - 1].kind == "name"381 && (i < 2 || ts[i - 2].text != "function")382 })?;383 let mut c = Context {384 name: ts[at - 1].text.clone(),385 ..Context::default()386 };387 if at >= 3 && ts[at - 2].text == "." {388 c.receiver = receiver_before(source, ts[at - 2].to).unwrap_or_default()389 }390 let mut depth = 0;391 let mut start = at + 1;392 for i in at + 1..ts.len() {393 let t = &ts[i];394 if ["(", "[", "{"].contains(&t.text.as_str()) {395 depth += 1396 } else if [")", "]", "}"].contains(&t.text.as_str()) {397 depth -= 1398 } else if depth == 0 {399 if t.text == "," {400 if c.parameter.is_empty() && i > start {401 c.positional += 1402 }403 c.parameter.clear();404 start = i + 1;405 }406 if i == start && t.kind == "name" && ts.get(i + 1).is_some_and(|t| t.text == "=") {407 c.parameter = t.text.clone();408 c.used.push(t.text.clone());409 }410 }411 }412 if ts.len() == start + 1 && ts[start].kind == "name" {413 c.prefix = ts[start].text.clone()414 }415 Some(c)416}417impl LanguageService {418 pub fn new(locale: &str) -> Self {419 Self {420 documents: BTreeMap::new(),421 locale: locale.into(),422 ..Self::default()423 }424 }425 pub fn update(&mut self, uri: &str, text: &str) {426 self.documents.insert(uri.into(), text.into());427 }428 pub fn remove(&mut self, uri: &str) {429 self.documents.remove(uri);430 }431 fn en(&self) -> bool {432 !self.locale.to_lowercase().starts_with("zh")433 }434 fn source(&self, uri: &str) -> &str {435 self.documents.get(uri).map(String::as_str).unwrap_or("")436 }437 fn document_index(&self, uri: &str) -> index::Index {438 let source = self.source(uri);439 let mut hasher = std::collections::hash_map::DefaultHasher::new();440 source.hash(&mut hasher);441 let hash = hasher.finish();442 let en = self.en();443 let mut cached = self.indexes.borrow_mut();444 if !cached445 .get(uri)446 .is_some_and(|(h, e, _)| *h == hash && *e == en)447 {448 cached.insert(uri.into(), (hash, en, index(uri, source, en)));449 }450 cached[uri].2.clone()451 }452 fn binding_graph(&self) -> bindings::Graph {453 let mut hasher = std::collections::hash_map::DefaultHasher::new();454 self.documents.hash(&mut hasher);455 self.locale.hash(&mut hasher);456 let hash = hasher.finish();457 if self458 .bindings459 .borrow()460 .as_ref()461 .is_none_or(|(old, _)| *old != hash)462 {463 let graph = bindings::Graph::build(self);464 *self.bindings.borrow_mut() = Some((hash, graph));465 }466 self.bindings.borrow().as_ref().unwrap().1.clone()467 }468 fn visible(&self, uri: &str, offset: usize) -> Vec<Symbol> {469 let mut symbols = self.document_index(uri).symbols;470 symbols.retain(|s| {471 s.scope_from <= offset472 && s.scope_to >= offset473 && (s.from <= offset || s.kind == "function")474 });475 symbols.sort_by_key(|s| (usize::MAX - (s.scope_to - s.scope_from), s.from));476 let mut out = BTreeMap::new();477 for s in symbols {478 out.insert(s.name.clone(), s);479 }480 out.into_values().collect()481 }482 fn resolve(483 &self,484 uri: &str,485 name: &str,486 offset: usize,487 seen: &mut BTreeSet<String>,488 ) -> Option<Symbol> {489 if !seen.insert(format!("{uri}:{name}:{offset}")) {490 return None;491 }492 if let Some(s) = self493 .visible(uri, offset)494 .into_iter()495 .find(|s| s.name == name)496 {497 if !s.alias.is_empty() && s.doc.body.is_empty() && s.doc.variants.is_empty() {498 if let Some(target) = self.resolve(uri, &s.alias, s.from.saturating_sub(1), seen) {499 if target.kind == "function" {500 return Some(target);501 }502 }503 }504 return Some(s);505 }506 for imp in self.document_index(uri).imports {507 if imp.alias == name508 && imp.scope_from <= offset509 && imp.scope_to >= offset510 && imp.alias_from <= offset511 {512 let target = resolve(uri, &imp.source);513 let s = self514 .document_index(&target)515 .symbols516 .into_iter()517 .find(|s| s.exported && s.scope_from == 0 && s.name == imp.name)?;518 return if !s.alias.is_empty() {519 self.resolve(&target, &s.alias, s.from.saturating_sub(1), seen)520 .filter(|target| target.kind == "function")521 .or(Some(s))522 } else {523 Some(s)524 };525 }526 }527 None528 }529 fn named(&self, uri: &str, offset: usize) -> Vec<(String, Symbol)> {530 let mut out: BTreeMap<String, Symbol> = self531 .visible(uri, offset)532 .into_iter()533 .map(|s| (s.name.clone(), s))534 .collect();535 for imp in self.document_index(uri).imports {536 if !out.contains_key(&imp.alias) {537 if let Some(s) = self.resolve(uri, &imp.alias, offset, &mut BTreeSet::new()) {538 out.insert(imp.alias, s);539 }540 }541 }542 out.into_iter().collect()543 }544 fn type_of(&self, uri: &str, name: &str, offset: usize) -> Option<String> {545 self.type_of_inner(uri, name, offset, &mut BTreeSet::new())546 }547 fn type_of_inner(548 &self,549 uri: &str,550 name: &str,551 offset: usize,552 active: &mut BTreeSet<String>,553 ) -> Option<String> {554 if !active.insert(format!("{uri}:{name}:{offset}")) {555 return None;556 }557 let symbol = self.resolve(uri, name, offset, &mut BTreeSet::new())?;558 let declared = symbol.doc.select(self.en()).typ;559 if !declared.is_empty() {560 return Some(declared);561 }562 if let Some(factory) =563 self.resolve(&symbol.uri, &symbol.typ, symbol.from, &mut BTreeSet::new())564 {565 if let Some(returns) = factory.doc.select(self.en()).returns {566 if let Some(typ) = returns["type"].as_str() {567 return Some(typ.into());568 }569 }570 }571 if let Some(value) = &symbol.value {572 if let Some(inferred) = expression_type(value, &|other| {573 self.type_of_inner(574 &symbol.uri,575 other,576 symbol.from.saturating_sub(1),577 &mut active.clone(),578 )579 }) {580 return Some(inferred);581 }582 }583 if symbol.typ.is_empty() {584 let source = self.source(&symbol.uri);585 let tail = &source[symbol.to..];586 if let Some(rhs) = tail.trim_start().strip_prefix('=') {587 if let Some(expr) = expression(rhs.split('\n').next().unwrap_or("")) {588 let inferred = expression_type(&expr, &|other| {589 if other == name {590 None591 } else {592 self.type_of_inner(&symbol.uri, other, symbol.from, &mut active.clone())593 }594 });595 if inferred.is_some() {596 return inferred;597 }598 }599 }600 }601 Some(symbol.typ)602 }603 fn receiver_type(&self, uri: &str, receiver: &str, offset: usize) -> Option<String> {604 expression_type(&expression(receiver)?, &|name| {605 self.type_of(uri, name, offset)606 })607 }608 fn classes(&self) -> BTreeSet<String> {609 let pattern = Regex::new(r"\.([A-Za-z_][\w-]*)\s*(?:[.{,:>])").unwrap();610 self.documents611 .values()612 .flat_map(|source| pattern.captures_iter(source).map(|c| c[1].to_string()))613 .collect()614 }615 fn callable(&self, uri: &str, offset: usize, c: &Context) -> Option<J> {616 if c.receiver.is_empty() {617 if let Some(s) = self.resolve(uri, &c.name, offset, &mut BTreeSet::new()) {618 return if s.kind == "function" {619 Some(authored(&s, &c.name, self.en()))620 } else {621 None622 };623 }624 }625 let mut name = c.name.clone();626 if !c.receiver.is_empty() {627 if let Some(typ) = self.receiver_type(uri, &c.receiver, offset) {628 if let Some(prefix) = type_info(&typ)["methodPrefix"].as_str() {629 if type_info(&typ)["methods"][&name].is_string() {630 return builtin(&format!("{prefix}.{name}"), self.en());631 }632 }633 if ["plot", "polar_plot", "radar_plot"].contains(&typ.as_str()) {634 name = format!("plot.{name}")635 } else if typ == "instance" && ["data", "axis"].contains(&name.as_str()) {636 name = format!("instance.{name}")637 }638 }639 }640 builtin(&name, self.en())641 }642 pub fn signature(&self, uri: &str, offset: usize) -> J {643 let Some(c) = context(self.source(uri), offset) else {644 return J::Null;645 };646 let Some(mut entry) = self.callable(uri, offset, &c) else {647 return J::Null;648 };649 let mut label = format!("{}(", entry["name"].as_str().unwrap());650 let ps = entry["parameters"].as_array_mut().unwrap();651 for (i, p) in ps.iter_mut().enumerate() {652 if i > 0 {653 label.push_str(", ")654 }655 let from = label.encode_utf16().count();656 label.push_str(p["name"].as_str().unwrap());657 if let Some(d) = p["default"].as_str() {658 label.push('=');659 label.push_str(d)660 }661 p["label"] = json!([from, label.encode_utf16().count()]);662 }663 label.push(')');664 let active = if !c.parameter.is_empty() {665 ps.iter().position(|p| p["name"] == c.parameter)666 } else {667 let positional: Vec<usize> = ps668 .iter()669 .enumerate()670 .filter(|(_, p)| p["kind"] != "named")671 .map(|(i, _)| i)672 .collect();673 let candidate = positional.get(c.positional).copied().filter(|&i| {674 !c.used675 .contains(&ps[i]["name"].as_str().unwrap().to_string())676 });677 if candidate.is_some() && c.prefix.is_empty() {678 candidate679 } else {680 let consumed: BTreeSet<usize> =681 positional.iter().take(c.positional).copied().collect();682 let eligible: Vec<usize> = ps683 .iter()684 .enumerate()685 .filter(|(i, p)| {686 p["kind"] != "positional"687 && !consumed.contains(i)688 && !c.used.contains(&p["name"].as_str().unwrap().to_string())689 && (c.prefix.is_empty()690 || p["name"].as_str().unwrap().starts_with(&c.prefix))691 })692 .map(|(i, _)| i)693 .collect();694 if c.prefix.is_empty() || eligible.len() == 1 {695 eligible.first().copied()696 } else {697 None698 }699 }700 };701 entry["label"] = json!(label);702 if let Some(i) = active {703 entry["activeParameter"] = json!(i);704 }705 entry706 }707 pub fn hover(&self, uri: &str, offset: usize) -> J {708 let source = self.source(uri);709 let (from, to, w) = word(source, offset);710 if w.is_empty() {711 return J::Null;712 }713 let en = self.en();714 let canonical = w.replace('-', "_");715 if source[to..].trim_start().starts_with('=') {716 if let Some(c) = context(source, from) {717 if let Some(e) = self.callable(uri, offset, &c) {718 if let Some(p) = e["parameters"]719 .as_array()720 .unwrap()721 .iter()722 .find(|p| p["name"] == canonical)723 {724 return json!({"from":from,"to":to,"contents":p["documentation"]});725 }726 }727 }728 }729 if uri.ends_with(".lcss") || css_region(source, offset).is_some() {730 if let Some(p) = entries()731 .iter()732 .flat_map(|e| e["parameters"].as_array().unwrap())733 .find(|p| p["name"] == canonical)734 {735 return json!({"from":from,"to":to,"contents":parameter_doc(p,en,"")});736 }737 }738 let graph = self.binding_graph();739 let binding = graph.at(uri, offset);740 if let Some(s) = binding.and_then(|o| {741 self.resolve(uri, &w, offset, &mut BTreeSet::new())742 .or_else(|| {743 graph744 .indexes745 .get(&o.key.uri)?746 .symbols747 .iter()748 .find(|s| s.from == o.key.from)749 .cloned()750 })751 }) {752 let contents = if s.kind == "function" {753 let e = authored(&s, &w, en);754 format!(755 "```lay\n{}({})\n```\n\n{}",756 w,757 s.parameters758 .iter()759 .map(|p| p["name"].as_str().unwrap())760 .collect::<Vec<_>>()761 .join(", "),762 e["documentation"].as_str().unwrap()763 )764 } else {765 let typ = self766 .type_of(uri, &w, offset)767 .filter(|t| !t.is_empty())768 .or_else(|| (!s.doc.select(en).typ.is_empty()).then(|| s.doc.select(en).typ))769 .unwrap_or_else(|| "value".into());770 format!("```lay\n{w}: {typ}\n```\n\n{}", s.doc.markdown(en))771 };772 return json!({"from":from,"to":to,"contents":contents});773 }774 if !source[to..].trim_start().starts_with('(')775 && receiver_before(source, from).is_none()776 && !tokens(source)777 .iter()778 .any(|t| matches!(t.kind, "string" | "comment") && t.from <= from && t.to >= to)779 {780 if let Some(value) = predefined_value(&w) {781 return json!({"from":from,"to":to,"contents":variable_doc(&w,value)});782 }783 }784 let receiver = receiver_before(source, from).unwrap_or_default();785 let c = Context {786 name: w,787 receiver,788 ..Context::default()789 };790 if let Some(e) = self.callable(uri, offset, &c) {791 return json!({"from":from,"to":to,"contents":format!("```lay\n{}(...)\n```\n\n{}",e["name"].as_str().unwrap(),e["documentation"].as_str().unwrap())});792 }793 if let Some(typ) = self794 .receiver_type(uri, &c.receiver, offset)795 .and_then(|typ| geometry_member_type(&typ, &c.name))796 {797 return json!({"from":from,"to":to,"contents":format!("**{}** → `{typ}`\n\n{}",c.name,desc(&type_info(&typ),en))});798 }799 J::Null800 }801 pub fn definition(&self, uri: &str, offset: usize) -> J {802 let source = self.source(uri);803 let (_, _, w) = word(source, offset);804 let graph = self.binding_graph();805 if let Some(o) = graph.at(uri, offset) {806 if let Some(s) = self807 .resolve(uri, &w, offset, &mut BTreeSet::new())808 .filter(|s| s.kind == "function")809 {810 return json!({"uri":s.uri,"from":s.from,"to":s.to});811 }812 if let Some(definition) = graph.definition(&o.key) {813 return definition;814 }815 }816 if let Some(t) = tokens(source)817 .iter()818 .find(|t| t.from <= offset && t.to >= offset && t.kind == "string")819 {820 let p = t.text.trim_matches(['\'', '"']);821 let target = resolve(uri, p);822 if self.documents.contains_key(&target) {823 return json!({"uri":target,"from":0,"to":0});824 }825 }826 if !w.is_empty() {827 let pattern = format!(828 r"(?:\.|--){}(?:\s|\{{|:)",829 regex::escape(w.trim_start_matches('-'))830 );831 let re = Regex::new(&pattern).unwrap();832 for (file, text) in &self.documents {833 if let Some(m) = re.find(text) {834 return json!({"uri":file,"from":m.start(),"to":m.end()-1});835 }836 }837 }838 J::Null839 }840 pub fn completions(&self, uri: &str, offset: usize) -> J {841 let source = self.source(uri);842 let offset = offset.min(source.len());843 let before = &source[..offset];844 let line = before.rsplit('\n').next().unwrap();845 let (from, to, _) = word(source, offset);846 let en = self.en();847 let option = |label: &str, kind: &str, info: String, apply: String, detail: &str| json!({"label":label,"type":kind,"info":info,"apply":apply,"detail":detail,"from":from,"to":to});848 if line.trim_start().starts_with("##") {849 let mut out = vec![];850 let decl = index(uri, source, en)851 .symbols852 .into_iter()853 .find(|s| s.kind == "function" && s.from > offset);854 if line.contains("@lang ") {855 for tag in ["zh-CN", "en"] {856 out.push(option(857 tag,858 "text",859 String::new(),860 tag.into(),861 "Documentation",862 ))863 }864 } else if line.contains("@param ") {865 if let Some(s) = decl {866 let section = before.rsplit("## @lang").next().unwrap_or(before);867 for p in s.parameters {868 let n = p["name"].as_str().unwrap();869 if !Regex::new(&format!(870 r"@param\s+(?:\{{[^}}]*\}}\s*)?{}\b",871 regex::escape(n)872 ))873 .unwrap()874 .is_match(section)875 {876 out.push(option(877 n,878 "text",879 String::new(),880 format!("{n} - "),881 "Documentation",882 ))883 }884 }885 }886 } else if line.trim() == "##" {887 let params = decl.map(|s| s.parameters).unwrap_or_default();888 let build = |english| {889 let mut a = vec![if english {890 "Describe this declaration.".into()891 } else {892 "说明此声明的用途。".into()893 }];894 for p in ¶ms {895 a.push(format!(896 "@param {} - {}",897 p["name"].as_str().unwrap(),898 if english {899 "Description."900 } else {901 "参数说明。"902 }903 ))904 }905 a.push(format!(906 "@returns {}",907 if english {908 "Result."909 } else {910 "返回结果。"911 }912 ));913 a914 };915 let template = build(en).join("\n## ");916 let mut bilingual = vec!["@lang zh-CN".into()];917 bilingual.extend(build(false));918 bilingual.push("@lang en".into());919 bilingual.extend(build(true));920 for (label, apply) in [921 (922 if en {923 "Documentation template"924 } else {925 "文档模板"926 },927 template,928 ),929 (930 if en {931 "Bilingual documentation template"932 } else {933 "双语文档模板"934 },935 bilingual.join("\n## "),936 ),937 ] {938 let mut o = option(label, "text", String::new(), apply, "Documentation");939 o["from"] = json!(offset);940 out.push(o)941 }942 } else {943 for tag in ["lang", "param", "returns", "type", "example", "see"] {944 let mut o = option(945 &format!("@{tag}"),946 "text",947 String::new(),948 format!("@{tag} "),949 "Documentation",950 );951 if from > 0 && source.as_bytes()[from - 1] == b'@' {952 o["from"] = json!(from - 1)953 }954 out.push(o)955 }956 }957 return json!(out);958 }959 if tokens(source)960 .iter()961 .any(|t| t.kind == "comment" && t.from <= offset && t.to >= offset)962 {963 return json!([]);964 }965 if let Some(c) = context(source, offset) {966 let preset_name = c.receiver.is_empty()967 && ["cmap", "palette"].contains(&c.name.as_str())968 && (c.parameter == "name" || c.parameter.is_empty() && c.positional == 0)969 && self970 .resolve(uri, &c.name, offset, &mut BTreeSet::new())971 .is_none();972 if preset_name || c.parameter == "cmap" && self.callable(uri, offset, &c).is_some() {973 let quoted = source[..from].trim_end().ends_with(['\'', '"']);974 return json!(975 laymesh_core::colormap::names(None, true)976 .unwrap()977 .into_iter()978 .map(|name| {979 option(980 &name,981 "value",982 format!("Matplotlib {}", laymesh_core::colormap::VERSION),983 if quoted {984 name.clone()985 } else {986 json!(name).to_string()987 },988 "cmap",989 )990 })991 .collect::<Vec<_>>()992 );993 }994 }995 let css = if uri.ends_with(".lcss") {996 Some(0)997 } else {998 css_region(source, offset)999 };1000 if let Some(start) = css {1001 let local = &source[start..offset];1002 if Regex::new(r"::[\w-]*$").unwrap().is_match(local) {1003 return json!(1004 [1005 "area",1006 "axis",1007 "axis-label",1008 "tick-label",1009 "legend",1010 "colorbar",1011 "marker",1012 "grid"1013 ]1014 .iter()1015 .map(|name| option(1016 name,1017 "keyword",1018 String::new(),1019 name.to_string(),1020 "LCSS part"1021 ))1022 .collect::<Vec<_>>()1023 );1024 }1025 if Regex::new(r"var\(\s*--[\w-]*$").unwrap().is_match(local) {1026 let pattern = Regex::new(r"(--[A-Za-z_][\w-]*)\s*:").unwrap();1027 let names: BTreeSet<String> = self1028 .documents1029 .values()1030 .flat_map(|text| pattern.captures_iter(text).map(|c| c[1].to_string()))1031 .collect();1032 return json!(1033 names1034 .iter()1035 .map(|name| option(1036 name,1037 "variable",1038 String::new(),1039 name.clone(),1040 "LCSS variable"1041 ))1042 .collect::<Vec<_>>()1043 );1044 }1045 if Regex::new(r"\.[\w-]*$").unwrap().is_match(local) {1046 let names = self.classes();1047 return json!(1048 names1049 .iter()1050 .map(|name| option(1051 name,1052 "class",1053 String::new(),1054 name.clone(),1055 "LCSS class"1056 ))1057 .collect::<Vec<_>>()1058 );1059 }1060 if local.rfind('{') > local.rfind('}') {1061 if let Some(cap) = Regex::new(r"([\w-]+)\s*:\s*([^;{}]*)$")1062 .unwrap()1063 .captures(local)1064 {1065 let key = cap[1].replace('-', "_");1066 let p = entries()1067 .iter()1068 .flat_map(|a| a["parameters"].as_array().unwrap())1069 .find(|p| p["name"] == key);1070 let mut values = p.map(|p| strs(&p["values"])).unwrap_or_default();1071 values.push("var()".into());1072 if key.contains("fill") || key == "background" {1073 values.extend(1074 [1075 "none",1076 "linear-gradient()",1077 "radial-gradient()",1078 "hatch()",1079 "url()",1080 ]1081 .map(str::to_string),1082 )1083 }1084 return json!(1085 values1086 .iter()1087 .map(|v| option(v, "keyword", String::new(), v.clone(), ""))1088 .collect::<Vec<_>>()1089 );1090 }1091 let mut names = BTreeSet::new();1092 let mut options = vec![];1093 for p in entries()1094 .iter()1095 .flat_map(|a| a["parameters"].as_array().unwrap())1096 {1097 let name = p["name"].as_str().unwrap().replace('_', "-");1098 if names.insert(name.clone()) {1099 options.push(option(1100 &name,1101 "property",1102 parameter_doc(p, en, ""),1103 format!("{name}: "),1104 "",1105 ))1106 }1107 }1108 return json!(options);1109 }1110 return json!(1111 entries()1112 .iter()1113 .filter(|a| !a["name"].as_str().unwrap().contains('.'))1114 .map(|a| option(1115 a["name"].as_str().unwrap(),1116 "keyword",1117 desc(a, en).into(),1118 a["name"].as_str().unwrap().into(),1119 ""1120 ))1121 .collect::<Vec<_>>()1122 );1123 }1124 if Regex::new(r#"\bclass\s*=\s*["'][^"']*$"#)1125 .unwrap()1126 .is_match(before)1127 {1128 return json!(1129 self.classes()1130 .iter()1131 .map(|name| option(name, "class", String::new(), name.clone(), "LCSS class"))1132 .collect::<Vec<_>>()1133 );1134 }1135 if let Some(receiver) = receiver_before(source, from) {1136 let typ = self1137 .receiver_type(uri, &receiver, offset)1138 .unwrap_or_default();1139 let names = if ["plot", "polar_plot", "radar_plot"].contains(&typ.as_str()) {1140 entries()1141 .iter()1142 .filter_map(|e| {1143 e["name"]1144 .as_str()1145 .unwrap()1146 .strip_prefix("plot.")1147 .map(str::to_string)1148 })1149 .collect()1150 } else if let Some(members) = type_info(&typ)["members"].as_object() {1151 members1152 .keys()1153 .chain(type_info(&typ)["methods"].as_object().unwrap().keys())1154 .cloned()1155 .chain((typ == "instance").then_some("data".into()))1156 .chain((typ == "instance").then_some("axis".into()))1157 .collect()1158 } else {1159 [1160 "add",1161 "fuse",1162 "width",1163 "height",1164 "top_left",1165 "top_center",1166 "top_right",1167 "middle_left",1168 "center",1169 "middle_right",1170 "bottom_left",1171 "bottom_center",1172 "bottom_right",1173 "plot_center",1174 "data",1175 "axis",1176 ]1177 .iter()1178 .map(|s| s.to_string())1179 .collect::<Vec<_>>()1180 };1181 return json!(1182 names1183 .iter()1184 .map(|n| {1185 let c = Context {1186 name: n.clone(),1187 receiver: receiver.clone(),1188 ..Context::default()1189 };1190 let e = self.callable(uri, offset, &c);1191 let selected = geometry_member_type(&typ, n);1192 let detail = selected1193 .as_deref()1194 .or_else(|| e.as_ref().and_then(|e| e["returns"].as_str()))1195 .unwrap_or("");1196 option(1197 n,1198 if e.is_some() { "function" } else { "property" },1199 e.as_ref()1200 .and_then(|e| e["documentation"].as_str())1201 .unwrap_or("")1202 .into(),1203 if e.is_some() {1204 format!("{n}(")1205 } else {1206 n.clone()1207 },1208 detail,1209 )1210 })1211 .collect::<Vec<_>>()1212 );1213 }1214 let named = self.named(uri, offset);1215 let mut out: Vec<J> = named1216 .iter()1217 .map(|(name, s)| {1218 option(1219 name,1220 if s.kind == "function" {1221 "function"1222 } else {1223 "variable"1224 },1225 s.doc.markdown(en),1226 if s.kind == "function" {1227 format!("{name}(")1228 } else {1229 name.clone()1230 },1231 &self1232 .type_of(uri, name, offset)1233 .unwrap_or_else(|| s.typ.clone()),1234 )1235 })1236 .collect();1237 if let Some(c) = context(source, offset) {1238 if let Some(e) = self.callable(uri, offset, &c) {1239 let positional = e["parameters"]1240 .as_array()1241 .unwrap()1242 .iter()1243 .filter(|p| p["kind"] != "named")1244 .nth(c.positional)1245 .and_then(|p| p["name"].as_str())1246 .unwrap_or("");1247 let selected = if c.parameter.is_empty() {1248 positional1249 } else {1250 &c.parameter1251 };1252 if let Some(api) = entry(e["name"].as_str().unwrap()) {1253 if let Some(p) = api["parameters"]1254 .as_array()1255 .unwrap()1256 .iter()1257 .find(|p| p["name"] == selected)1258 {1259 if p["values"].is_array() {1260 let quoted = tokens(source)1261 .into_iter()1262 .find(|t| t.kind == "string" && t.from < offset && offset <= t.to);1263 let mut choices = vec![];1264 for value in strs(&p["values"]) {1265 if let Some(var) =1266 predefined_variables().iter().find(|v| v["value"] == value)1267 {1268 let name = var["name"].as_str().unwrap();1269 let mut o = option(1270 if quoted.is_some() { &value } else { name },1271 "variable",1272 variable_doc(name, &value),1273 if quoted.is_some() {1274 value.clone()1275 } else {1276 name.into()1277 },1278 "string",1279 );1280 if let Some(t) = "ed {1281 let quote = t.text.chars().next().unwrap();1282 o["from"] = json!(t.from + 1);1283 o["to"] =1284 json!(if t.text.len() > 1 && t.text.ends_with(quote) {1285 t.to - 11286 } else {1287 t.to1288 });1289 }1290 if quoted.is_some() || !named.iter().any(|(n, _)| n == name) {1291 choices.push(o);1292 }1293 }1294 }1295 if quoted.is_some() {1296 return json!(choices);1297 }1298 if selected == "anchor"1299 && p["values"]1300 .as_array()1301 .unwrap()1302 .iter()1303 .any(|v| v == "top_left")1304 {1305 choices.push(option(1306 "self",1307 "variable",1308 parameter_doc(p, en, "add"),1309 "self.".into(),1310 "instance selector",1311 ));1312 }1313 choices.append(&mut out);1314 out = choices;1315 if !c.parameter.is_empty() {1316 return json!(out);1317 }1318 }1319 }1320 }1321 if c.parameter.is_empty() {1322 let consumed: Vec<_> = e["parameters"]1323 .as_array()1324 .unwrap()1325 .iter()1326 .filter(|p| p["kind"] != "named")1327 .take(c.positional)1328 .map(|p| p["name"].as_str().unwrap().to_string())1329 .collect();1330 for p in e["parameters"].as_array().unwrap() {1331 let n = p["name"].as_str().unwrap();1332 if p["kind"] != "positional"1333 && !c.used.contains(&n.to_string())1334 && !consumed.contains(&n.to_string())1335 {1336 out.push(option(1337 n,1338 "property",1339 p["documentation"].as_str().unwrap_or("").into(),1340 format!("{n}="),1341 p["type"].as_str().unwrap_or(""),1342 ))1343 }1344 }1345 return json!(out);1346 }1347 if let Some(api) = entry(e["name"].as_str().unwrap()) {1348 if let Some(p) = api["parameters"]1349 .as_array()1350 .unwrap()1351 .iter()1352 .find(|p| p["name"] == c.parameter)1353 {1354 if p["unit"].as_str().is_some_and(|unit| unit != "none") {1355 out.extend(["mm", "cm", "in", "inch", "pt", "px", "auto"].iter().map(1356 |v| option(v, "keyword", parameter_doc(p, en, ""), (*v).into(), ""),1357 ));1358 return json!(out);1359 }1360 }1361 }1362 }1363 }1364 let shadowed: BTreeSet<&str> = named.iter().map(|(n, _)| n.as_str()).collect();1365 if !tokens(source)1366 .iter()1367 .any(|t| t.kind == "string" && t.from < offset && offset <= t.to)1368 {1369 for var in predefined_variables() {1370 let n = var["name"].as_str().unwrap();1371 if !shadowed.contains(n) {1372 out.push(option(1373 n,1374 "variable",1375 variable_doc(n, var["value"].as_str().unwrap()),1376 n.into(),1377 "string",1378 ));1379 }1380 }1381 } else {1382 return json!([]);1383 }1384 for a in entries() {1385 let n = a["name"].as_str().unwrap();1386 if !n.contains('.') && !shadowed.contains(n) {1387 out.push(option(1388 n,1389 "function",1390 builtin(n, en).unwrap()["documentation"]1391 .as_str()1392 .unwrap()1393 .into(),1394 format!("{n}("),1395 "",1396 ))1397 }1398 }1399 json!(out)1400 }1401 pub fn diagnostics(&self, uri: &str) -> J {1402 diagnostics(self, uri)1403 }1404 pub fn references(&self, uri: &str, offset: usize, include_declaration: bool) -> J {1405 let graph = self.binding_graph();1406 json!(1407 graph1408 .at(uri, offset)1409 .map(|o| graph1410 .related_references(&o.key, include_declaration)1411 .into_iter()1412 .map(|o| o.json())1413 .collect::<Vec<_>>())1414 .unwrap_or_default()1415 )1416 }1417 pub fn highlights(&self, uri: &str, offset: usize) -> J {1418 json!(1419 self.references(uri, offset, true)1420 .as_array()1421 .unwrap()1422 .iter()1423 .filter(|o| o["uri"] == uri)1424 .cloned()1425 .collect::<Vec<_>>()1426 )1427 }1428 pub fn prepare_rename(&self, uri: &str, offset: usize) -> J {1429 let graph = self.binding_graph();1430 let Some(o) = graph.at(uri, offset).filter(|o| graph.can_rename(&o.key)) else {1431 return J::Null;1432 };1433 json!({"from":o.from,"to":o.to,"placeholder":&self.source(uri)[o.from..o.to]})1434 }1435 pub fn rename(&self, uri: &str, offset: usize, new_name: &str) -> Result<J, String> {1436 self.binding_graph()1437 .rename(uri, offset, new_name)1438 .map(|edits| json!(edits))1439 }1440 pub fn query(&self, method: &str, uri: &str, offset: usize) -> J {1441 match method {1442 "completions" => self.completions(uri, offset),1443 "hover" => self.hover(uri, offset),1444 "signature" => self.signature(uri, offset),1445 "definition" => self.definition(uri, offset),1446 "references" => self.references(uri, offset, true),1447 "highlights" => self.highlights(uri, offset),1448 "prepareRename" => self.prepare_rename(uri, offset),1449 "diagnostics" => self.diagnostics(uri),1450 "colors" => self.document_colors(uri),1451 _ => J::Null,1452 }1453 }1454}1455pub fn css_region(source: &str, offset: usize) -> Option<usize> {1456 let ts = tokens(source);1457 for i in 0..ts.len().saturating_sub(1) {1458 if ts[i].text == "style" && ts[i + 1].text == "{" {1459 let from = ts[i + 1].to;1460 let mut depth = 1;1461 let mut end = source.len();1462 for t in &ts[i + 2..] {1463 if t.text == "{" {1464 depth += 11465 } else if t.text == "}" {1466 depth -= 1;1467 if depth == 0 {1468 end = t.from;1469 break;1470 }1471 }1472 }1473 if offset >= from && offset <= end {1474 return Some(from);1475 }1476 }1477 }1478 None1479}1480pub fn byte_offset(source: &str, utf16: usize) -> usize {1481 let mut n = 0;1482 for (i, c) in source.char_indices() {1483 if n >= utf16 {1484 return i;1485 }1486 n += c.len_utf16()1487 }1488 source.len()1489}1490fn boundary(source: &str, byte: usize) -> usize {1491 let mut byte = byte.min(source.len());1492 while !source.is_char_boundary(byte) {1493 byte -= 1;1494 }1495 byte1496}1497pub fn utf16_offset(source: &str, byte: usize) -> usize {1498 source[..boundary(source, byte)].encode_utf16().count()1499}1500pub fn position(source: &str, byte: usize) -> J {1501 let before = &source[..boundary(source, byte)];1502 json!({"line":before.bytes().filter(|&b|b==b'\n').count(),"character":before.rsplit('\n').next().unwrap().encode_utf16().count()})1503}1504pub fn offset(source: &str, pos: &J) -> usize {1505 let line = pos["line"].as_u64().unwrap_or(0) as usize;1506 let col = pos["character"].as_u64().unwrap_or(0) as usize;1507 let start = source1508 .split_inclusive('\n')1509 .take(line)1510 .map(str::len)1511 .sum::<usize>()1512 .min(source.len());1513 start + byte_offset(source[start..].split('\n').next().unwrap(), col)1514}1515fn diagnostics(service: &LanguageService, uri: &str) -> J {1516 let source = service.source(uri);1517 let en = service.en();1518 let mut out = if uri.ends_with(".lcss") {1519 vec![]1520 } else {1521 index(uri, source, en).issues1522 };1523 let emit = |out: &mut Vec<J>,1524 from: usize,1525 to: usize,1526 code: &str,1527 message: String,1528 replacement: Option<&str>| {1529 let from = boundary(source, from);1530 let mut to = to.min(source.len());1531 while !source.is_char_boundary(to) {1532 to += 1;1533 }1534 let mut d = json!({"from":from,"to":to,"severity":"error","code":code,"message":message});1535 if let Some(r) = replacement {1536 d["replacement"] = json!(r)1537 }1538 out.push(d)1539 };1540 if uri.ends_with(".lcss") {1541 if let Err(error) = laymesh_core::style::validate_stylesheet(1542 source,1543 uri,1544 laymesh_core::Loc {1545 line: 1,1546 column: 1,1547 offset: 0,1548 },1549 ) {1550 emit(1551 &mut out,1552 error.loc.offset,1553 error.loc.offset + 1,1554 &error.code,1555 error.message,1556 None,1557 );1558 }1559 return json!(out);1560 }1561 let stmts = match parser::parse(source, uri) {1562 Ok(v) => v,1563 Err(e) => {1564 emit(1565 &mut out,1566 e.loc.offset,1567 e.loc.offset + 1,1568 &e.code,1569 e.message,1570 None,1571 );1572 return json!(out);1573 }1574 };1575 let mut declared: BTreeSet<String> = entries()1576 .iter()1577 .map(|a| a["name"].as_str().unwrap().into())1578 .chain(1579 predefined_variables()1580 .iter()1581 .map(|v| v["name"].as_str().unwrap().to_string()),1582 )1583 .chain(["null", "self"].map(str::to_string))1584 .collect();1585 fn declare(stmts: &[Stmt], names: &mut BTreeSet<String>) {1586 for stmt in stmts {1587 match &stmt.kind {1588 StmtKind::Bind(n, _, _) => {1589 names.insert(n.clone());1590 }1591 StmtKind::Function(n, ps, b, _) => {1592 names.insert(n.clone());1593 for (p, _) in ps {1594 names.insert(p.clone());1595 }1596 declare(b, names)1597 }1598 StmtKind::Import(ns, _) => {1599 for (_, a) in ns {1600 names.insert(a.clone());1601 }1602 }1603 StmtKind::For(n, _, b) => {1604 names.extend(n.names());1605 declare(b, names)1606 }1607 StmtKind::While(_, b) => declare(b, names),1608 StmtKind::If(bs, other) => {1609 for (_, b) in bs {1610 declare(b, names)1611 }1612 declare(other, names)1613 }1614 _ => {}1615 }1616 }1617 }1618 declare(&stmts, &mut declared);1619 let inferred: BTreeMap<String, String> = index(uri, source, en)1620 .symbols1621 .into_iter()1622 .map(|s| (s.name, s.typ))1623 .collect();1624 fn walk(1625 e: &Expr,1626 out: &mut Vec<J>,1627 names: &BTreeSet<String>,1628 inferred: &BTreeMap<String, String>,1629 source: &str,1630 en: bool,1631 ) {1632 let mut report =1633 |from: usize, to: usize, code: &str, message: String, replacement: Option<&str>| {1634 let from = boundary(source, from);1635 let mut to = to.min(source.len());1636 while !source.is_char_boundary(to) {1637 to += 1;1638 }1639 let mut d =1640 json!({"from":from,"to":to,"severity":"error","code":code,"message":message});1641 if let Some(r) = replacement {1642 d["replacement"] = json!(r)1643 }1644 out.push(d)1645 };1646 match &e.kind {1647 ExprKind::Ref(ps) => {1648 if !names.contains(&ps[0]) {1649 report(1650 e.loc.offset,1651 e.loc.offset + ps[0].len(),1652 "E_NAME",1653 format!(1654 "{} {}",1655 if en {1656 "Unknown name"1657 } else {1658 "未定义名称"1659 },1660 ps[0]1661 ),1662 None,1663 )1664 }1665 }1666 ExprKind::Call(ps, args) => {1667 if ps == &["arrow"] && !names.contains("arrow") {1668 let edit = laymesh_core::migration::arrow_edits(source)1669 .into_iter()1670 .find(|v| v.0 == e.loc.offset);1671 let to = edit.as_ref().map(|v| v.1).unwrap_or(e.loc.offset + 5);1672 report(1673 e.loc.offset,1674 to,1675 "E_API_MIGRATION",1676 if en {1677 "arrow has been removed; use line(..., end_head=head(...))"1678 } else {1679 "arrow 已移除,使用 line(..., end_head=head(...))"1680 }1681 .into(),1682 edit.as_ref().map(|v| v.2.as_str()),1683 );1684 } else if !names.contains(&ps[0]) {1685 report(1686 e.loc.offset,1687 e.loc.offset + ps[0].len(),1688 "E_NAME",1689 format!(1690 "{} {}",1691 if en {1692 "Unknown callable"1693 } else {1694 "未定义调用"1695 },1696 ps[0]1697 ),1698 None,1699 )1700 }1701 let last = ps.last().unwrap();1702 let typ = inferred.get(&ps[0]).map(String::as_str).unwrap_or("");1703 let surface = if ps.len() > 1 && ["plot", "polar_plot", "radar_plot"].contains(&typ)1704 {1705 format!("plot.{last}")1706 } else if ps.len() > 11707 && typ == "instance"1708 && ["data", "axis"].contains(&last.as_str())1709 {1710 format!("instance.{last}")1711 } else if ps.len() > 1 && ["dict", "table", "cmap"].contains(&typ) {1712 format!("{}.{last}", if typ == "table" { "dict" } else { typ })1713 } else if ps.len() > 1 {1714 geometry_surface(last).unwrap_or_else(|| last.clone())1715 } else {1716 last.clone()1717 };1718 if let Some(api) = entry(&surface) {1719 for (n, v) in args {1720 if let Some(n) = n {1721 let before = &source[e.loc.offset..v.loc.offset.min(source.len())];1722 let re = Regex::new(&format!(r"\b{}\s*=", regex::escape(n))).unwrap();1723 let from = re1724 .find_iter(before)1725 .last()1726 .map(|m| e.loc.offset + m.start())1727 .unwrap_or(v.loc.offset);1728 let rename = match n.as_str() {1729 "font" => Some("font_family"),1730 "weight" => Some("font_weight"),1731 "italic" => Some("font_style"),1732 "stroke" => Some("border_color"),1733 "stroke_width" => Some("border_width"),1734 "classes" => Some("class"),1735 "width" | "height" => Some("size"),1736 _ => None,1737 };1738 if let Some(rename) = rename {1739 report(1740 from,1741 from + n.len(),1742 "E_API_MIGRATION",1743 format!("{n} → {rename}"),1744 Some(rename),1745 );1746 continue;1747 }1748 let parameter = api["parameters"]1749 .as_array()1750 .unwrap()1751 .iter()1752 .find(|p| p["name"] == *n);1753 if parameter.is_none() {1754 report(1755 from,1756 from + n.len(),1757 "E_ARG",1758 format!(1759 "{surface}: {} {n}",1760 if en {1761 "unknown parameter"1762 } else {1763 "不支持参数"1764 }1765 ),1766 None,1767 );1768 continue;1769 }1770 let p = parameter.unwrap();1771 fn angle_in_length(v: &Expr) -> bool {1772 match &v.kind {1773 ExprKind::Number(_, unit) => unit == "deg" || unit == "rad",1774 ExprKind::List(values) => values.iter().any(angle_in_length),1775 _ => false,1776 }1777 }1778 if (p["unit"] == "geometry" || p["unit"] == "pt") && angle_in_length(v)1779 {1780 report(1781 v.loc.offset,1782 v.loc.offset + 1,1783 "E_UNIT",1784 if en {1785 "Expected a length, received an angle"1786 } else {1787 "需要长度,不能使用角度"1788 }1789 .into(),1790 None,1791 )1792 }1793 if n == "font_family"1794 && (matches!(v.kind, ExprKind::Number(..) | ExprKind::Bool(_))1795 || matches!(&v.kind, ExprKind::List(values) if values.iter().any(|v| matches!(v.kind, ExprKind::Number(..) | ExprKind::Bool(_)))))1796 {1797 report(1798 v.loc.offset,1799 v.loc.offset + 1,1800 "E_TYPE",1801 if en {1802 "Expected a font name or ordered string list"1803 } else {1804 "font_family 需要字体名称或有序字符串列表"1805 }1806 .into(),1807 None,1808 )1809 }1810 if matches!(v.kind, ExprKind::Bool(_))1811 && (p["unit"] == "geometry" || p["unit"] == "pt")1812 {1813 report(1814 v.loc.offset,1815 v.loc.offset + 1,1816 "E_TYPE",1817 if en {1818 "Expected a physical length"1819 } else {1820 "需要物理长度"1821 }1822 .into(),1823 None,1824 );1825 }1826 if n == "size" && matches!(&v.kind,ExprKind::List(xs)if xs.len()!=2) {1827 report(1828 v.loc.offset,1829 v.loc.offset + 1,1830 "E_TYPE",1831 "size requires two dimensions".into(),1832 None,1833 )1834 }1835 if n == "opacity"1836 && matches!(&v.kind,ExprKind::Number(n,u)if !u.is_empty()||*n<0.||*n>1.)1837 {1838 report(1839 v.loc.offset,1840 v.loc.offset + 1,1841 "E_VALUE",1842 "opacity: 0–1".into(),1843 None,1844 )1845 }1846 if let ExprKind::Ref(parts) = &v.kind {1847 if parts.len() == 1 && !inferred.contains_key(&parts[0]) {1848 if let Some(value) = predefined_value(&parts[0]) {1849 if p["values"].is_array()1850 && !strs(&p["values"]).contains(&value.to_string())1851 {1852 report(1853 v.loc.offset,1854 v.loc.offset + parts[0].len(),1855 "E_VALUE",1856 format!("{n}: {}", strs(&p["values"]).join(" / ")),1857 None,1858 );1859 }1860 }1861 }1862 }1863 if let ExprKind::String(s, _, _) = &v.kind {1864 if matches!(p["type"].as_str(), Some("color" | "paint"))1865 && laymesh_core::color::Color::parse(s).is_err()1866 {1867 report(v.loc.offset,v.loc.offset+1,"E_COLOR",if en {"Invalid color; use HEX, RGB, HSV or OKLCH with valid channels"} else {"无效颜色;使用通道合法的 HEX、RGB、HSV 或 OKLCH"}.into(),None);1868 }1869 if p["values"].is_array()1870 && !strs(&p["values"]).contains(s)1871 && n != "scale"1872 && n != "style"1873 {1874 report(1875 v.loc.offset,1876 v.loc.offset + 1,1877 "E_VALUE",1878 format!("{n}: {}", strs(&p["values"]).join(" / ")),1879 None,1880 )1881 }1882 }1883 }1884 }1885 }1886 fn number(e: &Expr) -> bool {1887 match &e.kind {1888 ExprKind::Number(..) => true,1889 ExprKind::Unary(op, v) if op == "-" || op == "+" => number(v),1890 _ => false,1891 }1892 }1893 if ps.len() == 11894 && ["rgb", "hsv", "oklch"].contains(&last.as_str())1895 && !inferred.contains_key(last)1896 && args.len() >= 31897 && args.iter().all(|(_, v)| number(v))1898 && laymesh_core::color::constant_expression(e).is_none()1899 {1900 report(e.loc.offset,e.loc.offset+last.len(),"E_COLOR",if en {"Invalid color channels: RGB 0–255, HSV S/V and OKLCH L 0–1, C ≥ 0, alpha 0–1; angle units only on hue"} else {"颜色通道无效:RGB 0–255,HSV S/V 和 OKLCH L 0–1,C ≥ 0,alpha 0–1;仅色相接受角度单位"}.into(),None);1901 }1902 if surface == "line" {1903 let keys = args1904 .iter()1905 .filter_map(|(k, _)| k.as_deref())1906 .collect::<Vec<_>>();1907 if (keys.contains(&"length") || keys.contains(&"angle"))1908 && (keys.contains(&"dx") || keys.contains(&"dy"))1909 {1910 report(1911 e.loc.offset,1912 e.loc.offset + 4,1913 "E_ARG",1914 if en {1915 "length/angle and dx/dy are mutually exclusive"1916 } else {1917 "length/angle 与 dx/dy 互斥"1918 }1919 .into(),1920 None,1921 );1922 }1923 if !keys.contains(&"angle")1924 && args.iter().any(|(k, v)| {1925 k.as_deref() == Some("length")1926 && matches!(&v.kind,ExprKind::Number(n,_)if *n==0.)1927 })1928 {1929 report(1930 e.loc.offset,1931 e.loc.offset + 4,1932 "E_ANCHOR_DIRECTION",1933 if en {1934 "Zero-length line requires an explicit angle"1935 } else {1936 "零长度线需要显式 angle"1937 }1938 .into(),1939 None,1940 );1941 }1942 }1943 for (_, v) in args {1944 walk(v, out, names, inferred, source, en)1945 }1946 }1947 ExprKind::Dict(es) => {1948 for (key, value) in es {1949 walk(key, out, names, inferred, source, en);1950 walk(value, out, names, inferred, source, en);1951 }1952 }1953 ExprKind::List(vs) => {1954 for v in vs {1955 walk(v, out, names, inferred, source, en)1956 }1957 }1958 ExprKind::Member(receiver, _) => walk(receiver, out, names, inferred, source, en),1959 ExprKind::Method(receiver, name, args) => {1960 walk(receiver, out, names, inferred, source, en);1961 let call = Expr {1962 loc: e.loc,1963 kind: ExprKind::Call(vec!["self".into(), name.clone()], args.clone()),1964 };1965 walk(&call, out, names, inferred, source, en);1966 }1967 ExprKind::Index(a, b) | ExprKind::Binary(_, a, b) => {1968 walk(a, out, names, inferred, source, en);1969 walk(b, out, names, inferred, source, en)1970 }1971 ExprKind::Unary(_, v) => walk(v, out, names, inferred, source, en),1972 _ => {}1973 }1974 }1975 fn statements(1976 stmts: &[Stmt],1977 out: &mut Vec<J>,1978 names: &BTreeSet<String>,1979 inferred: &BTreeMap<String, String>,1980 source: &str,1981 en: bool,1982 ) {1983 for s in stmts {1984 match &s.kind {1985 StmtKind::Bind(_, e, _) | StmtKind::Expr(e) | StmtKind::Return(e) => {1986 walk(e, out, names, inferred, source, en)1987 }1988 StmtKind::SetIndex(target, value) => {1989 walk(target, out, names, inferred, source, en);1990 walk(value, out, names, inferred, source, en);1991 }1992 StmtKind::Function(_, ps, b, _) => {1993 for (_, e) in ps {1994 if let Some(e) = e {1995 walk(e, out, names, inferred, source, en)1996 }1997 }1998 statements(b, out, names, inferred, source, en)1999 }2000 StmtKind::If(bs, other) => {2001 for (e, b) in bs {2002 walk(e, out, names, inferred, source, en);2003 statements(b, out, names, inferred, source, en)2004 }2005 statements(other, out, names, inferred, source, en)2006 }2007 StmtKind::For(_, e, b) | StmtKind::While(e, b) => {2008 walk(e, out, names, inferred, source, en);2009 statements(b, out, names, inferred, source, en)2010 }2011 StmtKind::Style(css) => {2012 if let Err(error) = laymesh_core::style::validate_stylesheet(css, "", s.loc) {2013 out.push(json!({"from":error.loc.offset.min(source.len()),"to":(error.loc.offset+1).min(source.len()),"severity":"error","code":error.code,"message":error.message}));2014 }2015 }2016 _ => {}2017 }2018 }2019 }2020 statements(&stmts, &mut out, &declared, &inferred, source, en);2021 json!(out)2022}