diff --git a/OneWare.slnx b/OneWare.slnx
index c86871a8..2ccfc40d 100644
--- a/OneWare.slnx
+++ b/OneWare.slnx
@@ -91,6 +91,7 @@
+
diff --git a/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs b/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs
new file mode 100644
index 00000000..b4380559
--- /dev/null
+++ b/src/OneWare.MarkdownViewer/MarkdownViewerModule.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.DependencyInjection;
+using OneWare.Essentials.Services;
+using OneWare.MarkdownViewer.ViewModels;
+
+namespace OneWare.MarkdownViewer;
+
+public class MarkdownViewerModule : OneWareModuleBase
+{
+ public override void RegisterServices(IServiceCollection services)
+ {
+ services.AddTransient();
+ }
+
+ public override void Initialize(IServiceProvider serviceProvider)
+ {
+ serviceProvider.Resolve()
+ .RegisterDocumentView(".md", ".markdown");
+ }
+}
+
diff --git a/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj b/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj
new file mode 100644
index 00000000..48b97a29
--- /dev/null
+++ b/src/OneWare.MarkdownViewer/OneWare.MarkdownViewer.csproj
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs b/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs
new file mode 100644
index 00000000..361b41d9
--- /dev/null
+++ b/src/OneWare.MarkdownViewer/ViewModels/MarkdownEditViewModel.cs
@@ -0,0 +1,128 @@
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.Logging;
+using OneWare.Core.Services;
+using OneWare.Core.ViewModels.DockViews;
+using OneWare.Essentials.LanguageService;
+using OneWare.Essentials.Models;
+using OneWare.Essentials.Services;
+
+namespace OneWare.MarkdownViewer.ViewModels;
+
+///
+/// An for markdown files that adds a live preview pane.
+/// The editor and the preview can be toggled independently to allow editor-only,
+/// preview-only or split-screen layouts.
+///
+public class MarkdownEditViewModel : EditViewModel
+{
+ private readonly CompositeDisposable _markdownComposite = new();
+
+ private bool _showEditor = true;
+ private bool _showPreview = true;
+ private string? _markdownText;
+
+ public MarkdownEditViewModel(string fullPath, ILogger logger, IFileIconService fileIconService,
+ ISettingsService settingsService, IMainDockService mainDockService, ILanguageManager languageManager,
+ IWindowService windowService, IProjectExplorerService projectExplorerService, IErrorService errorService,
+ BackupService backupService) : base(fullPath, logger, fileIconService, settingsService, mainDockService,
+ languageManager, windowService, projectExplorerService, errorService, backupService)
+ {
+ ToggleEditorCommand = new RelayCommand(() => ShowEditor = !ShowEditor);
+ TogglePreviewCommand = new RelayCommand(() => ShowPreview = !ShowPreview);
+
+ // Keep the preview in sync with the editor content. The document instance can be
+ // replaced when the file is (re)loaded, so we listen to both events.
+ Editor.DocumentChanged += OnEditorChanged;
+
+ Observable.FromEventPattern(
+ h => Editor.TextChanged += h,
+ h => Editor.TextChanged -= h)
+ .Throttle(TimeSpan.FromMilliseconds(250))
+ .Subscribe(_ => UpdateMarkdown())
+ .DisposeWith(_markdownComposite);
+ }
+
+ public RelayCommand ToggleEditorCommand { get; }
+
+ public RelayCommand TogglePreviewCommand { get; }
+
+ public string? MarkdownText
+ {
+ get => _markdownText;
+ private set => SetProperty(ref _markdownText, value);
+ }
+
+ public bool ShowEditor
+ {
+ get => _showEditor;
+ set
+ {
+ // Always keep at least one pane visible.
+ if (!value && !ShowPreview) ShowPreview = true;
+ if (SetProperty(ref _showEditor, value))
+ {
+ OnPropertyChanged(nameof(IsSplitterVisible));
+ OnPropertyChanged(nameof(EditorColumnWidth));
+ }
+ }
+ }
+
+ public bool ShowPreview
+ {
+ get => _showPreview;
+ set
+ {
+ // Always keep at least one pane visible.
+ if (!value && !ShowEditor) ShowEditor = true;
+ if (SetProperty(ref _showPreview, value))
+ {
+ OnPropertyChanged(nameof(IsSplitterVisible));
+ OnPropertyChanged(nameof(PreviewColumnWidth));
+ if (value) UpdateMarkdown();
+ }
+ }
+ }
+
+ public bool IsSplitterVisible => ShowEditor && ShowPreview;
+
+ /// Star width when the editor is visible, collapsed otherwise.
+ public GridLength EditorColumnWidth =>
+ ShowEditor ? new GridLength(1, GridUnitType.Star) : new GridLength(0, GridUnitType.Auto);
+
+ /// Star width when the preview is visible, collapsed otherwise.
+ public GridLength PreviewColumnWidth =>
+ ShowPreview ? new GridLength(1, GridUnitType.Star) : new GridLength(0, GridUnitType.Auto);
+
+ private void OnEditorChanged(object? sender, EventArgs e)
+ {
+ UpdateMarkdown();
+ }
+
+ private void UpdateMarkdown()
+ {
+ if (Dispatcher.UIThread.CheckAccess())
+ MarkdownText = Editor.Document?.Text ?? string.Empty;
+ else
+ Dispatcher.UIThread.Post(() => MarkdownText = Editor.Document?.Text ?? string.Empty);
+ }
+
+ public override bool OnClose()
+ {
+ var result = base.OnClose();
+ if (!result) return false;
+
+ Editor.DocumentChanged -= OnEditorChanged;
+ _markdownComposite.Dispose();
+ return true;
+ }
+}
+
+
+
+
+
+
diff --git a/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml
new file mode 100644
index 00000000..309330d6
--- /dev/null
+++ b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs
new file mode 100644
index 00000000..785eaffa
--- /dev/null
+++ b/src/OneWare.MarkdownViewer/Views/MarkdownEditView.axaml.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace OneWare.MarkdownViewer.Views;
+
+public partial class MarkdownEditView : UserControl
+{
+ public MarkdownEditView()
+ {
+ InitializeComponent();
+ }
+}
+
diff --git a/studio/OneWare.Studio/OneWare.Studio.csproj b/studio/OneWare.Studio/OneWare.Studio.csproj
index accb5e86..9b832698 100644
--- a/studio/OneWare.Studio/OneWare.Studio.csproj
+++ b/studio/OneWare.Studio/OneWare.Studio.csproj
@@ -21,6 +21,7 @@
+
diff --git a/studio/OneWare.Studio/StudioApp.cs b/studio/OneWare.Studio/StudioApp.cs
index 7760944e..a907f80f 100644
--- a/studio/OneWare.Studio/StudioApp.cs
+++ b/studio/OneWare.Studio/StudioApp.cs
@@ -8,6 +8,7 @@
using OneWare.CruviAdapterExtensions;
using OneWare.Essentials.Models;
using OneWare.Essentials.Services;
+using OneWare.MarkdownViewer;
using OneWare.Settings;
using OneWare.Studio.Styles;
using OneWare.UniversalFpgaProjectSystem;
@@ -98,5 +99,6 @@ protected override void ConfigureModuleCatalog(OneWareModuleCatalog moduleCatalo
moduleCatalog.AddModule();
moduleCatalog.AddModule();
moduleCatalog.AddModule();
+ moduleCatalog.AddModule();
}
}
\ No newline at end of file