From e8d89a98545640cc4aef56bda6f5b0c3c6649a5b Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Fri, 11 Jul 2025 10:39:03 -0500 Subject: [PATCH 1/3] Add Sqlite option for PSReadline --- PSReadLine.build.ps1 | 2 +- PSReadLine/Cmdlets.cs | 43 +++- PSReadLine/History.cs | 336 +++++++++++++++++++++++++++- PSReadLine/Options.cs | 22 +- PSReadLine/PSReadLine.csproj | 13 +- PSReadLine/PSReadLine.format.ps1xml | 3 + PSReadLine/Prediction.cs | 5 + PSReadLine/ReadLine.cs | 16 +- nuget.config | 3 +- test/HistoryTest.cs | 76 +++++-- 10 files changed, 479 insertions(+), 40 deletions(-) diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index e245dfae3..bb1ab77d4 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -125,7 +125,7 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { Copy-Item "Polyfill/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/netstd" -Force Copy-Item "Polyfill/bin/$Configuration/net6.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net6plus" -Force - $binPath = "PSReadLine/bin/$Configuration/netstandard2.0/publish" + $binPath = "PSReadLine/bin/$Configuration/netstandard2.0/win-x64/publish" Copy-Item $binPath/Microsoft.PowerShell.PSReadLine.dll $targetDir Copy-Item $binPath/Microsoft.PowerShell.Pager.dll $targetDir diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 7fecf4b4e..e6b18a951 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -61,7 +61,14 @@ public enum AddToHistoryOption { SkipAdding, MemoryOnly, - MemoryAndFile + MemoryAndFile, + SQLite + } + + public enum HistoryType + { + Text, + SQLite } public enum PredictionSource @@ -106,6 +113,12 @@ public class PSConsoleReadLineOptions public const string DefaultContinuationPrompt = ">> "; + /// + /// The default history format is text. The SQLite is experimental. + /// It is not recommended to use it for production yet. + /// + public const HistoryType DefaultHistoryType = HistoryType.Text; + /// /// The maximum number of commands to store in the history. /// @@ -201,6 +214,7 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) { ResetColors(); EditMode = DefaultEditMode; + HistoryType = DefaultHistoryType; ContinuationPrompt = DefaultContinuationPrompt; ContinuationPromptColor = Console.ForegroundColor; ExtraPromptLineCount = DefaultExtraPromptLineCount; @@ -235,6 +249,13 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) "PowerShell", "PSReadLine", historyFileName); + SqliteHistorySavePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft", + "Windows", + "PowerShell", + "PSReadLine", + hostName + "_history.db"); } else { @@ -336,6 +357,7 @@ public object ContinuationPromptColor /// that do invoke the script block - this covers the most useful cases. /// public HashSet CommandsToValidateScriptBlockArguments { get; set; } + public HistoryType HistoryType { get; set; } = HistoryType.Text; /// /// When true, duplicates will not be recalled from history more than once. @@ -375,6 +397,11 @@ public object ContinuationPromptColor /// The path to the saved history. /// public string HistorySavePath { get; set; } + + /// + /// The path to the SQLite history database. + /// + public string SqliteHistorySavePath { get; set; } public HistorySaveStyle HistorySaveStyle { get; set; } /// @@ -656,6 +683,20 @@ public EditMode EditMode [AllowEmptyString] public string ContinuationPrompt { get; set; } + [Parameter] + public HistoryType HistoryType + { + get => _historyType.GetValueOrDefault(); + set + { + _historyType = value; + _historyTypeSpecified = true; + } + } + + public HistoryType? _historyType = HistoryType.Text; + internal bool _historyTypeSpecified; + [Parameter] public SwitchParameter HistoryNoDuplicates { diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index c1490a230..42e1c6043 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -13,7 +13,9 @@ using System.Text.RegularExpressions; using System.Threading; using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; using Microsoft.PowerShell.PSReadLine; +using Microsoft.Data.Sqlite; namespace Microsoft.PowerShell { @@ -82,6 +84,11 @@ public class HistoryItem /// public bool FromHistoryFile { get; internal set; } + /// + /// The location where the command was run, if available. + /// + public string Location { get; internal set; } + internal bool _saved; internal bool _sensitive; internal List _edits; @@ -155,6 +162,12 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi return AddToHistoryOption.SkipAdding; } + if (Options.HistoryType is HistoryType.SQLite) + { + InitializeSQLiteDatabase(); + return AddToHistoryOption.SQLite; + } + if (!fromHistoryFile && Options.AddToHistoryHandler != null) { if (Options.AddToHistoryHandler == PSConsoleReadLineOptions.DefaultAddToHistoryHandler) @@ -197,10 +210,69 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi return AddToHistoryOption.MemoryAndFile; } + private void InitializeSQLiteDatabase() + { + // Path to the SQLite database file + string baseConnectionString = $"Data Source={_options.HistorySavePath}"; + var connectionString = new SqliteConnectionStringBuilder(baseConnectionString) + { + Mode = SqliteOpenMode.ReadWriteCreate + }.ToString(); + + try + { + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + // Check if the "History" table exists + using var command = connection.CreateCommand(); + command.CommandText = @" + SELECT name + FROM sqlite_master + WHERE type='table' AND name=@TableName"; + command.Parameters.AddWithValue("@TableName", "History"); + + var result = command.ExecuteScalar(); + + // If the table doesn't exist, create it + if (result == null) + { + using var createTableCommand = connection.CreateCommand(); + createTableCommand.CommandText = @" + CREATE TABLE IF NOT EXISTS History ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CommandLine TEXT NOT NULL, + StartTime DATETIME NOT NULL, + ElapsedTime INTEGER NOT NULL, + Location TEXT NOT NULL + )"; + createTableCommand.ExecuteNonQuery(); + } + } + catch (SqliteException ex) + { + // Handle SQLite-specific exceptions + Console.WriteLine($"SQLite error initializing database: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error initializing SQLite database: {ex.Message}"); + + Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + + if (ex.InnerException != null) + { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + Console.WriteLine($"Inner Exception Stack Trace: {ex.InnerException.StackTrace}"); + } + } + } + private string MaybeAddToHistory( string result, List edits, int undoEditIndex, + string location = null, bool fromDifferentSession = false, bool fromInitialRead = false) { @@ -216,6 +288,7 @@ private string MaybeAddToHistory( _undoEditIndex = undoEditIndex, _editGroupStart = -1, _saved = fromHistoryFile, + Location = location ?? _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown", FromOtherSession = fromDifferentSession, FromHistoryFile = fromInitialRead, }; @@ -277,7 +350,69 @@ private void IncrementalHistoryWrite() i -= 1; } - WriteHistoryRange(i + 1, _history.Count - 1, overwritten: false); + if (_options.HistoryType == HistoryType.Text) + { + WriteHistoryRange(i + 1, _history.Count - 1, overwritten: false); + } + + if (_options.HistoryType == HistoryType.SQLite) + { + WriteHistoryToSQLite(i + 1, _history.Count - 1); + } + } + + private void WriteHistoryToSQLite(int start, int end) + { + if (_historyFileMutex == null) + { + _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); + } + + WithHistoryFileMutexDo(1000, () => + { + try + { + string baseConnectionString = $"Data Source={_options.HistorySavePath}"; + var connectionString = new SqliteConnectionStringBuilder(baseConnectionString) + { + Mode = SqliteOpenMode.ReadWrite + }.ToString(); + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + using var transaction = connection.BeginTransaction(); + using var command = connection.CreateCommand(); + command.CommandText = "INSERT INTO History (CommandLine, StartTime, ElapsedTime, Location) VALUES (@CommandLine, @StartTime, @ElapsedTime, @Location)"; + command.Parameters.Add(new SqliteParameter("@CommandLine", string.Empty)); + command.Parameters.Add(new SqliteParameter("@StartTime", string.Empty)); + command.Parameters.Add(new SqliteParameter("@ElapsedTime", 0L)); + command.Parameters.Add(new SqliteParameter("@Location", string.Empty)); + + for (var i = start; i <= end; i++) + { + var item = _history[i]; + item._saved = true; + + if (item._sensitive) + { + continue; + } + + command.Parameters["@CommandLine"].Value = item.CommandLine; + command.Parameters["@StartTime"].Value = item.StartTime.ToString("o"); + command.Parameters["@ElapsedTime"].Value = item.ApproximateElapsedTime.Ticks; + command.Parameters["@Location"].Value = _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown"; + command.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (Exception e) + { + ReportHistoryFileError(e); + } + }); } private void SaveHistoryAtExit() @@ -411,6 +546,7 @@ private void WriteHistoryRange(int start, int end, bool overwritten) /// private List ReadHistoryFileIncrementally() { + // Read history from a text file var fileInfo = new FileInfo(Options.HistorySavePath); if (fileInfo.Exists && fileInfo.Length != _historyFileLastSavedSize) { @@ -433,22 +569,173 @@ private List ReadHistoryFileIncrementally() return null; } + private List ReadHistorySQLiteIncrementally() + { + var historyItems = new List(); + try + { + string baseConnectionString = $"Data Source={_options.HistorySavePath}"; + var connectionString = new SqliteConnectionStringBuilder(baseConnectionString) + { + Mode = SqliteOpenMode.ReadOnly + }.ToString(); + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + using (var command = connection.CreateCommand()) + { + command.CommandText = @" + SELECT CommandLine, StartTime, ElapsedTime, Location + FROM History + WHERE Id > @LastId + ORDER BY Id ASC"; + command.Parameters.AddWithValue("@LastId", _historyFileLastSavedSize); + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var item = new HistoryItem + { + CommandLine = reader.GetString(0), + StartTime = DateTime.Parse(reader.GetString(1)), + ApproximateElapsedTime = TimeSpan.FromTicks(reader.GetInt64(2)), + Location = reader.GetString(3), + FromHistoryFile = true, + FromOtherSession = true, + _edits = new List { EditItemInsertString.Create(reader.GetString(0), 0) }, + _undoEditIndex = 1, + _editGroupStart = -1 + }; + historyItems.Add(item); + } + } + + // Update the last saved size to the latest ID in the database + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT MAX(Id) FROM History"; + var result = command.ExecuteScalar(); + if (result != DBNull.Value) + { + _historyFileLastSavedSize = Convert.ToInt64(result); + } + } + } + catch (SqliteException ex) + { + Console.WriteLine($"SQLite error reading history: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error reading history from SQLite: {ex.Message}"); + } + + return historyItems.Count > 0 ? historyItems : null; + } + private bool MaybeReadHistoryFile() { if (Options.HistorySaveStyle == HistorySaveStyle.SaveIncrementally) { return WithHistoryFileMutexDo(1000, () => { - List historyLines = ReadHistoryFileIncrementally(); - if (historyLines != null) + if (_options.HistoryType == HistoryType.SQLite) { - UpdateHistoryFromFile(historyLines, fromDifferentSession: true, fromInitialRead: false); + List historyItems = ReadHistorySQLiteIncrementally(); + if (historyItems != null) + { + foreach (var item in historyItems) + { + // Use the full metadata from SQLite + MaybeAddToHistory( + item.CommandLine, + item._edits ?? new List { EditItemInsertString.Create(item.CommandLine, 0) }, + item._undoEditIndex, + item.Location, + fromDifferentSession: true, + fromInitialRead: false); + } + } + } + else + { + List historyLines = ReadHistoryFileIncrementally(); + if (historyLines != null) + { + UpdateHistoryFromFile(historyLines, fromDifferentSession: true, fromInitialRead: false); + } } }); } // true means no errors, not that we actually read the file return true; +} + + private void ReadSQLiteHistory(bool fromOtherSession) + { + _historyFileMutex ??= new Mutex(false, GetHistorySaveFileMutexName()); + + WithHistoryFileMutexDo(1000, () => + { + try + { + string baseConnectionString = $"Data Source={_options.HistorySavePath}"; + var connectionString = new SqliteConnectionStringBuilder(baseConnectionString) + { + Mode = SqliteOpenMode.ReadOnly + }.ToString(); + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = @" + SELECT CommandLine, StartTime, ElapsedTime, Location + FROM History + ORDER BY Id ASC"; + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var item = new HistoryItem + { + CommandLine = reader.GetString(0), + StartTime = DateTime.Parse(reader.GetString(1)), + ApproximateElapsedTime = TimeSpan.FromTicks(reader.GetInt64(2)), + Location = reader.GetString(3), + FromHistoryFile = true, + FromOtherSession = fromOtherSession, + _edits = new List { EditItemInsertString.Create(reader.GetString(0), 0) }, + _undoEditIndex = 1, + _editGroupStart = -1 + }; + + // Add to the history queue. + _history.Enqueue(item); + } + + // Update the last saved size to the latest ID in the database + using (var idCommand = connection.CreateCommand()) + { + idCommand.CommandText = "SELECT MAX(Id) FROM History"; + var result = idCommand.ExecuteScalar(); + if (result != DBNull.Value) + { + _historyFileLastSavedSize = Convert.ToInt64(result); + } + } + } + catch (SqliteException ex) + { + ReportHistoryFileError(ex); + } + catch (Exception ex) + { + ReportHistoryFileError(ex); + } + }); } private void ReadHistoryFile() @@ -529,13 +816,13 @@ void UpdateHistoryFromFile(IEnumerable historyLines, bool fromDifferentS sb.Append(line); var l = sb.ToString(); var editItems = new List {EditItemInsertString.Create(l, 0)}; - MaybeAddToHistory(l, editItems, 1, fromDifferentSession, fromInitialRead); + MaybeAddToHistory(l, editItems, 1, null, fromDifferentSession, fromInitialRead); sb.Clear(); } else { var editItems = new List {EditItemInsertString.Create(line, 0)}; - MaybeAddToHistory(line, editItems, 1, fromDifferentSession, fromInitialRead); + MaybeAddToHistory(line, editItems, 1, null, fromDifferentSession, fromInitialRead); } } } @@ -883,6 +1170,21 @@ private void SaveCurrentLine() } } + private int FindHistoryIndexByLocation(int startIndex, int direction) + { + int index = startIndex; + while (index >= 0 && index < _history.Count) + { + var currentLocation = _engineIntrinsics?.SessionState.Path.CurrentLocation.Path; + if (string.Equals(_history[index].Location, currentLocation, StringComparison.OrdinalIgnoreCase)) + { + return index; + } + index += direction; + } + return -1; // Not found + } + private void HistoryRecall(int direction) { if (_recallHistoryCommandCount == 0 && LineIsMultiLine()) @@ -901,7 +1203,14 @@ private void HistoryRecall(int direction) int newHistoryIndex = _currentHistoryIndex; while (count > 0) { - newHistoryIndex += direction; + if (_options.HistoryType == HistoryType.SQLite) + { + newHistoryIndex = FindHistoryIndexByLocation(newHistoryIndex + direction, direction); + } + else + { + newHistoryIndex += direction; + } if (newHistoryIndex < 0 || newHistoryIndex >= _history.Count) { break; @@ -931,7 +1240,18 @@ private void HistoryRecall(int direction) } } _recallHistoryCommandCount += 1; - if (newHistoryIndex >= 0 && newHistoryIndex <= _history.Count) + + // For SQLite/location-filtered history, allow returning to the current line + if (_options.HistoryType == HistoryType.SQLite && + (newHistoryIndex < 0 || newHistoryIndex >= _history.Count)) + { + _currentHistoryIndex = _history.Count; + var moveCursor = InViCommandMode() && !_options.HistorySearchCursorMovesToEnd + ? HistoryMoveCursor.ToBeginning + : HistoryMoveCursor.ToEnd; + UpdateFromHistory(moveCursor); + } + else if (newHistoryIndex >= 0 && newHistoryIndex <= _history.Count) { _currentHistoryIndex = newHistoryIndex; var moveCursor = InViCommandMode() && !_options.HistorySearchCursorMovesToEnd diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 7485154b4..910cfa791 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -26,6 +26,26 @@ private void SetOptionsInternal(SetPSReadLineOption options) { Options.ContinuationPrompt = options.ContinuationPrompt; } + if (options._historyTypeSpecified) + { + Options.HistoryType = options.HistoryType; + if (Options.HistoryType is HistoryType.SQLite) + { + Options.HistorySavePath = Options.SqliteHistorySavePath; + // Check if the SQLite history file exists + if (!string.IsNullOrEmpty(Options.HistorySavePath) && !System.IO.File.Exists(Options.HistorySavePath)) + { + _historyFileMutex?.Dispose(); + _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); + InitializeSQLiteDatabase(); + _historyFileLastSavedSize = 0; + } + // For now remove all text history + _singleton._history?.Clear(); + _singleton._currentHistoryIndex = 0; + ReadSQLiteHistory(fromOtherSession: false); + } + } if (options._historyNoDuplicates.HasValue) { Options.HistoryNoDuplicates = options.HistoryNoDuplicates; @@ -127,7 +147,7 @@ private void SetOptionsInternal(SetPSReadLineOption options) Options.HistorySavePath = options.HistorySavePath; _historyFileMutex?.Dispose(); _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); - _historyFileLastSavedSize = 0; + _historyFileLastSavedSize = 0; } if (options._ansiEscapeTimeout.HasValue) { diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 1c8090f84..81c556dfc 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -13,12 +13,16 @@ true false 9.0 + true + win-x64 - - - + + + + + @@ -28,5 +32,6 @@ + - + \ No newline at end of file diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 24f70799a..f86c7a25f 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -90,6 +90,9 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) EditMode + + HistoryType + AddToHistoryHandler diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index bd1448b9a..179c01d57 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -57,6 +57,11 @@ private static void UpdatePredictionClient(Runspace runspace, EngineIntrinsics e } } + private static string GetCurrentLocation(EngineIntrinsics engineIntrinsics) + { + return engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown"; + } + // Stub helper methods so prediction can be mocked [ExcludeFromCodeCoverage] Task> IPSConsoleReadLineMockableMethods.PredictInputAsync(Ast ast, Token[] tokens) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 677025aab..366e5eec7 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -536,7 +536,15 @@ private string InputLoop() if (_inputAccepted) { _acceptedCommandLine = _buffer.ToString(); - MaybeAddToHistory(_acceptedCommandLine, _edits, _undoEditIndex); + if (_options.HistoryType == HistoryType.SQLite) + { + string location = _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path; + MaybeAddToHistory(_acceptedCommandLine, _edits, _undoEditIndex, location); + } + else + { + MaybeAddToHistory(_acceptedCommandLine, _edits, _undoEditIndex); + } _prediction.OnCommandLineAccepted(_acceptedCommandLine); return _acceptedCommandLine; @@ -884,10 +892,14 @@ private void DelayedOneTimeInitialize() { } - if (readHistoryFile) + if (readHistoryFile && _options.HistoryType == HistoryType.Text) { ReadHistoryFile(); } + else if (readHistoryFile && _options.HistoryType == HistoryType.SQLite) + { + ReadSQLiteHistory(fromOtherSession: false); + } _killIndex = -1; // So first add indexes 0. _killRing = new List(Options.MaximumKillRingCount); diff --git a/nuget.config b/nuget.config index a10ce9b3d..a079cf1b5 100644 --- a/nuget.config +++ b/nuget.config @@ -1,8 +1,7 @@ - - + diff --git a/test/HistoryTest.cs b/test/HistoryTest.cs index ad6b95f0c..d400d6046 100644 --- a/test/HistoryTest.cs +++ b/test/HistoryTest.cs @@ -40,7 +40,8 @@ public void ParallelHistorySaving() TestSetup(KeyMode.Cmd); string historySavingFile = Path.GetTempFileName(); - var options = new SetPSReadLineOption { + var options = new SetPSReadLineOption + { HistorySaveStyle = HistorySaveStyle.SaveIncrementally, MaximumHistoryCount = 3, }; @@ -756,48 +757,53 @@ public void SearchHistory() SetHistory(); Test(" ", Keys(' ', _.UpArrow, _.DownArrow)); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = false}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistorySearchCursorMovesToEnd = false }); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(1); }), - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(1); - }))); + }))); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = true}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistorySearchCursorMovesToEnd = true }); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }), - _.DownArrow, CheckThat(() => { + _.DownArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), - _.UpArrow, CheckThat(() => + _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', @@ -813,31 +819,34 @@ public void HistorySearchCursorMovesToEnd() new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistorySearchCursorMovesToEnd = true}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistorySearchCursorMovesToEnd = true }); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); SetHistory("dosomething", "ps p*", "dir", "echo zzz"); Test("dosomething", Keys( "d", - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), - _.UpArrow, CheckThat(() => { + _.UpArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "osomething"); AssertCursorLeftIs(11); }), - _.DownArrow, CheckThat(() => { + _.DownArrow, CheckThat(() => + { AssertScreenIs(1, emphasisColors, 'd', TokenClassification.Command, "ir"); AssertCursorLeftIs(3); }), - _.UpArrow, CheckThat(() => + _.UpArrow, CheckThat(() => { AssertScreenIs(1, emphasisColors, 'd', @@ -1122,7 +1131,7 @@ public void InteractiveHistorySearch() public void AddToHistoryHandler() { TestSetup(KeyMode.Cmd); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {AddToHistoryHandler = s => s.StartsWith("z")}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { AddToHistoryHandler = s => s.StartsWith("z") }); SetHistory("zzzz", "azzz"); Test("zzzz", Keys(_.UpArrow)); @@ -1132,14 +1141,14 @@ public void AddToHistoryHandler() public void HistoryNoDuplicates() { TestSetup(KeyMode.Cmd); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = false}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistoryNoDuplicates = false }); SetHistory("zzzz", "aaaa", "bbbb", "bbbb", "cccc"); Assert.Equal(5, PSConsoleReadLine.GetHistoryItems().Length); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); // Changing the option should affect existing history. - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistoryNoDuplicates = true }); Test("zzzz", Keys(Enumerable.Repeat(_.UpArrow, 4))); SetHistory("aaaa", "bbbb", "bbbb", "cccc"); @@ -1164,7 +1173,7 @@ public void HistorySearchNoDuplicates() new KeyHandler("UpArrow", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("DownArrow", PSConsoleReadLine.HistorySearchForward)); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistoryNoDuplicates = true }); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo aaaa", Keys("echo", Enumerable.Repeat(_.UpArrow, 3))); @@ -1180,7 +1189,7 @@ public void InteractiveHistorySearchNoDuplicates() { TestSetup(KeyMode.Emacs); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {HistoryNoDuplicates = true}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistoryNoDuplicates = true }); SetHistory("0000", "echo aaaa", "1111", "echo bbbb", "2222", "echo bbbb", "3333", "echo cccc", "4444"); Test("echo aaaa", Keys( _.Ctrl_r, "echo", _.Ctrl_r, _.Ctrl_r)); @@ -1203,7 +1212,7 @@ public void HistoryCount() // There should be 4 items in history, the following should remove the // oldest history item. - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {MaximumHistoryCount = 3}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { MaximumHistoryCount = 3 }); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); Test("zzzz", Keys("zzzz")); @@ -1212,5 +1221,30 @@ public void HistoryCount() Test("cccc", Keys("cccc")); Test("aaaa", Keys(Enumerable.Repeat(_.UpArrow, 4))); } + + [SkippableFact] + public void SetPSReadLineOption_HistoryType_AcceptsSQLite() + { + var method = typeof(PSConsoleReadLine).GetMethod("SetOptions"); + Assert.NotNull(method); + + var optionsType = typeof(SetPSReadLineOption); + var historyTypeProperty = optionsType.GetProperty("HistoryType"); + Assert.NotNull(historyTypeProperty); + + var options = new SetPSReadLineOption(); + historyTypeProperty.SetValue(options, HistoryType.SQLite); + + Exception ex = Record.Exception(() => PSConsoleReadLine.SetOptions(options)); + Assert.Null(ex); + + Assert.Equal(HistoryType.SQLite, historyTypeProperty.GetValue(options)); + + // Set the history type back to the default. + historyTypeProperty.SetValue(options, HistoryType.Text); + Assert.Equal(HistoryType.Text, historyTypeProperty.GetValue(options)); + } + + } } From 98a00b574333636763f42d9455f19598cbf45ce3 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:20:50 -0500 Subject: [PATCH 2/3] Use normalized tables --- .vscode/launch.json | 2 +- PSReadLine/History.cs | 451 +++++++++++++++++++++++++++++------------ PSReadLine/Options.cs | 1 + PSReadLine/ReadLine.cs | 4 - 4 files changed, 328 insertions(+), 130 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c01006234..dd3132076 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "-NoProfile", "-NoExit", "-Command", - "Import-Module '${workspaceFolder}/PSReadLine/bin/Debug/netstandard2.0/PSReadLine.psd1'" + "Import-Module '${workspaceFolder}/PSReadLine/bin/Debug/netstandard2.0/win-x64/PSReadLine.psd1'" ], "console": "integratedTerminal", "justMyCode": false, diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 42e1c6043..be4e1822b 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -164,7 +164,6 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi if (Options.HistoryType is HistoryType.SQLite) { - InitializeSQLiteDatabase(); return AddToHistoryOption.SQLite; } @@ -212,7 +211,6 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi private void InitializeSQLiteDatabase() { - // Path to the SQLite database file string baseConnectionString = $"Data Source={_options.HistorySavePath}"; var connectionString = new SqliteConnectionStringBuilder(baseConnectionString) { @@ -224,47 +222,188 @@ private void InitializeSQLiteDatabase() using var connection = new SqliteConnection(connectionString); connection.Open(); - // Check if the "History" table exists + // Check if the "Commands" table exists (our primary table for new schema) using var command = connection.CreateCommand(); command.CommandText = @" - SELECT name - FROM sqlite_master - WHERE type='table' AND name=@TableName"; - command.Parameters.AddWithValue("@TableName", "History"); +SELECT name +FROM sqlite_master +WHERE type='table' AND name=@TableName"; + command.Parameters.AddWithValue("@TableName", "Commands"); var result = command.ExecuteScalar(); + bool isNewDatabase = result == null; - // If the table doesn't exist, create it - if (result == null) + // If the table doesn't exist, create the normalized schema + if (isNewDatabase) { - using var createTableCommand = connection.CreateCommand(); - createTableCommand.CommandText = @" - CREATE TABLE IF NOT EXISTS History ( - Id INTEGER PRIMARY KEY AUTOINCREMENT, - CommandLine TEXT NOT NULL, - StartTime DATETIME NOT NULL, - ElapsedTime INTEGER NOT NULL, - Location TEXT NOT NULL - )"; - createTableCommand.ExecuteNonQuery(); + using var createTablesCommand = connection.CreateCommand(); + createTablesCommand.CommandText = @" +-- Table for storing unique command lines +CREATE TABLE Commands ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CommandLine TEXT NOT NULL UNIQUE, + CommandHash TEXT NOT NULL UNIQUE +); + +-- Table for storing unique locations +CREATE TABLE Locations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Path TEXT NOT NULL UNIQUE +); + +-- Table for storing execution history with foreign keys +CREATE TABLE ExecutionHistory ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + CommandId INTEGER NOT NULL, + LocationId INTEGER NOT NULL, + StartTime INTEGER NOT NULL, + ElapsedTime INTEGER NOT NULL, + ExecutionCount INTEGER DEFAULT 1, + LastExecuted INTEGER NOT NULL, + FOREIGN KEY (CommandId) REFERENCES Commands(Id), + FOREIGN KEY (LocationId) REFERENCES Locations(Id), + UNIQUE(CommandId, LocationId) +); + +-- Create indexes for optimal performance +CREATE INDEX idx_commands_hash ON Commands(CommandHash); +CREATE INDEX idx_locations_path ON Locations(Path); +CREATE INDEX idx_execution_last_executed ON ExecutionHistory(LastExecuted DESC); +CREATE INDEX idx_execution_count ON ExecutionHistory(ExecutionCount DESC); +CREATE INDEX idx_execution_location_time ON ExecutionHistory(LocationId, LastExecuted DESC); + +-- Create a view for easy querying (mimics the old single-table structure) +CREATE VIEW HistoryView AS +SELECT + eh.Id, + c.CommandLine, + c.CommandHash, + l.Path as Location, + eh.StartTime, + eh.ElapsedTime, + eh.ExecutionCount, + eh.LastExecuted +FROM ExecutionHistory eh +JOIN Commands c ON eh.CommandId = c.Id +JOIN Locations l ON eh.LocationId = l.Id;"; + createTablesCommand.ExecuteNonQuery(); + + // Migrate existing text file history if it exists + // MigrateTextHistoryToSQLite(connection); } } catch (SqliteException ex) { - // Handle SQLite-specific exceptions Console.WriteLine($"SQLite error initializing database: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Error initializing SQLite database: {ex.Message}"); + } + } - Console.WriteLine($"Stack Trace: {ex.StackTrace}"); + private void MigrateTextHistoryToSQLite(SqliteConnection connection) + { + // Check if text history file exists + string textHistoryPath = _options.HistorySavePath.Replace(".sqlite", ".txt"); + if (!File.Exists(textHistoryPath)) + { + // Try default history path pattern + textHistoryPath = Path.ChangeExtension(_options.HistorySavePath, ".txt"); + if (!File.Exists(textHistoryPath)) + { + return; // No text history to migrate + } + } - if (ex.InnerException != null) + try + { + // Read existing text history using the existing connection (don't open a new one) + var historyLines = ReadHistoryLinesImpl(textHistoryPath, int.MaxValue); + var historyItems = new List(); + + // Convert text lines to HistoryItems + var sb = new StringBuilder(); + foreach (var line in historyLines) { - Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); - Console.WriteLine($"Inner Exception Stack Trace: {ex.InnerException.StackTrace}"); + if (line.EndsWith("`", StringComparison.Ordinal)) + { + sb.Append(line, 0, line.Length - 1); + sb.Append('\n'); + } + else if (sb.Length > 0) + { + sb.Append(line); + historyItems.Add(new HistoryItem + { + CommandLine = sb.ToString(), + StartTime = DateTime.UtcNow.AddMinutes(-historyItems.Count), // Approximate timestamps + ApproximateElapsedTime = TimeSpan.Zero, + Location = "Unknown" + }); + sb.Clear(); + } + else + { + historyItems.Add(new HistoryItem + { + CommandLine = line, + StartTime = DateTime.UtcNow.AddMinutes(-historyItems.Count), // Approximate timestamps + ApproximateElapsedTime = TimeSpan.Zero, + Location = "Unknown" + }); + } } + + // Insert into SQLite database using the new normalized schema + using var transaction = connection.BeginTransaction(); + + foreach (var item in historyItems) + { + try + { + // Generate command hash using SHA256 + string commandHash = ComputeCommandHash(item.CommandLine); + string location = item.Location ?? "Unknown"; + + // Get or create command and location IDs + long commandId = GetOrCreateCommandId(connection, item.CommandLine, commandHash); + long locationId = GetOrCreateLocationId(connection, location); + + // Convert DateTime to Unix timestamp (INTEGER) + long startTimeUnix = ((DateTimeOffset)item.StartTime).ToUnixTimeSeconds(); + long lastExecutedUnix = startTimeUnix; + + // Insert or update execution history using the new schema + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = @" +INSERT INTO ExecutionHistory (CommandId, LocationId, StartTime, ElapsedTime, ExecutionCount, LastExecuted) +VALUES (@CommandId, @LocationId, @StartTime, @ElapsedTime, 1, @LastExecuted) +ON CONFLICT(CommandId, LocationId) DO UPDATE SET + ExecutionCount = ExecutionCount + 1, + LastExecuted = excluded.LastExecuted"; + + command.Parameters.AddWithValue("@CommandId", commandId); + command.Parameters.AddWithValue("@LocationId", locationId); + command.Parameters.AddWithValue("@StartTime", startTimeUnix); + command.Parameters.AddWithValue("@ElapsedTime", item.ApproximateElapsedTime.Ticks); + command.Parameters.AddWithValue("@LastExecuted", lastExecutedUnix); + command.ExecuteNonQuery(); + } + catch (Exception itemEx) + { + Console.WriteLine($"Error migrating history item: {itemEx.Message}"); + // Continue with next item + } + } + + transaction.Commit(); + Console.WriteLine($"Migrated {historyItems.Count} history items from text file to SQLite"); + } + catch (Exception ex) + { + Console.WriteLine($"Error migrating text history: {ex.Message}"); } } @@ -361,13 +500,61 @@ private void IncrementalHistoryWrite() } } - private void WriteHistoryToSQLite(int start, int end) + // Helper method to get or create a command ID + private long GetOrCreateCommandId(SqliteConnection connection, string commandLine, string commandHash) + { + // First try to get existing command + using var selectCommand = connection.CreateCommand(); + selectCommand.CommandText = "SELECT Id FROM Commands WHERE CommandHash = @CommandHash"; + selectCommand.Parameters.AddWithValue("@CommandHash", commandHash); + + var existingId = selectCommand.ExecuteScalar(); + if (existingId != null) + { + return Convert.ToInt64(existingId); + } + + // Insert new command + using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = @" +INSERT INTO Commands (CommandLine, CommandHash) +VALUES (@CommandLine, @CommandHash) +RETURNING Id"; + insertCommand.Parameters.AddWithValue("@CommandLine", commandLine); + insertCommand.Parameters.AddWithValue("@CommandHash", commandHash); + + return Convert.ToInt64(insertCommand.ExecuteScalar()); + } + + // Helper method to get or create a location ID + private long GetOrCreateLocationId(SqliteConnection connection, string location) { - if (_historyFileMutex == null) + // First try to get existing location + using var selectCommand = connection.CreateCommand(); + selectCommand.CommandText = "SELECT Id FROM Locations WHERE Path = @Path"; + selectCommand.Parameters.AddWithValue("@Path", location); + + var existingId = selectCommand.ExecuteScalar(); + if (existingId != null) { - _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); + return Convert.ToInt64(existingId); } + // Insert new location + using var insertCommand = connection.CreateCommand(); + insertCommand.CommandText = @" +INSERT INTO Locations (Path) +VALUES (@Path) +RETURNING Id"; + insertCommand.Parameters.AddWithValue("@Path", location); + + return Convert.ToInt64(insertCommand.ExecuteScalar()); + } + + private void WriteHistoryToSQLite(int start, int end) + { + _historyFileMutex ??= new Mutex(false, GetHistorySaveFileMutexName()); + WithHistoryFileMutexDo(1000, () => { try @@ -382,12 +569,6 @@ private void WriteHistoryToSQLite(int start, int end) connection.Open(); using var transaction = connection.BeginTransaction(); - using var command = connection.CreateCommand(); - command.CommandText = "INSERT INTO History (CommandLine, StartTime, ElapsedTime, Location) VALUES (@CommandLine, @StartTime, @ElapsedTime, @Location)"; - command.Parameters.Add(new SqliteParameter("@CommandLine", string.Empty)); - command.Parameters.Add(new SqliteParameter("@StartTime", string.Empty)); - command.Parameters.Add(new SqliteParameter("@ElapsedTime", 0L)); - command.Parameters.Add(new SqliteParameter("@Location", string.Empty)); for (var i = start; i <= end; i++) { @@ -399,10 +580,33 @@ private void WriteHistoryToSQLite(int start, int end) continue; } - command.Parameters["@CommandLine"].Value = item.CommandLine; - command.Parameters["@StartTime"].Value = item.StartTime.ToString("o"); - command.Parameters["@ElapsedTime"].Value = item.ApproximateElapsedTime.Ticks; - command.Parameters["@Location"].Value = _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown"; + // Generate command hash using SHA256 + string commandHash = ComputeCommandHash(item.CommandLine); + string location = item.Location ?? _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown"; + + // Get or create command and location IDs + long commandId = GetOrCreateCommandId(connection, item.CommandLine, commandHash); + long locationId = GetOrCreateLocationId(connection, location); + + // Convert DateTime to Unix timestamp (INTEGER) + long startTimeUnix = ((DateTimeOffset)item.StartTime).ToUnixTimeSeconds(); + long lastExecutedUnix = ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds(); + + // Insert or update execution history + using var command = connection.CreateCommand(); + command.CommandText = @" +INSERT INTO ExecutionHistory (CommandId, LocationId, StartTime, ElapsedTime, ExecutionCount, LastExecuted) +VALUES (@CommandId, @LocationId, @StartTime, @ElapsedTime, 1, @LastExecuted) +ON CONFLICT(CommandId, LocationId) DO UPDATE SET + ExecutionCount = ExecutionCount + 1, + LastExecuted = excluded.LastExecuted, + ElapsedTime = excluded.ElapsedTime"; + + command.Parameters.AddWithValue("@CommandId", commandId); + command.Parameters.AddWithValue("@LocationId", locationId); + command.Parameters.AddWithValue("@StartTime", startTimeUnix); + command.Parameters.AddWithValue("@ElapsedTime", item.ApproximateElapsedTime.Ticks); + command.Parameters.AddWithValue("@LastExecuted", lastExecutedUnix); command.ExecuteNonQuery(); } @@ -415,6 +619,13 @@ private void WriteHistoryToSQLite(int start, int end) }); } + private static string ComputeCommandHash(string command) + { + using var sha256 = System.Security.Cryptography.SHA256.Create(); + byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(command)); + return BitConverter.ToString(hashBytes).Replace("-", ""); + } + private void SaveHistoryAtExit() { WriteHistoryRange(0, _history.Count - 1, overwritten: true); @@ -585,11 +796,12 @@ private List ReadHistorySQLiteIncrementally() using (var command = connection.CreateCommand()) { + // Use the HistoryView to get all the joined data, filtering by ExecutionHistory.Id command.CommandText = @" - SELECT CommandLine, StartTime, ElapsedTime, Location - FROM History - WHERE Id > @LastId - ORDER BY Id ASC"; +SELECT CommandLine, StartTime, ElapsedTime, Location +FROM HistoryView +WHERE Id > @LastId +ORDER BY Id ASC"; command.Parameters.AddWithValue("@LastId", _historyFileLastSavedSize); using var reader = command.ExecuteReader(); @@ -598,7 +810,7 @@ WHERE Id > @LastId var item = new HistoryItem { CommandLine = reader.GetString(0), - StartTime = DateTime.Parse(reader.GetString(1)), + StartTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64(1)).DateTime, ApproximateElapsedTime = TimeSpan.FromTicks(reader.GetInt64(2)), Location = reader.GetString(3), FromHistoryFile = true, @@ -611,10 +823,10 @@ WHERE Id > @LastId } } - // Update the last saved size to the latest ID in the database + // Update the last saved size to the latest ID in the ExecutionHistory table using (var command = connection.CreateCommand()) { - command.CommandText = "SELECT MAX(Id) FROM History"; + command.CommandText = "SELECT MAX(Id) FROM ExecutionHistory"; var result = command.ExecuteScalar(); if (result != DBNull.Value) { @@ -647,14 +859,8 @@ private bool MaybeReadHistoryFile() { foreach (var item in historyItems) { - // Use the full metadata from SQLite - MaybeAddToHistory( - item.CommandLine, - item._edits ?? new List { EditItemInsertString.Create(item.CommandLine, 0) }, - item._undoEditIndex, - item.Location, - fromDifferentSession: true, - fromInitialRead: false); + _history.Enqueue(item); + _currentHistoryIndex = _history.Count; } } } @@ -691,18 +897,30 @@ private void ReadSQLiteHistory(bool fromOtherSession) connection.Open(); using var command = connection.CreateCommand(); + + int limit = Options.MaximumHistoryCount switch + { + <= 10000 => 10000, // Similar to 0.5MB text optimization + <= 20000 => 20000, // Similar to 1MB text optimization + _ => Options.MaximumHistoryCount + }; + + // Use the view for easy querying command.CommandText = @" - SELECT CommandLine, StartTime, ElapsedTime, Location - FROM History - ORDER BY Id ASC"; +SELECT CommandLine, StartTime, ElapsedTime, Location +FROM HistoryView +ORDER BY LastExecuted DESC +LIMIT @Limit"; + command.Parameters.AddWithValue("@Limit", limit); + var historyItems = new List(); using var reader = command.ExecuteReader(); while (reader.Read()) { var item = new HistoryItem { CommandLine = reader.GetString(0), - StartTime = DateTime.Parse(reader.GetString(1)), + StartTime = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64(1)).DateTime, ApproximateElapsedTime = TimeSpan.FromTicks(reader.GetInt64(2)), Location = reader.GetString(3), FromHistoryFile = true, @@ -712,19 +930,23 @@ FROM History _editGroupStart = -1 }; - // Add to the history queue. + historyItems.Add(item); + } + + historyItems.Reverse(); + + foreach (var item in historyItems) + { _history.Enqueue(item); } // Update the last saved size to the latest ID in the database - using (var idCommand = connection.CreateCommand()) + using var idCommand = connection.CreateCommand(); + idCommand.CommandText = "SELECT MAX(Id) FROM ExecutionHistory"; + var result = idCommand.ExecuteScalar(); + if (result != DBNull.Value) { - idCommand.CommandText = "SELECT MAX(Id) FROM History"; - var result = idCommand.ExecuteScalar(); - if (result != DBNull.Value) - { - _historyFileLastSavedSize = Convert.ToInt64(result); - } + _historyFileLastSavedSize = Convert.ToInt64(result); } } catch (SqliteException ex) @@ -750,54 +972,54 @@ private void ReadHistoryFile() _historyFileLastSavedSize = fileInfo.Length; }); } + } - static IEnumerable ReadHistoryLinesImpl(string path, int historyCount) - { - const long offset_1mb = 1048576; - const long offset_05mb = 524288; + private IEnumerable ReadHistoryLinesImpl(string path, int historyCount) + { + const long offset_1mb = 1048576; + const long offset_05mb = 524288; - // 1mb content contains more than 34,000 history lines for a typical usage, which should be - // more than enough to cover 20,000 history records (a history record could be a multi-line - // command). Similarly, 0.5mb content should be enough to cover 10,000 history records. - // We optimize the file reading when the history count falls in those ranges. If the history - // count is even larger, which should be very rare, we just read all lines. - long offset = historyCount switch - { - <= 10000 => offset_05mb, - <= 20000 => offset_1mb, - _ => 0, - }; + // 1mb content contains more than 34,000 history lines for a typical usage, which should be + // more than enough to cover 20,000 history records (a history record could be a multi-line + // command). Similarly, 0.5mb content should be enough to cover 10,000 history records. + // We optimize the file reading when the history count falls in those ranges. If the history + // count is even larger, which should be very rare, we just read all lines. + long offset = historyCount switch + { + <= 10000 => offset_05mb, + <= 20000 => offset_1mb, + _ => 0, + }; - using var fs = new FileStream(path, FileMode.Open); - using var sr = new StreamReader(fs); + using var fs = new FileStream(path, FileMode.Open); + using var sr = new StreamReader(fs); - if (offset > 0 && fs.Length > offset) - { - // When the file size is larger than the offset, we only read that amount of content from the end. - fs.Seek(-offset, SeekOrigin.End); + if (offset > 0 && fs.Length > offset) + { + // When the file size is larger than the offset, we only read that amount of content from the end. + fs.Seek(-offset, SeekOrigin.End); - // After seeking, the current position may point at the middle of a history record, or even at a - // byte within a UTF-8 character (history file is saved with UTF-8 encoding). So, let's ignore the - // first line read from that position. - sr.ReadLine(); + // After seeking, the current position may point at the middle of a history record, or even at a + // byte within a UTF-8 character (history file is saved with UTF-8 encoding). So, let's ignore the + // first line read from that position. + sr.ReadLine(); - string line; - while ((line = sr.ReadLine()) is not null) + string line; + while ((line = sr.ReadLine()) is not null) + { + if (!line.EndsWith("`", StringComparison.Ordinal)) { - if (!line.EndsWith("`", StringComparison.Ordinal)) - { - // A complete history record is guaranteed to start from the next line. - break; - } + // A complete history record is guaranteed to start from the next line. + break; } } + } - // Read lines in the streaming way, so it won't consume to much memory even if we have to - // read all lines from a large history file. - while (!sr.EndOfStream) - { - yield return sr.ReadLine(); - } + // Read lines in the streaming way, so it won't consume to much memory even if we have to + // read all lines from a large history file. + while (!sr.EndOfStream) + { + yield return sr.ReadLine(); } } @@ -1170,21 +1392,6 @@ private void SaveCurrentLine() } } - private int FindHistoryIndexByLocation(int startIndex, int direction) - { - int index = startIndex; - while (index >= 0 && index < _history.Count) - { - var currentLocation = _engineIntrinsics?.SessionState.Path.CurrentLocation.Path; - if (string.Equals(_history[index].Location, currentLocation, StringComparison.OrdinalIgnoreCase)) - { - return index; - } - index += direction; - } - return -1; // Not found - } - private void HistoryRecall(int direction) { if (_recallHistoryCommandCount == 0 && LineIsMultiLine()) @@ -1203,14 +1410,8 @@ private void HistoryRecall(int direction) int newHistoryIndex = _currentHistoryIndex; while (count > 0) { - if (_options.HistoryType == HistoryType.SQLite) - { - newHistoryIndex = FindHistoryIndexByLocation(newHistoryIndex + direction, direction); - } - else - { - newHistoryIndex += direction; - } + newHistoryIndex += direction; + if (newHistoryIndex < 0 || newHistoryIndex >= _history.Count) { break; diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 910cfa791..45c58227e 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -43,6 +43,7 @@ private void SetOptionsInternal(SetPSReadLineOption options) // For now remove all text history _singleton._history?.Clear(); _singleton._currentHistoryIndex = 0; + ReadSQLiteHistory(fromOtherSession: false); } } diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 5d96701ac..411484ffe 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -896,10 +896,6 @@ private void DelayedOneTimeInitialize() { ReadHistoryFile(); } - else if (readHistoryFile && _options.HistoryType == HistoryType.SQLite) - { - ReadSQLiteHistory(fromOtherSession: false); - } _killIndex = -1; // So first add indexes 0. _killRing = new List(Options.MaximumKillRingCount); From 3a7a6045494cb6fd9ea77787b30a81632549171e Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:09:58 -0500 Subject: [PATCH 3/3] Remove runtime identifier and use conditional packages --- PSReadLine.build.ps1 | 2 +- PSReadLine/PSReadLine.csproj | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index bb1ab77d4..e245dfae3 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -125,7 +125,7 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { Copy-Item "Polyfill/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/netstd" -Force Copy-Item "Polyfill/bin/$Configuration/net6.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net6plus" -Force - $binPath = "PSReadLine/bin/$Configuration/netstandard2.0/win-x64/publish" + $binPath = "PSReadLine/bin/$Configuration/netstandard2.0/publish" Copy-Item $binPath/Microsoft.PowerShell.PSReadLine.dll $targetDir Copy-Item $binPath/Microsoft.PowerShell.Pager.dll $targetDir diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 81c556dfc..22ce78b29 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -14,7 +14,6 @@ false 9.0 true - win-x64 @@ -22,7 +21,13 @@ - + + + + + + +