1use std::{io, path::Path};
9
10use ingot_compiler::{
11 compile_path, compile_source, format_source as compiler_format_source, Compilation,
12};
13use ingot_diagnostics::{DiagnosticBag, Severity};
14use ingot_source::{SourceFile, SourceMap, Span};
15use ingot_syntax::{
16 AgentDecl, Arg, BudgetLimit, DottedName, EffectDecl, Expr, FieldDecl, FlowBlock, FunctionDecl,
17 Ident, InterpolationPath, ModelRequirement, OutputDecl, PathRoot, PolicyAction, PolicyRule,
18 Program, Stmt, StringLit, StringPart, ToolDecl, TypeDecl, TypeExpr, VerifierDecl,
19};
20use ingot_types::{PolicySubject, MODEL_CAPABILITIES};
21use serde::Serialize;
22
23pub mod canvas;
24
25pub const LANGUAGE_SERVICE_SCHEMA_VERSION: u32 = 1;
26
27#[derive(Debug, Default, Clone, Copy)]
28pub struct LanguageService;
29
30impl LanguageService {
31 pub fn new() -> Self {
32 LanguageService
33 }
34
35 pub fn check_source(&self, name: impl Into<String>, text: impl Into<String>) -> CheckResult {
36 CheckResult::from_compilation(compile_source(name, text))
37 }
38
39 pub fn check_file(&self, path: impl AsRef<Path>) -> io::Result<CheckResult> {
40 compile_path(path).map(CheckResult::from_compilation)
41 }
42
43 pub fn completion_items(
44 &self,
45 name: impl Into<String>,
46 text: impl Into<String>,
47 position: EditorPosition,
48 ) -> CompletionResult {
49 let text = text.into();
50 let compilation = compile_source(name, text.clone());
51 let diagnostics = collect_diagnostics(&compilation.sources, &compilation.diagnostics);
52 let prefix = word_at_position(&text, position).unwrap_or_default();
53 let symbols = collect_symbols(&compilation);
54 let mut items = builtin_completion_items();
55
56 for symbol in symbols {
57 items.push(CompletionItem {
58 label: symbol.name,
59 kind: completion_kind_for_symbol(symbol.kind),
60 detail: Some(symbol.detail),
61 documentation: symbol.documentation,
62 insert_text: None,
63 });
64 }
65
66 if !prefix.is_empty() {
67 items.retain(|item| item.label.starts_with(&prefix) || item.label.contains(&prefix));
68 }
69 items.sort_by(|left, right| {
70 completion_sort_key(left.kind, &left.label)
71 .cmp(&completion_sort_key(right.kind, &right.label))
72 });
73 items.dedup_by(|left, right| left.label == right.label && left.kind == right.kind);
74
75 CompletionResult {
76 schema_version: LANGUAGE_SERVICE_SCHEMA_VERSION,
77 diagnostics,
78 items,
79 }
80 }
81
82 pub fn hover(
83 &self,
84 name: impl Into<String>,
85 text: impl Into<String>,
86 position: EditorPosition,
87 ) -> HoverResult {
88 let text = text.into();
89 let compilation = compile_source(name, text.clone());
90 let diagnostics = collect_diagnostics(&compilation.sources, &compilation.diagnostics);
91 let offset = byte_offset_for_position(&text, position);
92 let word = word_at_byte_offset(&text, offset).unwrap_or_default();
93 let symbols = collect_symbols(&compilation);
94
95 let hover = symbols
96 .iter()
97 .find(|symbol| contains_byte(&symbol.name_range, offset))
98 .or_else(|| {
99 (!word.is_empty())
100 .then(|| symbols.iter().find(|symbol| symbol.name == word))
101 .flatten()
102 })
103 .map(|symbol| HoverItem {
104 contents: hover_markdown(symbol),
105 range: symbol.name_range.clone(),
106 })
107 .or_else(|| keyword_hover(&word, &compilation.sources, &text, offset));
108
109 HoverResult {
110 schema_version: LANGUAGE_SERVICE_SCHEMA_VERSION,
111 diagnostics,
112 hover,
113 }
114 }
115
116 pub fn definition(
117 &self,
118 name: impl Into<String>,
119 text: impl Into<String>,
120 position: EditorPosition,
121 ) -> DefinitionResult {
122 let text = text.into();
123 let compilation = compile_source(name, text.clone());
124 let diagnostics = collect_diagnostics(&compilation.sources, &compilation.diagnostics);
125 let offset = byte_offset_for_position(&text, position);
126 let word = word_at_byte_offset(&text, offset).unwrap_or_default();
127 let symbols = collect_symbols(&compilation);
128
129 let definition = symbols
130 .iter()
131 .filter(|symbol| is_definition_target(symbol))
132 .find(|symbol| contains_byte(&symbol.name_range, offset))
133 .or_else(|| {
134 (!word.is_empty())
135 .then(|| {
136 symbols
137 .iter()
138 .filter(|symbol| is_definition_target(symbol))
139 .find(|symbol| symbol.name == word)
140 })
141 .flatten()
142 })
143 .map(|symbol| DefinitionItem {
144 name: symbol.name.clone(),
145 kind: symbol.kind,
146 target: symbol.name_range.clone(),
147 declaration: symbol.declaration_range.clone(),
148 });
149
150 DefinitionResult {
151 schema_version: LANGUAGE_SERVICE_SCHEMA_VERSION,
152 diagnostics,
153 definition,
154 }
155 }
156
157 pub fn format_source(
158 &self,
159 name: impl Into<String>,
160 text: impl Into<String>,
161 ) -> DocumentFormatResult {
162 let original = text.into();
163 let result = compiler_format_source(name, original.clone());
164 let diagnostics = collect_diagnostics(&result.sources, &result.diagnostics);
165 let edits = match result.formatted {
166 Some(formatted) if formatted != original => {
167 vec![TextEdit {
168 range: full_document_range(&result.sources),
169 new_text: formatted,
170 }]
171 }
172 _ => Vec::new(),
173 };
174
175 DocumentFormatResult {
176 schema_version: LANGUAGE_SERVICE_SCHEMA_VERSION,
177 diagnostics,
178 edits,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub struct CheckResult {
186 pub schema_version: u32,
187 pub has_errors: bool,
188 pub error_count: usize,
189 pub warning_count: usize,
190 pub diagnostics: Vec<EditorDiagnostic>,
191}
192
193impl CheckResult {
194 fn from_compilation(compilation: Compilation) -> Self {
195 let diagnostics = collect_diagnostics(&compilation.sources, &compilation.diagnostics);
196 CheckResult {
197 schema_version: LANGUAGE_SERVICE_SCHEMA_VERSION,
198 has_errors: compilation.has_errors(),
199 error_count: compilation.error_count(),
200 warning_count: compilation.warning_count(),
201 diagnostics,
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize)]
207#[serde(rename_all = "camelCase")]
208pub struct DocumentFormatResult {
209 pub schema_version: u32,
210 pub diagnostics: Vec<EditorDiagnostic>,
211 pub edits: Vec<TextEdit>,
212}
213
214#[derive(Debug, Clone, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct CompletionResult {
217 pub schema_version: u32,
218 pub diagnostics: Vec<EditorDiagnostic>,
219 pub items: Vec<CompletionItem>,
220}
221
222#[derive(Debug, Clone, Serialize)]
223#[serde(rename_all = "camelCase")]
224pub struct CompletionItem {
225 pub label: String,
226 pub kind: CompletionKind,
227 pub detail: Option<String>,
228 pub documentation: Option<String>,
229 pub insert_text: Option<String>,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
233#[serde(rename_all = "camelCase")]
234pub enum CompletionKind {
235 Keyword,
236 Type,
237 Tool,
238 Verifier,
239 Function,
240 Agent,
241 Field,
242 Binding,
243 Output,
244 Builtin,
245 Section,
246 Value,
247}
248
249#[derive(Debug, Clone, Serialize)]
250#[serde(rename_all = "camelCase")]
251pub struct HoverResult {
252 pub schema_version: u32,
253 pub diagnostics: Vec<EditorDiagnostic>,
254 pub hover: Option<HoverItem>,
255}
256
257#[derive(Debug, Clone, Serialize)]
258#[serde(rename_all = "camelCase")]
259pub struct HoverItem {
260 pub contents: String,
261 pub range: SourceRange,
262}
263
264#[derive(Debug, Clone, Serialize)]
265#[serde(rename_all = "camelCase")]
266pub struct DefinitionResult {
267 pub schema_version: u32,
268 pub diagnostics: Vec<EditorDiagnostic>,
269 pub definition: Option<DefinitionItem>,
270}
271
272#[derive(Debug, Clone, Serialize)]
273#[serde(rename_all = "camelCase")]
274pub struct DefinitionItem {
275 pub name: String,
276 pub kind: SymbolKind,
277 pub target: SourceRange,
278 pub declaration: SourceRange,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
282#[serde(rename_all = "camelCase")]
283pub enum SymbolKind {
284 Package,
285 Type,
286 Tool,
287 Verifier,
288 Function,
289 Agent,
290 Field,
291 Parameter,
292 State,
293 Output,
294 Binding,
295 LoopVariable,
296 ModelCapability,
297 BudgetKey,
298 PolicySubject,
299}
300
301#[derive(Debug, Clone, Serialize)]
302#[serde(rename_all = "camelCase")]
303pub struct EditorDiagnostic {
304 pub code: String,
305 pub severity: Severity,
306 pub message: String,
307 pub range: SourceRange,
308 pub labels: Vec<EditorLabel>,
309 pub notes: Vec<String>,
310 pub help: Option<String>,
311}
312
313#[derive(Debug, Clone, Serialize)]
314#[serde(rename_all = "camelCase")]
315pub struct EditorLabel {
316 pub primary: bool,
317 pub message: String,
318 pub range: SourceRange,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
322#[serde(rename_all = "camelCase")]
323pub struct SourceRange {
324 pub file: String,
325 pub start_byte: u32,
326 pub end_byte: u32,
327 pub range: EditorRange,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
331#[serde(rename_all = "camelCase")]
332pub struct EditorRange {
333 pub start: EditorPosition,
334 pub end: EditorPosition,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
338#[serde(rename_all = "camelCase")]
339pub struct EditorPosition {
340 pub line: u32,
342 pub character: u32,
344}
345
346#[derive(Debug, Clone, Serialize)]
347#[serde(rename_all = "camelCase")]
348pub struct TextEdit {
349 pub range: SourceRange,
350 pub new_text: String,
351}
352
353fn collect_diagnostics(map: &SourceMap, diagnostics: &DiagnosticBag) -> Vec<EditorDiagnostic> {
354 diagnostics
355 .iter()
356 .map(|diagnostic| {
357 let labels: Vec<EditorLabel> = diagnostic
358 .labels
359 .iter()
360 .map(|label| EditorLabel {
361 primary: label.primary,
362 message: label.message.clone(),
363 range: source_range(map, label.span),
364 })
365 .collect();
366 let range = labels
367 .iter()
368 .find(|label| label.primary)
369 .or_else(|| labels.first())
370 .map(|label| label.range.clone())
371 .unwrap_or_else(|| zero_range(map));
372
373 EditorDiagnostic {
374 code: diagnostic.code.to_string(),
375 severity: diagnostic.severity,
376 message: diagnostic.message.clone(),
377 range,
378 labels,
379 notes: diagnostic.notes.clone(),
380 help: diagnostic.help.clone(),
381 }
382 })
383 .collect()
384}
385
386pub(crate) fn source_range(map: &SourceMap, span: Span) -> SourceRange {
387 let file = map.file(span.file);
388 SourceRange {
389 file: file.name().to_string(),
390 start_byte: span.start,
391 end_byte: span.end,
392 range: editor_range(file.text(), span.start, span.end),
393 }
394}
395
396fn full_document_range(map: &SourceMap) -> SourceRange {
397 let file = first_file(map);
398 SourceRange {
399 file: file.name().to_string(),
400 start_byte: 0,
401 end_byte: file.text().len() as u32,
402 range: editor_range(file.text(), 0, file.text().len() as u32),
403 }
404}
405
406fn zero_range(map: &SourceMap) -> SourceRange {
407 let file = first_file(map);
408 SourceRange {
409 file: file.name().to_string(),
410 start_byte: 0,
411 end_byte: 0,
412 range: editor_range(file.text(), 0, 0),
413 }
414}
415
416fn first_file(map: &SourceMap) -> &SourceFile {
417 map.files()
418 .next()
419 .expect("language service results always have a registered source file")
420}
421
422fn editor_range(text: &str, start: u32, end: u32) -> EditorRange {
423 EditorRange {
424 start: position_at_byte_offset(text, start),
425 end: position_at_byte_offset(text, end),
426 }
427}
428
429fn position_at_byte_offset(text: &str, offset: u32) -> EditorPosition {
430 let mut offset = (offset as usize).min(text.len());
431 while !text.is_char_boundary(offset) {
432 offset -= 1;
433 }
434
435 let mut line = 0u32;
436 let mut line_start = 0usize;
437 for (index, ch) in text.char_indices() {
438 if index >= offset {
439 break;
440 }
441 if ch == '\n' {
442 line += 1;
443 line_start = index + ch.len_utf8();
444 }
445 }
446
447 let character = text[line_start..offset]
448 .chars()
449 .map(|ch| ch.len_utf16() as u32)
450 .sum();
451 EditorPosition { line, character }
452}
453
454fn byte_offset_for_position(text: &str, position: EditorPosition) -> u32 {
455 let mut line = 0u32;
456 let mut line_start = 0usize;
457 for (index, ch) in text.char_indices() {
458 if line == position.line {
459 break;
460 }
461 if ch == '\n' {
462 line += 1;
463 line_start = index + ch.len_utf8();
464 }
465 }
466
467 if line < position.line {
468 return text.len() as u32;
469 }
470
471 let mut utf16 = 0u32;
472 for (relative, ch) in text[line_start..].char_indices() {
473 if ch == '\n' || utf16 >= position.character {
474 return (line_start + relative) as u32;
475 }
476 let width = ch.len_utf16() as u32;
477 if utf16 + width > position.character {
478 return (line_start + relative) as u32;
479 }
480 utf16 += width;
481 }
482
483 text.len() as u32
484}
485
486#[derive(Debug, Clone)]
487struct EditorSymbol {
488 name: String,
489 kind: SymbolKind,
490 detail: String,
491 documentation: Option<String>,
492 name_range: SourceRange,
493 declaration_range: SourceRange,
494}
495
496fn collect_symbols(compilation: &Compilation) -> Vec<EditorSymbol> {
497 let mut symbols = Vec::new();
498 add_program_symbols(&compilation.sources, &compilation.program, &mut symbols);
499 symbols
500}
501
502fn add_program_symbols(map: &SourceMap, program: &Program, symbols: &mut Vec<EditorSymbol>) {
503 if let Some(package) = &program.package {
504 symbols.push(symbol_from_dotted(
505 map,
506 package,
507 package.span,
508 SymbolKind::Package,
509 format!("package {}", package.text()),
510 None,
511 ));
512 }
513
514 for decl in &program.types {
515 add_type_symbols(map, decl, symbols);
516 }
517 for decl in &program.tools {
518 add_tool_symbols(map, decl, symbols);
519 }
520 for decl in &program.verifiers {
521 add_verifier_symbols(map, decl, symbols);
522 }
523 for decl in &program.functions {
524 add_function_symbols(map, decl, symbols);
525 }
526 for decl in &program.agents {
527 add_agent_symbols(map, decl, symbols);
528 }
529}
530
531fn add_type_symbols(map: &SourceMap, decl: &TypeDecl, symbols: &mut Vec<EditorSymbol>) {
532 symbols.push(symbol_from_ident(
533 map,
534 &decl.name,
535 decl.span,
536 SymbolKind::Type,
537 format!("type {} {{ ... }}", decl.name.text),
538 decl.doc.clone(),
539 ));
540 for field in &decl.fields {
541 add_field_symbol(map, field, SymbolKind::Field, symbols);
542 add_type_reference(map, &field.ty, symbols);
543 }
544}
545
546fn add_tool_symbols(map: &SourceMap, decl: &ToolDecl, symbols: &mut Vec<EditorSymbol>) {
547 symbols.push(symbol_from_dotted(
548 map,
549 &decl.name,
550 decl.span,
551 SymbolKind::Tool,
552 format!(
553 "tool {}({}) -> {}{}",
554 decl.name.text(),
555 params_text(&decl.params),
556 decl.ret.text(),
557 effects_text(&decl.effects),
558 ),
559 decl.doc.clone(),
560 ));
561 for param in &decl.params {
562 add_field_symbol(map, param, SymbolKind::Parameter, symbols);
563 add_type_reference(map, ¶m.ty, symbols);
564 }
565 add_type_reference(map, &decl.ret, symbols);
566}
567
568fn add_verifier_symbols(map: &SourceMap, decl: &VerifierDecl, symbols: &mut Vec<EditorSymbol>) {
569 symbols.push(symbol_from_ident(
570 map,
571 &decl.name,
572 decl.span,
573 SymbolKind::Verifier,
574 format!("verifier {}({})", decl.name.text, params_text(&decl.params)),
575 decl.doc.clone(),
576 ));
577 for param in &decl.params {
578 add_field_symbol(map, param, SymbolKind::Parameter, symbols);
579 add_type_reference(map, ¶m.ty, symbols);
580 }
581}
582
583fn add_function_symbols(map: &SourceMap, decl: &FunctionDecl, symbols: &mut Vec<EditorSymbol>) {
584 symbols.push(symbol_from_ident(
585 map,
586 &decl.name,
587 decl.span,
588 SymbolKind::Function,
589 format!(
590 "fn {}({}) -> {}",
591 decl.name.text,
592 params_text(&decl.params),
593 decl.ret.text()
594 ),
595 decl.doc.clone(),
596 ));
597 for param in &decl.params {
598 add_field_symbol(map, param, SymbolKind::Parameter, symbols);
599 add_type_reference(map, ¶m.ty, symbols);
600 }
601 add_type_reference(map, &decl.ret, symbols);
602 add_expr_symbols(map, &decl.body, symbols);
603}
604
605fn add_agent_symbols(map: &SourceMap, decl: &AgentDecl, symbols: &mut Vec<EditorSymbol>) {
606 symbols.push(symbol_from_ident(
607 map,
608 &decl.name,
609 decl.span,
610 SymbolKind::Agent,
611 format!(
612 "agent {}({}){}",
613 decl.name.text,
614 params_text(&decl.params),
615 decl.output
616 .as_ref()
617 .map(|output| format!(" -> {}<{}>", output.name.text, output.content.text))
618 .unwrap_or_default()
619 ),
620 decl.doc.clone(),
621 ));
622 for param in &decl.params {
623 add_field_symbol(map, param, SymbolKind::Parameter, symbols);
624 add_type_reference(map, ¶m.ty, symbols);
625 }
626 if let Some(output) = &decl.output {
627 add_output_symbol(map, output, symbols);
628 }
629 if let Some(model) = &decl.model {
630 match model {
631 ingot_syntax::ModelBlock::Requires { requirements, .. } => {
632 for requirement in requirements {
633 if let ModelRequirement::Capability(ident) = requirement {
634 symbols.push(symbol_from_ident(
635 map,
636 ident,
637 ident.span,
638 SymbolKind::ModelCapability,
639 format!("model capability {}", ident.text),
640 Some("Capability the runtime model must provide.".to_string()),
641 ));
642 }
643 }
644 }
645 ingot_syntax::ModelBlock::Exact { .. } => {}
646 }
647 }
648 if let Some(tools) = &decl.tools {
649 for grant in &tools.grants {
650 symbols.push(symbol_from_dotted(
651 map,
652 &grant.name,
653 grant.span,
654 SymbolKind::Tool,
655 format!(
656 "tool grant {} via {}",
657 grant.name.text(),
658 grant.transport.text
659 ),
660 None,
661 ));
662 }
663 }
664 if let Some(memory) = &decl.memory {
665 if let Some(working) = &memory.working {
666 for field in &working.fields {
667 add_field_symbol(map, field, SymbolKind::State, symbols);
668 }
669 }
670 }
671 if let Some(budget) = &decl.budget {
672 for limit in &budget.limits {
673 add_budget_limit_symbol(map, limit, symbols);
674 }
675 }
676 if let Some(policy) = &decl.policy {
677 for rule in &policy.rules {
678 add_policy_rule_symbol(map, rule, symbols);
679 }
680 }
681 if let Some(flow) = &decl.flow {
682 add_flow_symbols(map, flow, symbols);
683 }
684}
685
686fn add_flow_symbols(map: &SourceMap, flow: &FlowBlock, symbols: &mut Vec<EditorSymbol>) {
687 add_statement_symbols(map, &flow.statements, symbols);
688}
689
690fn add_statement_symbols(map: &SourceMap, statements: &[Stmt], symbols: &mut Vec<EditorSymbol>) {
691 for statement in statements {
692 match statement {
693 Stmt::Bind { name, value, span } => {
694 symbols.push(symbol_from_ident(
695 map,
696 name,
697 *span,
698 SymbolKind::Binding,
699 format!("binding {}", name.text),
700 Some("Value bound inside this flow.".to_string()),
701 ));
702 add_expr_symbols(map, value, symbols);
703 }
704 Stmt::StateWrite { field, value, .. } => {
705 symbols.push(symbol_from_ident(
706 map,
707 field,
708 field.span,
709 SymbolKind::State,
710 format!("state.{}", field.text),
711 None,
712 ));
713 add_expr_symbols(map, value, symbols);
714 }
715 Stmt::Expr { value, .. } => add_expr_symbols(map, value, symbols),
716 Stmt::Verify {
717 validator, args, ..
718 } => {
719 symbols.push(symbol_from_ident(
720 map,
721 validator,
722 validator.span,
723 SymbolKind::Verifier,
724 format!("verify {}", validator.text),
725 None,
726 ));
727 add_arg_symbols(map, args, symbols);
728 }
729 Stmt::Emit { output, value, .. } => {
730 symbols.push(symbol_from_ident(
731 map,
732 output,
733 output.span,
734 SymbolKind::Output,
735 format!("emit {}", output.text),
736 None,
737 ));
738 add_expr_symbols(map, value, symbols);
739 }
740 Stmt::If {
741 condition,
742 then_branch,
743 else_branch,
744 ..
745 } => {
746 add_expr_symbols(map, condition, symbols);
747 add_statement_symbols(map, then_branch, symbols);
748 if let Some(else_branch) = else_branch {
749 add_statement_symbols(map, else_branch, symbols);
750 }
751 }
752 Stmt::Loop { guard, body, .. } => {
753 if let Some(guard) = guard {
754 add_expr_symbols(map, guard, symbols);
755 }
756 add_statement_symbols(map, body, symbols);
757 }
758 Stmt::Checkpoint { label, .. } => add_string_symbols(map, label, symbols),
759 Stmt::Error { .. } => {}
760 }
761 }
762}
763
764fn add_expr_symbols(map: &SourceMap, expr: &Expr, symbols: &mut Vec<EditorSymbol>) {
765 match expr {
766 Expr::Str(literal) => add_string_symbols(map, literal, symbols),
767 Expr::List { items, .. } => {
768 for item in items {
769 add_expr_symbols(map, item, symbols);
770 }
771 }
772 Expr::Path(path) => {
773 match &path.root {
774 PathRoot::Binding(ident) => symbols.push(symbol_from_ident(
775 map,
776 ident,
777 path.span,
778 SymbolKind::Binding,
779 format!("binding {}", ident.text),
780 None,
781 )),
782 PathRoot::State { span } => symbols.push(EditorSymbol {
783 name: "state".to_string(),
784 kind: SymbolKind::State,
785 detail: "state".to_string(),
786 documentation: Some("Agent working memory root.".to_string()),
787 name_range: source_range(map, *span),
788 declaration_range: source_range(map, path.span),
789 }),
790 PathRoot::Memory { span } => symbols.push(EditorSymbol {
791 name: "memory".to_string(),
792 kind: SymbolKind::State,
793 detail: "memory".to_string(),
794 documentation: Some(
795 "Agent persistent memory root. Survives the run.".to_string(),
796 ),
797 name_range: source_range(map, *span),
798 declaration_range: source_range(map, path.span),
799 }),
800 }
801 for segment in &path.segments {
802 symbols.push(symbol_from_ident(
803 map,
804 segment,
805 segment.span,
806 SymbolKind::Field,
807 format!("field {}", segment.text),
808 None,
809 ));
810 }
811 }
812 Expr::Ask { result, args, .. } => {
813 add_type_reference(map, result, symbols);
814 add_arg_symbols(map, args, symbols);
815 }
816 Expr::Consult { args, .. } => add_arg_symbols(map, args, symbols),
817 Expr::Call { callee, args, span } => {
818 symbols.push(symbol_from_dotted(
819 map,
820 callee,
821 *span,
822 SymbolKind::Tool,
823 format!("call {}", callee.text()),
824 None,
825 ));
826 add_arg_symbols(map, args, symbols);
827 }
828 Expr::ParallelMap {
829 source,
830 binder,
831 body,
832 span,
833 } => {
834 add_expr_symbols(map, source, symbols);
835 symbols.push(symbol_from_ident(
836 map,
837 binder,
838 *span,
839 SymbolKind::LoopVariable,
840 format!("loop variable {}", binder.text),
841 None,
842 ));
843 add_statement_symbols(map, body, symbols);
844 }
845 Expr::Builtin { name, args, .. } => {
846 symbols.push(symbol_from_ident(
847 map,
848 name,
849 name.span,
850 SymbolKind::Binding,
851 format!("builtin {}", name.text),
852 builtin_doc(&name.text).map(str::to_string),
853 ));
854 for arg in args {
855 add_expr_symbols(map, arg, symbols);
856 }
857 }
858 Expr::FunctionCall { callee, args, .. } => {
859 symbols.push(symbol_from_ident(
860 map,
861 callee,
862 callee.span,
863 SymbolKind::Function,
864 format!("function {}", callee.text),
865 None,
866 ));
867 add_arg_symbols(map, args, symbols);
868 }
869 Expr::Fallback {
870 attempt, fallback, ..
871 } => {
872 add_expr_symbols(map, attempt, symbols);
873 add_expr_symbols(map, fallback, symbols);
874 }
875 Expr::Unary { operand, .. } => add_expr_symbols(map, operand, symbols),
876 Expr::Binary { lhs, rhs, .. } => {
877 add_expr_symbols(map, lhs, symbols);
878 add_expr_symbols(map, rhs, symbols);
879 }
880 Expr::Int { .. } | Expr::Float { .. } | Expr::Bool { .. } | Expr::Error { .. } => {}
881 }
882}
883
884fn add_arg_symbols(map: &SourceMap, args: &[Arg], symbols: &mut Vec<EditorSymbol>) {
885 for arg in args {
886 if let Some(name) = &arg.name {
887 symbols.push(symbol_from_ident(
888 map,
889 name,
890 arg.span,
891 SymbolKind::Parameter,
892 format!("argument {}", name.text),
893 None,
894 ));
895 }
896 add_expr_symbols(map, &arg.value, symbols);
897 }
898}
899
900fn add_string_symbols(map: &SourceMap, literal: &StringLit, symbols: &mut Vec<EditorSymbol>) {
901 for part in &literal.parts {
902 if let StringPart::Interpolation(path) = part {
903 add_interpolation_symbols(map, path, symbols);
904 }
905 }
906}
907
908fn add_interpolation_symbols(
909 map: &SourceMap,
910 path: &InterpolationPath,
911 symbols: &mut Vec<EditorSymbol>,
912) {
913 match &path.root {
914 PathRoot::Binding(ident) => symbols.push(symbol_from_ident(
915 map,
916 ident,
917 path.span,
918 SymbolKind::Binding,
919 format!("binding {}", ident.text),
920 None,
921 )),
922 PathRoot::State { span } => symbols.push(EditorSymbol {
923 name: "state".to_string(),
924 kind: SymbolKind::State,
925 detail: "state".to_string(),
926 documentation: Some("Agent working memory root.".to_string()),
927 name_range: source_range(map, *span),
928 declaration_range: source_range(map, path.span),
929 }),
930 PathRoot::Memory { span } => symbols.push(EditorSymbol {
931 name: "memory".to_string(),
932 kind: SymbolKind::State,
933 detail: "memory".to_string(),
934 documentation: Some("Agent persistent memory root. Survives the run.".to_string()),
935 name_range: source_range(map, *span),
936 declaration_range: source_range(map, path.span),
937 }),
938 }
939 for segment in &path.segments {
940 symbols.push(symbol_from_ident(
941 map,
942 segment,
943 segment.span,
944 SymbolKind::Field,
945 format!("field {}", segment.text),
946 None,
947 ));
948 }
949}
950
951fn add_type_reference(map: &SourceMap, ty: &TypeExpr, symbols: &mut Vec<EditorSymbol>) {
952 match ty {
953 TypeExpr::Named(ident) => symbols.push(symbol_from_ident(
954 map,
955 ident,
956 ident.span,
957 SymbolKind::Type,
958 format!("type {}", ident.text),
959 primitive_type_doc(&ident.text).map(str::to_string),
960 )),
961 TypeExpr::List { element, .. } => add_type_reference(map, element, symbols),
962 TypeExpr::Optional { inner, .. } => add_type_reference(map, inner, symbols),
963 TypeExpr::Union { options, .. } => {
964 for option in options {
965 add_type_reference(map, option, symbols);
966 }
967 }
968 }
969}
970
971fn add_field_symbol(
972 map: &SourceMap,
973 field: &FieldDecl,
974 kind: SymbolKind,
975 symbols: &mut Vec<EditorSymbol>,
976) {
977 symbols.push(symbol_from_ident(
978 map,
979 &field.name,
980 field.span,
981 kind,
982 format!("{}: {}", field.name.text, field.ty.text()),
983 None,
984 ));
985}
986
987fn add_output_symbol(map: &SourceMap, output: &OutputDecl, symbols: &mut Vec<EditorSymbol>) {
988 symbols.push(symbol_from_ident(
989 map,
990 &output.name,
991 output.span,
992 SymbolKind::Output,
993 format!("output {}<{}>", output.name.text, output.content.text),
994 None,
995 ));
996 symbols.push(symbol_from_ident(
997 map,
998 &output.content,
999 output.content.span,
1000 SymbolKind::Type,
1001 format!("content type {}", output.content.text),
1002 primitive_type_doc(&output.content.text).map(str::to_string),
1003 ));
1004}
1005
1006fn add_budget_limit_symbol(map: &SourceMap, limit: &BudgetLimit, symbols: &mut Vec<EditorSymbol>) {
1007 symbols.push(symbol_from_ident(
1008 map,
1009 &limit.key,
1010 limit.span,
1011 SymbolKind::BudgetKey,
1012 format!("budget key {}", limit.key.text),
1013 Some("Budget limits constrain static steps, model tokens or cost.".to_string()),
1014 ));
1015}
1016
1017fn add_policy_rule_symbol(map: &SourceMap, rule: &PolicyRule, symbols: &mut Vec<EditorSymbol>) {
1018 symbols.push(symbol_from_ident(
1019 map,
1020 &rule.subject,
1021 rule.span,
1022 SymbolKind::PolicySubject,
1023 format!("policy subject {}", rule.subject.text),
1024 Some("Policy subjects map capabilities to allow/deny/approval decisions.".to_string()),
1025 ));
1026 match &rule.action {
1027 PolicyAction::Allow {
1028 qualifier: Some(qualifier),
1029 ..
1030 }
1031 | PolicyAction::Deny {
1032 qualifier: Some(qualifier),
1033 ..
1034 } => symbols.push(symbol_from_ident(
1035 map,
1036 qualifier,
1037 qualifier.span,
1038 SymbolKind::PolicySubject,
1039 format!("policy qualifier {}", qualifier.text),
1040 None,
1041 )),
1042 PolicyAction::Allow { .. }
1043 | PolicyAction::Deny { .. }
1044 | PolicyAction::RequireApproval { .. } => {}
1045 }
1046}
1047
1048fn symbol_from_ident(
1049 map: &SourceMap,
1050 ident: &Ident,
1051 declaration_span: Span,
1052 kind: SymbolKind,
1053 detail: String,
1054 documentation: Option<String>,
1055) -> EditorSymbol {
1056 EditorSymbol {
1057 name: ident.text.clone(),
1058 kind,
1059 detail,
1060 documentation,
1061 name_range: source_range(map, ident.span),
1062 declaration_range: source_range(map, declaration_span),
1063 }
1064}
1065
1066fn symbol_from_dotted(
1067 map: &SourceMap,
1068 name: &DottedName,
1069 declaration_span: Span,
1070 kind: SymbolKind,
1071 detail: String,
1072 documentation: Option<String>,
1073) -> EditorSymbol {
1074 EditorSymbol {
1075 name: name.text(),
1076 kind,
1077 detail,
1078 documentation,
1079 name_range: source_range(map, name.span),
1080 declaration_range: source_range(map, declaration_span),
1081 }
1082}
1083
1084fn params_text(params: &[FieldDecl]) -> String {
1085 params
1086 .iter()
1087 .map(|param| format!("{}: {}", param.name.text, param.ty.text()))
1088 .collect::<Vec<_>>()
1089 .join(", ")
1090}
1091
1092fn effects_text(effects: &[EffectDecl]) -> String {
1097 if effects.is_empty() {
1098 String::new()
1099 } else {
1100 format!(
1101 " {}",
1102 effects
1103 .iter()
1104 .map(|effect| {
1105 let mut text = format!("!{}", effect.name.text);
1106 if effect.parenthesised {
1107 let values: Vec<String> = effect
1108 .values
1109 .iter()
1110 .map(|value| format!("\"{}\"", value.template()))
1111 .collect();
1112 text.push_str(&format!("({})", values.join(", ")));
1113 }
1114 text
1115 })
1116 .collect::<Vec<_>>()
1117 .join(" ")
1118 )
1119 }
1120}
1121
1122fn builtin_completion_items() -> Vec<CompletionItem> {
1123 let mut items = Vec::new();
1124 for (label, detail, doc, insert_text) in KEYWORD_COMPLETIONS {
1125 items.push(CompletionItem {
1126 label: (*label).to_string(),
1127 kind: CompletionKind::Keyword,
1128 detail: Some((*detail).to_string()),
1129 documentation: Some((*doc).to_string()),
1130 insert_text: insert_text.map(str::to_string),
1131 });
1132 }
1133 for ty in PRIMITIVE_TYPES {
1134 items.push(CompletionItem {
1135 label: (*ty).to_string(),
1136 kind: CompletionKind::Type,
1137 detail: Some(format!("built-in type {ty}")),
1138 documentation: primitive_type_doc(ty).map(str::to_string),
1139 insert_text: None,
1140 });
1141 }
1142 for capability in MODEL_CAPABILITIES {
1143 items.push(CompletionItem {
1144 label: (*capability).to_string(),
1145 kind: CompletionKind::Value,
1146 detail: Some("model capability".to_string()),
1147 documentation: Some("Capability a selected model must support.".to_string()),
1148 insert_text: None,
1149 });
1150 }
1151 for subject in PolicySubject::all() {
1152 items.push(CompletionItem {
1153 label: subject.as_str().to_string(),
1154 kind: CompletionKind::Value,
1155 detail: Some("policy subject".to_string()),
1156 documentation: Some(format!(
1157 "Policy subject for the `{}` capability.",
1158 subject.effect()
1159 )),
1160 insert_text: None,
1161 });
1162 }
1163 for (label, detail, doc) in BUILTIN_FUNCTIONS {
1164 items.push(CompletionItem {
1165 label: (*label).to_string(),
1166 kind: CompletionKind::Builtin,
1167 detail: Some((*detail).to_string()),
1168 documentation: Some((*doc).to_string()),
1169 insert_text: None,
1170 });
1171 }
1172 items
1173}
1174
1175fn completion_kind_for_symbol(kind: SymbolKind) -> CompletionKind {
1176 match kind {
1177 SymbolKind::Package => CompletionKind::Section,
1178 SymbolKind::Type => CompletionKind::Type,
1179 SymbolKind::Tool => CompletionKind::Tool,
1180 SymbolKind::Verifier => CompletionKind::Verifier,
1181 SymbolKind::Function => CompletionKind::Function,
1182 SymbolKind::Agent => CompletionKind::Agent,
1183 SymbolKind::Field => CompletionKind::Field,
1184 SymbolKind::Parameter => CompletionKind::Binding,
1185 SymbolKind::State => CompletionKind::Field,
1186 SymbolKind::Output => CompletionKind::Output,
1187 SymbolKind::Binding | SymbolKind::LoopVariable => CompletionKind::Binding,
1188 SymbolKind::ModelCapability | SymbolKind::BudgetKey | SymbolKind::PolicySubject => {
1189 CompletionKind::Value
1190 }
1191 }
1192}
1193
1194fn completion_sort_key(kind: CompletionKind, label: &str) -> (u8, String) {
1195 let rank = match kind {
1196 CompletionKind::Keyword => 0,
1197 CompletionKind::Section => 1,
1198 CompletionKind::Agent => 2,
1199 CompletionKind::Tool => 3,
1200 CompletionKind::Verifier => 4,
1201 CompletionKind::Function => 5,
1202 CompletionKind::Type => 6,
1203 CompletionKind::Binding => 7,
1204 CompletionKind::Field => 8,
1205 CompletionKind::Output => 9,
1206 CompletionKind::Builtin => 10,
1207 CompletionKind::Value => 11,
1208 };
1209 (rank, label.to_string())
1210}
1211
1212fn hover_markdown(symbol: &EditorSymbol) -> String {
1213 let mut contents = format!("```ingot\n{}\n```", symbol.detail);
1214 if let Some(doc) = &symbol.documentation {
1215 contents.push_str("\n\n");
1216 contents.push_str(doc);
1217 }
1218 contents
1219}
1220
1221fn keyword_hover(word: &str, map: &SourceMap, text: &str, offset: u32) -> Option<HoverItem> {
1222 let (_, detail, doc, _) = KEYWORD_COMPLETIONS
1223 .iter()
1224 .find(|(label, _, _, _)| *label == word)?;
1225 let range = word_range_at_byte_offset(text, offset).unwrap_or((offset, offset));
1226 Some(HoverItem {
1227 contents: format!("```ingot\n{}\n```\n\n{}", detail, doc),
1228 range: source_range(map, Span::new(first_file(map).id(), range.0, range.1)),
1229 })
1230}
1231
1232fn word_at_position(text: &str, position: EditorPosition) -> Option<String> {
1233 let offset = byte_offset_for_position(text, position);
1234 word_at_byte_offset(text, offset)
1235}
1236
1237fn word_at_byte_offset(text: &str, offset: u32) -> Option<String> {
1238 let (start, end) = word_range_at_byte_offset(text, offset)?;
1239 Some(text[start as usize..end as usize].to_string())
1240}
1241
1242fn word_range_at_byte_offset(text: &str, offset: u32) -> Option<(u32, u32)> {
1243 if text.is_empty() {
1244 return None;
1245 }
1246 let mut offset = (offset as usize).min(text.len());
1247 while offset > 0 && !text.is_char_boundary(offset) {
1248 offset -= 1;
1249 }
1250 if offset == text.len() && offset > 0 {
1251 offset -= 1;
1252 while offset > 0 && !text.is_char_boundary(offset) {
1253 offset -= 1;
1254 }
1255 }
1256 if !is_symbol_char(text[offset..].chars().next()?) && offset > 0 {
1257 let previous = previous_char_start(text, offset)?;
1258 if is_symbol_char(text[previous..].chars().next()?) {
1259 offset = previous;
1260 }
1261 }
1262 if !is_symbol_char(text[offset..].chars().next()?) {
1263 return None;
1264 }
1265
1266 let mut start = offset;
1267 while let Some(previous) = previous_char_start(text, start) {
1268 let ch = text[previous..].chars().next()?;
1269 if !is_symbol_char(ch) {
1270 break;
1271 }
1272 start = previous;
1273 }
1274
1275 let mut end = offset;
1276 for (relative, ch) in text[offset..].char_indices() {
1277 if !is_symbol_char(ch) {
1278 break;
1279 }
1280 end = offset + relative + ch.len_utf8();
1281 }
1282 Some((start as u32, end as u32))
1283}
1284
1285fn previous_char_start(text: &str, offset: usize) -> Option<usize> {
1286 text[..offset]
1287 .char_indices()
1288 .next_back()
1289 .map(|(index, _)| index)
1290}
1291
1292fn is_symbol_char(ch: char) -> bool {
1293 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '.')
1294}
1295
1296fn contains_byte(range: &SourceRange, offset: u32) -> bool {
1297 range.start_byte <= offset && offset <= range.end_byte
1298}
1299
1300fn is_definition_target(symbol: &EditorSymbol) -> bool {
1301 match symbol.kind {
1302 SymbolKind::Package => symbol.detail.starts_with("package "),
1303 SymbolKind::Type => symbol
1304 .detail
1305 .starts_with(&format!("type {} {{", symbol.name)),
1306 SymbolKind::Tool => symbol.detail.starts_with(&format!("tool {}(", symbol.name)),
1307 SymbolKind::Verifier => symbol
1308 .detail
1309 .starts_with(&format!("verifier {}(", symbol.name)),
1310 SymbolKind::Function => symbol.detail.starts_with(&format!("fn {}(", symbol.name)),
1311 SymbolKind::Agent => symbol
1312 .detail
1313 .starts_with(&format!("agent {}(", symbol.name)),
1314 SymbolKind::Field
1315 | SymbolKind::Parameter
1316 | SymbolKind::State
1317 | SymbolKind::Output
1318 | SymbolKind::Binding
1319 | SymbolKind::LoopVariable
1320 | SymbolKind::ModelCapability
1321 | SymbolKind::BudgetKey
1322 | SymbolKind::PolicySubject => false,
1323 }
1324}
1325
1326const PRIMITIVE_TYPES: &[&str] = &[
1327 "string", "int", "float", "bool", "json", "bytes", "text", "markdown", "file",
1328];
1329
1330const BUILTIN_FUNCTIONS: &[(&str, &str, &str)] = &[(
1331 "len",
1332 "len(value) -> int",
1333 "Returns the length of a string, list or object-like value.",
1334)];
1335
1336const KEYWORD_COMPLETIONS: &[(&str, &str, &str, Option<&str>)] = &[
1337 (
1338 "language",
1339 "language 0.1",
1340 "Declares the Ingot language version.",
1341 Some("language 0.1"),
1342 ),
1343 (
1344 "package",
1345 "package name",
1346 "Groups declarations under a namespace.",
1347 None,
1348 ),
1349 (
1350 "import",
1351 "import \"./shared.ing\" { ... }",
1352 "Imports shared type, tool or verifier declarations from another Ingot source file.",
1353 Some("import \"./shared.ing\" {\n type Name\n}"),
1354 ),
1355 (
1356 "type",
1357 "type Name { ... }",
1358 "Declares a typed record shape.",
1359 None,
1360 ),
1361 (
1362 "tool",
1363 "tool name(args) -> type",
1364 "Declares a callable external capability.",
1365 None,
1366 ),
1367 (
1368 "verifier",
1369 "verifier Name(args)",
1370 "Declares a deterministic check.",
1371 None,
1372 ),
1373 (
1374 "fn",
1375 "fn name(args) -> type = expression",
1376 "Declares a pure helper expression.",
1377 None,
1378 ),
1379 (
1380 "agent",
1381 "agent Name(args) -> output<content> { ... }",
1382 "Declares an agent artifact.",
1383 None,
1384 ),
1385 (
1386 "model",
1387 "model requires { ... }",
1388 "Constrains or pins the runtime model.",
1389 None,
1390 ),
1391 (
1392 "requires",
1393 "requires { ... }",
1394 "Lists model capabilities.",
1395 None,
1396 ),
1397 (
1398 "exact",
1399 "exact \"provider/model\"",
1400 "Pins a model reference.",
1401 None,
1402 ),
1403 (
1404 "tools",
1405 "tools { ... }",
1406 "Grants declared tools to an agent.",
1407 None,
1408 ),
1409 (
1410 "mcp",
1411 "mcp tool.name",
1412 "Uses the MCP transport for a tool grant.",
1413 None,
1414 ),
1415 (
1416 "memory",
1417 "memory { ... }",
1418 "Declares agent working memory.",
1419 None,
1420 ),
1421 (
1422 "working",
1423 "working ephemeral { ... }",
1424 "Declares working memory fields.",
1425 None,
1426 ),
1427 (
1428 "ephemeral",
1429 "ephemeral",
1430 "Keeps working memory local to the run.",
1431 None,
1432 ),
1433 (
1434 "budget",
1435 "budget { ... }",
1436 "Constrains steps, tokens or cost.",
1437 None,
1438 ),
1439 (
1440 "policy",
1441 "policy { ... }",
1442 "Declares capability decisions.",
1443 None,
1444 ),
1445 ("allow", "allow", "Allows a policy subject.", None),
1446 ("deny", "deny", "Denies a policy subject.", None),
1447 (
1448 "require",
1449 "require approval",
1450 "Requires approval before a capability is used.",
1451 Some("require approval"),
1452 ),
1453 (
1454 "approval",
1455 "approval",
1456 "The second word in `require approval`.",
1457 None,
1458 ),
1459 (
1460 "flow",
1461 "flow { ... }",
1462 "Declares executable agent steps.",
1463 None,
1464 ),
1465 (
1466 "ask",
1467 "ask<type>(prompt)",
1468 "Requests model output of a specific type.",
1469 None,
1470 ),
1471 (
1472 "call",
1473 "call name(args)",
1474 "Calls a granted tool or sub-agent.",
1475 None,
1476 ),
1477 (
1478 "parallel",
1479 "parallel map items as item { ... }",
1480 "Runs a map body over a list.",
1481 None,
1482 ),
1483 ("map", "map", "Part of a parallel map expression.", None),
1484 ("as", "as", "Names the loop item in a parallel map.", None),
1485 (
1486 "verify",
1487 "verify Name(value)",
1488 "Runs a declared verifier.",
1489 None,
1490 ),
1491 (
1492 "emit",
1493 "emit output = value",
1494 "Emits the agent's declared output.",
1495 None,
1496 ),
1497 (
1498 "if",
1499 "if condition { ... }",
1500 "Branches flow execution.",
1501 None,
1502 ),
1503 (
1504 "else",
1505 "else { ... }",
1506 "Fallback branch for an if statement.",
1507 None,
1508 ),
1509 (
1510 "loop",
1511 "loop max N while condition { ... }",
1512 "Runs a bounded loop.",
1513 None,
1514 ),
1515 (
1516 "max",
1517 "max N",
1518 "Sets a static upper bound for a loop.",
1519 None,
1520 ),
1521 ("while", "while condition", "Adds a loop guard.", None),
1522 (
1523 "checkpoint",
1524 "checkpoint \"label\"",
1525 "Marks a named flow checkpoint.",
1526 None,
1527 ),
1528 (
1529 "state",
1530 "state.field",
1531 "Reads or writes working memory.",
1532 None,
1533 ),
1534 ("true", "true", "Boolean literal.", None),
1535 ("false", "false", "Boolean literal.", None),
1536];
1537
1538fn primitive_type_doc(name: &str) -> Option<&'static str> {
1539 Some(match name {
1540 "string" => "UTF-8 string data.",
1541 "int" => "Signed integer value.",
1542 "float" => "Floating-point numeric value.",
1543 "bool" => "Boolean value.",
1544 "json" => "JSON-compatible structured data.",
1545 "bytes" => "Opaque byte data.",
1546 "text" => "Plain text artifact content.",
1547 "markdown" => "Markdown artifact content.",
1548 "file" => "File handle produced or consumed by a tool.",
1549 _ => return None,
1550 })
1551}
1552
1553fn builtin_doc(name: &str) -> Option<&'static str> {
1554 BUILTIN_FUNCTIONS
1555 .iter()
1556 .find(|(label, _, _)| *label == name)
1557 .map(|(_, _, doc)| *doc)
1558}
1559
1560#[cfg(test)]
1561mod tests {
1562 use std::path::{Path, PathBuf};
1563
1564 use ingot_compiler::compile_source;
1565
1566 use super::*;
1567
1568 const BROKEN_SOURCE: &str = r#"language 0.1
1569package demo
1570
1571agent Brief(topic: string) -> report<markdown> {
1572 flow {
1573 emit report = ask<markdown>("Write about ${topci}")
1574 }
1575}
1576"#;
1577
1578 const AUTHORING_SOURCE: &str = r#"language 0.1
1579package demo
1580
1581/// Search result returned by the web tool.
1582type search_result { title: string url: string }
1583
1584/// Search the web.
1585tool web.search(query: string) -> search_result[] !network
1586
1587verifier CitationCheck(draft: markdown)
1588
1589agent Research(topic: string) -> report<markdown> {
1590 tools { mcp web.search }
1591 memory { working ephemeral { notes: string[] } }
1592 budget { steps <= 10 tokens <= 1000 }
1593 policy { network allow }
1594 flow {
1595
1596 hits = call web.search(topic)
1597 emit report = ask<markdown>("Report ${topic}")
1598 }
1599}
1600"#;
1601
1602 fn repo_root() -> PathBuf {
1603 Path::new(env!("CARGO_MANIFEST_DIR"))
1604 .parent()
1605 .and_then(Path::parent)
1606 .expect("the crate must live two levels below the repository root")
1607 .to_path_buf()
1608 }
1609
1610 fn position_for(text: &str, needle: &str) -> EditorPosition {
1611 let offset = text
1612 .find(needle)
1613 .unwrap_or_else(|| panic!("test source must contain {needle:?}"));
1614 position_at_byte_offset(text, offset as u32)
1615 }
1616
1617 fn position_after(text: &str, needle: &str) -> EditorPosition {
1618 let offset = text
1619 .find(needle)
1620 .unwrap_or_else(|| panic!("test source must contain {needle:?}"))
1621 + needle.len();
1622 position_at_byte_offset(text, offset as u32)
1623 }
1624
1625 #[test]
1626 fn editor_and_cli_diagnostics_are_identical() {
1627 let service = LanguageService::new();
1628 let editor = service.check_source("broken.ing", BROKEN_SOURCE);
1629 let cli = compile_source("broken.ing", BROKEN_SOURCE);
1630
1631 let editor_keys: Vec<_> = editor
1632 .diagnostics
1633 .iter()
1634 .map(|diagnostic| {
1635 (
1636 diagnostic.code.as_str(),
1637 diagnostic.range.start_byte,
1638 diagnostic.range.end_byte,
1639 diagnostic.message.as_str(),
1640 )
1641 })
1642 .collect();
1643 let cli_keys: Vec<_> = cli
1644 .diagnostics
1645 .iter()
1646 .map(|diagnostic| {
1647 let span = diagnostic
1648 .primary_span()
1649 .expect("compiler diagnostics used by editors must have a span");
1650 (
1651 diagnostic.code,
1652 span.start,
1653 span.end,
1654 diagnostic.message.as_str(),
1655 )
1656 })
1657 .collect();
1658
1659 assert_eq!(editor_keys, cli_keys);
1660 }
1661
1662 #[test]
1663 fn formatting_returns_a_full_document_edit() {
1664 let service = LanguageService::new();
1665 let source = r#"language 0.1
1666package demo
1667
1668agent Brief(topic:string)->report<markdown>{flow{emit report=ask<markdown>("ok")}}
1669"#;
1670
1671 let result = service.format_source("main.ing", source);
1672
1673 assert!(result.diagnostics.is_empty());
1674 assert_eq!(result.edits.len(), 1);
1675 assert_eq!(result.edits[0].range.start_byte, 0);
1676 assert_eq!(result.edits[0].range.end_byte, source.len() as u32);
1677 assert!(result.edits[0]
1678 .new_text
1679 .contains("agent Brief(topic: string)"));
1680 }
1681
1682 #[test]
1683 fn syntax_errors_return_diagnostics_without_format_edits() {
1684 let service = LanguageService::new();
1685 let result = service.format_source("broken.ing", "language 0.1\nagent\n");
1686
1687 assert!(!result.diagnostics.is_empty());
1688 assert!(result.edits.is_empty());
1689 }
1690
1691 #[test]
1692 fn reference_examples_are_clean_through_the_language_service() {
1693 let service = LanguageService::new();
1694 for example in [
1695 "document-summarizer",
1696 "research-agent",
1697 "code-review-team",
1698 "repo-digest",
1699 ] {
1700 let entry = repo_root().join("examples").join(example).join("main.ing");
1701 let result = service
1702 .check_file(&entry)
1703 .unwrap_or_else(|error| panic!("{} must be readable: {error}", entry.display()));
1704 assert!(
1705 !result.has_errors,
1706 "{example} should be clean through the language service"
1707 );
1708 }
1709 }
1710
1711 #[test]
1712 fn completion_includes_language_keywords_and_declared_symbols() {
1713 let service = LanguageService::new();
1714 let result = service.completion_items(
1715 "main.ing",
1716 AUTHORING_SOURCE,
1717 position_after(AUTHORING_SOURCE, "flow {\n"),
1718 );
1719 let labels: Vec<_> = result
1720 .items
1721 .iter()
1722 .map(|item| item.label.as_str())
1723 .collect();
1724
1725 assert!(labels.contains(&"agent"));
1726 assert!(labels.contains(&"search_result"));
1727 assert!(labels.contains(&"web.search"));
1728 assert!(labels.contains(&"CitationCheck"));
1729 assert!(labels.contains(&"len"));
1730 }
1731
1732 #[test]
1733 fn hover_returns_symbol_detail_and_doc_comment() {
1734 let service = LanguageService::new();
1735 let hover = service
1736 .hover(
1737 "main.ing",
1738 AUTHORING_SOURCE,
1739 position_for(AUTHORING_SOURCE, "search_result {"),
1740 )
1741 .hover
1742 .expect("type hover should be available");
1743
1744 assert!(hover.contents.contains("type search_result"));
1745 assert!(hover
1746 .contents
1747 .contains("Search result returned by the web tool."));
1748 assert_eq!(
1749 &AUTHORING_SOURCE[hover.range.start_byte as usize..hover.range.end_byte as usize],
1750 "search_result"
1751 );
1752 }
1753
1754 #[test]
1755 fn definition_goes_from_tool_use_to_tool_declaration() {
1756 let service = LanguageService::new();
1757 let definition = service
1758 .definition(
1759 "main.ing",
1760 AUTHORING_SOURCE,
1761 position_for(AUTHORING_SOURCE, "web.search(topic)"),
1762 )
1763 .definition
1764 .expect("tool definition should be available");
1765
1766 assert_eq!(definition.name, "web.search");
1767 assert_eq!(definition.kind, SymbolKind::Tool);
1768 assert_eq!(
1769 &AUTHORING_SOURCE
1770 [definition.target.start_byte as usize..definition.target.end_byte as usize],
1771 "web.search"
1772 );
1773 assert!(definition.declaration.start_byte < definition.target.start_byte);
1774 }
1775
1776 #[test]
1777 fn positions_are_zero_based_and_utf16_encoded() {
1778 let text = format!("a{}\nb", '\u{1f600}');
1779 let newline = text.find('\n').expect("test text has a newline") as u32;
1780
1781 assert_eq!(
1782 position_at_byte_offset(&text, newline),
1783 EditorPosition {
1784 line: 0,
1785 character: 3
1786 }
1787 );
1788 assert_eq!(
1789 position_at_byte_offset(&text, newline + 1),
1790 EditorPosition {
1791 line: 1,
1792 character: 0
1793 }
1794 );
1795 }
1796}