diff --git a/.vscode/launch.json b/.vscode/launch.json
index c0100623..dd313207 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/Cmdlets.cs b/PSReadLine/Cmdlets.cs
index 7fecf4b4..e6b18a95 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 c1490a23..be4e1822 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,11 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi
return AddToHistoryOption.SkipAdding;
}
+ if (Options.HistoryType is HistoryType.SQLite)
+ {
+ return AddToHistoryOption.SQLite;
+ }
+
if (!fromHistoryFile && Options.AddToHistoryHandler != null)
{
if (Options.AddToHistoryHandler == PSConsoleReadLineOptions.DefaultAddToHistoryHandler)
@@ -197,10 +209,209 @@ private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFi
return AddToHistoryOption.MemoryAndFile;
}
+ private void InitializeSQLiteDatabase()
+ {
+ 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 "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", "Commands");
+
+ var result = command.ExecuteScalar();
+ bool isNewDatabase = result == null;
+
+ // If the table doesn't exist, create the normalized schema
+ if (isNewDatabase)
+ {
+ 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)
+ {
+ Console.WriteLine($"SQLite error initializing database: {ex.Message}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error initializing SQLite database: {ex.Message}");
+ }
+ }
+
+ 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
+ }
+ }
+
+ 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)
+ {
+ 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}");
+ }
+ }
+
private string MaybeAddToHistory(
string result,
List edits,
int undoEditIndex,
+ string location = null,
bool fromDifferentSession = false,
bool fromInitialRead = false)
{
@@ -216,6 +427,7 @@ private string MaybeAddToHistory(
_undoEditIndex = undoEditIndex,
_editGroupStart = -1,
_saved = fromHistoryFile,
+ Location = location ?? _engineIntrinsics?.SessionState?.Path?.CurrentLocation?.Path ?? "Unknown",
FromOtherSession = fromDifferentSession,
FromHistoryFile = fromInitialRead,
};
@@ -277,7 +489,141 @@ 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);
+ }
+ }
+
+ // 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)
+ {
+ // 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)
+ {
+ 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
+ {
+ 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();
+
+ for (var i = start; i <= end; i++)
+ {
+ var item = _history[i];
+ item._saved = true;
+
+ if (item._sensitive)
+ {
+ continue;
+ }
+
+ // 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();
+ }
+
+ transaction.Commit();
+ }
+ catch (Exception e)
+ {
+ ReportHistoryFileError(e);
+ }
+ });
+ }
+
+ 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()
@@ -411,6 +757,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 +780,184 @@ 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())
+ {
+ // Use the HistoryView to get all the joined data, filtering by ExecutionHistory.Id
+ command.CommandText = @"
+SELECT CommandLine, StartTime, ElapsedTime, Location
+FROM HistoryView
+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 = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64(1)).DateTime,
+ 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 ExecutionHistory table
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandText = "SELECT MAX(Id) FROM ExecutionHistory";
+ 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)
+ {
+ _history.Enqueue(item);
+ _currentHistoryIndex = _history.Count;
+ }
+ }
+ }
+ 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();
+
+ 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 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 = DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64(1)).DateTime,
+ 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
+ };
+
+ 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();
+ idCommand.CommandText = "SELECT MAX(Id) FROM ExecutionHistory";
+ 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()
@@ -463,54 +972,54 @@ private void ReadHistoryFile()
_historyFileLastSavedSize = fileInfo.Length;
});
}
+ }
- static IEnumerable ReadHistoryLinesImpl(string path, int historyCount)
+ 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
{
- const long offset_1mb = 1048576;
- const long offset_05mb = 524288;
+ <= 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();
}
}
@@ -529,13 +1038,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);
}
}
}
@@ -902,6 +1411,7 @@ private void HistoryRecall(int direction)
while (count > 0)
{
newHistoryIndex += direction;
+
if (newHistoryIndex < 0 || newHistoryIndex >= _history.Count)
{
break;
@@ -931,7 +1441,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 7485154b..45c58227 100644
--- a/PSReadLine/Options.cs
+++ b/PSReadLine/Options.cs
@@ -26,6 +26,27 @@ 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 +148,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 1c8090f8..22ce78b2 100644
--- a/PSReadLine/PSReadLine.csproj
+++ b/PSReadLine/PSReadLine.csproj
@@ -13,12 +13,21 @@
true
false
9.0
+ true
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -28,5 +37,6 @@
+
-
+
\ No newline at end of file
diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml
index 24f70799..f86c7a25 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 bd1448b9..179c01d5 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 d810a328..411484ff 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,7 +892,7 @@ private void DelayedOneTimeInitialize()
{
}
- if (readHistoryFile)
+ if (readHistoryFile && _options.HistoryType == HistoryType.Text)
{
ReadHistoryFile();
}
diff --git a/nuget.config b/nuget.config
index a10ce9b3..a079cf1b 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,8 +1,7 @@
-
-
+
diff --git a/test/HistoryTest.cs b/test/HistoryTest.cs
index ad6b95f0..d400d604 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));
+ }
+
+
}
}