Files
Print_server/template_editor_plan.md
2026-01-22 11:13:07 +01:00

1749 lines
64 KiB
Markdown

# 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
```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-t30iii`
- `0x13``tm-u220ii`
### 5.3 Resolution Priority
1. Exact match: `receiptType` + `printerProfile`
2. Type only: `receiptType` + `printerProfile: null`
3. Fallback: `fallbackTemplate`
---
## 6. Public API
### 6.1 TemplateEngine
```csharp
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
```csharp
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
```csharp
// 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` - matches `KitchenReceiptConverter`
- `kitchen-u220.template` - compact version for narrow printers
- `bar-default.template` - matches `BarReceiptConverter`
- `final-receipt.template` - matches `FinalReceiptConverter`
- `orders-overview.template` - matches `OrderItemsReceiptConverter`
- `fallback.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
1. Create `Inspectron.Epson.Templates` project
2. Implement and test template engine
3. Create default templates matching current output
4. Add `TemplateReceiptConverterFactory` to DI as alternative
5. Integration test: compare output of both factories
6. Switch DI registration to use template factory
7. 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 `.template` files
- 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:**
```json
{
"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:**
```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):**
```csharp
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:**
```csharp
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...@end` blocks
- Fold `@foreach...@end` blocks
- Fold `@row...@endrow` blocks
- 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
```csharp
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`
```csharp
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
```csharp
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:**
```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):**
```csharp
// 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):**
```typescript
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:**
```csharp
// 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 `.template` files
- Context menu: Rename, Delete, Duplicate, New
- Click to open
**Samples Tree View:**
- Lists all `.json` sample 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)
```csharp
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)
```csharp
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:
```csharp
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)
```csharp
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)
```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` - Save
- `Ctrl+Space` - Trigger autocomplete
- `F8` / `Shift+F8` - Navigate problems
- `Ctrl+G` - Go to line
- `Ctrl+F` / `Ctrl+H` - Find / Replace
---
## 8. Configuration
### Extension Settings (contributes.configuration)
```json
{
"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
```json
// .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:**
```xml
<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:**
```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 `.template` file - 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
```bash
# 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
1. **Local VSIX install:**
```bash
code --install-extension vscode-rtl-1.0.0.vsix
```
2. **VS Code Marketplace:** (optional, for public distribution)
```bash
npx @vscode/vsce publish
```
3. **Manual installation:**
- Copy extracted folder to:
- Windows: `%USERPROFILE%\.vscode\extensions\`
- macOS/Linux: `~/.vscode/extensions/`
---
## 12. Timeline
### Phase 1: Core Engine (Foundation) - C#
1. Project setup and structure
2. Lexer implementation + tests
3. Parser implementation + tests
4. Interpreter implementation + tests
5. Configuration and resolver
6. File storage
7. Factory integration
8. Default templates
9. Integration testing
10. Migration and deployment
### Phase 2: VS Code Extension + Language Server (After Phase 1 Complete)
**Language Server (C#):**
1. Project setup with OmniSharp LSP
2. Server initialization and stdio transport
3. TextDocumentSyncHandler (open/change/close)
4. SemanticTokensHandler (syntax highlighting)
5. DiagnosticService + PublishDiagnostics
6. FoldingRangeHandler
7. HoverHandler
8. CompletionHandler - styles and control flow
9. SchemaService and ScopeTracker
10. CompletionHandler - data bindings with scope awareness
11. Custom request: rtl/renderPreview
12. Custom request: rtl/getSchema, rtl/listSamples
13. 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