64 KiB
Receipt Template Language (RTL) - Project Specification
Phase 1: Core Template Engine
1. Overview
A file-based template system for configuring receipt layouts per receipt type and printer model. Replaces hardcoded C# converters with editable .template files.
Deliverables
- New project:
Inspectron.Epson.Templates - Template language lexer, parser, and interpreter
- File-based configuration loader
- Integration with existing
IReceiptConverterFactory - Fallback template for undefined combinations
- Unit tests
2. Project Structure
Inspectron.Epson.Templates/
├── Inspectron.Epson.Templates.csproj
├── Language/
│ ├── Lexer.cs
│ ├── Parser.cs
│ ├── Tokens.cs
│ └── Nodes/
│ ├── TemplateNode.cs
│ ├── TextNode.cs
│ ├── StyledTextNode.cs
│ ├── BindingNode.cs
│ ├── SeparatorNode.cs
│ ├── RowNode.cs
│ ├── ColumnNode.cs
│ ├── IfNode.cs
│ ├── ForeachNode.cs
│ └── EmptyLineNode.cs
├── Interpreter/
│ ├── TemplateInterpreter.cs
│ ├── DataContext.cs
│ └── InterpreterException.cs
├── Configuration/
│ ├── TemplateConfiguration.cs
│ ├── TemplateAssignment.cs
│ ├── PrinterProfile.cs
│ ├── PrinterProfileRegistry.cs
│ └── TemplateResolver.cs
├── Storage/
│ ├── ITemplateStorage.cs
│ └── FileTemplateStorage.cs
├── TemplateEngine.cs
├── TemplateReceiptConverter.cs
└── TemplateReceiptConverterFactory.cs
3. File System Layout
templates/
├── kitchen-default.template # Default kitchen receipt
├── kitchen-u220.template # Kitchen receipt for TM-U220II
├── bar-default.template # Bar receipt
├── final-receipt.template # Customer receipt
├── orders-overview.template # Orders overview
├── fallback.template # Used when no match found
├── assignments.json # Maps (receiptType, printer) → template
└── samples/ # Sample data for preview/testing
├── kitchen-default.json
├── bar-default.json
├── final-receipt.json
└── orders-overview.json
4. Template Language Specification
4.1 Line Types
| Line Type | Syntax | Example |
|---|---|---|
| Styled Text | #styles# content |
#bold,center# {title} |
| Plain Text | content |
Hello World |
| Separator | ---, ===, ***, ~~~ |
--- |
| Row Start | @row ratios [styles] |
@row 60:40 bold |
| Row Content | content | content |
{name} | >{price} |
| Row End | @endrow |
@endrow |
| Condition | @if condition |
@if comment |
| Loop | @foreach item in collection |
@foreach dish in dishes |
| Block End | @end |
@end |
| Empty | (blank line) |
4.2 Styles
| Style | Description |
|---|---|
bold |
Bold text |
big |
Large font (2x magnification) |
red |
Red color (two-color printers only) |
center |
Center-aligned |
right |
Right-aligned |
spacing:N |
Set line spacing (0-255) |
spacing:default |
Reset to default spacing |
Syntax: #style1,style2,...# content
Examples:
#bold,center,big# {title}
#red# WARNING
#spacing:0,bold# Compact line
4.3 Data Binding
Syntax: {path} or {path:format}
Path navigation:
{title} // Root property
{order.date} // Nested property
{items.count} // Collection count
Format specifiers:
{price:F2} // Decimal, 2 places → "12.50"
{date:dd-MMM-yy} // Date → "25-Jan-26"
{date:HH:mm} // Time → "14:30"
{number:D4} // Padded integer → "0042"
Missing data behavior: Returns empty string (silent).
4.4 Separators
--- // Dashed line (fills line width)
=== // Equals line
*** // Asterisk line
~~~ // Tilde line
Minimum 3 characters. Fills PrinterProfile.LineWidth.
4.5 Rows (Columns)
Multi-line syntax:
@row 60:40
{leftContent} | >{rightContent}
@endrow
Single-line shorthand:
@row 60:40 | {left} | >{right} |
Ratios:
@row 50:50 // Two equal columns
@row 60:30:10 // Three columns (percentages)
@row *:20 // First fills remainder, second is 20%
@row 30:*:20 // Middle fills remainder
Column alignment (prefix in content):
{text} // Left-aligned (default)
>{text} // Right-aligned
^{text} // Center-aligned
Row styles (optional, applied to all columns):
@row 50:50 bold
@row 70:30 big,red
4.6 Conditionals
Syntax:
@if condition
content
@end
Conditions:
@if propertyName // Truthy (not null, not empty, not false, not 0)
@if !propertyName // Falsy
@if items.count > 0 // Comparison
@if status == "active" // String equality
@if total >= 100 // Numeric comparison
Operators: ==, !=, >, <, >=, <=
4.7 Loops
Syntax:
@foreach item in collection
content using {item.property}
@end
Loop variables:
{item} // Current item (if primitive)
{item.property} // Item property
{item._index} // 0-based index
{item._number} // 1-based number
{item._first} // true if first iteration
{item._last} // true if last iteration
Nesting:
@foreach gang in gangs
#red# {gang.name}
@foreach dish in gang.dishes
{dish.number}x {dish.name}
@end
@end
5. Configuration
5.1 assignments.json
{
"assignments": [
{
"receiptType": "Receipt",
"printerProfile": null,
"templateFile": "final-receipt.template"
},
{
"receiptType": "WorkareaTicket",
"printerProfile": null,
"templateFile": "kitchen-default.template"
},
{
"receiptType": "WorkareaTicket",
"printerProfile": "tm-u220ii",
"templateFile": "kitchen-u220.template"
},
{
"receiptType": "NextCourse",
"printerProfile": null,
"templateFile": "kitchen-default.template"
},
{
"receiptType": "OrdersOverview",
"printerProfile": null,
"templateFile": "orders-overview.template"
}
],
"fallbackTemplate": "fallback.template"
}
5.2 Printer Profiles (built-in)
| Profile ID | Printer | LineWidth | BigLineWidth | SupportsRed |
|---|---|---|---|---|
tm-t30iii |
TM-T30III | 48 | 24 | false |
tm-u220ii |
TM-U220II | 33 | 20 | true |
Mapped from printer ID byte:
0x01→tm-t30iii0x13→tm-u220ii
5.3 Resolution Priority
- Exact match:
receiptType+printerProfile - Type only:
receiptType+printerProfile: null - Fallback:
fallbackTemplate
6. Public API
6.1 TemplateEngine
namespace Inspectron.Epson.Templates;
public class TemplateEngine
{
public TemplateEngine(string templatesDirectory);
/// <summary>
/// Renders a template with the given data.
/// </summary>
public List<PrintCommand> Render(
string templateContent,
string jsonData,
PrinterProfile profile);
/// <summary>
/// Parses template and returns errors/warnings (for validation).
/// </summary>
public TemplateValidationResult Validate(string templateContent);
}
public class TemplateValidationResult
{
public bool IsValid { get; }
public List<TemplateError> Errors { get; }
public List<TemplateWarning> Warnings { get; }
}
public class TemplateError
{
public int Line { get; }
public int Column { get; }
public string Message { get; }
public string Code { get; } // e.g., "RTL001"
}
public class TemplateWarning
{
public int Line { get; }
public int Column { get; }
public string Message { get; }
public string Code { get; } // e.g., "RTL100"
}
6.2 TemplateReceiptConverterFactory
namespace Inspectron.Epson.Templates;
public class TemplateReceiptConverterFactory : IReceiptConverterFactory
{
public TemplateReceiptConverterFactory(string templatesDirectory);
/// <summary>
/// Creates a converter for the given receipt type and printer.
/// Falls back to fallback template if no match found.
/// </summary>
public IReceiptConverter Create(int receiptType, byte printerId);
/// <summary>
/// Reloads templates and assignments from disk.
/// </summary>
public void Reload();
}
6.3 Integration Example
// In EpsonPrintService startup
var converterFactory = new TemplateReceiptConverterFactory("./templates");
// In DI registration (Ninject)
kernel.Bind<IReceiptConverterFactory>()
.ToConstant(converterFactory)
.InSingletonScope();
// Usage remains unchanged
var converter = converterFactory.Create(receiptType, printerId);
var commands = converter.Convert(jsonContent);
7. Error Codes
Errors (prevent rendering)
| Code | Message |
|---|---|
| RTL001 | Unexpected token '{token}' at line {line} |
| RTL002 | Unclosed block '@{block}' started at line {line} |
| RTL003 | '@end' without matching '@if' or '@foreach' |
| RTL004 | Invalid style '{style}' |
| RTL005 | Invalid ratio format '{ratio}' |
| RTL006 | Column count mismatch: expected {expected}, got {actual} |
| RTL007 | Invalid condition syntax |
| RTL008 | Invalid binding path '{path}' |
| RTL009 | Unclosed binding '{' at line {line} |
| RTL010 | Unclosed style marker '#' at line {line} |
Warnings (render anyway)
| Code | Message |
|---|---|
| RTL100 | Unused loop variable '{variable}' |
| RTL101 | Empty @if block |
| RTL102 | Empty @foreach block |
| RTL103 | Style '{style}' has no effect on this printer |
8. Default Templates
Provide default templates matching current hardcoded behavior:
kitchen-default.template- matchesKitchenReceiptConverterkitchen-u220.template- compact version for narrow printersbar-default.template- matchesBarReceiptConverterfinal-receipt.template- matchesFinalReceiptConverterorders-overview.template- matchesOrderItemsReceiptConverterfallback.template- minimal template that dumps JSON fields
9. Testing
Unit Tests
Inspectron.Epson.Templates.Tests/
├── Lexer/
│ ├── LexerBasicTests.cs
│ ├── LexerStyleTests.cs
│ ├── LexerBindingTests.cs
│ └── LexerControlFlowTests.cs
├── Parser/
│ ├── ParserBasicTests.cs
│ ├── ParserRowTests.cs
│ ├── ParserNestingTests.cs
│ └── ParserErrorTests.cs
├── Interpreter/
│ ├── InterpreterTextTests.cs
│ ├── InterpreterBindingTests.cs
│ ├── InterpreterConditionTests.cs
│ ├── InterpreterLoopTests.cs
│ └── InterpreterRowTests.cs
├── Configuration/
│ ├── TemplateResolverTests.cs
│ └── PrinterProfileTests.cs
└── Integration/
├── KitchenReceiptTests.cs
├── FinalReceiptTests.cs
└── FallbackTests.cs
Test Coverage Goals
- Lexer: All token types, edge cases, error handling
- Parser: All node types, nesting, malformed input
- Interpreter: All features, format specifiers, missing data
- Integration: Match output of existing converters
10. Migration Plan
- Create
Inspectron.Epson.Templatesproject - Implement and test template engine
- Create default templates matching current output
- Add
TemplateReceiptConverterFactoryto DI as alternative - Integration test: compare output of both factories
- Switch DI registration to use template factory
- Remove legacy converter code (optional, can keep as reference)
Phase 2: Template Editor (VS Code Extension + C# Language Server)
1. Overview
VS Code extension for creating and editing receipt templates with live preview, syntax highlighting, autocomplete, and validation. The extension uses a C# Language Server (LSP) that reuses the lexer, parser, and interpreter from Phase 1. The VS Code extension is a thin TypeScript client that connects to the language server.
Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ VS Code │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ vscode-rtl (TypeScript) │ │
│ │ - Language client (connects to LSP) │ │
│ │ - Preview webview (renders HTML) │ │
│ │ - Assignments webview │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ LSP (JSON-RPC over stdio) │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Inspectron.Epson.Templates.LanguageServer (C#) │ │
│ │ - Semantic tokens (syntax highlighting) │ │
│ │ - Completion provider │ │
│ │ - Diagnostics (validation) │ │
│ │ - Hover provider │ │
│ │ - Folding ranges │ │
│ │ - Document symbols │ │
│ │ - Custom requests (preview rendering) │ │
│ │ │ │
│ │ References: Inspectron.Epson.Templates (Phase 1) │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Deliverables
- C# Language Server:
Inspectron.Epson.Templates.LanguageServer - VS Code extension:
vscode-rtl(thin TypeScript client) - Language registration for
.templatefiles - Semantic token provider for syntax highlighting (via LSP)
- Completion provider (via LSP)
- Diagnostic provider for real-time validation (via LSP)
- Webview panel for live preview with sample data
- Webview panel for template assignment management
Advantages
- No code duplication - reuses Phase 1 lexer, parser, interpreter
- Single source of truth - language behavior defined once in C#
- Mature editing features - VS Code provides find/replace, multi-cursor, undo/redo, git
- Cross-platform - .NET 8 runs on Windows, macOS, Linux
- Easy distribution - VSIX bundles extension + language server executable
2. Project Structure
2.1 Language Server (C#)
Inspectron.Epson.Templates.LanguageServer/
├── Inspectron.Epson.Templates.LanguageServer.csproj
├── Program.cs # Entry point, stdio server setup
├── RtlLanguageServer.cs # Main server class
├── Handlers/
│ ├── TextDocumentSyncHandler.cs # Document open/change/close
│ ├── SemanticTokensHandler.cs # Syntax highlighting
│ ├── CompletionHandler.cs # Autocomplete
│ ├── DiagnosticHandler.cs # Validation errors/warnings
│ ├── HoverHandler.cs # Hover information
│ ├── FoldingRangeHandler.cs # Code folding
│ ├── DocumentSymbolHandler.cs # Outline/breadcrumbs
│ └── CustomRequestHandlers.cs # Preview rendering, schema extraction
├── Services/
│ ├── DocumentManager.cs # Track open documents
│ ├── DiagnosticService.cs # Run validation, publish diagnostics
│ ├── CompletionService.cs # Build completion items
│ ├── SemanticTokenService.cs # Generate semantic tokens
│ ├── SchemaService.cs # Extract schema from sample JSON
│ └── ScopeTracker.cs # Track @foreach loop variables
└── Models/
├── RtlSemanticTokenTypes.cs # Token type definitions
└── RtlSemanticTokenModifiers.cs # Token modifier definitions
2.2 VS Code Extension (TypeScript - thin client)
vscode-rtl/
├── package.json # Extension manifest
├── tsconfig.json # TypeScript configuration
├── webpack.config.js # Bundler configuration
├── .vscodeignore # Files to exclude from package
├── language-configuration.json # Bracket matching, auto-closing pairs
├── snippets/
│ └── rtl.snippets.json # Code snippets (@if, @foreach, @row)
├── src/
│ ├── extension.ts # Extension entry point
│ ├── languageClient.ts # LSP client setup and management
│ ├── preview/
│ │ ├── previewPanel.ts # Webview panel management
│ │ └── previewStyles.css # Receipt visual styling
│ ├── assignments/
│ │ └── assignmentsPanel.ts # Assignments editor webview
│ └── utils/
│ └── serverPath.ts # Locate language server executable
├── media/
│ ├── preview.css # Receipt preview styling
│ ├── assignments.css # Assignments editor styling
│ └── icons/
│ ├── template.svg # File icon
│ └── preview.svg # Preview button icon
├── server/ # Bundled language server (published .exe/.dll)
│ └── (built from C# project)
└── test/
└── extension.test.ts # Basic extension activation tests
3. UI Layout
The extension integrates with VS Code's native UI:
┌─────────────────────────────────────────────────────────────────────────────┐
│ File Edit Selection View Go Run Terminal Help │
├─────────────────────────────────────────────────────────────────────────────┤
│ EXPLORER │ kitchen-default.template │ RTL Preview │
│ ┌─────────────────┐ │ ─────────────────────────────────────│───────────────│
│ │ ▼ TEMPLATES │ │ 1│#bold,center,big# {title} │┌─────────────┐│
│ │ kitchen-def...│ │ 2│--- ││ Warme Küche ││
│ │ kitchen-u220 │ │ 3│ ││─────────────││
│ │ bar-default │ │ 4│@row 60:40 ││ ││
│ │ final-receipt │ │ 5│{date:dd-MMM} | >{orderNumber} ││25-Jan Nr:42 ││
│ │ orders-overv..│ │ 6│@endrow ││ ││
│ │ fallback │ │ 7│#center# {waiter} ││Max Musterma.││
│ │ │ │ 8│ ││ ││
│ │ ▼ SAMPLES │ │ 9│@foreach dish in dishes ││Tisch: 12 ││
│ │ kitchen-def...│ │ 10│ {dish.quantity}x {dish.name} ││─────────────││
│ │ bar-default...│ │ 11│@end ││2x Tomatensup││
│ │ │ │ ││1x Schnitzel ││
│ │ ▼ RTL ASSIGNMENTS│ │ ││ - Pommes ││
│ │ Receipt → ...│ │ ││ + Reis ││
│ │ WorkareaTic..│ │ │└─────────────┘│
│ │ NextCourse →.│ │ │ │
│ └─────────────────┘ │ │Printer: ▼ │
│ │ │ TM-T30III │
├─────────────────────┴──────────────────────────────────────┴───────────────┤
│ PROBLEMS OUTPUT DEBUG CONSOLE TERMINAL │
│ ┌──────────────────────────────────────────────────────────────────────────┐│
│ │ ⚠ kitchen-default.template [2] ││
│ │ ⚠ Line 10: Unused loop variable 'dish' in nested scope [RTL100] ││
│ │ ❌ fallback.template [1] ││
│ │ ❌ Line 5: Unclosed '@foreach' block started at line 3 [RTL002] ││
│ └──────────────────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────────────────┤
│ RTL │ Ln 7, Col 12 │ UTF-8 │ CRLF │ {} kitchen-default.json │ ✓ │
└─────────────────────────────────────────────────────────────────────────────┘
UI Components
| Component | VS Code Feature | Implementation |
|---|---|---|
| Template list | Explorer tree view | Native file explorer or custom TreeDataProvider |
| Sample data list | Explorer tree view | Custom TreeDataProvider |
| Assignments list | Explorer tree view | Custom TreeDataProvider with inline editing |
| Editor | Native text editor | Language client connects to C# language server |
| Syntax highlighting | Semantic tokens | Language server provides tokens via LSP |
| Autocomplete | Completion items | Language server provides completions via LSP |
| Validation | Problems panel | Language server publishes diagnostics via LSP |
| Preview | Webview panel | TypeScript requests rendered HTML from language server |
| Status bar | Status bar items | Shows current sample file, printer profile |
4. Features
4.1 Language Registration (VS Code Extension)
package.json contribution:
{
"contributes": {
"languages": [{
"id": "rtl",
"aliases": ["Receipt Template Language", "RTL"],
"extensions": [".template"],
"configuration": "./language-configuration.json",
"icon": { "light": "./media/icons/template.svg", "dark": "./media/icons/template.svg" }
}]
}
}
Note: No TextMate grammar needed - syntax highlighting is provided via LSP semantic tokens.
language-configuration.json:
{
"comments": { "lineComment": "//" },
"brackets": [["@if", "@end"], ["@foreach", "@end"], ["@row", "@endrow"]],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "#", "close": "#" }
],
"surroundingPairs": [
{ "open": "{", "close": "}" },
{ "open": "#", "close": "#" }
]
}
4.2 Syntax Highlighting (LSP Semantic Tokens)
The language server provides semantic tokens via the textDocument/semanticTokens/full LSP method.
C# Semantic Token Types (SemanticTokensHandler.cs):
public static class RtlSemanticTokenTypes
{
public const string Keyword = "keyword"; // @if, @foreach, @end, @row, @endrow
public const string Variable = "variable"; // {binding}
public const string Property = "property"; // {object.property}
public const string Decorator = "decorator"; // #style#
public const string String = "string"; // Style names: bold, center
public const string Number = "number"; // Ratios: 60:40
public const string Operator = "operator"; // ==, !=, >, <, |, >, ^
public const string Comment = "comment"; // Separators: ---, ===
public const string Type = "type"; // Format specifiers: F2, dd-MMM
}
Token mapping:
| Element | Token Type | Default Color |
|---|---|---|
@if, @foreach, @end |
keyword |
Purple |
@row, @endrow |
keyword |
Purple |
#...# markers |
decorator |
Blue |
Style names bold, center |
string |
Green |
{...} bindings |
variable |
Teal |
Property paths .property |
property |
Teal |
Format specifiers :F2 |
type |
Orange |
Ratios 60:40 |
number |
Light Green |
Separators --- |
comment |
Gray |
Column delimiter | |
operator |
Yellow |
Alignment >, ^ |
operator |
Orange |
Comparison ==, != |
operator |
Red |
4.3 Code Folding (LSP Folding Ranges)
FoldingRangeHandler.cs:
public class FoldingRangeHandler : IFoldingRangeHandler
{
public Task<Container<FoldingRange>?> Handle(FoldingRangeRequestParam request, CancellationToken ct)
{
var ranges = new List<FoldingRange>();
// Parse document, find @if...@end, @foreach...@end, @row...@endrow blocks
// Add FoldingRange for each block
return Task.FromResult<Container<FoldingRange>?>(new Container<FoldingRange>(ranges));
}
}
Supports:
- Fold
@if...@endblocks - Fold
@foreach...@endblocks - Fold
@row...@endrowblocks - Nested folding
4.4 Autocomplete (LSP Completion)
CompletionHandler.cs triggers:
| Trigger | Context | Completions |
|---|---|---|
# |
Line start or after # |
Style keywords: bold, big, red, center, right, spacing: |
, |
Inside #...# |
Style keywords |
@ |
Line start | Control flow: if, foreach, end, row, endrow |
{ |
Anywhere | Root data properties from sample JSON + active loop variables |
. |
Inside {...} |
Child properties based on path + loop metadata (_index, _first, etc.) |
: |
Inside {...} after property |
Format specifiers: F2, D4, dd-MMM-yy, HH:mm |
in |
After @foreach x |
Collection properties from schema |
CompletionService.cs features:
- Context-aware (detects binding, style, control flow context)
- Completion item kinds (Property, Keyword, Snippet, etc.)
- Documentation for format specifiers
- Loop variable scope tracking via ScopeTracker
public class CompletionService
{
private readonly SchemaService _schemaService;
private readonly ScopeTracker _scopeTracker;
public List<CompletionItem> GetCompletions(DocumentUri uri, Position position)
{
var context = DetectContext(uri, position);
return context switch
{
CompletionContext.Style => GetStyleCompletions(),
CompletionContext.ControlFlow => GetControlFlowCompletions(),
CompletionContext.Binding => GetBindingCompletions(uri, position),
CompletionContext.BindingProperty => GetPropertyCompletions(uri, position),
CompletionContext.BindingFormat => GetFormatCompletions(),
_ => new List<CompletionItem>()
};
}
}
4.5 Diagnostics (LSP PublishDiagnostics)
DiagnosticService.cs:
- Validates on document open and change (debounced)
- Uses Phase 1
TemplateEngine.Validate()method - Publishes diagnostics via
textDocument/publishDiagnostics
public class DiagnosticService
{
private readonly TemplateEngine _templateEngine;
private readonly ILanguageServerFacade _server;
public void ValidateDocument(DocumentUri uri, string content)
{
var result = _templateEngine.Validate(content);
var diagnostics = result.Errors
.Select(e => new Diagnostic
{
Range = new Range(e.Line - 1, e.Column - 1, e.Line - 1, e.Column + 10),
Severity = DiagnosticSeverity.Error,
Code = e.Code,
Message = e.Message,
Source = "rtl"
})
.Concat(result.Warnings.Select(w => new Diagnostic
{
Range = new Range(w.Line - 1, w.Column - 1, w.Line - 1, w.Column + 10),
Severity = DiagnosticSeverity.Warning,
Code = w.Code,
Message = w.Message,
Source = "rtl"
}))
.ToList();
_server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams
{
Uri = uri,
Diagnostics = new Container<Diagnostic>(diagnostics)
});
}
}
4.6 Hover Provider (LSP Hover)
HoverHandler.cs:
- Hover over style → shows description
- Hover over binding → shows data type from schema
- Hover over control flow → shows syntax help
- Hover over format specifier → shows example output
public class HoverHandler : IHoverHandler
{
public Task<Hover?> Handle(HoverParams request, CancellationToken ct)
{
var content = GetHoverContent(request.TextDocument.Uri, request.Position);
if (content == null) return Task.FromResult<Hover?>(null);
return Task.FromResult<Hover?>(new Hover
{
Contents = new MarkedStringsOrMarkupContent(new MarkupContent
{
Kind = MarkupKind.Markdown,
Value = content
})
});
}
}
4.7 Code Snippets (VS Code Extension)
snippets/rtl.snippets.json:
{
"If Block": {
"prefix": "@if",
"body": ["@if ${1:condition}", " $0", "@end"],
"description": "Conditional block"
},
"Foreach Loop": {
"prefix": "@foreach",
"body": ["@foreach ${1:item} in ${2:collection}", " $0", "@end"],
"description": "Loop over collection"
},
"Row": {
"prefix": "@row",
"body": ["@row ${1:60}:${2:40}", "${3:left} | >${4:right}", "@endrow"],
"description": "Two-column row"
},
"Styled Text": {
"prefix": "#style",
"body": ["#${1|bold,big,red,center,right|}# ${0:text}"],
"description": "Styled text line"
}
}
4.8 Preview Panel (Webview + Custom LSP Request)
The preview uses a custom LSP request to get rendered HTML from the language server.
Custom request (Language Server):
// CustomRequestHandlers.cs
[Method("rtl/renderPreview")]
public record RenderPreviewParams
{
public DocumentUri Uri { get; init; }
public string PrinterProfile { get; init; } // "tm-t30iii" or "tm-u220ii"
public string? SampleDataPath { get; init; }
}
public record RenderPreviewResult
{
public string Html { get; init; } // Rendered receipt as HTML
public string? Error { get; init; } // Render error message if any
}
public class CustomRequestHandlers : IJsonRpcRequestHandler<RenderPreviewParams, RenderPreviewResult>
{
public Task<RenderPreviewResult> Handle(RenderPreviewParams request, CancellationToken ct)
{
// 1. Load template content
// 2. Load sample data JSON
// 3. Get printer profile
// 4. Call TemplateEngine.Render()
// 5. Convert PrintCommand[] to HTML
// 6. Return HTML or error
}
}
VS Code Extension (previewPanel.ts):
async function updatePreview() {
const result = await client.sendRequest<RenderPreviewResult>('rtl/renderPreview', {
uri: activeDocument.uri.toString(),
printerProfile: selectedPrinter,
sampleDataPath: selectedSamplePath
});
if (result.error) {
panel.webview.html = renderError(result.error);
} else {
panel.webview.html = wrapInReceiptStyle(result.html);
}
}
Features:
- Side-by-side preview (like Markdown preview)
- Printer profile selector (TM-T30III, TM-U220II)
- Live updates as template changes (debounced 300ms)
- Shows render errors inline
4.9 Sample Data Integration
Custom LSP requests:
// Get schema from sample JSON (for autocomplete)
[Method("rtl/getSchema")]
public record GetSchemaParams { public string SampleDataPath { get; init; } }
public record GetSchemaResult { public SchemaNode Schema { get; init; } }
// List available sample files
[Method("rtl/listSamples")]
public record ListSamplesResult { public List<string> SampleFiles { get; init; } }
Features:
- Status bar item shows current sample file
- Click status bar → quick pick to change sample
- Sample JSON files in
samples/directory - Automatic schema extraction for autocomplete
- Sample file association by naming convention
4.10 Tree Views (VS Code Extension)
Templates Tree View:
- Lists all
.templatefiles - Context menu: Rename, Delete, Duplicate, New
- Click to open
Samples Tree View:
- Lists all
.jsonsample files - Shows which template they're associated with
- Click to open
Assignments Tree View:
- Shows current assignments from
assignments.json - Inline display:
Receipt → final-receipt.template - Context menu: Edit, Delete
- "Add Assignment" button
4.11 Assignments Editor (Webview)
Webview panel for editing assignments.json:
┌─────────────────────────────────────────────────────────────────────┐
│ Template Assignments [Save] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Receipt Type Printer Template Actions │
│ ─────────────────────────────────────────────────────────────────── │
│ [Receipt ▼] [(Any) ▼] [final-receipt ▼] [Delete] │
│ [WorkareaTicket ▼] [(Any) ▼] [kitchen-default ▼] [Delete] │
│ [WorkareaTicket ▼] [TM-U220II ▼] [kitchen-u220 ▼] [Delete] │
│ [NextCourse ▼] [(Any) ▼] [kitchen-default ▼] [Delete] │
│ [OrdersOverview ▼] [(Any) ▼] [orders-overview ▼] [Delete] │
│ │
│ [+ Add Assignment] │
│ │
│ Fallback Template: [fallback.template ▼] │
│ │
└─────────────────────────────────────────────────────────────────────┘
5. Data Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ VS Code │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ vscode-rtl Extension (TypeScript) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Language │ │ Preview │ │ Assignments │ │ │
│ │ │ Client │ │ Webview │ │ Webview │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └────────────┬─────────────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼───────────────────┼─────────────────────────┼───────────────┘ │
│ │ LSP │ Custom Request │ File I/O │
│ │ (JSON-RPC) │ rtl/renderPreview │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Language Server (C# - stdio process) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Handlers │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │ Semantic │ │ Completion │ │ Diagnostic │ │ Custom │ │ │ │
│ │ │ │ Tokens │ │ Handler │ │ Handler │ │ Requests │ │ │ │
│ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │
│ │ └───────┼──────────────┼──────────────┼──────────────┼────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Services │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │ Document │ │ Schema │ │ Scope │ │ Diagnostic │ │ │ │
│ │ │ │ Manager │ │ Service │ │ Tracker │ │ Service │ │ │ │
│ │ │ └────────────┘ └─────┬──────┘ └────────────┘ └─────┬──────┘ │ │ │
│ │ └──────────────────────┼─────────────────────────────┼────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Inspectron.Epson.Templates (Phase 1 - Referenced) │ │ │
│ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │
│ │ │ │ Lexer │ │ Parser │ │ Template │ │ │ │
│ │ │ │ │ │ │ │ Engine │ │ │ │
│ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Key points:
- Language Server is a standalone C# console application communicating via stdio
- All language features (highlighting, completion, validation) reuse Phase 1 code
- Custom LSP requests handle preview rendering and schema extraction
- VS Code extension is a thin client (~500 lines of TypeScript)
6. Language Server Implementation Details
6.1 Context Detection (CompletionService.cs)
public enum CompletionContext
{
None,
Style, // Inside #...#
ControlFlow, // After @ at line start
Binding, // Inside {...}
BindingProperty, // After . inside {...}
BindingFormat, // After : inside {...}
ForeachCollection, // After "in" in @foreach
RowContent // Inside @row...@endrow
}
public class CompletionService
{
public CompletionContext DetectContext(string line, int column)
{
var textBefore = line[..column];
// Check for binding context
var lastOpenBrace = textBefore.LastIndexOf('{');
var lastCloseBrace = textBefore.LastIndexOf('}');
if (lastOpenBrace > lastCloseBrace)
{
var bindingText = textBefore[(lastOpenBrace + 1)..];
if (bindingText.Contains(':')) return CompletionContext.BindingFormat;
if (bindingText.Contains('.')) return CompletionContext.BindingProperty;
return CompletionContext.Binding;
}
// Check for style context
var hashCount = textBefore.Count(c => c == '#');
if (hashCount % 2 == 1) return CompletionContext.Style;
// Check for control flow
if (Regex.IsMatch(textBefore, @"^\s*@\w*$"))
return CompletionContext.ControlFlow;
// Check for foreach collection
if (Regex.IsMatch(textBefore, @"@foreach\s+\w+\s+in\s+\w*$"))
return CompletionContext.ForeachCollection;
return CompletionContext.None;
}
}
6.2 Schema Extraction (SchemaService.cs)
public abstract record SchemaNode(string Name);
public record ObjectNode(string Name, List<SchemaNode> Properties) : SchemaNode(Name);
public record ArrayNode(string Name, SchemaNode? ItemType) : SchemaNode(Name);
public record ValueNode(string Name, JsonValueKind ValueKind) : SchemaNode(Name);
public class SchemaService
{
private readonly Dictionary<string, SchemaNode> _schemaCache = new();
public SchemaNode ExtractSchema(string jsonContent)
{
using var doc = JsonDocument.Parse(jsonContent);
return AnalyzeElement("root", doc.RootElement);
}
private SchemaNode AnalyzeElement(string name, JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.Object => new ObjectNode(name,
element.EnumerateObject()
.Select(p => AnalyzeElement(p.Name, p.Value))
.ToList()),
JsonValueKind.Array => new ArrayNode(name,
element.GetArrayLength() > 0
? AnalyzeElement("item", element[0])
: null),
_ => new ValueNode(name, element.ValueKind)
};
}
public SchemaNode? ResolvePropertyPath(SchemaNode root, string path)
{
var parts = path.Split('.');
var current = root;
foreach (var part in parts)
{
current = current switch
{
ObjectNode obj => obj.Properties.FirstOrDefault(p => p.Name == part),
ArrayNode arr => arr.ItemType,
_ => null
};
if (current == null) return null;
}
return current;
}
}
6.3 Scope Tracking (ScopeTracker.cs)
Track active @foreach loops to resolve loop variable bindings:
public record LoopScope(
string VariableName,
string CollectionPath,
SchemaNode? ItemSchema,
int StartLine,
int EndLine);
public class ScopeTracker
{
private readonly SchemaService _schemaService;
public List<LoopScope> GetActiveScopes(string documentContent, int line, SchemaNode? rootSchema)
{
var lines = documentContent.Split('\n');
var stack = new Stack<(string Variable, string Collection, int Line)>();
var scopes = new List<LoopScope>();
for (int i = 0; i <= line && i < lines.Length; i++)
{
var currentLine = lines[i];
var foreachMatch = Regex.Match(currentLine, @"@foreach\s+(\w+)\s+in\s+(\S+)");
if (foreachMatch.Success)
{
stack.Push((foreachMatch.Groups[1].Value, foreachMatch.Groups[2].Value, i));
}
else if (Regex.IsMatch(currentLine, @"@end\b") && stack.Count > 0)
{
// Only pop if this @end is before or at the cursor line
if (i < line)
{
stack.Pop();
}
}
}
// Convert stack to scopes with schema info
foreach (var (variable, collection, startLine) in stack)
{
var collectionSchema = rootSchema != null
? _schemaService.ResolvePropertyPath(rootSchema, collection)
: null;
var itemSchema = collectionSchema is ArrayNode arr ? arr.ItemType : null;
scopes.Add(new LoopScope(variable, collection, itemSchema, startLine, -1));
}
return scopes;
}
public List<CompletionItem> GetLoopVariableCompletions(List<LoopScope> scopes)
{
var items = new List<CompletionItem>();
foreach (var scope in scopes)
{
// Add the loop variable itself
items.Add(new CompletionItem
{
Label = scope.VariableName,
Kind = CompletionItemKind.Variable,
Detail = $"Loop variable from @foreach {scope.VariableName} in {scope.CollectionPath}"
});
// Add loop metadata
items.AddRange(new[]
{
new CompletionItem { Label = $"{scope.VariableName}._index", Kind = CompletionItemKind.Property, Detail = "0-based index" },
new CompletionItem { Label = $"{scope.VariableName}._number", Kind = CompletionItemKind.Property, Detail = "1-based number" },
new CompletionItem { Label = $"{scope.VariableName}._first", Kind = CompletionItemKind.Property, Detail = "true if first iteration" },
new CompletionItem { Label = $"{scope.VariableName}._last", Kind = CompletionItemKind.Property, Detail = "true if last iteration" }
});
// Add item properties if schema is available
if (scope.ItemSchema is ObjectNode obj)
{
foreach (var prop in obj.Properties)
{
items.Add(new CompletionItem
{
Label = $"{scope.VariableName}.{prop.Name}",
Kind = CompletionItemKind.Property,
Detail = GetPropertyTypeDescription(prop)
});
}
}
}
return items;
}
}
6.4 Semantic Tokens (SemanticTokenService.cs)
public class SemanticTokenService
{
// Token types must match SemanticTokensLegend sent during initialization
private static readonly string[] TokenTypes = new[]
{
"keyword", // 0: @if, @foreach, @end, @row
"variable", // 1: {binding}
"property", // 2: .property
"decorator", // 3: #style#
"string", // 4: style names
"number", // 5: ratios
"operator", // 6: |, >, ^, ==
"comment", // 7: separators
"type" // 8: format specifiers
};
public SemanticTokens GetSemanticTokens(string content)
{
var tokens = new List<int>(); // [deltaLine, deltaStart, length, tokenType, tokenModifiers]
var lexer = new Lexer(content);
var allTokens = lexer.Tokenize();
int prevLine = 0;
int prevChar = 0;
foreach (var token in allTokens)
{
var tokenType = MapTokenType(token.Type);
if (tokenType < 0) continue;
var deltaLine = token.Line - 1 - prevLine;
var deltaStart = deltaLine == 0 ? token.Column - 1 - prevChar : token.Column - 1;
tokens.Add(deltaLine);
tokens.Add(deltaStart);
tokens.Add(token.Length);
tokens.Add(tokenType);
tokens.Add(0); // No modifiers
prevLine = token.Line - 1;
prevChar = token.Column - 1;
}
return new SemanticTokens { Data = tokens.ToImmutableArray() };
}
private int MapTokenType(TokenType type) => type switch
{
TokenType.If or TokenType.Foreach or TokenType.End or
TokenType.Row or TokenType.EndRow => 0, // keyword
TokenType.Binding => 1, // variable
TokenType.PropertyPath => 2, // property
TokenType.StyleMarker => 3, // decorator
TokenType.StyleName => 4, // string
TokenType.Ratio => 5, // number
TokenType.Operator or TokenType.ColumnDelimiter
or TokenType.AlignmentMarker => 6, // operator
TokenType.Separator => 7, // comment
TokenType.FormatSpecifier => 8, // type
_ => -1 // skip
};
}
7. Commands and Keybindings
Commands (package.json)
{
"contributes": {
"commands": [
{
"command": "rtl.openPreview",
"title": "Open Preview",
"category": "RTL",
"icon": "$(open-preview)"
},
{
"command": "rtl.openPreviewToSide",
"title": "Open Preview to the Side",
"category": "RTL",
"icon": "$(open-preview)"
},
{
"command": "rtl.refreshPreview",
"title": "Refresh Preview",
"category": "RTL"
},
{
"command": "rtl.selectSampleData",
"title": "Select Sample Data",
"category": "RTL"
},
{
"command": "rtl.editSampleData",
"title": "Edit Sample Data",
"category": "RTL"
},
{
"command": "rtl.editAssignments",
"title": "Edit Assignments",
"category": "RTL"
},
{
"command": "rtl.newTemplate",
"title": "New Template",
"category": "RTL",
"icon": "$(new-file)"
},
{
"command": "rtl.duplicateTemplate",
"title": "Duplicate Template",
"category": "RTL"
},
{
"command": "rtl.restartServer",
"title": "Restart Language Server",
"category": "RTL"
}
]
}
}
Keybindings
| Shortcut | Command | When |
|---|---|---|
Ctrl+Shift+V |
rtl.openPreview |
editorLangId == rtl |
Ctrl+K V |
rtl.openPreviewToSide |
editorLangId == rtl |
Ctrl+Shift+R |
rtl.refreshPreview |
editorLangId == rtl |
Note: Standard VS Code shortcuts work automatically:
Ctrl+S- SaveCtrl+Space- Trigger autocompleteF8/Shift+F8- Navigate problemsCtrl+G- Go to lineCtrl+F/Ctrl+H- Find / Replace
8. Configuration
Extension Settings (contributes.configuration)
{
"contributes": {
"configuration": {
"title": "RTL - Receipt Template Language",
"properties": {
"rtl.templatesDirectory": {
"type": "string",
"default": "./templates",
"description": "Path to templates directory"
},
"rtl.samplesDirectory": {
"type": "string",
"default": "./templates/samples",
"description": "Path to sample data directory"
},
"rtl.defaultPrinterProfile": {
"type": "string",
"enum": ["tm-t30iii", "tm-u220ii"],
"default": "tm-t30iii",
"description": "Default printer profile for preview"
},
"rtl.preview.debounceMs": {
"type": "number",
"default": 300,
"description": "Delay before updating preview after changes"
},
"rtl.server.path": {
"type": "string",
"default": "",
"description": "Path to language server executable (uses bundled server if empty)"
},
"rtl.trace.server": {
"type": "string",
"enum": ["off", "messages", "verbose"],
"default": "off",
"description": "Traces communication between VS Code and the language server"
}
}
}
}
}
Workspace Settings Example
// .vscode/settings.json
{
"rtl.templatesDirectory": "./templates",
"rtl.samplesDirectory": "./templates/samples",
"rtl.defaultPrinterProfile": "tm-t30iii"
}
9. Dependencies
9.1 Language Server (C#)
Inspectron.Epson.Templates.LanguageServer.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup>
<ItemGroup>
<!-- OmniSharp Language Server Protocol -->
<PackageReference Include="OmniSharp.Extensions.LanguageServer" Version="0.19.*" />
<PackageReference Include="OmniSharp.Extensions.LanguageProtocol" Version="0.19.*" />
<!-- Project Reference to Phase 1 -->
<ProjectReference Include="../Inspectron.Epson.Templates/Inspectron.Epson.Templates.csproj" />
</ItemGroup>
</Project>
9.2 VS Code Extension (TypeScript)
package.json:
{
"name": "vscode-rtl",
"displayName": "RTL - Receipt Template Language",
"description": "Syntax highlighting, autocomplete, and preview for Receipt Template Language",
"version": "1.0.0",
"publisher": "inspectron",
"engines": {
"vscode": "^1.85.0"
},
"categories": ["Programming Languages", "Formatters"],
"activationEvents": [
"onLanguage:rtl",
"workspaceContains:**/*.template"
],
"main": "./dist/extension.js",
"dependencies": {
"vscode-languageclient": "^9.0.0"
},
"devDependencies": {
"@types/vscode": "^1.85.0",
"@types/node": "^20.x",
"typescript": "^5.3.0",
"webpack": "^5.x",
"webpack-cli": "^5.x",
"ts-loader": "^9.x",
"@vscode/test-electron": "^2.x"
},
"scripts": {
"vscode:prepublish": "npm run package && npm run build-server",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"build-server": "dotnet publish ../Inspectron.Epson.Templates.LanguageServer -c Release -o ./server",
"test": "node ./out/test/runTest.js"
}
}
10. Testing
10.1 Language Server Tests (C# - xUnit)
Inspectron.Epson.Templates.LanguageServer.Tests/
├── Handlers/
│ ├── SemanticTokensHandlerTests.cs
│ ├── CompletionHandlerTests.cs
│ ├── DiagnosticHandlerTests.cs
│ └── HoverHandlerTests.cs
├── Services/
│ ├── CompletionServiceTests.cs
│ ├── SchemaServiceTests.cs
│ ├── ScopeTrackerTests.cs
│ └── SemanticTokenServiceTests.cs
└── Fixtures/
├── valid-template.template
├── invalid-template.template
└── sample-data.json
Test Cases
1. Semantic Tokens Tests
- Tokenize style markers
#bold,center#→ decorator + string - Tokenize bindings
{property.path}→ variable + property - Tokenize control flow → keyword
- Tokenize separators → comment
- Tokenize row syntax
@row 60:40→ keyword + number
2. Completion Tests
- Complete styles after
# - Complete properties after
{ - Complete child properties after
. - Complete format specifiers after
: - Complete loop variables in scope
- Complete loop metadata (
_index,_first, etc.)
3. Diagnostics Tests
- Error on unclosed
@foreach - Error on unclosed
{binding - Warning on unused loop variable
- Error on invalid style name
4. Schema Tests
- Extract schema from nested JSON
- Resolve property paths
- Handle arrays correctly
5. Scope Tracker Tests
- Track single @foreach scope
- Track nested @foreach scopes
- Close scope after @end
10.2 VS Code Extension Tests (TypeScript)
vscode-rtl/test/
├── extension.test.ts # Extension activation tests
├── languageClient.test.ts # LSP client connection tests
└── fixtures/
└── test-workspace/
├── test.template
└── test.json
Manual Test Checklist
- Install extension in VS Code
- Open
.templatefile - syntax highlighting works (semantic tokens) - Type
@at line start - autocomplete shows control flow - Type
{- autocomplete shows data properties - Type
{item.inside foreach - autocomplete shows item properties +_index,_first, etc. - Add syntax error - red squiggle appears, Problems panel updates
- Fix error - squiggle disappears
- Hover over style - shows description
- Hover over binding - shows type from schema
- Open preview (
Ctrl+Shift+V) - receipt preview appears - Edit template - preview updates live
- Change printer in preview - width adjusts
- Open assignments editor - table displays correctly
- Add/edit/delete assignment - saves to JSON
- Restart Language Server command works
11. Distribution
Build Process
# 1. Build language server for all platforms
dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r win-x64 -o vscode-rtl/server/win-x64
dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r linux-x64 -o vscode-rtl/server/linux-x64
dotnet publish Inspectron.Epson.Templates.LanguageServer -c Release -r osx-x64 -o vscode-rtl/server/osx-x64
# 2. Build VS Code extension
cd vscode-rtl
npm install
npm run package
# 3. Create VSIX
npx @vscode/vsce package
# Creates: vscode-rtl-1.0.0.vsix
VSIX Contents
vscode-rtl-1.0.0.vsix
├── extension/
│ ├── dist/
│ │ └── extension.js # Bundled TypeScript
│ ├── server/
│ │ ├── win-x64/ # Windows language server
│ │ ├── linux-x64/ # Linux language server
│ │ └── osx-x64/ # macOS language server
│ ├── media/
│ ├── snippets/
│ ├── language-configuration.json
│ └── package.json
└── [Content_Types].xml
Installation Options
-
Local VSIX install:
code --install-extension vscode-rtl-1.0.0.vsix -
VS Code Marketplace: (optional, for public distribution)
npx @vscode/vsce publish -
Manual installation:
- Copy extracted folder to:
- Windows:
%USERPROFILE%\.vscode\extensions\ - macOS/Linux:
~/.vscode/extensions/
- Windows:
- Copy extracted folder to:
12. Timeline
Phase 1: Core Engine (Foundation) - C#
- Project setup and structure
- Lexer implementation + tests
- Parser implementation + tests
- Interpreter implementation + tests
- Configuration and resolver
- File storage
- Factory integration
- Default templates
- Integration testing
- Migration and deployment
Phase 2: VS Code Extension + Language Server (After Phase 1 Complete)
Language Server (C#):
- Project setup with OmniSharp LSP
- Server initialization and stdio transport
- TextDocumentSyncHandler (open/change/close)
- SemanticTokensHandler (syntax highlighting)
- DiagnosticService + PublishDiagnostics
- FoldingRangeHandler
- HoverHandler
- CompletionHandler - styles and control flow
- SchemaService and ScopeTracker
- CompletionHandler - data bindings with scope awareness
- Custom request: rtl/renderPreview
- Custom request: rtl/getSchema, rtl/listSamples
- Unit tests
VS Code Extension (TypeScript): 14. Extension project setup (package.json, webpack) 15. Language client setup and server spawning 16. Language registration and configuration 17. Preview webview - calls rtl/renderPreview 18. Preview webview - printer selector, live updates 19. Tree views (templates, samples) - optional 20. Assignments editor webview - optional 21. Code snippets 22. Integration testing 23. Multi-platform build and VSIX packaging 24. Documentation and distribution