diff --git a/.nuke b/.nuke deleted file mode 100644 index 06ef971..0000000 --- a/.nuke +++ /dev/null @@ -1 +0,0 @@ -src/AvalonStudio.Shell.sln \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..013e93e --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,128 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CiAzureWindows", + "Clean", + "Compile", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "Restore" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" + }, + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..5d71eb1 --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "src/AvalonStudio.Shell.sln" +} diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..59a519d --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,223 @@ + +# Architecture + +**Analysis Date:** 2026-08-28 + +## System Overview + +```text +┌─────────────────────────────────────────────────────────────────────────┐ +│ Host application: `src/ShellExampleApp` │ +│ `App.paml.cs` → `MainWindow.xaml` → `ShellView.paml` │ +├─────────────────────────┬───────────────────────────┬───────────────────┤ +│ Extensibility contract │ Shell implementation │ Shared UI helpers │ +│ `AvalonStudio.Shell. │ `AvalonStudio.Shell` │ `AvalonStudio. │ +│ Extensibility` │ │ Utils` │ +└────────────┬────────────┴──────────────┬────────────┴───────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ MEF composition and extensibility │ +│ `CompositionRoot.cs`, `ExtensionManager.cs`, external `Extensions/` │ +└──────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Avalonia / ReactiveUI / Dock.Avalonia runtime and user settings │ +│ Application resources, Dock layout, `${BaseDirectory}/Settings/*.json` │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Component Responsibilities + +| Component | Responsibility | File | +|-----------|----------------|------| +| Desktop host | Creates the Avalonia application and supplies the main window/sample features. | `src/ShellExampleApp/App.paml.cs` | +| Shell bootstrap | Sets platform paths, builds MEF composition, initializes `IoC`, initializes the singleton shell, then starts the desktop lifetime. | `src/AvalonStudio.Shell/Shell.cs` | +| Extension contracts | Defines the public interfaces, metadata attributes, MVVM bases, UI controls, and cross-plugin service contracts. | `src/AvalonStudio.Shell.Extensibility/` | +| Shell workspace | Owns documents, perspectives, docking layout, key bindings, menu/toolbar view models, and modal-dialog state. | `src/AvalonStudio.Shell/ShellViewModel.cs` | +| Composition root | Combines already-loaded assemblies with assemblies declared in extension manifests into a MEF container. | `src/AvalonStudio.Shell/CompositionRoot.cs` | +| Extension discovery | Reads manifest files from the executable-relative `Extensions` directory. | `src/AvalonStudio.Shell/ExtensionManager.cs` | +| Docking adapter | Bridges domain document/tool view models to `Dock.Model` dockables and builds the default layout. | `src/AvalonStudio.Shell/DockExtensions.cs`, `src/AvalonStudio.Shell/Docking/` | +| Menus, toolbars, commands | Materializes MEF metadata exports into ordered UI models and Avalonia key bindings. | `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ToolbarService.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` | + +## Pattern Overview + +**Overall:** plugin-oriented desktop shell using Avalonia MVVM, MEF attribute composition, ReactiveUI state, and Dock.Avalonia workspace layout. + +**Key Characteristics:** + +- Put reusable, plugin-facing interfaces and attributes in `src/AvalonStudio.Shell.Extensibility`; keep concrete shell behavior in `src/AvalonStudio.Shell`. +- Use `[Export]`, `[Export(typeof(...))]`, `[ImportingConstructor]`, `[ImportMany]`, and `[Shared]` for service and feature discovery rather than manually constructing integrated features. +- Model document and tool content as `IDocumentTabViewModel` and `IToolViewModel`; adapt it to Dock.Avalonia only through `DockExtensions.Dock`. +- Resolve each view model's default view by replacing `ViewModel` with `View` in its fully-qualified type name through `ViewLocator.Build`. +- Keep application-specific sample features in `src/ShellExampleApp`; that project references the shell and extensibility projects rather than being referenced by them. + +## Layers + +**Host application layer:** + +- Purpose: starts Avalonia and selects the main window/application resources. +- Location: `src/ShellExampleApp/`. +- Contains: `App.paml.cs`, `MainWindow.xaml`, concrete document/tool view models, matching views, and sample command/menu/toolbar exports. +- Depends on: `AvalonStudio.Shell`, `AvalonStudio.Shell.Extensibility`, Avalonia desktop packages. +- Used by: end users running the `ShellExampleApp` executable. + +**Shell runtime layer:** + +- Purpose: orchestrates startup, MEF discovery, shell state, dock layout, shell controls, themes, and local extension-manifest parsing. +- Location: `src/AvalonStudio.Shell/`. +- Contains: `Shell.cs`, `ShellViewModel.cs`, `CompositionRoot.cs`, `Docking/`, `Controls/`, `Themes/`, and `Assets/`. +- Depends on: `AvalonStudio.Shell.Extensibility`, `AvalonStudio.Utils`, Avalonia, ReactiveUI, Dock.Avalonia, MEF. +- Used by: host applications and loaded extensions. + +**Extensibility layer:** + +- Purpose: provides stable contracts and base implementations for features loaded into a shell. +- Location: `src/AvalonStudio.Shell.Extensibility/`. +- Contains: `IExtension.cs`, `IoC.cs`, `Shell/IShell.cs`, `MVVM/`, `Documents/`, `Commands/`, `MainMenu/`, `Menus/`, `Toolbars/`, `Settings/`, and `Theme/`. +- Depends on: Avalonia, ReactiveUI, Dock.Avalonia, MEF, JSON serialization. +- Used by: the shell and application/third-party extensions. + +**Shared presentation utility layer:** + +- Purpose: supplies reusable Avalonia interaction behaviors that are not specific to the shell domain. +- Location: `src/AvalonStudio.Utils/Behaviors/`. +- Contains: focus, command, modal-window, and pointer-wheel behaviors. +- Depends on: Avalonia and Xaml.Behaviors. +- Used by: XAML in `src/AvalonStudio.Shell/` and `src/ShellExampleApp/`. + +## Data Flow + +### Primary startup and composition path + +1. `App.Main` invokes `BuildAvaloniaApp().StartShellApp(...)` (`src/ShellExampleApp/App.paml.cs:15`). +2. `Shell.StartShellApp` configures ReactiveUI, sets `Platform.AppName`, creates platform directories, discovers manifests, builds a MEF `CompositionHost`, and calls `IoC.Initialise` (`src/AvalonStudio.Shell/Shell.cs:14`). +3. `CompositionRoot.CreateContainer` composes all loaded assemblies plus each manifest's `mefComponents` assets (`src/AvalonStudio.Shell/CompositionRoot.cs:13`). +4. The shell resolves and initializes the MEF-exported `ShellViewModel`, activates `IActivatableExtension` implementations, and converts exported command gestures into `KeyBinding` objects (`src/AvalonStudio.Shell/ShellViewModel.cs:75`). +5. Avalonia creates `MainWindow`, whose view model resolves `IShell` from `IoC`, and `MainWindow.xaml` embeds `ShellView` and `StatusBar` (`src/ShellExampleApp/MainWindowViewModel.cs:8`, `src/ShellExampleApp/MainWindow.xaml`). + +### Extension discovery path + +1. `Platform.Initialise` ensures executable-relative `Extensions/` and user settings directories exist (`src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:15`). +2. `ExtensionManager` enumerates direct child directories and loads each present `extension.json` manifest (`src/AvalonStudio.Shell/ExtensionManager.cs:22`). +3. `ExtensionManifest` exposes manifest asset groups, and `GetMefComponents` selects the `mefComponents` group (`src/AvalonStudio.Shell/ExtensionManifest.cs:42`, `src/AvalonStudio.Shell.Extensibility/ExtensionManifestExtensions.cs:7`). +4. The composition root loads those assemblies and MEF makes their exported services, tools, commands, and menu items available (`src/AvalonStudio.Shell/CompositionRoot.cs:29`). + +### Tool/document workspace path + +1. A feature resolves `IShell` and calls `AddOrSelectDocument` or `MainPerspective.AddOrSelectTool`; the extension helpers supply reuse-by-type behavior (`src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs:9`). +2. `ShellViewModel.AddDocument` wraps document models as dockables using `DockExtensions.Dock`; `AvalonStudioPerspective.AddTool` creates or reuses location-specific tool docks (`src/AvalonStudio.Shell/ShellViewModel.cs:194`, `src/AvalonStudio.Shell/AvalonStudioPerspective.cs:55`). +3. `DefaultLayoutFactory` initializes the root layout and its `DocumentsPane`; `ShellView.paml` binds that layout to `DockControl` (`src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:37`, `src/AvalonStudio.Shell/ShellView.paml`). +4. Dock selection propagates to `SelectedDocument` and document/tool lifecycle hooks through `ShellViewModel` and the MVVM base classes (`src/AvalonStudio.Shell/ShellViewModel.cs:166`, `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs:92`). + +### View resolution path + +1. Dock tab context is a `IDockableViewModel` (`src/AvalonStudio.Shell/DockExtensions.cs:18`). +2. `ShellExampleApp/App.paml` maps `IDockable` to `ViewModelViewHost` (`src/ShellExampleApp/App.paml`). +3. `ViewModelViewHost` calls `ViewLocator.Build(DataContext)` after its data context updates (`src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs:68`). +4. `ViewLocator` converts a fully-qualified `*ViewModel` name to `*View`, looks in loaded assemblies, and creates a parameterless Avalonia `Control` (`src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs:9`). + +**State Management:** + +- ReactiveUI properties (`RaiseAndSetIfChanged`, `WhenAnyValue`) back shell, document, tool, menu, and status state. +- `ShellViewModel.Instance` stores the initialized shell globally; `IoC` stores the MEF `CompositionHost` globally (`src/AvalonStudio.Shell/ShellViewModel.cs:31`, `src/AvalonStudio.Shell.Extensibility/IoC.cs:10`). +- Runtime preferences are serialized under `Platform.SettingsDirectory`; global settings are handled by `Settings`, while command and menu services expose their own settings models (`src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:25`). + +## Key Abstractions + +**MEF extension:** + +- Purpose: a discoverable contribution to the host that may participate in pre-activation and activation. +- Examples: `src/AvalonStudio.Shell.Extensibility/IExtension.cs`, `src/ShellExampleApp/ConsoleViewModel.cs`. +- Pattern: export a type implementing `IExtension`; implement `IActivatableExtension` to add tools or perform setup during shell initialization. + +**Shell service:** + +- Purpose: feature-facing API for documents, perspectives, current selection, overlay, and modal dialogs. +- Examples: `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`. +- Pattern: resolve `IShell` through `IoC.Get()`; do not instantiate `ShellViewModel` directly. + +**Dockable view model:** + +- Purpose: domain-level content that participates in tabbed documents or edge tool panes. +- Examples: `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ToolViewModel.cs`. +- Pattern: subclass `DocumentTabViewModel` for central documents or `ToolViewModel` for an edge pane; give tools a `DefaultLocation` and pair the class with a parameterless `*View` control. + +**Metadata-driven UI contribution:** + +- Purpose: lets extensions declare commands and placement without the shell knowing feature types. +- Examples: `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/MainMenu/ExportMainMenuItemAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ExportToolbarItemAttribute.cs`. +- Pattern: decorate MEF-exported members with export and ordering/group metadata; services materialize a deterministic view model. + +## Entry Points + +**Desktop executable:** + +- Location: `src/ShellExampleApp/App.paml.cs`. +- Triggers: operating-system process launch. +- Responsibilities: configures Avalonia, invokes the reusable shell bootstrap, restores theme settings, and creates `MainWindow`. + +**Reusable shell bootstrap:** + +- Location: `src/AvalonStudio.Shell/Shell.cs`. +- Triggers: host application's `App.Main`. +- Responsibilities: composes extensions and starts `StartWithClassicDesktopLifetime`. + +**Build and solution entry points:** + +- Location: `src/AvalonStudio.Shell.sln`, `build/Build.cs`, `src/build.ps1`, and `src/build.sh`. +- Triggers: developer and CI builds. +- Responsibilities: define solution projects, Nuke build targets, and platform-specific build entry scripts. + +## Architectural Constraints + +- **Threading:** Avalonia runs on a desktop UI thread; the sample defers theme resource changes through `Dispatcher.UIThread.InvokeAsync` in `src/ShellExampleApp/App.paml.cs`. +- **Global state:** `IoC` retains one `CompositionHost`; `ShellViewModel.Instance` retains one initialized shell; `Platform` caches the base directory; `Settings.Instance` caches global settings (`src/AvalonStudio.Shell.Extensibility/IoC.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`). +- **Assembly boundary:** dependency direction is `ShellExampleApp` → `AvalonStudio.Shell` → `AvalonStudio.Shell.Extensibility`/`AvalonStudio.Utils`; do not add references from the extensibility project to the concrete shell or app projects. +- **Circular imports:** no project-reference cycle is defined in `src/AvalonStudio.Shell.sln`; dynamic MEF loading can make runtime dependencies less explicit. +- **View naming:** dynamic view lookup requires the same namespace and a `ViewModel` → `View` name substitution plus a public parameterless `Control` constructor (`src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs`). +- **Plugin location:** external manifests must be located in an immediate subdirectory of `Path.Combine(ExecutionPath, "Extensions")`, and declared assemblies must be in their manifest `mefComponents` asset group (`src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:42`, `src/AvalonStudio.Shell/ExtensionManager.cs:25`). + +## Anti-Patterns + +### Direct shell construction + +**What happens:** A feature creates `ShellViewModel`, a dock layout, or a menu service itself. +**Why it's wrong:** It bypasses the initialized MEF composition, shared shell document list, key bindings, and current perspective. +**Do this instead:** Export the feature and resolve `IShell` through `IoC.Get()`, as in `src/ShellExampleApp/ConsoleViewModel.cs`. + +### Manually binding a dock tab to a view + +**What happens:** Feature code creates a Dock.Avalonia tab directly and separately assigns content. +**Why it's wrong:** Document/tool lifecycle hooks, selected-state synchronization, and type-based reuse stay outside shell tracking. +**Do this instead:** Create an `IDocumentTabViewModel` or `IToolViewModel` and use the `IShell`/`IPerspective` extension helpers in `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +### Placing reusable plugin contracts in the shell project + +**What happens:** New plugin interfaces, attributes, or generic view-model bases are added under `src/AvalonStudio.Shell/`. +**Why it's wrong:** External features would need a dependency on concrete runtime internals rather than the contract assembly. +**Do this instead:** Place public extension contracts under the relevant domain folder in `src/AvalonStudio.Shell.Extensibility/`. + +## Error Handling + +**Strategy:** plugin discovery and assembly loading isolate failures per extension; settings loading generally falls back to a default settings object. + +**Patterns:** + +- `ExtensionManager.LoadExtensions` catches malformed-manifest failures so one extension does not prevent enumeration (`src/AvalonStudio.Shell/ExtensionManager.cs:33`). +- `CompositionRoot.LoadMefComponents` catches individual assembly-load failures and writes the failure to standard output (`src/AvalonStudio.Shell/CompositionRoot.cs:39`). +- `IoC.Get` returns `default` when an export is unavailable; callers must treat a missing optional export as possible (`src/AvalonStudio.Shell.Extensibility/IoC.cs:35`). +- `Settings.Load` creates and saves default global settings when a persisted file is absent or cannot deserialize (`src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:35`). + +## Cross-Cutting Concerns + +**Logging:** extension assembly failures use `System.Console.WriteLine` in `src/AvalonStudio.Shell/CompositionRoot.cs`; no centralized logging abstraction is present. + +**Validation:** manifest JSON requires `Name`, `Version`, and `Assets` through Newtonsoft.Json `JsonProperty(Required = Required.Always)` annotations in `src/AvalonStudio.Shell/ExtensionManifest.cs`. + +**Authentication:** not applicable; this is a local desktop shell with no authentication subsystem. + +--- + +*Architecture analysis: 2026-08-28* diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..c7c2fe0 --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,193 @@ +# Codebase Concerns + +**Analysis Date:** 2026-08-28 + +## Tech Debt + +**Unimplemented collection-change cases:** +- Issue: `BindCollections` throws for `NotifyCollectionChangedAction.Move` and `Replace`, so a bound source collection cannot safely reorder or replace entries. +- Files: `src/AvalonStudio.Shell.Extensibility/MVVM/ObservableCollectionExtensions.cs:33` +- Impact: UI code using this helper fails at runtime when a source collection performs either standard `ObservableCollection` operation. +- Fix approach: Implement move and replace propagation, retain the event subscription in a disposable return value, and add tests covering add, move, replace, remove, reset, and disposal. + +**Disabled persistence features:** +- Issue: layout serialization is entirely commented out and `MenuSettingsService` always constructs new settings instead of reading its declared JSON file. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:147`, `src/AvalonStudio.Shell/ShellViewModel.cs:183`, `src/AvalonStudio.Shell.Extensibility/Menus/Settings/MenuSettingsService.cs:34` +- Impact: dock layout and menu customizations do not survive an application restart, although save APIs and settings locations imply that they do. +- Fix approach: restore load/save paths with schema/version handling, validate deserialized data, and exercise restart persistence in integration tests. + +**Global service locators and mutable process state:** +- Issue: composition, shell access, settings, themes, platform configuration, and extension discovery depend on static mutable state. +- Files: `src/AvalonStudio.Shell.Extensibility/IoC.cs:10`, `src/AvalonStudio.Shell/ShellViewModel.cs:31`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:32`, `src/AvalonStudio.Shell.Extensibility/Theme/ColorTheme.cs:22`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:10`, `src/AvalonStudio.Shell/ExtensionManager.cs:16` +- Impact: startup order is implicit, tests cannot isolate application state, and multiple shell instances in one process share stale configuration and extension lists. +- Fix approach: pass dependencies through constructors, expose a scoped application host, and inject file-system/platform abstractions into services that currently reach static state. + +**Synchronous, unguarded settings I/O:** +- Issue: settings load occurs in the static `Settings` initializer and all settings save operations truncate and rewrite the target file directly without validation, recovery, synchronization, or atomic replacement. +- Files: `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:34`, `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs:80`, `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs:25`, `src/AvalonStudio.Shell.Extensibility/Menus/Settings/MenuSettingsService.cs:45` +- Impact: corrupt or partially written JSON can prevent startup or settings access; concurrent saves can lose updates; UI-thread saves can block on slow storage. +- Fix approach: load through exception-aware validation/migration, write to a same-directory temporary file then atomically replace, serialize access, and move I/O off the UI thread. + +**Unfinished Dock layout hooks:** +- Issue: the default layout factory subscribes to `VisibleDockables` and collection changes but both handlers contain no behavior; its dockable locator is also empty. +- Files: `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:82`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:91`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:137` +- Impact: Dock lifecycle behavior and restoration points are opaque, and a future persistence/restore implementation has no registered dockable factories. +- Fix approach: remove inert subscriptions or implement explicit lifecycle/persistence responsibilities with a tested registry of supported dockables. + +**Incomplete platform support:** +- Issue: custom title-bar support relies on a Windows `user32.dll` class-style mutation, while macOS behavior contains an explicitly unimplemented native title-bar path. +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs:32`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:74`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:94` +- Impact: window chrome behavior depends on Avalonia template internals and platform-specific native calls, increasing regression risk across Windows, Linux, macOS, and Avalonia upgrades. +- Fix approach: isolate platform-specific window APIs behind per-platform implementations and run UI smoke tests on all supported desktop targets. + +## Known Bugs + +**Dock view clone operation is not functional:** +- Symptoms: calling `HomeView.Clone()` throws `NotImplementedException`. +- Files: `src/AvalonStudio.Shell/Docking/Views/HomeView.cs:10` +- Trigger: any Dock operation that requests cloning of the `HomeView` dockable. +- Workaround: avoid clone-based Dock flows for `HomeView` until it returns a fully initialized dockable. + +**Document removal assumes an existing view mapping:** +- Symptoms: `RemoveDocument` indexes `_documentViews[document]` without checking for a mapping. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:293` +- Trigger: remove a document that was not added successfully, was removed twice, or has a view-map inconsistency. +- Workaround: callers must only remove a document once after a successful `AddDocument`. + +**Command gesture access has an initialization-order failure:** +- Symptoms: `GetGesture` dereferences `_keyGestures` before `GetKeyGesture` initializes it. +- Files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:45`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:55` +- Trigger: an extension asks for a command gesture before shell initialization calls `GetKeyGesture`. +- Workaround: initialize command gestures before calling `GetGesture`. + +**Menu and command registration reject duplicate metadata at runtime:** +- Symptoms: immutable-dictionary builders use `Add`, which throws on duplicate command names; command activation also parses stored gesture strings without validation. +- Files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:104`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs:117`, `src/AvalonStudio.Shell/ShellViewModel.cs:126` +- Trigger: installed extensions export duplicate command metadata names or a persisted command setting has an invalid key-gesture string. +- Workaround: maintain globally unique command names and repair the settings file before startup. + +**Declared `temporary` document behavior is ignored:** +- Symptoms: `AddDocument` accepts `temporary` but never reads it. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:272` +- Trigger: callers supply `temporary: true` expecting preview/temporary-tab behavior. +- Workaround: do not rely on the parameter; model temporary state in the document implementation until the shell implements the lifecycle. + +## Security Considerations + +**Extension manifests can select arbitrary load paths:** +- Risk: a discovered extension manifest converts asset values to absolute paths without ensuring they remain under its extension directory, then MEF loads every declared component with `Assembly.LoadFrom`. +- Files: `src/AvalonStudio.Shell/ExtensionManifest.cs:54`, `src/AvalonStudio.Shell.Extensibility/ExtensionManifestExtensions.cs:9`, `src/AvalonStudio.Shell/CompositionRoot.cs:37`, `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs:56` +- Current mitigation: extension folders are discovered only under `Extensions` beside the entry assembly, and individual load exceptions are caught. +- Recommendations: establish a trusted-extension policy; reject rooted and escaping paths after canonicalization; restrict loads to an allowed directory; verify publisher/signature or hashes before executing extension assemblies; log failures through the application logger rather than only standard output. + +**Package merger accepts unbounded archive input:** +- Risk: package bytes, every ZIP entry, and parsed XML are loaded into memory without entry-count, compressed-size, decompressed-size, or XML-complexity limits. +- Files: `Numerge/Numerge/LoadedPackage.cs:26`, `Numerge/Numerge/LoadedPackage.cs:50`, `Numerge/Numerge/LoadedPackage.cs:209`, `Numerge/Numerge/NugetPackageMerger.cs:26` +- Current mitigation: `NugetPackageMerger` reports only `MergeAbortedException` failures. +- Recommendations: preflight archive metadata, enforce configurable size/count/expansion limits, stream entries where possible, reject malformed package layouts, and convert expected ZIP/XML/I/O failures into reported merge errors. + +**Deserialization consumes user-writable files without containment or recovery controls:** +- Risk: global, command, menu, and extension configuration files are read directly from filesystem locations and malformed input is allowed to escape the loading paths. +- Files: `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs:43`, `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs:28`, `src/AvalonStudio.Shell/ExtensionManifest.cs:40`, `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs:93` +- Current mitigation: default JSON settings omit null/default values and extension loading skips manifests that throw. +- Recommendations: treat persisted files as untrusted input, validate schema and maximum size, catch parse failures at every public load boundary, and back up/quarantine invalid settings instead of failing startup silently or globally. + +## Performance Bottlenecks + +**Unbounded in-memory package merge:** +- Problem: every `.nupkg` or `.snupkg` in the input directory is loaded as a full byte array, then every archive entry is copied into another byte array before merging. +- Files: `Numerge/Numerge/NugetPackageMerger.cs:26`, `Numerge/Numerge/LoadedPackage.cs:29`, `Numerge/Numerge/LoadedPackage.cs:34` +- Cause: the merge model uses `Dictionary` for all package content and keeps all packages resident until output writing completes. +- Improvement path: use temporary streams/files for large entries, process one merge group at a time, retain only metadata and selected binaries in memory, and impose package size limits. + +**Forced full garbage collection after each document removal:** +- Problem: document removal forces a process-wide `GC.Collect()`. +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:293`, `src/AvalonStudio.Shell/ShellViewModel.cs:314` +- Cause: reclamation is requested synchronously rather than allowing the runtime to schedule collection. +- Improvement path: remove the forced collection, identify retained Dock/event references with memory profiling, and dispose subscriptions or clear mappings explicitly when ownership ends. + +**Asynchronous collection mutations hide ordering and completion:** +- Problem: every mutation queues work with `Dispatcher.UIThread.InvokeAsync` and returns immediately, including range operations that enumerate caller-provided sequences later. +- Files: `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:58`, `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:166`, `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs:191` +- Cause: dispatch operations are not awaited or exposed to callers. +- Improvement path: expose task-returning mutation APIs or dispatch synchronously when already on the UI thread; snapshot input ranges before queueing; test ordering under background producers. + +## Fragile Areas + +**Template-part dependent window chrome:** +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs:139`, `src/AvalonStudio.Shell/Controls/MetroWindow.cs:213`, `src/AvalonStudio.Shell/Controls/MetroWindowTheme.paml.cs` +- Why fragile: pointer handling dereferences many template parts without null checks, and each template application adds new event handlers without detaching previous handlers. +- Safe modification: keep all required named parts synchronized with the window theme, guard optional parts, and attach/detach handlers through explicit lifecycle methods. +- Test coverage: no repository test project covers template reapplication, pointer resize paths, or platform-specific window chrome. + +**Shell/Dock state synchronization:** +- Files: `src/AvalonStudio.Shell/ShellViewModel.cs:147`, `src/AvalonStudio.Shell/ShellViewModel.cs:272`, `src/AvalonStudio.Shell/ShellViewModel.cs:323`, `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs:19` +- Why fragile: documents are tracked in separate lists/dictionaries and Dock collections, perspectives are mutable, and navigation relies on casts and hard-coded dock IDs such as `DocumentsPane`. +- Safe modification: make one owner responsible for document lifecycle, validate required docks at layout initialization, and update all state stores atomically through tested commands. +- Test coverage: no repository test project exercises add, select, remove, perspective switch, or missing-dock failure paths. + +**NuGet package layout parser:** +- Files: `Numerge/Numerge/LoadedPackage.cs:40`, `Numerge/Numerge/LoadedPackage.cs:82`, `Numerge/Numerge/LoadedPackage.cs:212`, `Numerge/Numerge/LoadedPackage.cs:407` +- Why fragile: the parser assumes a `.nuspec`, `[Content_Types].xml`, grouped dependency XML, and `lib//` paths; malformed or valid alternative package layouts result in indexing, `First`, or attribute dereference exceptions. +- Safe modification: create a package-format validation layer, support direct `` nodes and content-type overrides, and return diagnostic errors instead of raw exceptions. +- Test coverage: no repository test project supplies package fixtures for valid, malformed, empty, multi-target, or symbol-package inputs. + +## Scaling Limits + +**Extension composition startup:** +- Current capacity: extension discovery scans every first-level directory and composition loads every MEF component assembly in the selected manifests during startup. +- Limit: startup time and memory grow with extension count and assembly dependency graphs; a failing component is reported only to standard output. +- Scaling path: cache validated manifests, load extensions lazily or in isolated load contexts, record structured diagnostics, and add startup telemetry around `src/AvalonStudio.Shell/ExtensionManager.cs:24` and `src/AvalonStudio.Shell/CompositionRoot.cs:31`. + +**Package-merger working set:** +- Current capacity: `Numerge` retains all discovered package contents in dictionaries until the merge finishes. +- Limit: the process working set scales with the combined uncompressed size of the input packages, not just the current output package. +- Scaling path: redesign `Numerge/Numerge/NugetPackageMerger.cs:21` and `Numerge/Numerge/LoadedPackage.cs:21` around bounded streaming and temporary storage. + +## Dependencies at Risk + +**Dual target framework maintenance:** +- Risk: every core project targets both `net8.0` and `net10.0`, while CI installs both SDKs and the build configuration has no test stage. +- Impact: incompatible dependency or Avalonia behavior can enter one target without automated runtime coverage. +- Migration plan: retain both targets only with a target-matrix test/build stage; otherwise define a supported LTS target policy in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj`, and `azure-pipelines.yml`. + +**NuGet v2 package source:** +- Risk: restore configuration points to the NuGet v2 endpoint. +- Impact: package restore remains dependent on the legacy protocol endpoint and lacks a repository-level locked-mode policy. +- Migration plan: move to the v3 service index and add lock files or a centrally enforced restore policy in `src/nuget.config` and the project build configuration. + +## Missing Critical Features + +**Extension lifecycle isolation and diagnostics:** +- Problem: extension discovery swallows manifest errors and extension component load failures only write to standard output; loaded extensions execute inside the host process with no version compatibility or trust checks. +- Blocks: reliable support triage, safe third-party extension distribution, and predictable recovery from extension faults. +- Files: `src/AvalonStudio.Shell/ExtensionManager.cs:20`, `src/AvalonStudio.Shell/CompositionRoot.cs:31`, `src/AvalonStudio.Shell.Extensibility/Utils/AppDomain.cs:18` + +**Package merger format compatibility layer:** +- Problem: the merger implements only narrow `netstandard2.0` fallback handling and preserves only simplified content-type/dependency structures. +- Blocks: dependable merging of modern multi-target NuGet packages and clear compatibility diagnostics. +- Files: `Numerge/Numerge/LoadedPackage.cs:95`, `Numerge/Numerge/LoadedPackage.cs:125`, `Numerge/Numerge/LoadedPackage.cs:403` + +## Test Coverage Gaps + +**Repository-wide automated tests:** +- What's not tested: no committed test project, test source files, or test-runner package references cover the shell, extensibility library, utilities, or `Numerge`. +- Files: `src/AvalonStudio.Shell.sln`, `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `Numerge/Numerge.sln`, `azure-pipelines.yml` +- Risk: regressions in desktop behavior, settings migrations, extension loading, and NuGet package output are detected only through manual builds or consumers. +- Priority: High. + +**Persistence and malformed-input recovery:** +- What's not tested: corrupted JSON, concurrent writes, partial writes, invalid key gestures, invalid extension manifests, malformed ZIP/XML, and unsupported package layouts. +- Files: `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`, `src/AvalonStudio.Shell/ExtensionManifest.cs`, `Numerge/Numerge/LoadedPackage.cs` +- Risk: startup and package processing can fail with unhandled exceptions or silently discard diagnostics. +- Priority: High. + +**Cross-platform desktop behavior:** +- What's not tested: title-bar template resolution, resize/move gestures, Windows native-handle behavior, macOS decoration behavior, and Linux shell startup. +- Files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs`, `src/AvalonStudio.Shell/Shell.cs`, `azure-pipelines.yml` +- Risk: platform-specific regressions can ship because CI runs only `windows-latest`. +- Priority: High. + +--- + +*Concerns audit: 2026-08-28* diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..b884c25 --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,107 @@ +# Coding Conventions + +**Analysis Date:** 2026-08-28 + +## Naming Patterns + +**Files:** +- Use PascalCase C# filenames that name the primary type: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`, `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`, and `src/ShellExampleApp/MainWindowViewModel.cs`. +- Pair Avalonia views with their code-behind using the same basename (for example, `src/ShellExampleApp/MainWindow.xaml` and `src/ShellExampleApp/MainWindow.xaml.cs`). Several reusable shell resources use the legacy `.paml.cs` suffix, such as `src/AvalonStudio.Shell/ShellView.paml.cs` and `src/AvalonStudio.Shell/Controls/StatusBar.paml.cs`; retain the existing suffix when extending those resources. +- Place types in a directory corresponding to their concern and namespace segment, such as `MVVM`, `Commands`, `Menus`, `Docking`, `Converters`, or `Behaviors` beneath `src/AvalonStudio.Shell.Extensibility/`. + +**Functions:** +- Use PascalCase for public, protected, and private methods (`GetMainMenu`, `ResolveCommands`, `LoadLayout`) in `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` and `src/AvalonStudio.Shell/ShellViewModel.cs`. +- Use `On...` for lifecycle hooks and event-style methods, including `OnOpen`, `OnClose`, `OnSelected`, and `OnFrameworkInitializationCompleted` in `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs` and `src/ShellExampleApp/App.paml.cs`. +- Use verb-led extension methods such as `Dock`, `AddOrSelectDocument`, and `GetOrCreate` in `src/AvalonStudio.Shell/DockExtensions.cs` and `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +**Variables:** +- Use camelCase for parameters, local variables, and pattern variables (`commandSettings`, `parentItem`, `activatable`) throughout `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`. +- Use private camelCase fields in most UI/view-model code (`_documents`, `_viewModel`, `modalDialog`) as in `src/AvalonStudio.Shell/ShellViewModel.cs`; legacy and binding-oriented classes also use non-underscore fields (`lineNumber`, `isVisible`) in `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs`. Follow the surrounding file's field style instead of mixing both conventions. +- Use the `s_` prefix for private static mutable fields, as in `s_compositionHost` in `src/AvalonStudio.Shell.Extensibility/IoC.cs`. + +**Types:** +- Use PascalCase for classes, interfaces, enums, attributes, and generic parameters. Prefix interfaces with `I` (`IShell`, `IMenuItemFactory`, `IObservableCollection`) in `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/Menus/IMenuItemFactory.cs`, and `src/AvalonStudio.Shell.Extensibility/MVVM/IObservableCollection.cs`. +- Name UI state classes with `ViewModel`, controls/views with `View`, and MEF attribute types with `Export...Attribute`, matching `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`. + +## Code Style + +**Formatting:** +- No repository-root formatter configuration is detected. The only editor configuration is `build/.editorconfig`, so it applies to the build project rather than the `src/` projects. +- In `build/.editorconfig`, omit redundant `this.` qualification and prefer expression-bodied properties, indexers, and accessors. It explicitly permits omitted accessibility modifiers (`dotnet_style_require_accessibility_modifiers = never`). +- Use block-scoped namespaces. Every production C# sample uses `namespace ... { ... }`, for example `src/AvalonStudio.Shell/CompositionRoot.cs` and `Numerge/Numerge/NugetPackageMerger.cs`. +- Place opening braces on a new line. Preserve the local indentation of the file: the repository contains both spaces and tabs, notably tabs in `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and spaces in `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`. +- Prefer concise expression-bodied members where already established (`Title => Platform.AppName` in `src/AvalonStudio.Shell/ShellViewModel.cs` and `BuildAvaloniaApp() => ...` in `src/ShellExampleApp/App.paml.cs`); use full accessors when setters contain notification or guard logic. + +**Linting:** +- No project-level analyzer, lint task, `TreatWarningsAsErrors`, `Nullable`, or `LangVersion` configuration is detected in the `.csproj` files or CI pipeline. +- `build/.editorconfig` sets the listed style diagnostics to warning severity. Do not assume additional analyzer enforcement. + +## Import Organization + +**Order:** +1. Avalonia, Dock, ReactiveUI, and other framework imports are commonly placed first. +2. `AvalonStudio.*` project imports generally follow. +3. `System.*` imports occur last in many UI files, including `src/AvalonStudio.Shell/ShellViewModel.cs` and `src/ShellExampleApp/Commands/ThemeCommands.cs`. + +- Import ordering is not mechanically consistent: `src/AvalonStudio.Shell/CompositionRoot.cs` starts with project imports while `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` starts with `System.*`. Keep imports explicit and preserve the grouping/order of the file being modified; do not introduce a new sorting rule in an isolated change. +- Use direct namespaces rather than a repository-wide global-using mechanism. Static imports are limited to build orchestration in `build/Build.cs`. + +**Path Aliases:** +- Not applicable. C# projects reference namespaces directly; no alias/import configuration is detected. + +## Error Handling + +**Patterns:** +- Let ordinary framework and programming errors propagate. Most public methods do not wrap exceptions, including layout and menu construction in `src/AvalonStudio.Shell/ShellViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`. +- Catch only a failure with a defined recovery result. `Numerge/Numerge/NugetPackageMerger.cs` catches `MergeAbortedException`, logs its message, and returns `false`; callers in `build/Build.cs` convert `false` to a build failure. +- At extension-discovery boundaries, catch broad load errors and continue loading remaining components, emitting diagnostic text through `System.Console.WriteLine` as in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- Service lookup treats unavailable optional exports as absence: `src/AvalonStudio.Shell.Extensibility/IoC.cs` returns `default` for single lookups and `Enumerable.Empty()` for plural lookup. Callers must safely handle a missing export. +- Use early null guards for optional arguments, as in `RemoveDocument` in `src/AvalonStudio.Shell/ShellViewModel.cs`; avoid adding broad catch-and-ignore blocks. + +## Logging + +**Framework:** console and Nuke build logging; no application logging abstraction is detected. + +**Patterns:** +- Use `Information(...)` for build diagnostics in `build/Build.cs`. +- `Numerge/Numerge/INumergeLogger.cs` defines `Info`, `Error`, and `Warning` for package-merge work; pass an `INumergeLogger` into merger operations rather than writing directly from that library. +- Use `System.Console.WriteLine` only for extension-loading diagnostics in `src/AvalonStudio.Shell/CompositionRoot.cs`. No structured logging provider is configured for shell runtime code. + +## Comments + +**When to Comment:** +- Comment non-obvious platform, plugin, or framework constraints. `src/AvalonStudio.Shell/CompositionRoot.cs` documents why its custom `AppDomain` loader remains, and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` explains the menu-parent registration rule. +- Preserve targeted suppression comments where required, such as the ReSharper suppressions at the top of `Numerge/Numerge/NugetPackageMerger.cs`; do not add blanket suppression files. +- TODO comments exist for explicit pending work (for example `src/AvalonStudio.Shell/CompositionRoot.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`). Keep a TODO adjacent to the constrained behavior and describe the intended outcome. + +**JSDoc/TSDoc:** +- Use XML documentation for reusable public/protected collection and framework-extension behavior. `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs` documents overridden collection operations and their parameters with `/// ` and `/// `. +- Small view models, services, and straightforward public models generally have no XML comments; avoid redundant documentation for self-evident members. + +## Function Design + +**Size:** +- Small helpers and model members are preferred, but orchestration classes may own longer stateful flows: `src/AvalonStudio.Shell/ShellViewModel.cs` manages shell lifecycle and docking, and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` builds menu trees. Keep UI state transitions with the owning view model/service rather than creating ad-hoc utility classes. + +**Parameters:** +- Use constructor injection with `[ImportingConstructor]` for MEF-composed services (`src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`). +- Use optional parameters for feature toggles/default UI behavior (`AddDocument(..., bool temporary = false, bool select = true)` in `src/AvalonStudio.Shell/ShellViewModel.cs`) and generic constraints for document/type factories in the same file. + +**Return Values:** +- Return immutable/read-only views of owned mutable state when exposing collections (`Documents => _documents.AsReadOnly()` in `src/AvalonStudio.Shell/ShellViewModel.cs`). +- Return `bool` for expected operation success/failure in the Numerge merge layer (`Numerge/Numerge/NugetPackageMerger.cs`); use exceptions for unrecoverable build failure in `build/Build.cs`. +- For reactive UI properties, update backing fields through `RaiseAndSetIfChanged` or explicitly raise dependent property notifications, following `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +## Module Design + +**Exports:** +- Use one primary public type per PascalCase file. Keep closely coupled helpers/implementations together when they are internal, such as the tab classes in `src/AvalonStudio.Shell/DockExtensions.cs`. +- Expose extension points through interfaces and MEF attributes: `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, and `[Export]/[Shared]` services in `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`. +- Use `internal` for example-app registration holders that should not form public API, as in `src/ShellExampleApp/Commands/ThemeCommands.cs` and `src/ShellExampleApp/MainMenu/ThemeMainMenuItem.cs`. + +**Barrel Files:** +- Not applicable. No C# barrel/export-aggregator files are detected; consumers import the concrete namespace/type they need. + +--- + +*Convention analysis: 2026-08-28* diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..ec20c4a --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,89 @@ +# External Integrations + +**Analysis Date:** 2026-08-28 + +## APIs & External Services + +**Package distribution:** +- NuGet.org - package restore source for the .NET solution. + - SDK/Client: .NET SDK/NuGet invoked by `DotNetRestore` in `build/Build.cs`. + - Auth: not configured in `src/nuget.config`; it contains only the public `nuget.org` source. + +**Source control and versioning:** +- Git/GitHub repository metadata - NUKE identifies the canonical repository and uses GitVersion metadata during builds. + - SDK/Client: `Nuke.Common` 8.1.4 and `GitVersion.Tool` 6.8.2 in `build/_build.csproj`. + - Auth: not handled by application code; CI checkout credentials are platform-managed. + +**CI service:** +- Azure Pipelines - builds every branch/tag on `windows-latest`, installs .NET SDK 8 and 10, runs the NUKE target, and publishes NuGet artifacts. + - SDK/Client: Azure Pipelines YAML tasks in `azure-pipelines.yml`; CI-state detection via `Nuke.Common.CI.AzurePipelines` in `build/BuildParameters.cs`. + - Auth: Azure Pipelines managed identity/token configuration is not defined in this repository. + +**Desktop extension boundary:** +- Local MEF extensions - the application discovers `extension.json` manifests under the executable's `Extensions` directory, then loads declared `mefComponents` assemblies. + - SDK/Client: `System.Composition` in `src/AvalonStudio.Shell/CompositionRoot.cs` and `src/AvalonStudio.Shell/ExtensionManager.cs`. + - Auth: none; extensions are filesystem-local rather than a remote API. + +No HTTP clients, third-party SaaS SDKs, payment services, messaging providers, or outbound application API calls are detected in `src/`. + +## Data Storage + +**Databases:** +- Not detected. No ORM, database client, connection string, or database configuration is present in `src/`. + +**File Storage:** +- Local filesystem only. The platform initialises a per-application base directory, `Settings` directory, and executable-relative `Extensions` directory in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. +- `GlobalSettings.json` is read and written via `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs`. +- `CommandSettings.json` is read and written via `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs`. +- Extension manifests are parsed from local `extension.json` files by `src/AvalonStudio.Shell/ExtensionManifest.cs`. +- Client: `Newtonsoft.Json` 13.0.3 through the shared serializer in `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. + +**Caching:** +- Local application storage only. `AVALON_CACHE_PATH` can relocate the base directory; no distributed cache is configured. + +## Authentication & Identity + +**Auth Provider:** +- Not detected. `src/` contains no authentication, authorization, OAuth/OpenID, JWT, or identity-provider integration. + - Implementation: not applicable. + +## Monitoring & Observability + +**Error Tracking:** +- None detected. No error-tracking SDK or telemetry service is referenced by the project files or source. + +**Logs:** +- The extension loader writes component-load failures to standard console output in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- NUKE writes build diagnostics through its logging API in `build/Build.cs`; no structured or remote log sink is configured. + +## CI/CD & Deployment + +**Hosting:** +- Not applicable. This is a desktop-library and NuGet-package repository, not a hosted service. + +**CI Pipeline:** +- Azure Pipelines configuration in `azure-pipelines.yml` executes `CiAzureWindows` from `build/Build.cs` and uploads `artifacts/nuget` as a pipeline artifact. +- Packaging is local to the build agent: `build/Build.cs` restores, compiles, packs, and merges packages. No NuGet feed push/publish task is configured. + +## Environment Configuration + +**Required env vars:** +- None for normal application startup. +- Optional `AVALON_CACHE_PATH` overrides the runtime settings base path in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. +- Azure builds expect the non-secret `BUILD_BUILDID` CI variable when generating CI prerelease versions in `build/BuildParameters.cs`. + +**Secrets location:** +- No repository-managed secret file or secret-store integration is detected. No `.env` file is present. +- CI and source-control authentication, if needed, remain external to repository code and configuration. + +## Webhooks & Callbacks + +**Incoming:** +- None detected. The repository contains no web server, endpoint, or webhook handler. + +**Outgoing:** +- None detected from application code. The CI pipeline publishes artifacts to Azure Pipelines; it does not define application callbacks. + +--- + +*Integration audit: 2026-08-28* diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..b401903 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,88 @@ +# Technology Stack + +**Analysis Date:** 2026-08-28 + +## Languages + +**Primary:** +- C# (`LangVersion` `Latest`) - application libraries, the `ShellExampleApp` desktop executable, and the NUKE build code in `src/` and `build/`. +- Avalonia XAML markup - desktop views and resources in `src/AvalonStudio.Shell/**/*.xaml`, `src/AvalonStudio.Shell.Extensibility/**/*.xaml`, and `src/ShellExampleApp/**/*.xaml`. + +**Secondary:** +- MSBuild XML - project, dependency, and version configuration in `src/*.csproj`, `src/Directory.Build.props`, `src/Versions.props`, `src/Packages.targets`, and `build/SharedVersion.props`. +- JSON - NUKE build metadata in `.nuke/parameters.json` and local extension/settings payloads managed by `src/AvalonStudio.Shell/ExtensionManifest.cs` and `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. +- YAML - Azure Pipelines CI definition in `azure-pipelines.yml`. +- PowerShell and Bash - cross-platform build bootstrap scripts in `src/build.ps1` and `src/build.sh`; Linux launcher in `src/AvalonStudio.Shell/avalonstudio.sh`. + +## Runtime + +**Environment:** +- .NET SDK 8.0 and 10.0 - explicitly installed by `azure-pipelines.yml`. +- .NET targets - the reusable shell projects and sample app target `net8.0;net10.0` in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`, `src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj`, `src/AvalonStudio.Utils/AvalonStudio.Utils.csproj`, and `src/ShellExampleApp/ShellExampleApp.csproj`. +- Build tooling targets `net8.0` in `build/_build.csproj`; its bundled `Numerge` utility targets `netstandard2.0` in `Numerge/Numerge/Numerge.csproj`. +- Desktop runtime - Avalonia platform detection is started by `src/ShellExampleApp/App.paml.cs`; Windows, Linux, and macOS are handled in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. + +**Package Manager:** +- NuGet through the .NET SDK/MSBuild. The repository source is defined in `src/nuget.config` as `nuget.org`. +- Lockfile: missing (`packages.lock.json` is not present); restore resolves dependencies from project and central MSBuild version files. + +## Frameworks + +**Core:** +- Avalonia 12.1.1 - cross-platform desktop UI, resources, and platform integration. Version is centralized in `src/Versions.props`; desktop references are in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj` and `src/ShellExampleApp/ShellExampleApp.csproj`. +- ReactiveUI 24.1.0 and ReactiveUI.Avalonia 12.1.1 - reactive MVVM support in the shell and extensibility projects; versions are centralized in `src/Versions.props`. +- Dock.Avalonia 12.1.0 - docking layout model and UI in `src/AvalonStudio.Shell/AvalonStudio.Shell.csproj`; the sample app uses the Fluent docking theme in `src/ShellExampleApp/ShellExampleApp.csproj`. +- System.Composition 10.0.11 - MEF-style extension discovery and composition in `src/AvalonStudio.Shell/CompositionRoot.cs` and export attributes throughout `src/AvalonStudio.Shell.Extensibility/`. + +**Testing:** +- Not detected. No test projects, test-framework references, or test configuration files are present. + +**Build/Dev:** +- MSBuild/.NET SDK - project compilation and packaging, with common properties imported by `src/Directory.Build.props` and package versions applied by `src/Directory.Build.targets`. +- NUKE 8.1.4 - build orchestration in `build/_build.csproj` and `build/Build.cs`. +- GitVersion.Tool 6.8.2 - build-time version metadata downloaded by `build/_build.csproj` and consumed by `build/Build.cs`. +- Numerge - repository-local NuGet package merger referenced by `build/_build.csproj` and configured by `build/numerge.config`. + +## Key Dependencies + +**Critical:** +- `Avalonia`, `Avalonia.Desktop`, and `Avalonia.Themes.Fluent` 12.1.1 - UI framework, desktop host, and Fluent styling; version declarations are in `src/Versions.props` and package use is in the `src/*.csproj` files. +- `ReactiveUI` 24.1.0 and `ReactiveUI.Avalonia` 12.1.1 - bindings and view-model behavior used across `src/AvalonStudio.Shell/` and `src/AvalonStudio.Shell.Extensibility/`. +- `Dock.Avalonia`, `Dock.Model.ReactiveUI`, and Dock theme packages 12.1.0 - docking UI, models, and themes configured through `src/Packages.targets`. +- `Newtonsoft.Json` 13.0.3 - extension manifest and settings serialization in `src/AvalonStudio.Shell/ExtensionManifest.cs` and `src/AvalonStudio.Shell.Extensibility/Utils/SerializedObject.cs`. +- `Microsoft.Extensions.DependencyModel` 10.0.11 - runtime dependency inspection for MEF assembly loading in `src/AvalonStudio.Shell.Extensibility/Utils/AppDomain.cs`. + +**Infrastructure:** +- `System.Composition` and `System.Composition.AttributedModel` 10.0.11 - plugin exports and the composition host in `src/AvalonStudio.Shell/CompositionRoot.cs`. +- `System.Reactive` 7.0.0 and `System.Collections.Immutable` 9.0.0 - reactive streams and immutable collections referenced by the shell projects. +- `Xaml.Behaviors.Avalonia` 12.0.7 - reusable UI behaviors in `src/AvalonStudio.Shell/Behaviors/` and `src/AvalonStudio.Utils/Behaviors/`. + +## Configuration + +**Environment:** +- No `.env` files or application secret configuration were detected. +- `AVALON_CACHE_PATH` is an optional runtime override for the application base directory in `src/AvalonStudio.Shell.Extensibility/Platforms/Platform.cs`. Without it, the desktop host uses `%UserProfile%/{AppName}` on Windows or `$HOME/{AppName}` on Unix-like platforms. +- `BUILD_BUILDID` is read only while an Azure build derives a CI prerelease version in `build/BuildParameters.cs`. +- Local configuration is JSON persisted under the platform settings directory by `src/AvalonStudio.Shell.Extensibility/GlobalSettings/GlobalSettings.cs` and `src/AvalonStudio.Shell.Extensibility/Commands/Settings/CommandSettingsService.cs`. + +**Build:** +- Central language and deterministic-build configuration: `src/Directory.Build.props`. +- Central dependency versions: `src/Versions.props`; common package-version updates: `src/Packages.targets` imported by `src/Directory.Build.targets`. +- Product metadata and package version baseline: `build/SharedVersion.props`. +- NUKE build parameters and selected solution: `.nuke/parameters.json`; build targets: `build/Build.cs`. +- CI configuration: `azure-pipelines.yml`; NuGet source configuration: `src/nuget.config`. + +## Platform Requirements + +**Development:** +- Install .NET SDK 8 and .NET SDK 10 to match `azure-pipelines.yml`, then restore/build `src/AvalonStudio.Shell.sln` with `dotnet`. +- Use `dotnet run --project build/_build.csproj -- --target CiAzureWindows` to execute the CI-equivalent NUKE target, as defined in `azure-pipelines.yml`. +- Windows, Linux, and macOS desktop development are supported by Avalonia platform detection in `src/ShellExampleApp/App.paml.cs`; the Linux launcher invokes Mono/Skia in `src/AvalonStudio.Shell/avalonstudio.sh`. + +**Production:** +- Distributable output is reusable `net8.0`/`net10.0` .NET libraries plus the `ShellExampleApp` desktop example. `build/Build.cs` packages the solution and merges NuGet outputs into `artifacts/nuget`. +- No server, container, or web-host deployment target is configured. + +--- + +*Stack analysis: 2026-08-28* diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..cedff85 --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,210 @@ +# Codebase Structure + +**Analysis Date:** 2026-08-28 + +## Directory Layout + +```text +AvalonStudio.Shell/ +├── src/ # Main solution and product source +│ ├── AvalonStudio.Shell.Extensibility/ # Public plugin contracts and reusable UI/MVVM APIs +│ ├── AvalonStudio.Shell/ # Concrete shell runtime, controls, docking, themes +│ ├── AvalonStudio.Utils/ # Generic Avalonia behavior helpers +│ ├── ShellExampleApp/ # Executable sample host and sample contributions +│ ├── AvalonStudio.Shell.sln # Visual Studio solution +│ ├── Directory.Build.props # Source-wide compiler settings +│ └── Versions.props # Central package versions +├── build/ # Nuke build project and shared build/version props +├── Numerge/ # NuGet package merge utility used by the build +├── .nuke/ # Nuke generated/build parameter metadata +├── azure-pipelines.yml # Azure Pipelines definition +└── README.md # Project description +``` + +## Directory Purposes + +**`src/AvalonStudio.Shell.Extensibility/`:** + +- Purpose: public API surface consumed by host applications and extension assemblies. +- Contains: interfaces, MEF export attributes/metadata, ReactiveUI MVVM bases, controls, settings helpers, menu/command/toolbar services, and platform paths. +- Key files: `src/AvalonStudio.Shell.Extensibility/IoC.cs`, `src/AvalonStudio.Shell.Extensibility/IExtension.cs`, `src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ToolViewModel.cs`. + +**`src/AvalonStudio.Shell/`:** + +- Purpose: concrete implementation of the reusable desktop shell. +- Contains: startup/composition code, MEF extension discovery, shell state, Avalonia XAML, custom controls, Dock.Avalonia adapters, themes, and bundled assets. +- Key files: `src/AvalonStudio.Shell/Shell.cs`, `src/AvalonStudio.Shell/CompositionRoot.cs`, `src/AvalonStudio.Shell/ShellViewModel.cs`, `src/AvalonStudio.Shell/ShellView.paml`. + +**`src/AvalonStudio.Shell/Docking/`:** + +- Purpose: creates and maintains the Dock.Avalonia layout used by the shell workspace. +- Contains: the default factory, custom document/tool dock types, and Dock sample views. +- Key files: `src/AvalonStudio.Shell/Docking/DefaultLayoutFactory.cs`, `src/AvalonStudio.Shell/Docking/AvalonStudioDocumentDock.cs`, `src/AvalonStudio.Shell/Docking/AvalonStudioToolDock.cs`. + +**`src/AvalonStudio.Shell/Controls/`:** + +- Purpose: concrete shell controls and their matching XAML/PAML styles and code-behind. +- Contains: `MetroWindow`, `ModalDialog`, `StatusBar`, and `ToolBar` implementations. +- Key files: `src/AvalonStudio.Shell/Controls/MetroWindow.cs`, `src/AvalonStudio.Shell/Controls/MetroWindow.paml`, `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +**`src/AvalonStudio.Shell.Extensibility/Commands/`, `MainMenu/`, `Menus/`, and `Toolbars/`:** + +- Purpose: define metadata attributes and build UI models from MEF-imported feature exports. +- Contains: export attributes, metadata containers, item models, services, and settings types. +- Key files: `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs`, `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs`, `src/AvalonStudio.Shell.Extensibility/Toolbars/ToolbarService.cs`. + +**`src/AvalonStudio.Shell.Extensibility/MVVM/` and `Documents/`:** + +- Purpose: define feature-facing bases for reactive view models, dockable tools, documents, and conventional view lookup. +- Contains: `ViewModel`, `ToolViewModel`, `DocumentTabViewModel`, location enum, collection helpers, and `ViewLocator`. +- Key files: `src/AvalonStudio.Shell.Extensibility/MVVM/GenericViewModel.cs`, `src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs`, `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs`. + +**`src/AvalonStudio.Utils/Behaviors/`:** + +- Purpose: generic declarative XAML interaction behavior collection. +- Contains: focus, command, modal-window, and pointer-wheel behaviors. +- Key files: `src/AvalonStudio.Utils/Behaviors/OpenCloseWindowBehavior.cs`, `src/AvalonStudio.Utils/Behaviors/CommandBasedBehavior.cs`. + +**`src/ShellExampleApp/`:** + +- Purpose: executable reference host demonstrating shell bootstrap and feature contribution. +- Contains: `App`, `MainWindow`, paired view/view-model classes, sample settings, sample commands, main-menu items, and toolbar exports. +- Key files: `src/ShellExampleApp/App.paml.cs`, `src/ShellExampleApp/MainWindow.xaml`, `src/ShellExampleApp/ConsoleViewModel.cs`, `src/ShellExampleApp/Commands/ThemeCommands.cs`. + +**`build/`:** + +- Purpose: Nuke build orchestration, package/version metadata, and CI helpers. +- Contains: C# build targets, shared version props, and build project files. +- Key files: `build/Build.cs`, `build/BuildParameters.cs`, `build/SharedVersion.props`, `build/_build.csproj`. + +**`Numerge/`:** + +- Purpose: separate NuGet package-merging utility included in the solution. +- Contains: merger library and console project. +- Key files: `Numerge/Numerge/NugetPackageMerger.cs`, `Numerge/Numerge.Console/Program.cs`. + +## Key File Locations + +**Entry Points:** + +- `src/ShellExampleApp/App.paml.cs`: executable `Main`, Avalonia builder, and sample application initialization. +- `src/AvalonStudio.Shell/Shell.cs`: reusable `StartShellApp` extension method for host applications. +- `src/AvalonStudio.Shell.sln`: groups the product, build, and Numerge projects. +- `build/Build.cs`: Nuke build target definition. + +**Configuration:** + +- `src/Directory.Build.props`: source-wide language and Avalonia build properties. +- `src/Versions.props`: centrally pinned package versions. +- `src/Directory.Build.targets`: source-wide MSBuild targets. +- `src/Packages.targets`: package-related build imports. +- `azure-pipelines.yml`: CI pipeline definition. +- `src/AvalonStudio.Shell/app.config`: legacy assembly binding redirects distributed with the shell project. + +**Core Logic:** + +- `src/AvalonStudio.Shell/CompositionRoot.cs`: MEF composition and external extension assembly loading. +- `src/AvalonStudio.Shell/ShellViewModel.cs`: document/perspective state, layout initialization, command bindings, and feature activation. +- `src/AvalonStudio.Shell/AvalonStudioPerspective.cs`: tool dock creation and selected-tool behavior. +- `src/AvalonStudio.Shell/DockExtensions.cs`: model-to-dockable adapter. +- `src/AvalonStudio.Shell.Extensibility/IoC.cs`: global MEF resolution facade. + +**Testing:** + +- Not detected: no dedicated test project or `*.test.*` / `*.spec.*` files are present under `src/`. + +## Naming Conventions + +**Files:** + +- Use PascalCase file names matching their primary C# type: `ShellViewModel.cs`, `CommandService.cs`, and `DefaultLayoutFactory.cs`. +- Pair Avalonia views with code-behind using the same basename: `MainWindow.xaml` with `MainWindow.xaml.cs`, `StatusBar.paml` with `StatusBar.paml.cs`. +- Use `*ViewModel.cs` for view-state classes and a matching `*View.xaml` or `*View.paml` control so `ViewLocator` can discover it: `src/ShellExampleApp/ConsoleViewModel.cs` and `src/ShellExampleApp/ConsoleView.xaml`. +- Use `Generic*.cs` for generic MVVM bases and the shorter conventional name for the non-generic convenience subclass: `src/AvalonStudio.Shell.Extensibility/Documents/GenericDocumentTabViewModel.cs` and `src/AvalonStudio.Shell.Extensibility/Documents/DocumentTabViewModel.cs`. +- Use `*Attribute.cs` for MEF metadata attributes and `*Metadata.cs` for their metadata objects: `src/AvalonStudio.Shell.Extensibility/Commands/ExportCommandDefinitionAttribute.cs`, `src/AvalonStudio.Shell.Extensibility/Commands/CommandDefinitionMetadata.cs`. + +**Directories:** + +- Group reusable contracts by domain under `src/AvalonStudio.Shell.Extensibility/` rather than by caller: `Commands/`, `Menus/`, `Toolbars/`, `Settings/`, `Shell/`, `Theme/`, and `Platforms/`. +- Group shell implementation by UI/runtime concern under `src/AvalonStudio.Shell/`: `Controls/`, `Docking/`, `Themes/`, `Styles/`, `Converters/`, and `Behaviors/`. +- Put application-level UI contributions in `src/ShellExampleApp/Commands/`, `src/ShellExampleApp/MainMenu/`, and `src/ShellExampleApp/Toolbars/`. + +## Where to Add New Code + +**New Plugin-Facing Feature Contract:** + +- Public interface, base class, export attribute, or metadata: add it to the closest domain folder in `src/AvalonStudio.Shell.Extensibility/`. +- Shell-specific realization: add it to the closest runtime folder in `src/AvalonStudio.Shell/`. +- Do not make the extensibility project reference the shell project; maintain the direction declared in `src/AvalonStudio.Shell.sln` and the `.csproj` project references. + +**New Document Feature:** + +- Primary code: subclass `DocumentTabViewModel` in the owning application/extension feature directory, as in `src/ShellExampleApp/DocumentViewModel.cs`. +- View: create a matching `*View.xaml` plus `*View.xaml.cs` in the same namespace/directory, as in `src/ShellExampleApp/DocumentView.xaml`. +- Open/select: call `IShell.AddOrSelectDocument` through `src/AvalonStudio.Shell.Extensibility/Shell/IShellExtensions.cs`. + +**New Tool Pane:** + +- Primary code: subclass `ToolViewModel`, set `DefaultLocation`, implement `IActivatableExtension` when it should appear at startup, and export it with `[ExportToolControl]` in the owning application/extension project. +- Example: `src/ShellExampleApp/SolutionExplorerViewModel.cs`. +- View: co-locate a parameterless matching `SolutionExplorerView.xaml` / `.xaml.cs` type for conventional resolution. + +**New Command/Menu/Toolbar Contribution:** + +- Command definition: add a MEF-composed provider under the host/extension's `Commands/` directory and use `ExportCommandDefinitionAttribute`; see `src/ShellExampleApp/Commands/ThemeCommands.cs`. +- Main menu item: add an attributed MEF provider under `MainMenu/`; see `src/ShellExampleApp/MainMenu/ThemeMainMenuItem.cs`. +- Toolbar: add its declarative export under `Toolbars/`; see `src/ShellExampleApp/Toolbars/StandardToolbar.cs`. + +**New Shell Control or Shared Behavior:** + +- Shell-specific custom control: use `src/AvalonStudio.Shell/Controls/` with paired `.paml` and `.paml.cs` files when it has a template/style. +- Contract-layer reusable control: use `src/AvalonStudio.Shell.Extensibility/Controls/`. +- Framework-agnostic shell behavior: use `src/AvalonStudio.Utils/Behaviors/`. + +**Utilities:** + +- Shell-only helper: place next to its shell domain in `src/AvalonStudio.Shell/` or the existing `Docking/` / `Converters/` folders. +- Plugin-safe helper: place under `src/AvalonStudio.Shell.Extensibility/Utils/` only when it belongs to the public extensibility API. +- Generic Avalonia interaction helper: place under `src/AvalonStudio.Utils/Behaviors/`. + +## Special Directories + +**`src/AvalonStudio.Shell/Assets/`:** + +- Purpose: packaged fonts and image resources consumed by shell XAML and themes. +- Generated: No. +- Committed: Yes. + +**`src/AvalonStudio.Shell/Themes/`, `Styles/`, and `Icons/`:** + +- Purpose: Avalonia resource dictionaries included by the sample app's `App.paml`. +- Generated: No. +- Committed: Yes. + +**`src/**/bin/` and `src/**/obj/`:** + +- Purpose: MSBuild output and intermediates. +- Generated: Yes. +- Committed: No; exclude these paths from source changes and exploration. + +**`src/.vs/`:** + +- Purpose: Visual Studio workspace caches and user-local indexes. +- Generated: Yes. +- Committed: No. + +**`.nuke/`:** + +- Purpose: Nuke build parameter schema and generated build metadata. +- Generated: Mixed; treat files as build tooling metadata. +- Committed: Yes. + +**`Numerge/`:** + +- Purpose: separately versioned package-merge code nested with its own repository metadata. +- Generated: No. +- Committed: Yes at the superproject level; preserve its internal `.git` metadata and do not treat it as normal `src/` product code. + +--- + +*Structure analysis: 2026-08-28* diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..ccc160e --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,117 @@ +# Testing Patterns + +**Analysis Date:** 2026-08-28 + +## Test Framework + +**Runner:** +- Not detected. No test project, test SDK package, runner configuration, or `*.test.*` / `*.spec.*` source is present in the repository. +- The production solution at `src/AvalonStudio.Shell.sln` contains `AvalonStudio.Shell.Extensibility`, `AvalonStudio.Shell`, `AvalonStudio.Utils`, `ShellExampleApp`, `_build`, and Numerge; it contains no test project. `Numerge/Numerge.sln` likewise contains only the Numerge library and console project. +- Config: Not applicable; no `xunit.runner.json`, `nunit`, MSTest, NUnit, xUnit, or Playwright configuration file is detected. + +**Assertion Library:** +- Not detected. No assertions or test-framework imports are present in tracked C# source. + +**Run Commands:** +```powershell +# No test command is configured. +# `azure-pipelines.yml` invokes `dotnet run --project build/_build.csproj --configuration Release -- --target CiAzureWindows`. +# That Nuke target builds and packs; `build/Build.cs` defines no test target or DotNetTest invocation. +``` + +## Test File Organization + +**Location:** +- Not applicable. There is no `tests/` directory despite `TestsDirectory => RootDirectory / "tests"` being declared in `build/Build.cs`. + +**Naming:** +- Not established. Do not infer a `*.Tests` project naming convention from this repository. + +**Structure:** +```text +AvalonStudio.Shell/ +├── src/ # Production projects only +├── Numerge/ # Package-merger library and console projects +└── build/ # Nuke build project; no test target +``` + +## Test Structure + +**Suite Organization:** +```csharp +// Not applicable: no test classes, fixtures, or suites are present. +// Production verification currently occurs by building the solutions through `build/Build.cs`. +``` + +**Patterns:** +- Setup pattern: Not detected. +- Teardown pattern: Not detected. +- Assertion pattern: Not detected. +- The codebase relies on framework integration points that need isolation if tests are introduced: MEF composition in `src/AvalonStudio.Shell/CompositionRoot.cs`, the static IoC host in `src/AvalonStudio.Shell.Extensibility/IoC.cs`, Avalonia UI-thread dispatch in `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs`, and ReactiveUI notifications in `src/AvalonStudio.Shell/Controls/StatusBarViewModel.cs`. + +## Mocking + +**Framework:** +- Not detected. No mocking library package or mock calls are present. + +**Patterns:** +```csharp +// Not applicable: no mocks, fakes, or stubs are defined in this repository. +``` + +**What to Mock:** +- No repository-established rule exists. If a test suite is added, isolate the MEF/extension boundary consumed by `src/AvalonStudio.Shell.Extensibility/Commands/CommandService.cs` and `src/AvalonStudio.Shell.Extensibility/MainMenu/MainMenuService.cs` rather than requiring installed extensions. +- For merge logic, `Numerge/Numerge/NugetPackageMerger.cs` already accepts `INumergeLogger`; supply a test logger through that interface instead of intercepting console output. + +**What NOT to Mock:** +- No repository-established rule exists. Preserve actual pure collection and model behavior when testing `src/AvalonStudio.Shell.Extensibility/Menus/MenuPath.cs`, `src/AvalonStudio.Shell.Extensibility/GlobalSettings/Mapper.cs`, and similar deterministic classes. + +## Fixtures and Factories + +**Test Data:** +```csharp +// Not applicable: no fixtures, builders, factories, or test-data directories exist. +``` + +**Location:** +- Not applicable. No fixture location is established. + +## Coverage + +**Requirements:** No coverage target or coverage collection configuration is detected in `azure-pipelines.yml`, `build/Build.cs`, the solution files, or project files. + +**View Coverage:** +```powershell +# Not configured. No coverage collector or reporting command exists. +``` + +## Test Types + +**Unit Tests:** +- Not used. No unit-test sources or unit-test project are detected. + +**Integration Tests:** +- Not used. The CI workflow in `azure-pipelines.yml` invokes the `CiAzureWindows` target in `build/Build.cs`, which compiles and packages the project but does not execute a test command. + +**E2E Tests:** +- Not used. No UI automation framework or E2E configuration is detected for the Avalonia example app in `src/ShellExampleApp/`. + +## Common Patterns + +**Async Testing:** +```csharp +// Not applicable: no async test method exists. +// Production async UI work appears in `src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs` +// and UI-dispatched collection operations in `src/AvalonStudio.Shell.Extensibility/MVVM/BindableCollection.cs`. +``` + +**Error Testing:** +```csharp +// Not applicable: no exception or failure-result assertions exist. +// `Numerge/Numerge/NugetPackageMerger.cs` returns false for MergeAbortedException; +// `src/AvalonStudio.Shell/CompositionRoot.cs` logs and continues after a component load failure. +``` + +--- + +*Testing analysis: 2026-08-28* diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 908c2d9..ecce755 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,18 +13,23 @@ pool: vmImage: 'windows-latest' steps: - - task: CmdLine@2 - displayName: 'Install Nuke' + - task: UseDotNet@2 + displayName: 'Use .NET SDK 8' inputs: - script: | - dotnet tool install --global Nuke.GlobalTool --version 0.12.3 + packageType: 'sdk' + version: '8.0.x' + + - task: UseDotNet@2 + displayName: 'Use .NET SDK 10' + inputs: + packageType: 'sdk' + version: '10.0.x' - task: CmdLine@2 - displayName: 'Run Nuke' + displayName: 'Run build' inputs: script: | - set PATH=%PATH%;%USERPROFILE%\.dotnet\tools - nuke --target CiAzureWindows --configuration Release + dotnet run --project build\_build.csproj --configuration Release -- --target CiAzureWindows - task: PublishBuildArtifacts@1 inputs: diff --git a/build/Build.cs b/build/Build.cs index cd2f688..a263398 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -4,6 +4,7 @@ using Nuke.Common; using Nuke.Common.Execution; using Nuke.Common.Git; +using Nuke.Common.IO; using Nuke.Common.ProjectModel; using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; @@ -76,14 +77,14 @@ void ExecWait(string preamble, string command, string args) .Before(Restore) .Executes(() => { - DeleteDirectories(Parameters.BuildDirs); - EnsureCleanDirectories(Parameters.BuildDirs); - EnsureCleanDirectory(Parameters.ArtifactsDir); - EnsureCleanDirectory(Parameters.NugetIntermediateRoot); - EnsureCleanDirectory(Parameters.NugetRoot); - EnsureCleanDirectory(Parameters.ZipRoot); - EnsureCleanDirectory(Parameters.TestResultsRoot); - EnsureCleanDirectory(OutputDirectory); + foreach (var directory in Parameters.BuildDirs) + directory.DeleteDirectory(); + Parameters.ArtifactsDir.CreateOrCleanDirectory(); + Parameters.NugetIntermediateRoot.CreateOrCleanDirectory(); + Parameters.NugetRoot.CreateOrCleanDirectory(); + Parameters.ZipRoot.CreateOrCleanDirectory(); + Parameters.TestResultsRoot.CreateOrCleanDirectory(); + OutputDirectory.CreateOrCleanDirectory(); }); Target Restore => _ => _ @@ -100,8 +101,8 @@ void ExecWait(string preamble, string command, string args) DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) - .SetAssemblyVersion(GitVersion.GetNormalizedAssemblyVersion()) - .SetFileVersion(GitVersion.GetNormalizedFileVersion()) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) .SetInformationalVersion(GitVersion.InformationalVersion) .EnableNoRestore()); }); @@ -110,10 +111,11 @@ void ExecWait(string preamble, string command, string args) .DependsOn(Compile) .Executes(() => { - EnsureCleanDirectory(Parameters.NugetIntermediateRoot); + Parameters.NugetIntermediateRoot.CreateOrCleanDirectory(); - DotNetPack(Solution, x => - x.SetConfiguration(Configuration) + DotNetPack(x => x + .SetProject(Solution) + .SetConfiguration(Configuration) .SetOutputDirectory(Parameters.NugetIntermediateRoot) .AddProperty("PackageVersion", Parameters.Version)); }); @@ -125,7 +127,7 @@ void ExecWait(string preamble, string command, string args) { var logger = new NumergeNukeLogger(); var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "build" / "numerge.config"); - EnsureCleanDirectory(Parameters.NugetRoot); + Parameters.NugetRoot.CreateOrCleanDirectory(); if (!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, logger)) throw new Exception("Package merge failed"); diff --git a/build/BuildParameters.cs b/build/BuildParameters.cs index d050856..0c0d455 100644 --- a/build/BuildParameters.cs +++ b/build/BuildParameters.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using System.Xml.Linq; using Nuke.Common; -using Nuke.Common.BuildServers; +using Nuke.Common.CI.AzurePipelines; using Nuke.Common.Execution; using Nuke.Common.IO; using static Nuke.Common.IO.FileSystemTasks; @@ -53,7 +53,7 @@ public class BuildParameters public AbsolutePath BinRoot { get; } public AbsolutePath TestResultsRoot { get; } public string DirSuffix { get; } - public List BuildDirs { get; } + public List BuildDirs { get; } public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } @@ -75,19 +75,18 @@ public BuildParameters(Build b) MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = Host == HostType.Console; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.TeamServices || - Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; + IsRunningOnAzure = AzurePipelines.Instance is not null; + IsLocalBuild = !IsRunningOnAzure; if (IsRunningOnAzure) { - RepositoryName = TeamServices.Instance.RepositoryUri; - RepositoryBranch = TeamServices.Instance.SourceBranch; - IsPullRequest = TeamServices.Instance.PullRequestId.HasValue; - IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri); + RepositoryName = AzurePipelines.Instance.RepositoryUri; + RepositoryBranch = AzurePipelines.Instance.SourceBranch; + IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue; + IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri); } IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, @@ -122,7 +121,9 @@ public BuildParameters(Build b) ZipRoot = ArtifactsDir / "zip"; BinRoot = ArtifactsDir / "bin"; TestResultsRoot = ArtifactsDir / "test-results"; - BuildDirs = GlobDirectories(RootDirectory, "**bin").Concat(GlobDirectories(RootDirectory, "**obj")).ToList(); + BuildDirs = RootDirectory.GlobDirectories("**/bin") + .Concat(RootDirectory.GlobDirectories("**/obj")) + .ToList(); DirSuffix = Configuration; FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); diff --git a/build/Shims.cs b/build/Shims.cs index 461d617..0c472e5 100644 --- a/build/Shims.cs +++ b/build/Shims.cs @@ -19,9 +19,9 @@ static void Information(string info, params object[] args) Logger.Info(info, args); } - private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); + private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable()); - private void Zip(PathConstruction.AbsolutePath target, IEnumerable paths) + private void Zip(AbsolutePath target, IEnumerable paths) { var targetPath = target.ToString(); bool finished = false, atLeastOneFileAdded = false; diff --git a/build/_build.csproj b/build/_build.csproj index 4988ad3..33038f7 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -2,7 +2,8 @@ Exe - netcoreapp2.0 + net8.0 + true false False @@ -10,8 +11,11 @@ - - + + + + + @@ -22,7 +26,7 @@ - + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings index 96e392e..28494fb 100644 --- a/build/_build.csproj.DotSettings +++ b/build/_build.csproj.DotSettings @@ -1,4 +1,4 @@ - + False Implicit Implicit @@ -13,11 +13,15 @@ False <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> True True True True + True True True True - True + True + True diff --git a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj index 8746b64..0e8f0a4 100644 --- a/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj +++ b/src/AvalonStudio.Shell.Extensibility/AvalonStudio.Shell.Extensibility.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0;net10.0 @@ -10,17 +10,15 @@ - - - - + - + + - + diff --git a/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs b/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs index ea90886..23de599 100644 --- a/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs +++ b/src/AvalonStudio.Shell.Extensibility/Behaviors/AttachNativeMenuBehavior.cs @@ -33,7 +33,7 @@ protected override void OnAttached() { if (Menu != null) { - if (AssociatedObject.GetVisualRoot() is TopLevel tl) + if (TopLevel.GetTopLevel(AssociatedObject) is TopLevel tl) { NativeMenu.SetMenu(tl, Menu); } diff --git a/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs b/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs index 40a766a..6132ee4 100644 --- a/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs +++ b/src/AvalonStudio.Shell.Extensibility/Behaviors/HideWhenNativeMenuExportedBehavior.cs @@ -17,7 +17,7 @@ protected override void OnAttached() .Take(1) .Subscribe(x => { - if (AssociatedObject.GetVisualRoot() is TopLevel tl) + if (TopLevel.GetTopLevel(AssociatedObject) is TopLevel tl) { if (NativeMenu.GetIsNativeMenuExported(tl)) { diff --git a/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs b/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs index 08a9637..dd149e1 100644 --- a/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs +++ b/src/AvalonStudio.Shell.Extensibility/Commands/CommandIconService.cs @@ -16,7 +16,7 @@ public DrawingGroup GetCompletionKindImage(string icon) { if (!_cache.TryGetValue(icon, out var image)) { - if (Application.Current.Styles.TryGetResource(icon.ToString(), out object resource)) + if (Application.Current.Styles.TryGetResource(icon.ToString(), null, out object resource)) { image = resource as DrawingGroup; _cache.Add(icon, image); diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs index c3a43ac..6225034 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.cs @@ -2,11 +2,6 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using System.Collections.Specialized; -using System.Linq; -using Avalonia.LogicalTree; -using AvalonStudio.Utils; -using Avalonia.Controls.Generators; namespace AvalonStudio.Controls { @@ -33,43 +28,5 @@ public IDataTemplate HeaderTemplate set { SetValue(HeaderTemplateProperty, value); } } - protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - base.ItemsCollectionChanged(sender, e); - } - - protected override void ItemsChanged(AvaloniaPropertyChangedEventArgs e) - { - base.ItemsChanged(e); - - if (Items.Count() > 0) - { - SelectedIndex = 0; - } - } - - /// - /// Selects the content of a tab item. - /// - /// The tab item. - /// The content. - private static object SelectContent(object o) - { - var content = o as IContentControl; - - if (content != null) - { - return content.Content; - } - else - { - return o; - } - } - - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return null; - } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml index 1f0292d..20d56e9 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabControl.xaml @@ -7,10 +7,10 @@ - + @@ -23,7 +23,7 @@ - + diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabItem.cs b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabItem.cs index 6090ad6..c8e1fb6 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabItem.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/DocumentTabItem.cs @@ -51,19 +51,19 @@ private void UpdatePseudoClasses (bool? isFocused, Avalonia.Controls.Dock? dock) } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == IsFocusedProperty) { - UpdatePseudoClasses(change.NewValue.GetValueOrDefault(), null); + UpdatePseudoClasses(change.NewValue is bool value && value, null); } else if (change.Property == DockPanel.DockProperty) { - UpdatePseudoClasses(null, change.NewValue.GetValueOrDefault()); + UpdatePseudoClasses(null, change.NewValue is Avalonia.Controls.Dock dock ? dock : (Avalonia.Controls.Dock?)null); } } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/EditableTextBlock.cs b/src/AvalonStudio.Shell.Extensibility/Controls/EditableTextBlock.cs index 264f7aa..a2dbb22 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/EditableTextBlock.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/EditableTextBlock.cs @@ -53,7 +53,7 @@ public EditableTextBlock() if (!InEditMode) { - var properties = e.GetPointerPoint(this).Properties; + var properties = e.GetCurrentPoint(this).Properties; if (e.ClickCount == 1 && properties.IsLeftButtonPressed && IsFocused) { _editClickTimer.Start(); @@ -71,11 +71,9 @@ public EditableTextBlock() }, RoutingStrategies.Tunnel); } - public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwner( - o => o.Text, - (o, v) => o.Text = v, - defaultBindingMode: BindingMode.TwoWay, - enableDataValidation: true); + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); [Content] public string Text @@ -142,7 +140,6 @@ private void EnterEditMode() { EditText = Text; InEditMode = true; - (VisualRoot as IInputRoot).MouseDevice.Capture(_textBox); _textBox.CaretIndex = Text.Length; _textBox.SelectionStart = 0; _textBox.SelectionEnd = Text.Length; @@ -165,16 +162,15 @@ private void ExitEditMode(bool restore = false) } InEditMode = false; - (VisualRoot as IInputRoot).MouseDevice.Capture(null); } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == InEditModeProperty) { - PseudoClasses.Set(":editing", change.NewValue.GetValueOrDefault()); + PseudoClasses.Set(":editing", change.NewValue is bool value && value); } } } diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/StyledText.cs b/src/AvalonStudio.Shell.Extensibility/Controls/StyledText.cs index 974156d..7132ab2 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/StyledText.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/StyledText.cs @@ -4,6 +4,25 @@ namespace AvalonStudio.Controls { + /// + /// Describes a brush applied to a range of text. Avalonia 11 removed the + /// former FormattedTextStyleSpan type; this lightweight value keeps + /// the StyledText API source-compatible while formatting remains optional. + /// + public sealed class FormattedTextStyleSpan + { + public FormattedTextStyleSpan(int start, int length, IBrush foreground) + { + Start = start; + Length = length; + Foreground = foreground; + } + + public int Start { get; } + public int Length { get; } + public IBrush Foreground { get; } + } + public class StyledText { private StringBuilder _builder; diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/ViewLocatorDataTemplate.cs b/src/AvalonStudio.Shell.Extensibility/Controls/ViewLocatorDataTemplate.cs index 0653948..5d81c84 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/ViewLocatorDataTemplate.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/ViewLocatorDataTemplate.cs @@ -8,7 +8,7 @@ public class ViewLocatorDataTemplate : IDataTemplate { public bool SupportsRecycling => false; - public IControl Build(object data) + public Control Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); var type = Type.GetType(name); diff --git a/src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs b/src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs index db794a4..a888b25 100644 --- a/src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs +++ b/src/AvalonStudio.Shell.Extensibility/Controls/ViewModelViewHost.cs @@ -19,12 +19,10 @@ namespace AvalonStudio.Controls public class ViewModelViewHost : TemplatedControl { public static readonly AvaloniaProperty ViewModelProperty = - AvaloniaProperty.Register(nameof(ViewModel), null, false, BindingMode.OneWay, null, - notifying: somethingChanged); + AvaloniaProperty.Register(nameof(ViewModel), defaultBindingMode: BindingMode.OneWay); public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent), null, false, BindingMode.OneWay, null, - notifying: somethingChanged); + AvaloniaProperty.Register(nameof(DefaultContent), defaultBindingMode: BindingMode.OneWay); public static readonly AvaloniaProperty ViewContractObservableProperty = AvaloniaProperty.Register>(nameof(ViewContractObservable), @@ -84,7 +82,7 @@ protected override void OnDataContextEndUpdate() } } - private static void somethingChanged(IAvaloniaObject dependencyObject, bool changed) + private static void somethingChanged(AvaloniaObject dependencyObject, bool changed) { if (changed) { @@ -100,4 +98,4 @@ public object Content set { SetValue(ContentProperty, value); } } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Converters/BitmapValueConverter.cs b/src/AvalonStudio.Shell.Extensibility/Converters/BitmapValueConverter.cs index b27c7f7..938de5a 100644 --- a/src/AvalonStudio.Shell.Extensibility/Converters/BitmapValueConverter.cs +++ b/src/AvalonStudio.Shell.Extensibility/Converters/BitmapValueConverter.cs @@ -14,7 +14,7 @@ public class BitmapValueConverter : IValueConverter public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - if (value is string && targetType == typeof(IBitmap)) + if (value is string) { var uri = new Uri((string)value, UriKind.RelativeOrAbsolute); var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file"; @@ -25,8 +25,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn return new Bitmap((string)value); default: - var assets = AvaloniaLocator.Current.GetService(); - return new Bitmap(assets.Open(uri)); + return new Bitmap(AssetLoader.Open(uri)); } } diff --git a/src/AvalonStudio.Shell.Extensibility/Converters/NativeMenuConverter.cs b/src/AvalonStudio.Shell.Extensibility/Converters/NativeMenuConverter.cs index 39ead1e..8902b9d 100644 --- a/src/AvalonStudio.Shell.Extensibility/Converters/NativeMenuConverter.cs +++ b/src/AvalonStudio.Shell.Extensibility/Converters/NativeMenuConverter.cs @@ -24,11 +24,11 @@ private IList GetNativeItems (IEnumerable ite { if (menu != null) { - menu.Add(new NativeMenuItemSeperator()); + menu.Add(new NativeMenuItemSeparator()); } else { - result.Add(new NativeMenuItemSeperator()); + result.Add(new NativeMenuItemSeparator()); } } else diff --git a/src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs b/src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs index 4263caa..5dae6d1 100644 --- a/src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs +++ b/src/AvalonStudio.Shell.Extensibility/Dialogs/ModalDialogViewModelBase.cs @@ -25,7 +25,7 @@ public ModalDialogViewModelBase(string title, bool okayButton = true, bool cance isVisible = false; this.title = title; - CancelCommand = ReactiveCommand.Create(() => Close(false)); + CancelCommand = ReactiveCommand.Create(_ => { Close(false); return Unit.Default; }); } public bool CancelButtonVisible diff --git a/src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs b/src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs index 1c6f65e..7a7445e 100644 --- a/src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs +++ b/src/AvalonStudio.Shell.Extensibility/MVVM/ViewLocator.cs @@ -6,7 +6,7 @@ namespace AvalonStudio.MVVM { public class ViewLocator { - public static IControl Build(object data) + public static Control Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); @@ -40,4 +40,4 @@ public static IControl Build(object data) return new TextBlock { Text = $"View Locator Error: Unable to find type {name}" }; } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml index 81f8b46..bb396f8 100644 --- a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml +++ b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:AvalonStudio.Extensibility.Converters;assembly=AvalonStudio.Shell.Extensibility" xmlns:beh="clr-namespace:AvalonStudio.Shell.Extensibility.Behaviors;assembly=AvalonStudio.Shell.Extensibility" - xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" + xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Xaml.Behaviors.Interactivity" xmlns:mod="clr-namespace:AvalonStudio.Menus.Models;assembly=AvalonStudio.Shell.Extensibility" xmlns:vmod="clr-namespace:AvalonStudio.Menus.ViewModels;assembly=AvalonStudio.Shell.Extensibility"> @@ -19,7 +19,7 @@ - + @@ -30,17 +30,10 @@ - \ No newline at end of file + diff --git a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs index 52e05c4..3ba2e99 100644 --- a/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs +++ b/src/AvalonStudio.Shell.Extensibility/MainMenu/Views/MainMenuView.paml.cs @@ -3,7 +3,7 @@ namespace AvalonStudio.Shell.Extensibility.MainMenu.Views { - public class MainMenuView : UserControl + public partial class MainMenuView : UserControl { public MainMenuView() { @@ -15,4 +15,4 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } } -} \ No newline at end of file +} diff --git a/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs b/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs index 2f7c1b3..4b6f60f 100644 --- a/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs +++ b/src/AvalonStudio.Shell.Extensibility/Shell/IShell.cs @@ -2,7 +2,7 @@ using AvalonStudio.Documents; using AvalonStudio.Extensibility.Dialogs; using AvalonStudio.MVVM; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; using System.Collections.Generic; @@ -45,6 +45,6 @@ public interface IShell IReadOnlyList Documents { get; } - IPanel Overlay { get; } + Panel Overlay { get; } } } diff --git a/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs b/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs index c94682d..7609be6 100644 --- a/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs +++ b/src/AvalonStudio.Shell.Extensibility/Utils/IEnumerableUtils.cs @@ -50,7 +50,7 @@ public static int Count(this IEnumerable items, Func predicate) public static int IndexOf(this IEnumerable items, object item) { - Contract.Requires(items != null); + if (items == null) throw new ArgumentNullException(nameof(items)); var list = items as IList; @@ -78,7 +78,7 @@ public static int IndexOf(this IEnumerable items, object item) public static object ElementAt(this IEnumerable items, int index) { - Contract.Requires(items != null); + if (items == null) throw new ArgumentNullException(nameof(items)); var list = items as IList; diff --git a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj index 647b2b4..8ee7b4b 100644 --- a/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj +++ b/src/AvalonStudio.Shell/AvalonStudio.Shell.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0;net10.0 @@ -11,21 +11,16 @@ - - - - - - + + - - - + - + + diff --git a/src/AvalonStudio.Shell/AvalonStudioPerspective.cs b/src/AvalonStudio.Shell/AvalonStudioPerspective.cs index 1b73134..d66805a 100644 --- a/src/AvalonStudio.Shell/AvalonStudioPerspective.cs +++ b/src/AvalonStudio.Shell/AvalonStudioPerspective.cs @@ -1,6 +1,6 @@ using AvalonStudio.Extensibility; using AvalonStudio.MVVM; -using Dock.Model; +using Dock.Model.Core; using Dock.Model.Controls; using ReactiveUI; using System; diff --git a/src/AvalonStudio.Shell/Controls/MetroWindow.cs b/src/AvalonStudio.Shell/Controls/MetroWindow.cs index 454e5b7..968fdff 100644 --- a/src/AvalonStudio.Shell/Controls/MetroWindow.cs +++ b/src/AvalonStudio.Shell/Controls/MetroWindow.cs @@ -5,7 +5,6 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Media; -using Avalonia.Styling; using AvalonStudio.Extensibility.Theme; using AvalonStudio.Extensibility.Utils; using System; @@ -13,7 +12,7 @@ namespace AvalonStudio.Shell.Controls { - public class MetroWindow : Window, IStyleable + public class MetroWindow : Window { public enum ClassLongIndex : int { @@ -65,16 +64,26 @@ public MetroWindow() if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { // do this in code or we get a delay in osx. - HasSystemDecorations = false; ClientDecorations = true; + // The template supplies its own title bar and window controls. Keep + // the platform title bar hidden so Windows does not render a second + // minimise/maximise/close button set above it. + WindowDecorations = Avalonia.Controls.WindowDecorations.None; + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var classes = (int)GetClassLongPtr(this.PlatformImpl.Handle.Handle, (int)ClassLongIndex.GCL_STYLE); + // Avalonia 11+ exposes the native handle through TopLevel rather + // than PlatformImpl.Handle. + var nativeHandle = TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + if (nativeHandle != IntPtr.Zero) + { + var classes = (int)GetClassLongPtr(nativeHandle, (int)ClassLongIndex.GCL_STYLE); - classes |= (int)0x00020000; + classes |= (int)0x00020000; - SetClassLong(this.PlatformImpl.Handle.Handle, ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + SetClassLong(nativeHandle, ClassLongIndex.GCL_STYLE, new IntPtr(classes)); + } } } else @@ -84,8 +93,6 @@ public MetroWindow() if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - HasSystemDecorations = true; - // This will need implementing properly once this is supported by avalonia itself. //var color = (ColorTheme.CurrentTheme.Background as SolidColorBrush).Color; //(PlatformImpl as Avalonia.Native.WindowImpl).SetTitleBarColor(color); @@ -106,8 +113,6 @@ public MetroWindow() private Grid _leftVerticalGrip; private Button _minimiseButton; - private bool _mouseDown; - private Point _mouseDownPosition; private Button _restoreButton; private Path _restoreButtonPanelPath; private Grid _rightVerticalGrip; @@ -129,7 +134,7 @@ public Control TitleBarContent set { SetValue(TitleBarContentProperty, value); } } - Type IStyleable.StyleKey => typeof(MetroWindow); + protected override Type StyleKeyOverride => typeof(MetroWindow); protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -167,26 +172,17 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) } else if (_titleBar.IsPointerOver) { - _mouseDown = true; - _mouseDownPosition = e.GetPosition(this); - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { BeginMoveDrag(e); - _mouseDown = false; } } - else - { - _mouseDown = false; - } base.OnPointerPressed(e); } protected override void OnPointerReleased(PointerReleasedEventArgs e) { - _mouseDown = false; base.OnPointerReleased(e); } @@ -204,13 +200,13 @@ private void ToggleWindowState() } } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if(change.Property == WindowStateProperty) { - PseudoClasses.Set(":maximised", change.NewValue.HasValue && change.NewValue.GetValueOrDefault() == WindowState.Maximized); + PseudoClasses.Set(":maximised", change.NewValue is WindowState state && state == WindowState.Maximized); } } diff --git a/src/AvalonStudio.Shell/Controls/MetroWindow.paml b/src/AvalonStudio.Shell/Controls/MetroWindow.paml index d7983d8..b463b0b 100644 --- a/src/AvalonStudio.Shell/Controls/MetroWindow.paml +++ b/src/AvalonStudio.Shell/Controls/MetroWindow.paml @@ -28,11 +28,11 @@ -