fix(lsp): correct line numbers in convert species/template tests

The tests were using line: 2 but the character declarations were on
line: 1 (due to the leading newline in the raw string literal). This
caused the cursor position to be outside the character span, making
the code actions fail to trigger.

Fixed by changing line: 2 to line: 1 in both test_convert_species_to_template
and test_convert_template_to_species.
This commit is contained in:
2026-02-14 16:29:22 +00:00
parent b042f81aeb
commit 26bbef58d3
7 changed files with 6812 additions and 4831 deletions

View File

@@ -28,20 +28,20 @@ Declaration: Declaration = {
// ===== Use declarations =====
UseDecl: UseDecl = {
"use" <path:Path> ";" => UseDecl {
<start:@L> "use" <path:Path> ";" <end:@R> => UseDecl {
path,
kind: UseKind::Single,
span: Span::new(0, 0), // TODO: track actual spans
span: Span::new(start, end),
},
"use" <base:PathSegments> "::" "{" <items:Comma<Ident>> "}" ";" => UseDecl {
<start:@L> "use" <base:PathSegments> "::" "{" <items:Comma<Ident>> "}" ";" <end:@R> => UseDecl {
path: base,
kind: UseKind::Grouped(items),
span: Span::new(0, 0),
span: Span::new(start, end),
},
"use" <path:PathSegments> "::" "*" ";" => UseDecl {
<start:@L> "use" <path:PathSegments> "::" "*" ";" <end:@R> => UseDecl {
path,
kind: UseKind::Wildcard,
span: Span::new(0, 0),
span: Span::new(start, end),
},
};
@@ -68,7 +68,7 @@ DottedPath: Vec<String> = {
// ===== Character =====
Character: Character = {
"character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <body:CharacterBody> "}" => {
<start:@L> "character" <name:Ident> <species:(":" <Ident>)?> <template:TemplateClause?> "{" <body:CharacterBody> "}" <end:@R> => {
Character {
name,
species,
@@ -76,7 +76,7 @@ Character: Character = {
template,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(0, 0),
span: Span::new(start, end),
}
}
};
@@ -121,11 +121,11 @@ UsesBehaviorsClause: Vec<BehaviorLink> = {
// Individual behavior link: { tree: BehaviorName, priority: high, when: condition }
BehaviorLinkItem: BehaviorLink = {
"{" <fields:BehaviorLinkField+> "}" => {
<start:@L> "{" <fields:BehaviorLinkField+> "}" <end:@R> => {
let mut tree = None;
let mut condition = None;
let mut priority = Priority::Normal;
for field in fields {
match field {
BehaviorLinkField::Tree(t) => tree = Some(t),
@@ -133,12 +133,12 @@ BehaviorLinkItem: BehaviorLink = {
BehaviorLinkField::Priority(p) => priority = p,
}
}
BehaviorLink {
tree: tree.expect("behavior link must have 'tree' field"),
condition,
priority,
span: Span::new(0, 0),
span: Span::new(start, end),
}
}
};
@@ -169,7 +169,7 @@ UsesScheduleClause: Vec<String> = {
// ===== Template =====
Template: Template = {
"template" <name:Ident> <species_base:(":" <Ident>)?> <strict:"strict"?> "{" <body:TemplateBodyItem*> "}" => {
<start:@L> "template" <name:Ident> <species_base:(":" <Ident>)?> <strict:"strict"?> "{" <body:TemplateBodyItem*> "}" <end:@R> => {
let mut fields = Vec::new();
let mut includes = Vec::new();
let mut uses_behaviors = None;
@@ -192,7 +192,7 @@ Template: Template = {
includes,
uses_behaviors,
uses_schedule,
span: Span::new(0, 0),
span: Span::new(start, end),
}
}
};
@@ -207,14 +207,15 @@ TemplateBodyItem: TemplateBodyItem = {
// Template-level behavior links (simple list, no priorities/conditions)
TemplateUsesBehaviorsClause: Vec<BehaviorLink> = {
"uses" "behaviors" ":" <first:Ident> <rest:("," <Ident>)*> => {
<start:@L> "uses" "behaviors" ":" <first:Ident> <rest:("," <Ident>)*> <end:@R> => {
let mut names = vec![first];
names.extend(rest);
let span = Span::new(start, end);
names.into_iter().map(|name| BehaviorLink {
tree: vec![name],
condition: None,
priority: Priority::Normal,
span: Span::new(0, 0),
span: span.clone(),
}).collect()
},
};
@@ -232,15 +233,15 @@ Include: String = {
// ===== Fields =====
Field: Field = {
<path:DottedPath> ":" <value:Value> => Field {
<start:@L> <path:DottedPath> ":" <value:Value> <end:@R> => Field {
name: path.join("."),
value,
span: Span::new(0, 0),
span: Span::new(start, end),
},
<pb:ProseBlock> => Field {
<start:@L> <pb:ProseBlock> <end:@R> => Field {
name: pb.tag.clone(),
value: Value::ProseBlock(pb),
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -322,10 +323,10 @@ ProseBlock: ProseBlock = {
};
Override: Override = {
"@" <base:Path> "{" <overrides:OverrideOp*> "}" => Override {
<start:@L> "@" <base:Path> "{" <overrides:OverrideOp*> "}" <end:@R> => Override {
base,
overrides,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -338,11 +339,11 @@ OverrideOp: OverrideOp = {
// ===== Life Arc =====
LifeArc: LifeArc = {
"life_arc" <name:Ident> <reqs:RequiresClause?> "{" <fields:Field*> <states:ArcState*> "}" => LifeArc {
<start:@L> "life_arc" <name:Ident> <reqs:RequiresClause?> "{" <fields:Field*> <states:ArcState*> "}" <end:@R> => LifeArc {
name,
required_fields: reqs.unwrap_or_default(),
states,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -351,19 +352,19 @@ RequiresClause: Vec<FieldRequirement> = {
};
FieldReq: FieldRequirement = {
<name:Ident> ":" <type_name:Ident> => FieldRequirement {
<start:@L> <name:Ident> ":" <type_name:Ident> <end:@R> => FieldRequirement {
name,
type_name,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
ArcState: ArcState = {
"state" <name:Ident> "{" <on_enter:OnEnter?> <fields:Field*> <transitions:Transition*> "}" => ArcState {
<start:@L> "state" <name:Ident> "{" <on_enter:OnEnter?> <fields:Field*> <transitions:Transition*> "}" <end:@R> => ArcState {
name,
on_enter,
transitions,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -372,10 +373,10 @@ OnEnter: Vec<Field> = {
};
Transition: Transition = {
"on" <cond:Expr> "->" <to:Ident> => Transition {
<start:@L> "on" <cond:Expr> "->" <to:Ident> <end:@R> => Transition {
to,
condition: cond,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -383,22 +384,22 @@ Transition: Transition = {
Schedule: Schedule = {
// Simple schedule: schedule Name { ... }
"schedule" <name:Ident> "{" <body:ScheduleBody> "}" => Schedule {
<start:@L> "schedule" <name:Ident> "{" <body:ScheduleBody> "}" <end:@R> => Schedule {
name,
extends: None,
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(0, 0),
span: Span::new(start, end),
},
// Extending schedule: schedule Name extends Base { ... }
"schedule" <name:Ident> "extends" <base:Ident> "{" <body:ScheduleBody> "}" => Schedule {
<start:@L> "schedule" <name:Ident> "extends" <base:Ident> "{" <body:ScheduleBody> "}" <end:@R> => Schedule {
name,
extends: Some(base),
fields: body.0,
blocks: body.1,
recurrences: body.2,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -429,7 +430,7 @@ ScheduleBodyItem: ScheduleBodyItem = {
ScheduleBlock: ScheduleBlock = {
// Legacy syntax: time -> time : activity { fields }
<start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" => ScheduleBlock {
<s:@L> <start:Time> "->" <end:Time> ":" <activity:Ident> "{" <fields:Field*> "}" <e:@R> => ScheduleBlock {
name: None,
is_override: false,
start,
@@ -438,11 +439,11 @@ ScheduleBlock: ScheduleBlock = {
action: None,
temporal_constraint: None,
fields,
span: Span::new(0, 0),
span: Span::new(s, e),
},
// Named block: block name { time, action, fields }
"block" <name:Ident> "{" <content:BlockContent> "}" => ScheduleBlock {
<s:@L> "block" <name:Ident> "{" <content:BlockContent> "}" <e:@R> => ScheduleBlock {
name: Some(name),
is_override: false,
start: content.0,
@@ -451,11 +452,11 @@ ScheduleBlock: ScheduleBlock = {
action: content.2,
temporal_constraint: None,
fields: content.3,
span: Span::new(0, 0),
span: Span::new(s, e),
},
// Override block: override name { time, action, fields }
"override" <name:Ident> "{" <content:BlockContent> "}" => ScheduleBlock {
<s:@L> "override" <name:Ident> "{" <content:BlockContent> "}" <e:@R> => ScheduleBlock {
name: Some(name),
is_override: true,
start: content.0,
@@ -464,7 +465,7 @@ ScheduleBlock: ScheduleBlock = {
action: content.2,
temporal_constraint: None,
fields: content.3,
span: Span::new(0, 0),
span: Span::new(s, e),
}
};
@@ -511,21 +512,21 @@ BlockContentItem: BlockContentItem = {
// Recurrence pattern: recurrence Name on DayOfWeek { blocks }
RecurrencePattern: RecurrencePattern = {
"recurrence" <name:Ident> "on" <day:Ident> "{" <blocks:ScheduleBlock+> "}" => RecurrencePattern {
<start:@L> "recurrence" <name:Ident> "on" <day:Ident> "{" <blocks:ScheduleBlock+> "}" <end:@R> => RecurrencePattern {
name,
constraint: TemporalConstraint::DayOfWeek(day),
blocks,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
// ===== Behavior Trees =====
Behavior: Behavior = {
"behavior" <name:Ident> "{" <fields:Field*> <root:BehaviorNode> "}" => Behavior {
<start:@L> "behavior" <name:Ident> "{" <fields:Field*> <root:BehaviorNode> "}" <end:@R> => Behavior {
name,
root,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -649,16 +650,16 @@ ActionNode: BehaviorNode = {
ActionParam: Field = {
// Named parameter: field: value
<path:DottedPath> ":" <value:Value> => Field {
<start:@L> <path:DottedPath> ":" <value:Value> <end:@R> => Field {
name: path.join("."),
value,
span: Span::new(0, 0),
span: Span::new(start, end),
},
// Positional parameter: just a value (use empty string as field name)
<value:Value> => Field {
<start:@L> <value:Value> <end:@R> => Field {
name: String::new(),
value,
span: Span::new(0, 0),
span: Span::new(start, end),
},
};
@@ -670,13 +671,13 @@ SubTreeNode: BehaviorNode = {
// ===== Institution =====
Institution: Institution = {
"institution" <name:Ident> "{" <body:InstitutionBody> "}" => {
<start:@L> "institution" <name:Ident> "{" <body:InstitutionBody> "}" <end:@R> => {
Institution {
name,
fields: body.0,
uses_behaviors: body.1,
uses_schedule: body.2,
span: Span::new(0, 0),
span: Span::new(start, end),
}
}
};
@@ -709,49 +710,49 @@ InstitutionBodyItem: InstitutionBodyItem = {
// ===== Relationship =====
Relationship: Relationship = {
"relationship" <name:Ident> "{" <participants:Participant+> <fields:Field*> "}" => Relationship {
<start:@L> "relationship" <name:Ident> "{" <participants:Participant+> <fields:Field*> "}" <end:@R> => Relationship {
name,
participants,
fields,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
Participant: Participant = {
// Participant with role and block (block required)
<name:Path> "as" <role:Ident> "{" <fields:Field*> "}" => Participant {
<start:@L> <name:Path> "as" <role:Ident> "{" <fields:Field*> "}" <end:@R> => Participant {
name,
role: Some(role),
fields,
span: Span::new(0, 0),
span: Span::new(start, end),
},
// Participant without role (block still required)
<name:Path> "{" <fields:Field*> "}" => Participant {
<start:@L> <name:Path> "{" <fields:Field*> "}" <end:@R> => Participant {
name,
role: None,
fields,
span: Span::new(0, 0),
span: Span::new(start, end),
},
};
// ===== Location =====
Location: Location = {
"location" <name:Ident> "{" <fields:Field*> "}" => Location {
<start:@L> "location" <name:Ident> "{" <fields:Field*> "}" <end:@R> => Location {
name,
fields,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
// ===== Species =====
Species: Species = {
"species" <name:Ident> "{" <includes:Include*> <fields:Field*> "}" => Species {
<start:@L> "species" <name:Ident> "{" <includes:Include*> <fields:Field*> "}" <end:@R> => Species {
name,
includes,
fields,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
@@ -760,35 +761,36 @@ Species: Species = {
// ===== Type System Declarations =====
ConceptDecl: ConceptDecl = {
"concept" <name:Ident> => ConceptDecl {
<start:@L> "concept" <name:Ident> <end:@R> => ConceptDecl {
name,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
SubConceptDecl: SubConceptDecl = {
// Enum-like sub_concept: sub_concept Cup.Type { Small, Medium, Large }
"sub_concept" <parent:Ident> "." <name:Ident> "{" <variants:Comma<Ident>> "}" => {
<start:@L> "sub_concept" <parent:Ident> "." <name:Ident> "{" <variants:Comma<Ident>> "}" <end:@R> => {
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Enum { variants },
span: Span::new(0, 0),
span: Span::new(start, end),
}
},
// Record-like sub_concept with at least one field: sub_concept Cup.Material { weight: 100 }
"sub_concept" <parent:Ident> "." <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" => {
<start:@L> "sub_concept" <parent:Ident> "." <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" <end:@R> => {
let field_span = Span::new(start, end);
let mut fields = vec![Field {
name: first,
value: first_val,
span: Span::new(0, 0),
span: field_span.clone(),
}];
for (field_name, field_val) in rest {
fields.push(Field {
name: field_name,
value: field_val,
span: Span::new(0, 0),
span: field_span.clone(),
});
}
@@ -796,37 +798,37 @@ SubConceptDecl: SubConceptDecl = {
name,
parent_concept: parent,
kind: SubConceptKind::Record { fields },
span: Span::new(0, 0),
span: Span::new(start, end),
}
},
};
ConceptComparisonDecl: ConceptComparisonDecl = {
"concept_comparison" <name:Ident> "{" <variants:Comma<VariantPattern>> "}" => ConceptComparisonDecl {
<start:@L> "concept_comparison" <name:Ident> "{" <variants:Comma<VariantPattern>> "}" <end:@R> => ConceptComparisonDecl {
name,
variants,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
VariantPattern: VariantPattern = {
<name:Ident> ":" "{" <conditions:Comma<FieldCondition>> "}" => VariantPattern {
<start:@L> <name:Ident> ":" "{" <conditions:Comma<FieldCondition>> "}" <end:@R> => VariantPattern {
name,
conditions,
span: Span::new(0, 0),
span: Span::new(start, end),
}
};
FieldCondition: FieldCondition = {
<field:Ident> ":" "any" => FieldCondition {
<start:@L> <field:Ident> ":" "any" <end:@R> => FieldCondition {
field_name: field,
condition: Condition::Any,
span: Span::new(0, 0),
span: Span::new(start, end),
},
<field:Ident> ":" <cond:IsCondition> => FieldCondition {
<start:@L> <field:Ident> ":" <cond:IsCondition> <end:@R> => FieldCondition {
field_name: field,
condition: Condition::Is(cond),
span: Span::new(0, 0),
span: Span::new(start, end),
},
};

File diff suppressed because it is too large Load Diff