Expiriments with AvaloniaEdit and tracking document changes
This commit is contained in:
parent
29a141a971
commit
6325409d25
53 changed files with 643 additions and 627 deletions
51
.ai-guidelines.md
Normal file
51
.ai-guidelines.md
Normal file
|
|
@ -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
|
||||
|
|
@ -6,16 +6,19 @@
|
|||
<ItemGroup>
|
||||
<!-- Avalonia packages -->
|
||||
<!-- Important: keep version in sync! -->
|
||||
<PackageVersion Include="Avalonia" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
|
||||
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.4" />
|
||||
<PackageVersion Include="Avalonia.iOS" Version="11.2.1" />
|
||||
<PackageVersion Include="Avalonia.Browser" Version="11.2.1" />
|
||||
<PackageVersion Include="Avalonia.Android" Version="11.2.1" />
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.3" />
|
||||
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.4" />
|
||||
<PackageVersion Include="AvaloniaEdit.TextMate" Version="11.3.0" />
|
||||
<PackageVersion Include="FluentResults" Version="4.0.0" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
@ -23,19 +26,16 @@
|
|||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Npgsql" Version="9.0.3" />
|
||||
<PackageVersion Include="OneOf" Version="3.0.271" />
|
||||
<PackageVersion Include="Pure.DI" Version="2.1.38" />
|
||||
<PackageVersion Include="ReactiveUI.SourceGenerators" Version="2.3.1" />
|
||||
<PackageVersion Include="ReactiveUI.SourceGenerators.Analyzers.CodeFixes" Version="2.3.1" />
|
||||
<PackageVersion Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageVersion>
|
||||
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageVersion>
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
12
ROADMAP.md
Normal file
12
ROADMAP.md
Normal file
|
|
@ -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.
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Avalonia;
|
||||
using Avalonia.Android;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace pgLabII.Android;
|
||||
|
||||
[Activity(
|
||||
Label = "pgLabII.Android",
|
||||
Theme = "@style/MyTheme.NoActionBar",
|
||||
Icon = "@drawable/icon",
|
||||
MainLauncher = true,
|
||||
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
|
||||
public class MainActivity : AvaloniaMainActivity<App>
|
||||
{
|
||||
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
|
||||
{
|
||||
return base.CustomizeAppBuilder(builder)
|
||||
.WithInterFont()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<application android:label="pgLabII" android:icon="@drawable/Icon" />
|
||||
</manifest>
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<group
|
||||
android:name="wrapper"
|
||||
android:translateX="21"
|
||||
android:translateY="21">
|
||||
<group android:name="group">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
|
||||
android:strokeWidth="1"/>
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
|
||||
android:strokeWidth="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:duration="1000"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#161c2d"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:duration="1000"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#f9f9fb"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_2">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:duration="1000"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#f9f9fb"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="128"
|
||||
android:viewportHeight="128">
|
||||
<group
|
||||
android:name="wrapper"
|
||||
android:translateX="21"
|
||||
android:translateY="21">
|
||||
<group android:name="group">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
|
||||
android:fillColor="#00ffffff"
|
||||
android:strokeWidth="1"/>
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
|
||||
android:fillColor="#00ffffff"
|
||||
android:strokeWidth="1"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
|
||||
android:fillColor="#00ffffff"
|
||||
android:strokeWidth="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_2">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:startOffset="100"
|
||||
android:duration="900"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#161c2d"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:duration="500"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#f9f9fb"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:propertyName="fillColor"
|
||||
android:startOffset="100"
|
||||
android:duration="900"
|
||||
android:valueFrom="#00ffffff"
|
||||
android:valueTo="#161c2d"
|
||||
android:valueType="colorType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item>
|
||||
<color android:color="@color/splash_background"/>
|
||||
</item>
|
||||
|
||||
<item android:drawable="@drawable/icon"
|
||||
android:width="120dp"
|
||||
android:height="120dp"
|
||||
android:gravity="center" />
|
||||
|
||||
</layer-list>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="splash_background">#212121</color>
|
||||
</resources>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<resources>
|
||||
|
||||
<style name="MyTheme">
|
||||
</style>
|
||||
|
||||
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
|
||||
<item name="android:windowSplashScreenAnimationDuration">1000</item>
|
||||
<item name="postSplashScreenTheme">@style/MyTheme.Main</item>
|
||||
|
||||
</style>
|
||||
<style name="MyTheme.Main"
|
||||
parent ="MyTheme.NoActionBar">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="splash_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<resources>
|
||||
|
||||
<style name="MyTheme">
|
||||
</style>
|
||||
|
||||
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowBackground">@drawable/splash_screen</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-android</TargetFramework>
|
||||
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationId>com.CompanyName.pgLabII</ApplicationId>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<AndroidPackageFormat>apk</AndroidPackageFormat>
|
||||
<AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Icon.png">
|
||||
<Link>Resources\drawable\Icon.png</Link>
|
||||
</AndroidResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Android" />
|
||||
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\pgLabII\pgLabII.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<OutputType>WinExe</OutputType>
|
||||
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
|
||||
One for Windows with net8.0-windows TFM, one for MacOS with net8.0-macos and one with net8.0 TFM for Linux.-->
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ public class PqConnectionStringParserTests
|
|||
tokenizer
|
||||
.AddString(kw)
|
||||
.AddEquals()
|
||||
.AddString(val);
|
||||
.AddString(val)
|
||||
.AddEof();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -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<PqConnectionStringParserException>(() => 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<PqConnectionStringParserException>(() => 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<PqConnectionStringParserException>(() => subject.GetValue());
|
||||
var result = subject.GetValue();
|
||||
ResultAssert.Failed(result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs
Normal file
38
pgLabII.PgUtils.Tests/ConnectionStrings/Util/ResultAssert.cs
Normal file
|
|
@ -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<T>(Result<T> result)
|
||||
{
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
public static void Success<T>(Result<T> result, T expected)
|
||||
{
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(expected, result.Value);
|
||||
}
|
||||
|
||||
public static void Success<T>(Result<T> result, Action<T> assert)
|
||||
{
|
||||
Assert.True(result.IsSuccess);
|
||||
assert(result.Value);
|
||||
}
|
||||
|
||||
public static void Failed(Result result)
|
||||
{
|
||||
Assert.True(result.IsFailed);
|
||||
}
|
||||
|
||||
public static void Failed<T>(Result<T> result)
|
||||
{
|
||||
Assert.True(result.IsFailed);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +1,93 @@
|
|||
using pgLabII.PgUtils.ConnectionStrings;
|
||||
using FluentResults;
|
||||
using pgLabII.PgUtils.ConnectionStrings;
|
||||
using OneOf;
|
||||
|
||||
namespace pgLabII.PgUtils.Tests.ConnectionStrings;
|
||||
|
||||
using Elem = OneOf<PqToken, string, IError>;
|
||||
|
||||
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<Elem> _tokens = [];
|
||||
private readonly List<Elem> _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<string> GetKeyword()
|
||||
{
|
||||
EnsureNotEof();
|
||||
EnsureNotEol();
|
||||
var elem = Consume();
|
||||
if (elem.Result == PqToken.Equals)
|
||||
return;
|
||||
|
||||
throw new Exception("Unexpected call to ConsumeEquals");
|
||||
return elem.Match<Result<string>>(
|
||||
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<Result>(
|
||||
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<string> GetValue()
|
||||
{
|
||||
EnsureNotEol();
|
||||
var elem = Consume();
|
||||
return elem.Match<Result<string>>(
|
||||
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?");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Exception>(() =>
|
||||
{
|
||||
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<ArgumentNullException>(() => _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<ArgumentNullException>(() => _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]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="OneOf" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=acls/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=connectionstrings_005Cutil/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
|
|
@ -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<string> GetKeyword();
|
||||
Result ConsumeEquals();
|
||||
Result<string> GetValue();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using FluentResults;
|
||||
using Npgsql;
|
||||
|
||||
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||
|
||||
/// <summary>
|
||||
/// Parser for converting a libpq style connection string into a dictionary of key value pairs
|
||||
/// </summary>
|
||||
public ref struct PqConnectionStringParser
|
||||
{
|
||||
// Note possible keywords
|
||||
|
|
@ -51,64 +55,38 @@ public ref struct PqConnectionStringParser
|
|||
).Parse();
|
||||
}
|
||||
|
||||
private readonly IPqConnectionStringTokenizer tokenizer;
|
||||
private readonly Dictionary<string, string> result = new();
|
||||
private readonly IPqConnectionStringTokenizer _tokenizer;
|
||||
private readonly Dictionary<string, string> _result = new();
|
||||
|
||||
public PqConnectionStringParser(IPqConnectionStringTokenizer tokenizer)
|
||||
{
|
||||
this.tokenizer = tokenizer;
|
||||
this._tokenizer = tokenizer;
|
||||
}
|
||||
|
||||
public IDictionary<string, string> 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<char> v)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> 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<string> 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<string> 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<string> 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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentResults" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<App>
|
||||
#pragma warning restore CA1711 // Identifiers should not have incorrect suffix
|
||||
{
|
||||
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
|
||||
{
|
||||
return base.CustomizeAppBuilder(builder)
|
||||
.WithInterFont()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>pgLabII</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>companyName.pgLabII</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="480" />
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text=" Copyright (c) 2022 " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines"
|
||||
minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
|
||||
<rect key="frame" x="20" y="439" width="441" height="21" />
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17" />
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
|
||||
<nil key="highlightedColor" />
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="pgLabII" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines"
|
||||
minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
|
||||
<rect key="frame" x="20" y="140" width="441" height="43" />
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor" />
|
||||
<nil key="highlightedColor" />
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
|
||||
<constraints>
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
|
||||
<constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
|
||||
<constraint firstAttribute="bottom" secondItem="8ie-xW-0ye" secondAttribute="bottom" constant="20" id="Kzo-t9-V3l" />
|
||||
<constraint firstItem="8ie-xW-0ye" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="MfP-vx-nX0" />
|
||||
<constraint firstAttribute="centerX" secondItem="8ie-xW-0ye" secondAttribute="centerX" id="ZEH-qu-HZ9" />
|
||||
<constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics" />
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
|
||||
<point key="canvasLocation" x="548" y="455" />
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-ios</TargetFramework>
|
||||
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.iOS" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\pgLabII\pgLabII.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml" />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
13
pgLabII/Contracts/IEditHistoryManager.cs
Normal file
13
pgLabII/Contracts/IEditHistoryManager.cs
Normal file
|
|
@ -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<EditHistoryEntry> GetHistory();
|
||||
}
|
||||
12
pgLabII/EditHistoryManager/EditBuffer.cs
Normal file
12
pgLabII/EditHistoryManager/EditBuffer.cs
Normal file
|
|
@ -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; }
|
||||
}
|
||||
163
pgLabII/EditHistoryManager/EditHistoryManager.cs
Normal file
163
pgLabII/EditHistoryManager/EditHistoryManager.cs
Normal file
|
|
@ -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<EditHistoryEntry> _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<EditHistoryEntry> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
pgLabII/EditHistoryManager/EditOperation.cs
Normal file
11
pgLabII/EditHistoryManager/EditOperation.cs
Normal file
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -6,9 +6,11 @@ using pgLabII.Model;
|
|||
|
||||
namespace pgLabII.Infra;
|
||||
|
||||
internal class LocalDb : DbContext
|
||||
public class LocalDb : DbContext
|
||||
{
|
||||
public DbSet<ServerConfiguration> ServerConfigurations => Set<ServerConfiguration>();
|
||||
public DbSet<Document> Documents => Set<Document>();
|
||||
public DbSet<EditHistoryEntry> EditHistory => Set<EditHistoryEntry>();
|
||||
|
||||
public string DbPath { get; }
|
||||
|
||||
|
|
@ -26,9 +28,10 @@ internal class LocalDb : DbContext
|
|||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
|
||||
|
||||
new ServerConfigurationEntityConfiguration().Configure(modelBuilder.Entity<ServerConfiguration>());
|
||||
new ServerUserEntityConfiguration().Configure(modelBuilder.Entity<ServerUser>());
|
||||
new DocumentEntityConfiguration().Configure(modelBuilder.Entity<Document>());
|
||||
new EditHistoryEntityConfiguration().Configure(modelBuilder.Entity<EditHistoryEntry>());
|
||||
}
|
||||
|
||||
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
||||
|
|
@ -56,3 +59,26 @@ public class ServerUserEntityConfiguration : IEntityTypeConfiguration<ServerUser
|
|||
b.HasKey(e => e.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public class DocumentEntityConfiguration : IEntityTypeConfiguration<Document>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Document> 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<EditHistoryEntry>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<EditHistoryEntry> b)
|
||||
{
|
||||
b.HasKey(e => e.Id);
|
||||
b.Property(e => e.Timestamp).IsRequired();
|
||||
b.HasIndex(e => new { e.DocumentId, e.Timestamp});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
pgLabII/Model/Document.cs
Normal file
15
pgLabII/Model/Document.cs
Normal file
|
|
@ -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<EditHistoryEntry> EditHistory { get; set; } = [];
|
||||
}
|
||||
14
pgLabII/Model/EditHistoryEntry.cs
Normal file
14
pgLabII/Model/EditHistoryEntry.cs
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<MainViewModel>();
|
||||
collection.AddTransient<ServerListViewModel>();
|
||||
collection.AddScoped<LocalDb>();
|
||||
collection.AddTransient<CodeEditorView>();
|
||||
collection.AddTransient<IEditHistoryManager, EditHistoryManager>();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
6
pgLabII/Services/DocumentSession.cs
Normal file
6
pgLabII/Services/DocumentSession.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace pgLabII.Services;
|
||||
|
||||
public class DocumentSession
|
||||
{
|
||||
|
||||
}
|
||||
18
pgLabII/Services/DocumentSessionFactory.cs
Normal file
18
pgLabII/Services/DocumentSessionFactory.cs
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
10
pgLabII/ViewModels/CodeEditorViewModel.cs
Normal file
10
pgLabII/ViewModels/CodeEditorViewModel.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using AvaloniaEdit.Document;
|
||||
using ReactiveUI.SourceGenerators;
|
||||
|
||||
namespace pgLabII.ViewModels;
|
||||
|
||||
public partial class CodeEditorViewModel : ViewModelBase
|
||||
{
|
||||
[Reactive] private TextDocument _document = new();
|
||||
|
||||
}
|
||||
18
pgLabII/Views/Controls/CodeEditorView.axaml
Normal file
18
pgLabII/Views/Controls/CodeEditorView.axaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
|
||||
x:Class="pgLabII.Views.Controls.CodeEditorView">
|
||||
<DockPanel LastChildFill="True">
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Orientation="Horizontal" DockPanel.Dock="Top">
|
||||
<Button Name="SaveButton" Content="Save" Click="OnSaveClicked"/>
|
||||
</StackPanel>
|
||||
<!-- Editor -->
|
||||
<avaloniaEdit:TextEditor Name="Editor"
|
||||
ShowLineNumbers="True"
|
||||
/>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
50
pgLabII/Views/Controls/CodeEditorView.axaml.cs
Normal file
50
pgLabII/Views/Controls/CodeEditorView.axaml.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.IO;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using AvaloniaEdit.Document;
|
||||
using AvaloniaEdit.TextMate;
|
||||
using TextMateSharp.Grammars;
|
||||
|
||||
namespace pgLabII.Views.Controls;
|
||||
|
||||
public partial class CodeEditorView : UserControl
|
||||
{
|
||||
private TextMate.Installation? _textMate;
|
||||
private readonly IEditHistoryManager _editHistoryManager = new EditHistoryManager(
|
||||
new(),
|
||||
new()
|
||||
{
|
||||
OriginalFilename = "",
|
||||
BaseCopyFilename = "",
|
||||
});
|
||||
|
||||
public CodeEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
|
||||
var registryOptions = new RegistryOptions(ThemeName.DarkPlus);
|
||||
_textMate = Editor.InstallTextMate(registryOptions);
|
||||
_textMate.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".sql").Id));
|
||||
|
||||
Editor.Document.Changed += DocumentChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnSaveClicked(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
// Example save logic
|
||||
File.WriteAllText("final.sql", Editor.Text);
|
||||
}
|
||||
|
||||
|
||||
private void DocumentChanged(object? sender, DocumentChangeEventArgs e)
|
||||
{
|
||||
_editHistoryManager.AddEdit(e.Offset, e.InsertedText.Text, e.RemovedText.Text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ public partial class ServerListView : UserControl
|
|||
{
|
||||
//DataContext = new ServerListViewModel();
|
||||
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:pgLabII.ViewModels"
|
||||
xmlns:controls="clr-namespace:pgLabII.Views.Controls"
|
||||
xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="pgLabII.Views.SingleDatabaseWindow"
|
||||
Title="SingleDatabaseWindow"
|
||||
|
|
@ -45,11 +47,12 @@
|
|||
|
||||
<TabControl.ContentTemplate>
|
||||
<DataTemplate DataType="viewModels:QueryToolViewModel">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBox Text="{Binding Query, Mode=TwoWay}" />
|
||||
<Button Command="{Binding EditCommand}">TEST</Button>
|
||||
</StackPanel>
|
||||
<DockPanel LastChildFill="True">
|
||||
<TextBox Text="{Binding Query, Mode=TwoWay}" DockPanel.Dock="Top"/>
|
||||
<Button Command="{Binding EditCommand}" DockPanel.Dock="Top">TEST</Button>
|
||||
|
||||
<controls:CodeEditorView />
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</TabControl.ContentTemplate>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
|
|
@ -22,6 +23,8 @@
|
|||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Avalonia.ReactiveUI" />
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" />
|
||||
<PackageReference Include="FluentResults" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=contracts/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=views_005Ccontrols/@EntryIndexedValue">False</s:Boolean></wpf:ResourceDictionary>
|
||||
Loading…
Add table
Add a link
Reference in a new issue