From 6325409d25225346b1d398e5544021dae803b8bf Mon Sep 17 00:00:00 2001 From: eelke Date: Sat, 30 Aug 2025 19:41:10 +0200 Subject: [PATCH] Expiriments with AvaloniaEdit and tracking document changes --- .ai-guidelines.md | 51 ++++++ Directory.Packages.props | 30 ++-- ROADMAP.md | 12 ++ pgLabII.Android/Icon.png | Bin 14349 -> 0 bytes pgLabII.Android/MainActivity.cs | 23 --- .../Properties/AndroidManifest.xml | 5 - pgLabII.Android/Resources/AboutResources.txt | 44 ----- .../drawable-night-v31/avalonia_anim.xml | 66 ------- .../Resources/drawable-v31/avalonia_anim.xml | 71 -------- .../Resources/drawable/splash_screen.xml | 13 -- .../Resources/values-night/colors.xml | 4 - .../Resources/values-v31/styles.xml | 21 --- pgLabII.Android/Resources/values/colors.xml | 4 - pgLabII.Android/Resources/values/styles.xml | 12 -- pgLabII.Android/pgLabII.Android.csproj | 28 --- pgLabII.Desktop/pgLabII.Desktop.csproj | 2 +- .../PqConnectionStringParserTests.cs | 3 +- .../PqConnectionStringTokenizerTests.cs | 22 ++- .../ConnectionStrings/Util/ResultAssert.cs | 38 ++++ .../Util/UnitTestTokenizer.cs | 97 ++++++----- .../Util/UnitTestTokenizerTests.cs | 39 +++-- .../pgLabII.PgUtils.Tests.csproj | 1 + .../pgLabII.PgUtils.Tests.csproj.DotSettings | 3 + .../Pq/IPqConnectionStringTokenizer.cs | 12 +- .../ConnectionStrings/Pq/KeywordMapping.cs | 18 -- .../Pq/PqConnectionStringParser.cs | 68 +++----- .../Pq/PqConnectionStringParserException.cs | 16 -- .../Pq/PqConnectionStringTokenizer.cs | 30 ++-- pgLabII.PgUtils/pgLabII.PgUtils.csproj | 1 + pgLabII.iOS/AppDelegate.cs | 25 --- pgLabII.iOS/Entitlements.plist | 5 - pgLabII.iOS/Info.plist | 43 ----- pgLabII.iOS/Main.cs | 14 -- pgLabII.iOS/Resources/LaunchScreen.xib | 43 ----- pgLabII.iOS/pgLabII.iOS.csproj | 16 -- pgLabII/App.axaml | 1 + pgLabII/Contracts/IEditHistoryManager.cs | 13 ++ pgLabII/EditHistoryManager/EditBuffer.cs | 12 ++ .../EditHistoryManager/EditHistoryManager.cs | 163 ++++++++++++++++++ pgLabII/EditHistoryManager/EditOperation.cs | 11 ++ pgLabII/Infra/LocalDb.cs | 34 +++- pgLabII/Model/Document.cs | 15 ++ pgLabII/Model/EditHistoryEntry.cs | 14 ++ pgLabII/ServiceCollectionExtensions.cs | 3 + pgLabII/Services/DocumentSession.cs | 6 + pgLabII/Services/DocumentSessionFactory.cs | 18 ++ pgLabII/ViewModels/CodeEditorViewModel.cs | 10 ++ pgLabII/Views/Controls/CodeEditorView.axaml | 18 ++ .../Views/Controls/CodeEditorView.axaml.cs | 50 ++++++ pgLabII/Views/ServerListView.axaml.cs | 1 + pgLabII/Views/SingleDatabaseWindow.axaml | 13 +- pgLabII/pgLabII.csproj | 5 +- pgLabII/pgLabII.csproj.DotSettings | 3 +- 53 files changed, 643 insertions(+), 627 deletions(-) create mode 100644 .ai-guidelines.md create mode 100644 ROADMAP.md delete mode 100644 pgLabII.Android/Icon.png delete mode 100644 pgLabII.Android/MainActivity.cs delete mode 100644 pgLabII.Android/Properties/AndroidManifest.xml delete mode 100644 pgLabII.Android/Resources/AboutResources.txt delete mode 100644 pgLabII.Android/Resources/drawable-night-v31/avalonia_anim.xml delete mode 100644 pgLabII.Android/Resources/drawable-v31/avalonia_anim.xml delete mode 100644 pgLabII.Android/Resources/drawable/splash_screen.xml delete mode 100644 pgLabII.Android/Resources/values-night/colors.xml delete mode 100644 pgLabII.Android/Resources/values-v31/styles.xml delete mode 100644 pgLabII.Android/Resources/values/colors.xml delete mode 100644 pgLabII.Android/Resources/values/styles.xml delete mode 100644 pgLabII.Android/pgLabII.Android.csproj create mode 100644 pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs create mode 100644 pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj.DotSettings delete mode 100644 pgLabII.PgUtils/ConnectionStrings/Pq/KeywordMapping.cs delete mode 100644 pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParserException.cs delete mode 100644 pgLabII.iOS/AppDelegate.cs delete mode 100644 pgLabII.iOS/Entitlements.plist delete mode 100644 pgLabII.iOS/Info.plist delete mode 100644 pgLabII.iOS/Main.cs delete mode 100644 pgLabII.iOS/Resources/LaunchScreen.xib delete mode 100644 pgLabII.iOS/pgLabII.iOS.csproj create mode 100644 pgLabII/Contracts/IEditHistoryManager.cs create mode 100644 pgLabII/EditHistoryManager/EditBuffer.cs create mode 100644 pgLabII/EditHistoryManager/EditHistoryManager.cs create mode 100644 pgLabII/EditHistoryManager/EditOperation.cs create mode 100644 pgLabII/Model/Document.cs create mode 100644 pgLabII/Model/EditHistoryEntry.cs create mode 100644 pgLabII/Services/DocumentSession.cs create mode 100644 pgLabII/Services/DocumentSessionFactory.cs create mode 100644 pgLabII/ViewModels/CodeEditorViewModel.cs create mode 100644 pgLabII/Views/Controls/CodeEditorView.axaml create mode 100644 pgLabII/Views/Controls/CodeEditorView.axaml.cs diff --git a/.ai-guidelines.md b/.ai-guidelines.md new file mode 100644 index 0000000..82b8c08 --- /dev/null +++ b/.ai-guidelines.md @@ -0,0 +1,51 @@ +# pgLabII AI Assistant Guidelines + +## Project Context +This is a .NET 8/C# 13 Avalonia cross-platform application for document management. + +### Architecture Overview +- **Main Project**: pgLabII (Avalonia UI) +- **Platform Projects**: pgLabII.Desktop +- **Utility Project**: pgLabII.PgUtils +- **Core Components**: DocumentSession, DocumentSessionFactory, LocalDb + +## Coding Standards + +### C# Guidelines +- Use C# 13 features and modern .NET patterns +- Prefer primary constructors for dependency injection +- Use `var` for obvious types, explicit types for clarity +- Implement proper async/await patterns for I/O operations +- Use nullable reference types consistently +- Prefer "target-typed new" +- Prefer "list initializer" over new + +### Avalonia-Specific +- Follow MVVM pattern strictly +- ViewModels should inherit from appropriate base classes +- Use ReactiveUI patterns where applicable +- Make use of annotations to generate bindable properties +- Implement proper data binding +- Consider cross-platform UI constraints + +### Project Patterns +- Services should use dependency injection +- Use the DocumentSession pattern for data operations +- Integrate with LocalDb for persistence +- Implement proper error handling and logging +- Consider performance for document operations +- Use FluentResults.Result for expected errors + +### Architecture Rules +- Keep platform-specific code in respective platform projects +- Shared business logic in main pgLabII project +- Utilities in pgLabII.PgUtils +- Follow clean architecture principles + +## Code Review Focus Areas +1. Memory management for document operations +2. Cross-platform compatibility +3. Proper async patterns +4. Error handling and user feedback +5. Performance considerations for large documents +6. UI responsiveness \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 94ea5dc..6a883b3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,16 +6,19 @@ - - - - - - + + + + + + + - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,19 +26,16 @@ + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..2b2be0c --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,12 @@ +# pgLabII development roadmap + +pgLabII is a developer tool for postgresql databases. Its goal is to allow for querying the database and also editing row data. +It also is going to give some user friendly views of the database structure. + +## UI overview + +There is a window for managing the configuration data of connections to actual database instances. +When a connection is opened a window specific for that database instance is openen. Within this window +the user can open many tabs to query the database and inspects it's tables, indexes etc. + + diff --git a/pgLabII.Android/Icon.png b/pgLabII.Android/Icon.png deleted file mode 100644 index 41a2a618fb02e4cb7f6a15caf572b693bfe1ebb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14349 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 -{ - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .WithInterFont() - .UseReactiveUI(); - } -} diff --git a/pgLabII.Android/Properties/AndroidManifest.xml b/pgLabII.Android/Properties/AndroidManifest.xml deleted file mode 100644 index a77e007..0000000 --- a/pgLabII.Android/Properties/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/pgLabII.Android/Resources/AboutResources.txt b/pgLabII.Android/Resources/AboutResources.txt deleted file mode 100644 index c2bca97..0000000 --- a/pgLabII.Android/Resources/AboutResources.txt +++ /dev/null @@ -1,44 +0,0 @@ -Images, layout descriptions, binary blobs and string dictionaries can be included -in your application as resource files. Various Android APIs are designed to -operate on the resource IDs instead of dealing with images, strings or binary blobs -directly. - -For example, a sample Android app that contains a user interface layout (main.axml), -an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) -would keep its resources in the "Resources" directory of the application: - -Resources/ - drawable/ - icon.png - - layout/ - main.axml - - values/ - strings.xml - -In order to get the build system to recognize Android resources, set the build action to -"AndroidResource". The native Android APIs do not operate directly with filenames, but -instead operate on resource IDs. When you compile an Android application that uses resources, -the build system will package the resources for distribution and generate a class called "R" -(this is an Android convention) that contains the tokens for each one of the resources -included. For example, for the above Resources layout, this is what the R class would expose: - -public class R { - public class drawable { - public const int icon = 0x123; - } - - public class layout { - public const int main = 0x456; - } - - public class strings { - public const int first_string = 0xabc; - public const int second_string = 0xbcd; - } -} - -You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main -to reference the layout/main.axml file, or R.strings.first_string to reference the first -string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/pgLabII.Android/Resources/drawable-night-v31/avalonia_anim.xml b/pgLabII.Android/Resources/drawable-night-v31/avalonia_anim.xml deleted file mode 100644 index dde4b5a..0000000 --- a/pgLabII.Android/Resources/drawable-night-v31/avalonia_anim.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pgLabII.Android/Resources/drawable-v31/avalonia_anim.xml b/pgLabII.Android/Resources/drawable-v31/avalonia_anim.xml deleted file mode 100644 index 94f27d9..0000000 --- a/pgLabII.Android/Resources/drawable-v31/avalonia_anim.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pgLabII.Android/Resources/drawable/splash_screen.xml b/pgLabII.Android/Resources/drawable/splash_screen.xml deleted file mode 100644 index 2e920b4..0000000 --- a/pgLabII.Android/Resources/drawable/splash_screen.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/pgLabII.Android/Resources/values-night/colors.xml b/pgLabII.Android/Resources/values-night/colors.xml deleted file mode 100644 index 3d47b6f..0000000 --- a/pgLabII.Android/Resources/values-night/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #212121 - diff --git a/pgLabII.Android/Resources/values-v31/styles.xml b/pgLabII.Android/Resources/values-v31/styles.xml deleted file mode 100644 index d5ecec4..0000000 --- a/pgLabII.Android/Resources/values-v31/styles.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - diff --git a/pgLabII.Android/Resources/values/colors.xml b/pgLabII.Android/Resources/values/colors.xml deleted file mode 100644 index 59279d5..0000000 --- a/pgLabII.Android/Resources/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - diff --git a/pgLabII.Android/Resources/values/styles.xml b/pgLabII.Android/Resources/values/styles.xml deleted file mode 100644 index 6e534de..0000000 --- a/pgLabII.Android/Resources/values/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - diff --git a/pgLabII.Android/pgLabII.Android.csproj b/pgLabII.Android/pgLabII.Android.csproj deleted file mode 100644 index 562450e..0000000 --- a/pgLabII.Android/pgLabII.Android.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - Exe - net8.0-android - 21 - enable - com.CompanyName.pgLabII - 1 - 1.0 - apk - false - - - - - Resources\drawable\Icon.png - - - - - - - - - - - - diff --git a/pgLabII.Desktop/pgLabII.Desktop.csproj b/pgLabII.Desktop/pgLabII.Desktop.csproj index b997c85..9002229 100644 --- a/pgLabII.Desktop/pgLabII.Desktop.csproj +++ b/pgLabII.Desktop/pgLabII.Desktop.csproj @@ -3,7 +3,7 @@ WinExe - net8.0 + net9.0 enable true AnyCPU;x64 diff --git a/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringParserTests.cs b/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringParserTests.cs index 34a4440..d953ffc 100644 --- a/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringParserTests.cs +++ b/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringParserTests.cs @@ -14,7 +14,8 @@ public class PqConnectionStringParserTests tokenizer .AddString(kw) .AddEquals() - .AddString(val); + .AddString(val) + .AddEof(); } [Fact] diff --git a/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringTokenizerTests.cs b/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringTokenizerTests.cs index 7ec407e..5cc6954 100644 --- a/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringTokenizerTests.cs +++ b/pgLabII.PgUtils.Tests/ConnectionStrings/PqConnectionStringTokenizerTests.cs @@ -1,4 +1,5 @@ using pgLabII.PgUtils.ConnectionStrings; +using pgLabII.PgUtils.Tests.ConnectionStrings.Util; namespace pgLabII.PgUtils.Tests.ConnectionStrings; @@ -11,18 +12,18 @@ public class PqConnectionStringTokenizerTests public void GetKeyword_Success(string input, string expected) { PqConnectionStringTokenizer subject = new(input); - - Assert.Equal(expected, subject.GetKeyword()); + var result = subject.GetKeyword(); + ResultAssert.Success(result, expected); } [Theory] [InlineData("=")] [InlineData("")] [InlineData(" ")] - public void GetKeyword_Throws(string input) + public void GetKeyword_Errors(string input) { PqConnectionStringTokenizer subject = new(input); - Assert.Throws(() => subject.GetKeyword()); + ResultAssert.Failed(subject.GetKeyword()); } [Theory] @@ -36,7 +37,7 @@ public class PqConnectionStringTokenizerTests { PqConnectionStringTokenizer subject = new(input); - Assert.Equal(expected, subject.Eof); + Assert.Equal(expected, subject.IsEof); } [Theory] @@ -46,7 +47,7 @@ public class PqConnectionStringTokenizerTests public void ConsumeEquals_Success(string input) { PqConnectionStringTokenizer subject = new(input); - subject.ConsumeEquals(); + ResultAssert.Success(subject.ConsumeEquals()); } [Theory] @@ -57,7 +58,8 @@ public class PqConnectionStringTokenizerTests public void ConsumeEquals_Throws(string input) { PqConnectionStringTokenizer subject = new(input); - Assert.Throws(() => subject.ConsumeEquals()); + var result = subject.ConsumeEquals(); + ResultAssert.Failed(result); } [Theory] @@ -69,7 +71,8 @@ public class PqConnectionStringTokenizerTests public void GetValue_Success(string input, string expected) { PqConnectionStringTokenizer subject = new(input); - Assert.Equal(expected, subject.GetValue()); + var result = subject.GetValue(); + ResultAssert.Success(result, expected); } [Theory] @@ -80,6 +83,7 @@ public class PqConnectionStringTokenizerTests public void GetValue_Throws(string input) { PqConnectionStringTokenizer subject = new(input); - Assert.Throws(() => subject.GetValue()); + var result = subject.GetValue(); + ResultAssert.Failed(result); } } diff --git a/pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs new file mode 100644 index 0000000..252d675 --- /dev/null +++ b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs @@ -0,0 +1,38 @@ +using FluentResults; + +namespace pgLabII.PgUtils.Tests.ConnectionStrings.Util; + +public static class ResultAssert +{ + public static void Success(Result result) + { + Assert.True(result.IsSuccess); + } + + public static void Success(Result result) + { + Assert.True(result.IsSuccess); + } + + public static void Success(Result result, T expected) + { + Assert.True(result.IsSuccess); + Assert.Equal(expected, result.Value); + } + + public static void Success(Result result, Action assert) + { + Assert.True(result.IsSuccess); + assert(result.Value); + } + + public static void Failed(Result result) + { + Assert.True(result.IsFailed); + } + + public static void Failed(Result result) + { + Assert.True(result.IsFailed); + } +} diff --git a/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizer.cs b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizer.cs index f3922e2..93e6789 100644 --- a/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizer.cs +++ b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizer.cs @@ -1,84 +1,93 @@ -using pgLabII.PgUtils.ConnectionStrings; +using FluentResults; +using pgLabII.PgUtils.ConnectionStrings; +using OneOf; namespace pgLabII.PgUtils.Tests.ConnectionStrings; +using Elem = OneOf; + internal class UnitTestTokenizer : IPqConnectionStringTokenizer { - private readonly struct Elem(PqToken result, object? output) - { - public readonly PqToken Result = result; - public readonly object? Output = output; - } - - private readonly List _tokens = []; + private readonly List _tokens = new(); private int _position = 0; - - public UnitTestTokenizer AddString(string? output) + public UnitTestTokenizer AddString(string output) { - _tokens.Add(new(PqToken.String, output)); + _tokens.Add(output); return this; } - public UnitTestTokenizer AddException(Exception? output) + public UnitTestTokenizer AddError(Error error) { - _tokens.Add(new(PqToken.Exception, output)); + _tokens.Add(error); return this; } public UnitTestTokenizer AddEquals() { - _tokens.Add(new(PqToken.Equals, null)); + _tokens.Add(PqToken.Equals); return this; } + public void AddEof() + { + _tokens.Add(PqToken.Eof); + } + // note we do no whitespace at end tests here - public bool Eof => _position >= _tokens.Count; - - public string GetKeyword() + public bool IsEof { - EnsureNotEof(); - var elem = Consume(); - if (elem.Result == PqToken.String) - return (string)elem.Output!; - - throw new Exception("Unexpected call to GetKeyword"); + get + { + EnsureNotEol(); + return _tokens[_position].IsT0 && _tokens[_position].AsT0 == PqToken.Eof; + } } - - public void ConsumeEquals() + public Result GetKeyword() { - EnsureNotEof(); + EnsureNotEol(); var elem = Consume(); - if (elem.Result == PqToken.Equals) - return; - - throw new Exception("Unexpected call to ConsumeEquals"); + return elem.Match>( + token => throw new Exception("Unexpected call to GetKeyword"), + str => str, + error => Result.Fail(error)); } - public string GetValue() + public Result ConsumeEquals() { - EnsureNotEof(); + EnsureNotEol(); var elem = Consume(); - if (elem.Result == PqToken.String) - return (string)elem.Output!; - throw new Exception("Unexpected call to GetValue"); + return elem.Match( + token => + { + if (token != PqToken.Equals) + throw new Exception("Unexpected call to GetKeyword"); + return Result.Ok(); + }, + str => throw new Exception("Unexpected call to ConsumeEquals"), + error => Result.Fail(error)); + } + + public Result GetValue() + { + EnsureNotEol(); + var elem = Consume(); + return elem.Match>( + token => throw new Exception("Unexpected call to GetValue"), + str => str, + error => Result.Fail(error)); } private Elem Consume() { - var elem = _tokens[_position++]; - if (elem.Result == PqToken.Exception) - { - throw (Exception)elem.Output!; - } - return elem; + return _tokens[_position++]; } - private void EnsureNotEof() + private void EnsureNotEol() { - if (Eof) - throw new Exception("unexpected eof in test, wrong parser call?"); + if (_position >= _tokens.Count) + throw new Exception("unexpected end of list in test, wrong parser call?"); } } diff --git a/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizerTests.cs b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizerTests.cs index 6bcb053..8c85e5f 100644 --- a/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizerTests.cs +++ b/pgLabII.PgUtils.Tests/ConnectionStrings/Util/UnitTestTokenizerTests.cs @@ -1,27 +1,40 @@ -namespace pgLabII.PgUtils.Tests.ConnectionStrings.Util; +using FluentResults; + +namespace pgLabII.PgUtils.Tests.ConnectionStrings.Util; public class UnitTestTokenizerTests { private readonly UnitTestTokenizer _sut = new(); [Fact] - public void Eof_True() + public void IsEof_Throws() { - Assert.True(_sut.Eof); + Assert.Throws(() => + { + bool _ = _sut.IsEof; + }); + } + + [Fact] + public void IsEof_True() + { + _sut.AddEof(); + Assert.True(_sut.IsEof); } [Fact] public void Eof_False() { _sut.AddString("a"); - Assert.False(_sut.Eof); + Assert.False(_sut.IsEof); } [Fact] public void GetKeyword_Success() { _sut.AddString("a"); - Assert.Equal("a", _sut.GetKeyword()); + var result = _sut.GetKeyword(); + ResultAssert.Success(result, "a"); } [Fact] @@ -34,15 +47,17 @@ public class UnitTestTokenizerTests [Fact] public void GetKeyword_SimulatesException() { - _sut.AddException(new ArgumentNullException()); - Assert.Throws(() => _sut.GetKeyword()); + _sut.AddError(new("test")); + var result = _sut.GetKeyword(); + ResultAssert.Failed(result); } [Fact] public void GetValue_Success() { _sut.AddString("a"); - Assert.Equal("a", _sut.GetValue()); + var result = _sut.GetValue(); + ResultAssert.Success(result, "a"); } [Fact] @@ -55,15 +70,17 @@ public class UnitTestTokenizerTests [Fact] public void GetValue_SimulatesException() { - _sut.AddException(new ArgumentNullException()); - Assert.Throws(() => _sut.GetValue()); + _sut.AddError(new("test")); + var result = _sut.GetValue(); + ResultAssert.Failed(result); } [Fact] public void ConsumeEquals_Success() { _sut.AddEquals(); - _sut.ConsumeEquals(); + var result = _sut.ConsumeEquals(); + ResultAssert.Success(result); } [Fact] diff --git a/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj b/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj index a99e569..6c549da 100644 --- a/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj +++ b/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj.DotSettings b/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj.DotSettings new file mode 100644 index 0000000..1a62760 --- /dev/null +++ b/pgLabII.PgUtils.Tests/pgLabII.PgUtils.Tests.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/pgLabII.PgUtils/ConnectionStrings/Pq/IPqConnectionStringTokenizer.cs b/pgLabII.PgUtils/ConnectionStrings/Pq/IPqConnectionStringTokenizer.cs index 98c42a6..346a9a6 100644 --- a/pgLabII.PgUtils/ConnectionStrings/Pq/IPqConnectionStringTokenizer.cs +++ b/pgLabII.PgUtils/ConnectionStrings/Pq/IPqConnectionStringTokenizer.cs @@ -1,9 +1,11 @@ -namespace pgLabII.PgUtils.ConnectionStrings; +using FluentResults; + +namespace pgLabII.PgUtils.ConnectionStrings; public interface IPqConnectionStringTokenizer { - bool Eof { get; } - string GetKeyword(); - void ConsumeEquals(); - string GetValue(); + bool IsEof { get; } + Result GetKeyword(); + Result ConsumeEquals(); + Result GetValue(); } diff --git a/pgLabII.PgUtils/ConnectionStrings/Pq/KeywordMapping.cs b/pgLabII.PgUtils/ConnectionStrings/Pq/KeywordMapping.cs deleted file mode 100644 index 995134e..0000000 --- a/pgLabII.PgUtils/ConnectionStrings/Pq/KeywordMapping.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace pgLabII.PgUtils.ConnectionStrings.Pq; - -enum Keyword -{ - Host, - HostAddr, - Port, - DatabaseName, - UserName, - Password, -} - diff --git a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParser.cs b/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParser.cs index 1cd51a8..cb83332 100644 --- a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParser.cs +++ b/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParser.cs @@ -1,8 +1,12 @@ using System.Collections.ObjectModel; +using FluentResults; using Npgsql; namespace pgLabII.PgUtils.ConnectionStrings; +/// +/// Parser for converting a libpq style connection string into a dictionary of key value pairs +/// public ref struct PqConnectionStringParser { // Note possible keywords @@ -51,64 +55,38 @@ public ref struct PqConnectionStringParser ).Parse(); } - private readonly IPqConnectionStringTokenizer tokenizer; - private readonly Dictionary result = new(); + private readonly IPqConnectionStringTokenizer _tokenizer; + private readonly Dictionary _result = new(); public PqConnectionStringParser(IPqConnectionStringTokenizer tokenizer) { - this.tokenizer = tokenizer; + this._tokenizer = tokenizer; } public IDictionary Parse() { - result.Clear(); + _result.Clear(); - while (!tokenizer.Eof) + while (!_tokenizer.IsEof) ParsePair(); - return result; + return _result; } - private void ParsePair() + private Result ParsePair() { - string kw = tokenizer.GetKeyword(); - tokenizer.ConsumeEquals(); - string v = tokenizer.GetValue(); - result.Add(kw, v); - //switch (kw) - //{ - // case "host": - // case "hostaddr": - // result.Host = v.ToString(); - // break; - // case "port": - // result.Port = int.Parse(v); - // break; - // case "dbname": - // result.Database = v.ToString(); - // break; - // case "user": - // result.Username = v.ToString(); - // break; - // case "password": - // result.Password = v.ToString(); - // break; - // case "connect_timeout": - // result.Timeout = int.Parse(v); - // break; - // case "application_name": - // result.ApplicationName = v.ToString(); - // break; - // case "options": - // result.Options = v.ToString(); - // break; - // case "sslmode": - // result.SslMode = ToSslMode(v); - // break; - // default: - // // Todo what do we do with values we do not support/recognize? - // break; - //} + var kwResult = _tokenizer.GetKeyword(); + if (kwResult.IsFailed) + return kwResult.ToResult(); + var result = _tokenizer.ConsumeEquals(); + if (result.IsFailed) + return result; + var valResult = _tokenizer.GetValue(); + if (valResult.IsFailed) + return valResult.ToResult(); + + _result.Add(kwResult.Value, valResult.Value); + return Result.Ok(); } private SslMode ToSslMode(ReadOnlySpan v) diff --git a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParserException.cs b/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParserException.cs deleted file mode 100644 index 0a80ba6..0000000 --- a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringParserException.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace pgLabII.PgUtils.ConnectionStrings; - -public class PqConnectionStringParserException : Exception -{ - public PqConnectionStringParserException() - { - } - - public PqConnectionStringParserException(string? message) : base(message) - { - } - - public PqConnectionStringParserException(string? message, Exception? innerException) : base(message, innerException) - { - } -} diff --git a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringTokenizer.cs b/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringTokenizer.cs index b8d7107..fd46bb8 100644 --- a/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringTokenizer.cs +++ b/pgLabII.PgUtils/ConnectionStrings/Pq/PqConnectionStringTokenizer.cs @@ -1,4 +1,5 @@ using System.Text; +using FluentResults; using static System.Net.Mime.MediaTypeNames; namespace pgLabII.PgUtils.ConnectionStrings; @@ -8,7 +9,7 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer private readonly string input; private int position = 0; - public bool Eof + public bool IsEof { get { @@ -23,37 +24,38 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer position = 0; } - public string GetKeyword() + public Result GetKeyword() { - if (Eof) - throw new PqConnectionStringParserException($"Unexpected end of file was expecting a keyword at position {position}"); + if (IsEof) + return Result.Fail($"Unexpected end of file was expecting a keyword at position {position}"); return GetString(forKeyword: true); } - public void ConsumeEquals() + public Result ConsumeEquals() { ConsumeWhitespace(); if (position < input.Length && input[position] == '=') { position++; + return Result.Ok(); } else - throw new PqConnectionStringParserException($"Was expecting '=' after keyword at position {position}"); + return Result.Fail($"Was expecting '=' after keyword at position {position}"); } - public string GetValue() + public Result GetValue() { - if (Eof) - throw new PqConnectionStringParserException($"Unexpected end of file was expecting a keyword at position {position}"); + if (IsEof) + return Result.Fail($"Unexpected end of file was expecting a keyword at position {position}"); return GetString(forKeyword: false); } - private string GetString(bool forKeyword) + private Result GetString(bool forKeyword) { if (forKeyword && input[position] == '=') - throw new PqConnectionStringParserException($"Unexpected '=' was expecting keyword at position {position}"); + return Result.Fail($"Unexpected '=' was expecting keyword at position {position}"); if (input[position] == '\'') return ParseQuotedText(); @@ -75,7 +77,7 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer return input.Substring(start, position - start); } - private string ParseQuotedText() + private Result ParseQuotedText() { bool escape = false; StringBuilder sb = new(); @@ -93,7 +95,7 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer escape = false; break; default: - throw new PqConnectionStringParserException($"Invalid escape sequence at position {position}"); + return Result.Fail($"Invalid escape sequence at position {position}"); } } else @@ -113,6 +115,6 @@ public class PqConnectionStringTokenizer : IPqConnectionStringTokenizer } } } - throw new PqConnectionStringParserException($"Missing end quote on value starting at {start}"); + return Result.Fail($"Missing end quote on value starting at {start}"); } } diff --git a/pgLabII.PgUtils/pgLabII.PgUtils.csproj b/pgLabII.PgUtils/pgLabII.PgUtils.csproj index d44f26c..f70bcc6 100644 --- a/pgLabII.PgUtils/pgLabII.PgUtils.csproj +++ b/pgLabII.PgUtils/pgLabII.PgUtils.csproj @@ -8,6 +8,7 @@ + diff --git a/pgLabII.iOS/AppDelegate.cs b/pgLabII.iOS/AppDelegate.cs deleted file mode 100644 index 2e21b1f..0000000 --- a/pgLabII.iOS/AppDelegate.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Foundation; -using UIKit; -using Avalonia; -using Avalonia.Controls; -using Avalonia.iOS; -using Avalonia.Media; -using Avalonia.ReactiveUI; - -namespace pgLabII.iOS; - -// The UIApplicationDelegate for the application. This class is responsible for launching the -// User Interface of the application, as well as listening (and optionally responding) to -// application events from iOS. -[Register("AppDelegate")] -#pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public partial class AppDelegate : AvaloniaAppDelegate -#pragma warning restore CA1711 // Identifiers should not have incorrect suffix -{ - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder) - .WithInterFont() - .UseReactiveUI(); - } -} diff --git a/pgLabII.iOS/Entitlements.plist b/pgLabII.iOS/Entitlements.plist deleted file mode 100644 index 0c67376..0000000 --- a/pgLabII.iOS/Entitlements.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/pgLabII.iOS/Info.plist b/pgLabII.iOS/Info.plist deleted file mode 100644 index 9ef8ec5..0000000 --- a/pgLabII.iOS/Info.plist +++ /dev/null @@ -1,43 +0,0 @@ - - - - - CFBundleDisplayName - pgLabII - CFBundleIdentifier - companyName.pgLabII - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSRequiresIPhoneOS - - MinimumOSVersion - 13.0 - UIDeviceFamily - - 1 - 2 - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/pgLabII.iOS/Main.cs b/pgLabII.iOS/Main.cs deleted file mode 100644 index 01ff99a..0000000 --- a/pgLabII.iOS/Main.cs +++ /dev/null @@ -1,14 +0,0 @@ -using UIKit; - -namespace pgLabII.iOS; - -public class Application -{ - // This is the main entry point of the application. - static void Main(string[] args) - { - // if you want to use a different Application Delegate class from "AppDelegate" - // you can specify it here. - UIApplication.Main(args, null, typeof(AppDelegate)); - } -} diff --git a/pgLabII.iOS/Resources/LaunchScreen.xib b/pgLabII.iOS/Resources/LaunchScreen.xib deleted file mode 100644 index f610a15..0000000 --- a/pgLabII.iOS/Resources/LaunchScreen.xib +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/pgLabII.iOS/pgLabII.iOS.csproj b/pgLabII.iOS/pgLabII.iOS.csproj deleted file mode 100644 index 2e94356..0000000 --- a/pgLabII.iOS/pgLabII.iOS.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - Exe - net8.0-ios - 13.0 - enable - - - - - - - - - - diff --git a/pgLabII/App.axaml b/pgLabII/App.axaml index 9af82b4..ef1df69 100644 --- a/pgLabII/App.axaml +++ b/pgLabII/App.axaml @@ -14,6 +14,7 @@ + \ No newline at end of file diff --git a/pgLabII/Contracts/IEditHistoryManager.cs b/pgLabII/Contracts/IEditHistoryManager.cs new file mode 100644 index 0000000..d93ddf8 --- /dev/null +++ b/pgLabII/Contracts/IEditHistoryManager.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using pgLabII.Model; +using pgLabII.Views.Controls; + +namespace pgLabII; + +public interface IEditHistoryManager +{ + void AddEdit(int offset, string insertedText, string removedText); + void FlushBuffer(); + void SaveToDatabase(); + IReadOnlyList GetHistory(); +} diff --git a/pgLabII/EditHistoryManager/EditBuffer.cs b/pgLabII/EditHistoryManager/EditBuffer.cs new file mode 100644 index 0000000..60e8f54 --- /dev/null +++ b/pgLabII/EditHistoryManager/EditBuffer.cs @@ -0,0 +1,12 @@ +using System; +using System.Text; + +namespace pgLabII; + +public class EditBuffer +{ + public int CurrentOffset { get; set; } + public StringBuilder InsertedText { get; set; } = new(); + public StringBuilder RemovedText { get; set; } = new(); + public DateTime LastEdit { get; set; } +} diff --git a/pgLabII/EditHistoryManager/EditHistoryManager.cs b/pgLabII/EditHistoryManager/EditHistoryManager.cs new file mode 100644 index 0000000..45ba53d --- /dev/null +++ b/pgLabII/EditHistoryManager/EditHistoryManager.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Timers; +using pgLabII.Infra; +using pgLabII.Model; + +namespace pgLabII; + +public class EditHistoryManager : IEditHistoryManager +{ + private readonly LocalDb _db; + private readonly Document _document; + private readonly List _pendingEdits = new(); + private EditBuffer? _currentBuffer; + private const int BufferTimeoutMs = 500; + private readonly Timer _idleFlushTimer; + private readonly object _sync = new object(); + + public EditHistoryManager(LocalDb db, Document document) + { + _db = db; + _document = document; + + _idleFlushTimer = new Timer(BufferTimeoutMs) + { + AutoReset = false, + Enabled = false + }; + _idleFlushTimer.Elapsed += OnIdleFlushTimerElapsed; + } + + public void Dispose() + { + _idleFlushTimer.Elapsed -= OnIdleFlushTimerElapsed; + _idleFlushTimer.Dispose(); + // Final flush on dispose to avoid losing last edits + lock (_sync) + { + FlushBuffer(); + if (_pendingEdits.Count > 0) + { + SaveToDatabase(); + } + } + } + + public void AddEdit(int offset, string insertedText, string removedText) + { + var now = DateTime.UtcNow; + bool isSimpleEdit = (insertedText.Length <= 1 && removedText.Length <= 1); + + if (TryCombineWithBuffer(offset, insertedText, removedText, now, isSimpleEdit)) + { + RestartIdleTimer(); + return; + } + + FlushBuffer(); + CreateNewBuffer(offset, insertedText, removedText, now); + RestartIdleTimer(); + } + + public void FlushBuffer() + { + if (_currentBuffer == null) return; + + var edit = new EditHistoryEntry + { + DocumentId = _document.Id, + Offset = _currentBuffer.CurrentOffset, + InsertedText = _currentBuffer.InsertedText.ToString(), + RemovedText = _currentBuffer.RemovedText.ToString(), + Timestamp = DateTime.UtcNow + }; + + _pendingEdits.Add(edit); + _currentBuffer = null; + + if (_pendingEdits.Count >= 10) + { + SaveToDatabase(); + } + } + + public void SaveToDatabase() + { + _db.EditHistory.AddRange(_pendingEdits); + _db.SaveChanges(); + _pendingEdits.Clear(); + } + + public IReadOnlyList GetHistory() => _pendingEdits.AsReadOnly(); + + private bool TryCombineWithBuffer(int offset, string insertedText, string removedText, DateTime now, bool isSimpleEdit) + { + // Try to combine with current buffer if it's a simple edit + if (_currentBuffer != null && + isSimpleEdit && + (now - _currentBuffer.LastEdit).TotalMilliseconds < BufferTimeoutMs) + { + // For consecutive inserts + if (insertedText.Length == 1 && + offset == _currentBuffer.CurrentOffset + _currentBuffer.InsertedText.Length) + { + _currentBuffer.InsertedText.Append(insertedText); + _currentBuffer.LastEdit = now; + return true; + } + + // For consecutive deletes or backspaces + if (removedText.Length == 1 && + (offset == _currentBuffer.CurrentOffset - 1 || // Backspace + offset == _currentBuffer.CurrentOffset)) // Delete + { + if (offset < _currentBuffer.CurrentOffset) + { + _currentBuffer.RemovedText.Insert(0, removedText); + _currentBuffer.CurrentOffset = offset; + } + else + { + _currentBuffer.RemovedText.Append(removedText); + } + _currentBuffer.LastEdit = now; + return true; + } + } + + return false; + } + + private void CreateNewBuffer(int offset, string insertedText, string removedText, DateTime now) + { + _currentBuffer = new EditBuffer + { + CurrentOffset = offset, + LastEdit = now + }; + _currentBuffer.InsertedText.Append(insertedText); + _currentBuffer.RemovedText.Append(removedText); + } + + private void RestartIdleTimer() + { + _idleFlushTimer.Stop(); + _idleFlushTimer.Interval = BufferTimeoutMs; + _idleFlushTimer.Start(); + } + + private void OnIdleFlushTimerElapsed(object? sender, ElapsedEventArgs e) + { + lock (_sync) + { + // On inactivity, flush any buffered edit and persist remaining pending edits. + FlushBuffer(); + if (_pendingEdits.Count > 0) + { + SaveToDatabase(); + } + } + } + +} diff --git a/pgLabII/EditHistoryManager/EditOperation.cs b/pgLabII/EditHistoryManager/EditOperation.cs new file mode 100644 index 0000000..b0fb10c --- /dev/null +++ b/pgLabII/EditHistoryManager/EditOperation.cs @@ -0,0 +1,11 @@ +using System; + +namespace pgLabII; + +public class EditOperation +{ + public required int Offset { get; set; } + public required string InsertedText { get; set; } + public required string RemovedText { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/pgLabII/Infra/LocalDb.cs b/pgLabII/Infra/LocalDb.cs index cb4c492..7dd09b4 100644 --- a/pgLabII/Infra/LocalDb.cs +++ b/pgLabII/Infra/LocalDb.cs @@ -6,10 +6,12 @@ using pgLabII.Model; namespace pgLabII.Infra; -internal class LocalDb : DbContext +public class LocalDb : DbContext { public DbSet ServerConfigurations => Set(); - + public DbSet Documents => Set(); + public DbSet EditHistory => Set(); + public string DbPath { get; } public LocalDb() @@ -26,9 +28,10 @@ internal class LocalDb : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { - - new ServerConfigurationEntityConfiguration().Configure(modelBuilder.Entity()); + new ServerUserEntityConfiguration().Configure(modelBuilder.Entity()); + new DocumentEntityConfiguration().Configure(modelBuilder.Entity()); + new EditHistoryEntityConfiguration().Configure(modelBuilder.Entity()); } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) @@ -56,3 +59,26 @@ public class ServerUserEntityConfiguration : IEntityTypeConfiguration e.Id); } } + +public class DocumentEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.HasKey(e => e.Id); + b.Property(e => e.OriginalFilename).IsRequired(); + b.Property(e => e.BaseCopyFilename).IsRequired(); + b.HasMany(e => e.EditHistory) + .WithOne(e => e.Document) + .HasForeignKey(e => e.DocumentId); + } +} + +public class EditHistoryEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder b) + { + b.HasKey(e => e.Id); + b.Property(e => e.Timestamp).IsRequired(); + b.HasIndex(e => new { e.DocumentId, e.Timestamp}); + } +} diff --git a/pgLabII/Model/Document.cs b/pgLabII/Model/Document.cs new file mode 100644 index 0000000..01e3f62 --- /dev/null +++ b/pgLabII/Model/Document.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace pgLabII.Model; + +public class Document +{ + public int Id { get; set; } + public required string OriginalFilename { get; set; } + public required string BaseCopyFilename { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } + + public List EditHistory { get; set; } = []; +} diff --git a/pgLabII/Model/EditHistoryEntry.cs b/pgLabII/Model/EditHistoryEntry.cs new file mode 100644 index 0000000..750d2c9 --- /dev/null +++ b/pgLabII/Model/EditHistoryEntry.cs @@ -0,0 +1,14 @@ +using System; + +namespace pgLabII.Model; + +public class EditHistoryEntry +{ + public int Id { get; set; } + public int DocumentId { get; set; } + public Document Document { get; set; } = null!; + public DateTime Timestamp { get; set; } + public int Offset { get; set; } + public string InsertedText { get; set; } = string.Empty; + public string RemovedText { get; set; } = string.Empty; +} diff --git a/pgLabII/ServiceCollectionExtensions.cs b/pgLabII/ServiceCollectionExtensions.cs index aab7efc..babfa67 100644 --- a/pgLabII/ServiceCollectionExtensions.cs +++ b/pgLabII/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using pgLabII.Infra; using pgLabII.ViewModels; using pgLabII.Views; +using pgLabII.Views.Controls; namespace pgLabII; @@ -13,6 +14,8 @@ internal static class ServiceCollectionExtensions collection.AddTransient(); collection.AddTransient(); collection.AddScoped(); + collection.AddTransient(); + collection.AddTransient(); } } diff --git a/pgLabII/Services/DocumentSession.cs b/pgLabII/Services/DocumentSession.cs new file mode 100644 index 0000000..221d479 --- /dev/null +++ b/pgLabII/Services/DocumentSession.cs @@ -0,0 +1,6 @@ +namespace pgLabII.Services; + +public class DocumentSession +{ + +} diff --git a/pgLabII/Services/DocumentSessionFactory.cs b/pgLabII/Services/DocumentSessionFactory.cs new file mode 100644 index 0000000..a16640b --- /dev/null +++ b/pgLabII/Services/DocumentSessionFactory.cs @@ -0,0 +1,18 @@ +using System; +using pgLabII.Infra; + +namespace pgLabII.Services; + +public class DocumentSessionFactory( + LocalDb localDb) +{ + public DocumentSession CreateNew() + { + throw new NotImplementedException(); + } + + public DocumentSession Open(string path) + { + throw new NotImplementedException(); + } +} diff --git a/pgLabII/ViewModels/CodeEditorViewModel.cs b/pgLabII/ViewModels/CodeEditorViewModel.cs new file mode 100644 index 0000000..082233b --- /dev/null +++ b/pgLabII/ViewModels/CodeEditorViewModel.cs @@ -0,0 +1,10 @@ +using AvaloniaEdit.Document; +using ReactiveUI.SourceGenerators; + +namespace pgLabII.ViewModels; + +public partial class CodeEditorViewModel : ViewModelBase +{ + [Reactive] private TextDocument _document = new(); + +} diff --git a/pgLabII/Views/Controls/CodeEditorView.axaml b/pgLabII/Views/Controls/CodeEditorView.axaml new file mode 100644 index 0000000..9e63fa7 --- /dev/null +++ b/pgLabII/Views/Controls/CodeEditorView.axaml @@ -0,0 +1,18 @@ + + + + + - - + + + + + + diff --git a/pgLabII/pgLabII.csproj b/pgLabII/pgLabII.csproj index 5c03e58..f2c36f9 100644 --- a/pgLabII/pgLabII.csproj +++ b/pgLabII/pgLabII.csproj @@ -1,6 +1,6 @@  - net8.0 + net9.0 enable latest true @@ -13,6 +13,7 @@ + @@ -22,6 +23,8 @@ All + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/pgLabII/pgLabII.csproj.DotSettings b/pgLabII/pgLabII.csproj.DotSettings index 127787e..c7563b2 100644 --- a/pgLabII/pgLabII.csproj.DotSettings +++ b/pgLabII/pgLabII.csproj.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + False \ No newline at end of file