This document defines the architectural and coding rules for the project. It is authoritative for all new code and refactors.
- Single Responsibility: every class has exactly one reason to change.
- Open/Closed: extend behavior via composition and interfaces; avoid modifying stable code paths when adding features.
- Liskov Substitution: derived types must be safely substitutable without altering expected behavior or contract.
- Interface Segregation: prefer small, focused interfaces; avoid "god" interfaces.
- Dependency Inversion: depend on abstractions; wire concrete types in the composition root only.
- Views are passive. No UI logic in code-behind beyond
InitializeComponent(). - All inputs are routed to ViewModels via bindings, commands, and behaviors.
- ViewModels are UI-framework agnostic and unit-testable.
- Models and services contain business logic and data access; ViewModels orchestrate them via DI.
- Prefer composition in ViewModels/services/code over inheritance wherever possible,
except where framework base types are required (e.g.,
ReactiveObjectfor ViewModels).
- UI (Avalonia Views + XAML): visual composition only.
- Presentation (ViewModels): state, commands, reactive composition.
- Domain/Services: business logic, parsing, validation, domain rules.
- Infrastructure: file system, persistence, external integrations.
- UI depends on Presentation; Presentation depends on Domain; Infrastructure is depended on by Domain or Presentation via interfaces.
- No reference from Domain to UI or Avalonia types.
Reference: https://github.com/AvaloniaUI/Avalonia
- Use XAML for layout and visuals; avoid creating controls in code.
- Define styles and resources in dedicated resource dictionaries and merge them
in
App.axamlto keep styling consistent and maintainable. - Prefer
StaticResourcefor immutable resources andDynamicResourcewhen runtime updates are required.
- Use compiled bindings only (no reflection bindings) with explicit
x:DataTypeon all binding scopes (views, DataTemplates, control themes, and resources). - Keep bindings one-way unless user input must update the ViewModel.
- Use
DataTemplatesor aViewLocator(custom, non-reflection) for view lookup.
- Use
StyledPropertyonly for values that must participate in styling. - Prefer
DirectPropertyfor non-styled properties to avoid extra overhead. - For best UI/UX, prefer custom control creation or re-templating using control themes instead of CRUD-style UI.
Reference: https://github.com/reactiveui/ReactiveUI
- All ViewModels inherit from
ReactiveObject. - Use
ReactiveCommandfor commands; never use event handlers in code-behind. - Use
WhenAnyValue,ObservableAsPropertyHelper, andInteraction<TIn,TOut>to model state, derived values, and dialogs. - Use
ReactiveUI.SourceGeneratorsfor INPC/ReactiveObject boilerplate where applicable. https://github.com/reactiveui/ReactiveUI.SourceGenerators
- Use
IScreenwith a singleRoutingStateas the navigation root. - All navigable ViewModels implement
IRoutableViewModel. - Views host navigation via
RoutedViewHost. - Use route segments that are stable, explicit, and testable.
- Use
ReactiveUI.Avalonia(latest) and do not useAvalonia.ReactiveUIdirectly. - If a third-party dependency requires
Avalonia.ReactiveUI(e.g., Dock integration), isolate it to the docking layer and do not reference it from app UI code. https://github.com/reactiveui/ReactiveUI.Avalonia
Reference: https://github.com/wieslawsoltes/Xaml.Behaviors
- All UI input and events are handled via behaviors/triggers.
- Prefer source-generator-based behaviors/actions (no reflection) wherever available.
- Use trigger behaviors (property, data, loaded/unloaded, routed event) for lifecycles and state transitions.
- Code-behind must not contain event handlers or direct ViewModel calls.
Reference: https://github.com/wieslawsoltes/Dock
- Use Dock.Model.* to represent the docking layout state.
- Use Dock.Avalonia for the view layer and Dock.Model.ReactiveUI for MVVM integration.
- Persist layout state to user settings and restore on startup.
- Keep layout logic in ViewModels; Views only render the Dock model.
Reference: https://github.com/AvaloniaUI/AvaloniaEdit
- Use AvaloniaEdit
TextEditorfor all code/text editing surfaces. - Enable syntax highlighting using TextMate grammars/themes.
- Keep editor configuration in ViewModels (options, text, selection) and bind to the view.
Reference: https://github.com/wieslawsoltes/ProDataGrid
- Use ProDataGrid
DataGridfor all tabular data, tree views, and list displays. - Always use the ProDataGrid model approach with code-based column bindings and fast paths.
- Always enable full filtering, searching, and sorting support.
- Configure services in a single composition root (App startup).
- Use
AddSingleton,AddScoped,AddTransientcorrectly:- Singleton: thread-safe, shared, expensive-to-create services.
- Scoped: per-document or per-operation services created within explicit scopes.
- Transient: stateless lightweight services.
- Never resolve scoped services from singletons without creating a scope.
- Do not dispose services resolved from the container manually.
- Prefer allocation-free APIs:
Span<T>,ReadOnlySpan<T>,Memory<T>,ValueTask,ArrayPool<T>, andSystem.Buffers. - Use SIMD (
System.Numerics.Vector<T>or hardware intrinsics) where it provides measurable wins and keeps code maintainable. - Avoid LINQ in hot paths; use loops and pre-sized collections.
- Minimize boxing, virtual dispatch in tight loops, and avoid unnecessary allocations in render/update loops.
- Profile before and after optimizations; document expected gains.
These rules are non-negotiable for production compiler/runtime paths.
- No reflection in source-generator emitted code. This is mandatory.
- No reflection in runtime helpers executed by emitted code (loader, markup-extension runtime, binding/runtime assignment helpers, hot-reload apply paths).
- Forbidden in emitted/runtime paths:
Type.GetType,GetProperty/GetField/GetMethod,PropertyInfo.SetValue,MethodInfo.Invoke,Activator.CreateInstance,dynamic, runtime expression compilation, runtime IL emit, or any equivalent late-bound invocation. - Implement behavior through compile-time semantic binding and strongly typed emitted calls, delegates, and registries.
- Any change introducing reflection into emitted/runtime paths must be rejected in review.
- Generated code and runtime support code must be fully NativeAOT/trimming compatible.
- Do not depend on dynamic code generation or runtime assembly scanning/loading in emitted/runtime paths.
- Do not add production-path dependencies on APIs requiring dynamic code or unreferenced member preservation unless the call site is fully compile-time guarded away from AOT targets.
- Compiler parity work must preserve this contract: feature completeness cannot be achieved by falling back to reflection.
- Reflection is allowed only in tests, diagnostics tooling, or development-only utilities that are outside production runtime and emitted-code execution paths.
References:
-
All production code must be covered by unit tests; xUnit is required for unit testing.
-
UI tests must use Avalonia Headless (xUnit integration) and follow the headless testing guidance and helpers for input simulation.
-
Unit-test ViewModels and Domain services.
-
Use integration tests for parsing, IO, and docking layout persistence.
-
UI tests should validate navigation flows, docking, and editor behaviors.
- No code-behind event handlers.
- Avoid static state (except truly immutable constants).
- Prefer explicit types where clarity is improved; avoid
varin public APIs. - All public APIs must be documented and unit-tested.
Version bumps must keep the .NET/package metadata and VS Code extension metadata in sync.
.NETpackage versioning is defined inDirectory.Build.propsviaVersionPrefixandVersionSuffix.- The VS Code extension version is defined in:
tools/vscode/axsg-language-server/package.jsontools/vscode/axsg-language-server/package-lock.json
- Increment
VersionSuffixinDirectory.Build.props(for examplealpha.23->alpha.24). - Update
tools/vscode/axsg-language-server/package.jsonto the matching semver prerelease (for example0.1.0-alpha.24). - Update the root
versionfields intools/vscode/axsg-language-server/package-lock.jsonto the same value. - Verify there are no stale references to the previous prerelease in the authoritative version files before committing.
- Keep the repository on a single prerelease version; do not leave
.NETand VS Code extension versions diverged. - Use a dedicated commit for version bumps when practical.
- Do not treat README examples like
0.1.0-localas release-version sources.
References:
-
Use Microsoft Fluent System Icons for all
PathIcon/IconPathDatageometry. -
Do not hand-draw or invent SVG path strings for product UI icons.
-
Prefer
regularvariants for standard UI chrome. -
Prefer
20size source icons for toolbar usage; render using the existing control size in XAML. -
Keep icon path data in named constants (for example
OpenFolderIconPath) and reuse it.
- Pick an icon name from the Fluent icon catalog (for example
folder_open_20_regular). - Download the SVG from npm/unpkg, for example:
https://unpkg.com/@fluentui/svg-icons@<version>/icons/<icon-name>.svg - Copy the value of the SVG
<path d="..."/>attribute exactly. - Use that value directly in
PathIcon.Dataor extensionIconPathData. - If an icon contains multiple
<path>elements, choose a Fluent icon variant with a single path forPathIcon.Data, or compose a geometry only when needed.
npm view @fluentui/svg-icons version
npm pack @fluentui/svg-icons@<version>
tar -xzf fluentui-svg-icons-<version>.tgz
cat package/icons/folder_open_20_regular.svgThen copy the d attribute from the <path> element into code.
These rules prevent regressions in the source-generator compiler pipeline.
- Preserve owner-qualified property tokens exactly as authored in XAML (
Owner.Property). Do not strip owner prefixes in the parser. - Keep design-time-only members (
Design.*, ignored design namespaces) out of runtime semantic binding and runtime code emission. - Keep diagnostics location-accurate (file, line, column) and deterministic.
- Property-element binding must follow this order:
- Skip design-only members.
- Handle explicit child-attachment aliases (
Content,Children,Items). - Bind object values.
- Resolve attached-property elements from owner-qualified tokens.
- Handle dictionary-merge semantics.
- Handle collection-add semantics.
- Handle Avalonia-property assignment.
- Fall back to CLR settable-property assignment.
- Never run scalar CLR setter cardinality checks before collection/add and Avalonia-property checks, otherwise valid multi-item collection property elements regress.
- For setter
Propertytokens, resolve owner-qualified attached-property tokens first. - If attached Avalonia property resolution succeeds, do not emit missing CLR-property
diagnostics (
AXSG0301/AXSG0303) for that setter. - Use resolved Avalonia property metadata for value type conversion and duplicate setter detection.
- If a value is binding-like (
Binding,MultiBinding, compiled/runtime binding expression), emit Avalonia binding assignment via indexer descriptor path, not CLR casts. - Do not emit direct typed CLR assignment for binding-like values (
(bool)binding,(string)binding, etc.). - Treat both inline markup-extension bindings and object-element binding nodes as binding-like.
- Treat warning reduction as semantic parity work, not blanket suppression.
- Preserve strict diagnostics where parity requires explicit author intent
(
x:DataTypefor compiled bindings, template validation, etc.). - Do not downgrade warnings to hide unsupported behavior; either implement behavior or keep the warning actionable.
- Generated hint names must be unique and stable per logical XAML input.
- Repeated runs and partial rebuilds must not emit duplicate hint names.
- AdditionalFiles discovery must avoid duplicate logical inputs and editor backup files.
- Hot reload path must be tolerant to transient invalid XAML while editing.
- On parse/semantic failure during hot reload, keep last known good generated output; do not apply partial invalid graph updates.
- Resume normal hot reload application automatically once XAML becomes valid again.
- Runtime hot reload must track and clean up removable graph artifacts (styles, resources, templates, merged dictionaries, theme entries) when they are removed from XAML.
- Applying an edit and then removing it must converge to the same runtime state as the original baseline.
- For app-specific side effects outside generated graph, use explicit hot-reload handler policies rather than implicit best-effort cleanup.
- Hot design and related editing tools must compute and propagate minimal text-diff metadata (replace start offset, removed length, inserted length) for each source update.
- Source persistence paths must support no-op diff detection and avoid rewriting source files when there is no effective text change.
- Minimal-diff behavior must be deterministic and test-covered for replace/insert/delete cases.
- Any new parser/binder/emitter feature is incomplete unless hot reload and hot design behavior is explicitly implemented and verified.
- For each new feature, define live-edit semantics for:
- apply while editing,
- revert/removal convergence to baseline,
- failure handling with last-known-good behavior when applicable.
- Add tests that exercise both compile-time emission and runtime live-edit behavior for the feature.
These rules are mandatory for parser, semantic model, transforms, binder, emitter, and runtime contracts. They apply to all new features and all refactors.
- Language semantics must follow the XAML standard and preserve behavioral parity with mature compile-time XAML compilers.
- Framework-specific behavior (for example Avalonia property systems, selectors, templates) must be implemented as framework adapters over a framework-neutral semantic core.
- Never implement feature parity using string-shape hacks, reflection, or runtime guesswork.
- Markup extension detection:
- Forbidden: lexical head-token heuristics on raw text.
- Required: parser-first structured markup parsing, then semantic dispatch by parsed extension name/type.
- Value classification:
- Forbidden: inferring semantic kind from emitted C# expression text.
- Required: typed conversion results carrying explicit value kind and runtime requirements.
- Template/style/control-theme classification:
- Forbidden: suffix checks such as
EndsWith("Template"). - Required: symbol-based or node-kind-based classification.
- Forbidden: suffix checks such as
- Static resource resolver usage:
- Forbidden: deciding resolver requirements by scanning generated expression strings.
- Required: explicit semantic flags propagated from conversion/binding results.
- Template validation:
- Forbidden: reparsing
RawXamlin binder validation paths. - Required: validate from parsed document-model nodes with source line/column from nodes.
- Forbidden: reparsing
- Property-element and setter resolution:
- Forbidden: early missing-property diagnostics before attached/Avalonia-property resolution.
- Required: resolve owner-qualified/attached/Avalonia property metadata first, then emit missing-property diagnostics only when all typed resolution paths fail.
- Fallback conversion policy:
- Forbidden: untracked implicit fallback that changes behavior silently.
- Required: policy-driven fallback (
strictvscompatibility) with explicit diagnostics where applicable and deterministic emission behavior.
- Type resolution:
- Forbidden: unordered probing and unstable candidate selection.
- Required: deterministic ordered resolution with ambiguity diagnostics and explicit compatibility switches for legacy fallback behavior.
- Do not ship heuristic patches or one-off hacks to "make it work".
- Every bug fix must start with root-cause analysis at the correct semantic layer (parser, binder, transform, emitter, or runtime contract).
- Implement fixes using established XAML compiler patterns and standard XAML semantics, not ad-hoc behavior guesses tied to specific samples.
- If behavior differs from mature XAML compilers, align semantics through typed models and deterministic rules, not special-case string or control-name checks.
- Any compatibility fallback must be explicit, documented, deterministic, and covered by diagnostics/tests; silent heuristic fallback is forbidden.
- Pipeline stages must stay explicit and ordered:
- Parse to immutable model.
- Semantic bind to typed symbols/contracts.
- Run transforms on typed model.
- Emit deterministic C# from typed model only.
- Every stage must be deterministic for identical inputs (including diagnostics ordering).
- Binder output models must be sufficient for emission without reparsing raw XAML text.
- Any framework-specific feature must be represented in typed semantic models first, then projected by framework emitter/runtime adapters.
- Keep core semantic abstractions free of Avalonia runtime types whenever possible.
- Isolate framework-specific constructs in adapter layers (
*.Avalonia), not in core parser or core semantic model. - When adding a new XAML feature, define:
- framework-neutral semantic representation in Core,
- framework adapter mapping in Avalonia layer,
- runtime contract only if strictly required.
- Do not introduce Avalonia-only assumptions into generic parsing/tokenization logic.
- All mini-language parsing/tokenization must use the shared parser infrastructure project
(
XamlToCSharpGenerator.MiniLanguageParsing) instead of ad-hoc binder/runtime parsing. - Add new parsers/tokenizers there first (framework-neutral), then consume them from framework adapters and runtime tooling.
- Live-edit tooling (hot reload/hot design) must reuse the same parser infrastructure whenever structural parsing is required, so behavior stays aligned across compile-time and runtime paths.
- Diagnostics must be actionable, deterministic, and source-accurate.
- Ambiguity diagnostics must list candidates and deterministic selection result.
- Strict-mode diagnostics must not be downgraded to hide unsupported semantics.
- Compatibility-mode behavior must be explicit and test-covered.
- Add/adjust unit tests for:
- parser output shape,
- binder semantic resolution,
- emitted C# contract,
- diagnostics IDs/messages/locations.
- Add differential tests for representative framework cases (templates, resources, bindings, setters, includes) against expected runtime behavior.
- Add guard tests preventing regression to prohibited patterns (raw-XAML reparse in binder, suffix heuristics, expression-text scanning for semantic decisions).