Skip to main content

ingot_language_service/
lib.rs

1//! Editor-neutral language services for `.ing` source.
2//!
3//! The language server and editor extensions should adapt this crate rather
4//! than reimplementing parsing, checking or formatting. Diagnostics come from
5//! `ingot-compiler`, then are projected into LSP-style ranges while keeping the
6//! original byte spans for exact CLI/editor comparisons.
7
8use 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    /// Zero-based line number.
341    pub line: u32,
342    /// Zero-based UTF-16 code-unit offset, matching LSP's `Position.character`.
343    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, &param.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, &param.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, &param.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, &param.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
1092/// `!network("arxiv.org") !filesystem_read`, for hover and completion detail.
1093///
1094/// The reach is shown, not summarised: hovering a tool to find out where it
1095/// goes and being told only that it goes somewhere is the question unanswered.
1096fn 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}