using System.Buffers; using System.Text; using Avalonia.Collections; using AvaloniaEdit.Document; using InkForge.Data; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace InkForge.Desktop.Models; public class TextDocumentStore(IDbContextFactory dbContextFactory) { private readonly AvaloniaDictionary _cache = []; public void Close(int id) { if (_cache.Remove(id, out var document)) { document.Remove(0, document.TextLength); } } public TextDocument Get(int id) { if (!_cache.TryGetValue(id, out var document)) { using var dbContext = dbContextFactory.CreateDbContext(); var connection = dbContext.Database.GetDbConnection(); using var command = connection.CreateCommand(); command.CommandText = $"SELECT rowid FROM Blobs WHERE Id={id}"; using var commandReader = command.ExecuteReader(); document = new(); if (commandReader.Read()) { using SqliteBlob stream = new((SqliteConnection)connection, "Blob", "Value", commandReader.GetInt64(0), true); CopyDocumentContent(stream, document); } _cache.Add(id, document); } return document; static void CopyDocumentContent(Stream stream, TextDocument document) { DocumentTextWriter writer = new(document, 0); StreamReader reader = new(stream, Encoding.UTF8); Span buffer = stackalloc char[128]; while (!reader.EndOfStream) { var read = reader.Read(buffer); writer.Write(buffer[..read]); } } } public int? Save(int? id, TextDocument textDocument) { var state = textDocument.CreateSnapshot(); var bytes = TextBytes(state); using var dbContext = dbContextFactory.CreateDbContext(); var connection = dbContext.Database.GetDbConnection(); using var transaction = dbContext.Database.BeginTransaction(); using var command = connection.CreateCommand(); command.CommandText = id switch { null => $"INSERT INTO Blobs(Value) VALUES(zeroblob({bytes})) RETURNING (Id, rowid)", _ => $"UPDATE Blobs SET Value=zeroblob({bytes}) WHERE Id={id} RETURNING (Id, rowid)" }; using var commandReader = command.ExecuteReader(); if (!commandReader.Read()) { return null; } id = commandReader.GetInt32(0); using (SqliteBlob stream = new((SqliteConnection)connection, "Blobs", "Value", commandReader.GetInt64(1))) { using StreamWriter writer = new(stream, Encoding.UTF8); state.WriteTextTo(writer); } transaction.Commit(); _cache.TryAdd((int)id, textDocument); return id; static int TextBytes(ITextSource text) { int length = 0; Span byteBuffer = stackalloc byte[128]; using var reader = text.CreateReader(); ArrayBufferWriter buffer = new(); var encoder = Encoding.UTF8.GetEncoder(); while (reader.Peek() != -1) { var memory = buffer.GetMemory(); buffer.Advance(reader.Read(memory.Span)); ReadOnlySpan chars = buffer.WrittenSpan; convert: encoder.Convert(buffer.WrittenSpan, byteBuffer, false, out var charsUsed, out var bytesUsed, out _); chars = chars[charsUsed..]; if (bytesUsed > 0) { length += bytesUsed; goto convert; } Span remaining = []; if (!chars.IsEmpty) { remaining = buffer.GetSpan(chars.Length); chars.CopyTo(remaining); } buffer.Clear(); if (!remaining.IsEmpty) { buffer.Write(remaining); } } return length; } } }