From e94afef3a04a9512ab21ec7b5e8cd8c53df67462 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Wed, 27 Apr 2022 10:56:26 -0700 Subject: [PATCH 001/127] Sync ReadKeyProc thread with pipeline thread (#3294) The pipeline thread was returning before the `ReadKeyProcThread` had finished processing the dummy input. This was occurring somewhat frequently when `ReadLine` was invoked multiple times in a row creating a race condition. With these changes, the pipeline thread will wait for dummy input to be received, dequeue the key, and continue with normal cancellation logic. --- PSReadLine/ReadLine.cs | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index e76c78d66..fa7d93a9c 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -33,15 +33,11 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods { private const int ConsoleExiting = 1; - private const int CancellationRequested = 2; - // *must* be initialized in the static ctor // because the static member _clipboard depends upon it // for its own initialization private static readonly PSConsoleReadLine _singleton; - private static readonly CancellationToken _defaultCancellationToken = new CancellationTokenSource().Token; - // This is used by PowerShellEditorServices (the backend of the PowerShell VSCode extension) // so that it can call PSReadLine from a delegate and not hit nested pipeline issues. #pragma warning disable CS0649 @@ -143,17 +139,7 @@ private void ReadOneOrMoreKeys() } while (_charMap.KeyAvailable) { - ConsoleKeyInfo keyInfo = _charMap.ReadKey(); - if (_cancelReadCancellationToken.IsCancellationRequested) - { - // If PSReadLine is running under a host that can cancel it, the - // cancellation will come at a time when ReadKey is stuck waiting for input. - // The next key press will be used to force it to return, and so we want to - // discard this key since we were already canceled. - continue; - } - - var key = PSKeyInfo.FromConsoleKeyInfo(keyInfo); + var key = PSKeyInfo.FromConsoleKeyInfo(_charMap.ReadKey()); _lastNKeys.Enqueue(key); _queuedKeys.Enqueue(key); } @@ -170,10 +156,6 @@ private void ReadKeyThreadProc() break; ReadOneOrMoreKeys(); - if (_cancelReadCancellationToken.IsCancellationRequested) - { - continue; - } // One or more keys were read - let ReadKey know we're done. _keyReadWaitHandle.Set(); @@ -208,7 +190,6 @@ internal static PSKeyInfo ReadKey() // - a key is pressed // - the console is exiting // - 300ms timeout - to process events if we're idle - // - ReadLine cancellation is requested externally handleId = WaitHandle.WaitAny(_singleton._requestKeyWaitHandles, 300); if (handleId != WaitHandle.WaitTimeout) { @@ -292,10 +273,12 @@ internal static PSKeyInfo ReadKey() throw new OperationCanceledException(); } - if (handleId == CancellationRequested) + if (_singleton._cancelReadCancellationToken.IsCancellationRequested) { - // ReadLine was cancelled. Save the current line to be restored next time ReadLine - // is called, clear the buffer and throw an exception so we can return an empty string. + // ReadLine was cancelled. Dequeue the dummy input sent by the host, save the current + // line to be restored next time ReadLine is called, clear the buffer and throw an + // exception so we can return an empty string. + _singleton._queuedKeys.Dequeue(); _singleton.SaveCurrentLine(); _singleton._getNextHistoryIndex = _singleton._history.Count; _singleton._current = 0; @@ -331,9 +314,7 @@ private void PrependQueuedKeys(PSKeyInfo key) /// The complete command line. public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, bool? lastRunStatus) { - // Use a default cancellation token instead of CancellationToken.None because the - // WaitHandle is shared and could be triggered accidently. - return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken, lastRunStatus); + return ReadLine(runspace, engineIntrinsics, CancellationToken.None, lastRunStatus); } /// @@ -396,7 +377,6 @@ public static string ReadLine( } _singleton._cancelReadCancellationToken = cancellationToken; - _singleton._requestKeyWaitHandles[2] = cancellationToken.WaitHandle; return _singleton.InputLoop(); } catch (OperationCanceledException) @@ -877,7 +857,7 @@ private void DelayedOneTimeInitialize() _readKeyWaitHandle = new AutoResetEvent(false); _keyReadWaitHandle = new AutoResetEvent(false); _closingWaitHandle = new ManualResetEvent(false); - _requestKeyWaitHandles = new WaitHandle[] {_keyReadWaitHandle, _closingWaitHandle, null}; + _requestKeyWaitHandles = new WaitHandle[] {_keyReadWaitHandle, _closingWaitHandle}; _threadProcWaitHandles = new WaitHandle[] {_readKeyWaitHandle, _closingWaitHandle}; // This is for a "being hosted in an alternate appdomain scenario" (the From 0d49231e0633745d246594b4c7f65ad33e7acaa9 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 27 Apr 2022 15:07:30 -0700 Subject: [PATCH 002/127] Prepare for the `2.2.4-beta1` release of PSReadLine (#3295) --- PSReadLine/Changes.txt | 7 +++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 1ebef1056..7f48464e9 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,10 @@ +### [2.2.4-beta1] - 2022-04-27 + +- Sync ReadKeyProc thread with pipeline thread (#3294) +- Update build to use net462 (#3285) + +[2.2.4-beta1]: https://github.com/PowerShell/PSReadLine/compare/v2.2.3...v2.2.4-beta1 + ### [2.2.3] - 2022-04-20 - Respect cancellation in `ReadOneOrMoreKeys()` (#3274, #3280) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 4126da0b7..0df48b19c 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.2.3.0 - 2.2.3 - 2.2.3 + 2.2.4.0 + 2.2.4 + 2.2.4-beta1 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 88359a900..f7e69bce7 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.2.3' +ModuleVersion = '2.2.4' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 60a48d617723f4bc07e460dfcdb550662a3ef5ea Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 3 May 2022 13:00:17 -0700 Subject: [PATCH 003/127] Prepare for the `v2.2.5` release of PSReadLine (#3300) --- PSReadLine/Changes.txt | 4 ++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 7f48464e9..bcdc9eb1a 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,7 @@ +### [2.2.5] - 2022-05-03 + +- Re-package the `2.2.4-beta1` version to `2.2.5` as an offical servicing release. + ### [2.2.4-beta1] - 2022-04-27 - Sync ReadKeyProc thread with pipeline thread (#3294) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 0df48b19c..11b195fe6 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.2.4.0 - 2.2.4 - 2.2.4-beta1 + 2.2.5.0 + 2.2.5 + 2.2.5 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index f7e69bce7..738099e48 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.2.4' +ModuleVersion = '2.2.5' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 2ef36fb77c0c448ba33e8a9309d9838b97b27d40 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 22 Jun 2022 12:15:17 -0700 Subject: [PATCH 004/127] Enable IntelliSense prediction by default (#3351) --- PSReadLine/Cmdlets.cs | 8 ++++++-- PSReadLine/KeyBindings.cs | 5 +++-- PSReadLine/ReadLine.cs | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 9aa051e46..762f39b70 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -165,7 +165,7 @@ public class PSConsoleReadLineOptions /// public const int DefaultAnsiEscapeTimeout = 100; - public PSConsoleReadLineOptions(string hostName) + public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) { ResetColors(); EditMode = DefaultEditMode; @@ -185,7 +185,11 @@ public PSConsoleReadLineOptions(string hostName) HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive; HistorySaveStyle = DefaultHistorySaveStyle; AnsiEscapeTimeout = DefaultAnsiEscapeTimeout; - PredictionSource = DefaultPredictionSource; + PredictionSource = usingLegacyConsole + ? PredictionSource.None + : Environment.Version.Major < 6 + ? PredictionSource.History + : PredictionSource.HistoryAndPlugin; PredictionViewStyle = DefaultPredictionViewStyle; MaximumHistoryCount = 0; diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index ce73a1006..9315cd560 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -230,6 +230,8 @@ void SetDefaultWindowsBindings() { Keys.AltMinus, MakeKeyHandler(DigitArgument, "DigitArgument") }, { Keys.AltQuestion, MakeKeyHandler(WhatIsKey, "WhatIsKey") }, { Keys.AltA, MakeKeyHandler(SelectCommandArgument, "SelectCommandArgument") }, + { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, + { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, { Keys.F3, MakeKeyHandler(CharacterSearch, "CharacterSearch") }, { Keys.ShiftF3, MakeKeyHandler(CharacterSearchBackward, "CharacterSearchBackward") }, @@ -239,8 +241,6 @@ void SetDefaultWindowsBindings() { Keys.AltD, MakeKeyHandler(KillWord, "KillWord") }, { Keys.CtrlAt, MakeKeyHandler(MenuComplete, "MenuComplete") }, { Keys.CtrlW, MakeKeyHandler(BackwardKillWord, "BackwardKillWord") }, - { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, - { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, }; // Some bindings are not available on certain platforms @@ -338,6 +338,7 @@ void SetDefaultEmacsBindings() { Keys.AltA, MakeKeyHandler(SelectCommandArgument, "SelectCommandArgument") }, { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, + { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, }; // Some bindings are not available on certain platforms diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index fa7d93a9c..e38b05c55 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -686,7 +686,9 @@ private PSConsoleReadLine() { hostName = PSReadLine; } - _options = new PSConsoleReadLineOptions(hostName); + + bool usingLegacyConsole = _console is PlatformWindows.LegacyWin32Console; + _options = new PSConsoleReadLineOptions(hostName, usingLegacyConsole); _prediction = new Prediction(this); SetDefaultBindings(_options.EditMode); } From d82abc88927c46aa4596fad56c54cbba64c1467f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jun 2022 13:34:14 -0700 Subject: [PATCH 005/127] Bump `Newtonsoft.Json` from 12.0.3 to 13.0.1 in test (#3353) --- test/PSReadLine.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index b9cea03c6..e79fa34e8 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -19,7 +19,7 @@ - + From f4da1260565b86166cc72f10d563588084b9652e Mon Sep 17 00:00:00 2001 From: "msftbot[bot]" <48340428+msftbot[bot]@users.noreply.github.com> Date: Fri, 24 Jun 2022 09:03:01 -0700 Subject: [PATCH 006/127] Migrate FabricBot Tasks to Config-as-Code (add `.github/fabricbot.json`) (#3355) --- .github/fabricbot.json | 948 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 948 insertions(+) create mode 100644 .github/fabricbot.json diff --git a/.github/fabricbot.json b/.github/fabricbot.json new file mode 100644 index 000000000..493058bb4 --- /dev/null +++ b/.github/fabricbot.json @@ -0,0 +1,948 @@ +{ + "version": "1.0", + "tasks": [ + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "config": { + "taskName": "Add needs-triage label to new issues", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "opened" + } + }, + { + "operator": "not", + "operands": [ + { + "name": "isPartOfProject", + "parameters": {} + } + ] + }, + { + "operator": "not", + "operands": [ + { + "name": "isAssignedToSomeone", + "parameters": {} + } + ] + } + ] + }, + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "Needs-Triage :mag:" + } + } + ], + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ] + }, + "id": "i3BHh5Qe9F" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueCommentResponder", + "version": "1.0", + "config": { + "taskName": "Replace needs author feedback label with needs attention label when the author comments on an issue", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "created" + } + }, + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + }, + { + "name": "isOpen", + "parameters": {} + } + ] + }, + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "Needs-Attention :wave:" + } + }, + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ], + "eventType": "issue", + "eventNames": [ + "issue_comment" + ] + }, + "id": "x_h9ia7zwG" + }, + { + "taskType": "trigger", + "capabilityId": "CodeFlowLink", + "subCapability": "CodeFlowLink", + "version": "1.0", + "config": { + "taskName": "Add a CodeFlow link to new pull requests" + }, + "id": "z2P6L1OkC4" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestReviewResponder", + "version": "1.0", + "config": { + "taskName": "Add needs author feedback label to pull requests when changes are requested", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "submitted" + } + }, + { + "name": "isReviewState", + "parameters": { + "state": "changes_requested" + } + } + ] + }, + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "pull_request_review" + ] + }, + "id": "s_Q6W352PU" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestResponder", + "version": "1.0", + "config": { + "taskName": "Remove needs author feedback label when the author responds to a pull request", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "operator": "not", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "closed" + } + } + ] + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "pull_request", + "issues", + "project_card" + ] + }, + "id": "2rzxLdRUQ7h" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestCommentResponder", + "version": "1.0", + "config": { + "taskName": "Remove needs author feedback label when the author comments on a pull request", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "issue_comment" + ] + }, + "id": "p7EkOxE_g3T" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestReviewResponder", + "version": "1.0", + "config": { + "taskName": "Remove needs author feedback label when the author responds to a pull request review comment", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "pull_request_review" + ] + }, + "id": "xIoNg4bVKyA" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestResponder", + "version": "1.0", + "config": { + "taskName": "Remove no recent activity label from pull requests", + "conditions": { + "operator": "and", + "operands": [ + { + "operator": "not", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "closed" + } + } + ] + }, + { + "name": "hasLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "pull_request", + "issues", + "project_card" + ] + }, + "id": "o_mQ1sO0zdO" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestCommentResponder", + "version": "1.0", + "config": { + "taskName": "Remove no recent activity label when a pull request is commented on", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "hasLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "issue_comment" + ] + }, + "id": "ibqxdSfreaD" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "PullRequestReviewResponder", + "version": "1.0", + "config": { + "taskName": "Remove no recent activity label when a pull request is reviewed", + "conditions": { + "operator": "and", + "operands": [ + { + "name": "hasLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ] + }, + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ], + "eventType": "pull_request", + "eventNames": [ + "pull_request_review" + ] + }, + "id": "7_EH-4ffdtY" + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "taskName": "Close stale pull requests", + "frequency": [ + { + "weekDay": 0, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 1, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 2, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 3, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 4, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 5, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 6, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + } + ], + "searchTerms": [ + { + "name": "isPr", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + } + ], + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + }, + "id": "X3R7HwIUme_" + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "taskName": "Add no recent activity label to pull requests", + "frequency": [ + { + "weekDay": 0, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 1, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 2, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 3, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 4, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 5, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + }, + { + "weekDay": 6, + "hours": [ + 3, + 9, + 15, + 21 + ], + "timezoneOffset": -8 + } + ], + "searchTerms": [ + { + "name": "isPr", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 14 + } + }, + { + "name": "noLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + } + ], + "actions": [ + { + "name": "addLabel", + "parameters": { + "label": "Status-No Recent Activity" + } + }, + { + "name": "addReply", + "parameters": { + "comment": "This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **14 days**. It will be closed if no further activity occurs **within 7 days of this comment**." + } + } + ] + }, + "id": "zYmFzaIHtT8" + }, + { + "taskType": "trigger", + "capabilityId": "AutoMerge", + "subCapability": "AutoMerge", + "version": "1.0", + "config": { + "taskName": "Automatically merge pull requests", + "label": "Auto Merge", + "silentMode": false, + "minMinutesOpen": "1440", + "mergeType": "squash", + "allowAutoMergeInstructionsWithoutLabel": false, + "deleteBranches": true, + "removeLabelOnPush": true + }, + "id": "BPA0tvDiHBN" + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssuesOnlyResponder", + "version": "1.0", + "id": "kvB9kCm1d", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "isAction", + "parameters": { + "action": "closed" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Triage :mag:" + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issues", + "project_card" + ], + "taskName": "Remove needs-triage label when an issue is closed", + "actions": [ + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Triage :mag:" + } + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "28wu6aj_J", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Author Feedback" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + } + ], + "taskName": "Close stale issues", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "InPrLabel", + "subCapability": "InPrLabel", + "version": "1.0", + "id": "dfYD-29Up", + "config": { + "taskName": "Add 'In-PR' label to issue", + "label_inPr": "In-PR", + "fixedLabelEnabled": true, + "label_fixed": "Resolution-Fixed" + } + }, + { + "taskType": "trigger", + "capabilityId": "ReleaseAnnouncement", + "subCapability": "ReleaseAnnouncement", + "version": "1.0", + "id": "_vafvxO3x", + "config": { + "taskName": "Release announcement for Issue/PR", + "prReply": ":tada: [`${version}`](https://github.com/PowerShell/PSReadLine/releases/tag/${version}) has been released which incorporates this pull request. :tada:\n", + "issueReply": ":tada: This issue was addressed in ${prNumber}, which has now been successfully released in [`${version}`](https://github.com/PowerShell/PSReadLine/releases/tag/${version}). :tada:", + "packageRegex": "(v\\d+\\.\\d+\\.\\d+(-\\w+)?)", + "packageVersionGroup": 0, + "referencedPrsRegex": "\\(#(\\d+)\\)" + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "id": "HDx9Yd09Z1iue7fv7A22t", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 1, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 2, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 3, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 4, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 5, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + }, + { + "weekDay": 6, + "hours": [ + 7, + 19 + ], + "timezoneOffset": -7 + } + ], + "searchTerms": [ + { + "name": "isIssue", + "parameters": {} + }, + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Question-Answered" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 1 + } + } + ], + "taskName": "Close answered issues", + "actions": [ + { + "name": "addReply", + "parameters": { + "comment": "This issue has been marked as answered and has not had any activity for **1 day**. It has been closed for housekeeping purposes." + } + }, + { + "name": "closeIssue", + "parameters": {} + } + ] + } + } + ], + "userGroups": [] +} From 59caa5f76271cb22fab053330d81c7bb10ddb6da Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Jun 2022 09:58:15 -0700 Subject: [PATCH 007/127] Prepare for the v2.2.6 release of PSReadLine (#3357) --- PSReadLine/Changes.txt | 4 ++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index bcdc9eb1a..f0680d4e6 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,7 @@ +### [2.2.6] - 2022-06-27 + +- Enable Predictive Intellisense by default (#3351) + ### [2.2.5] - 2022-05-03 - Re-package the `2.2.4-beta1` version to `2.2.5` as an offical servicing release. diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 11b195fe6..7ce0148ed 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.2.5.0 - 2.2.5 - 2.2.5 + 2.2.6.0 + 2.2.6 + 2.2.6 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 738099e48..befc6e11a 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.2.5' +ModuleVersion = '2.2.6' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 34e5c3077657ca03cfe79b7d787578d2393b1d7e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Jun 2022 10:26:19 -0700 Subject: [PATCH 008/127] Update module version in bot messages (#3361) --- PSReadLine/Changes.txt | 4 ++++ tools/issue-mgmt/CloseDupIssues.ps1 | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index f0680d4e6..a3822e36d 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -2,10 +2,14 @@ - Enable Predictive Intellisense by default (#3351) +[2.2.6]: https://github.com/PowerShell/PSReadLine/compare/v2.2.5...v2.2.6 + ### [2.2.5] - 2022-05-03 - Re-package the `2.2.4-beta1` version to `2.2.5` as an offical servicing release. +[2.2.5]: https://github.com/PowerShell/PSReadLine/compare/v2.2.4-beta1...v2.2.5 + ### [2.2.4-beta1] - 2022-04-27 - Sync ReadKeyProc thread with pipeline thread (#3294) diff --git a/tools/issue-mgmt/CloseDupIssues.ps1 b/tools/issue-mgmt/CloseDupIssues.ps1 index ab5d5726c..65587a733 100644 --- a/tools/issue-mgmt/CloseDupIssues.ps1 +++ b/tools/issue-mgmt/CloseDupIssues.ps1 @@ -11,7 +11,7 @@ class issue $repo_name = "PowerShell/PSReadLine" $root_url = "https://github.com/PowerShell/PSReadLine/issues" $msg_upgrade = @" -Please upgrade to the [2.2.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.3) from PowerShell Gallery. +Please upgrade to the [2.2.6 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.6) from PowerShell Gallery. See the [upgrading section](https://github.com/PowerShell/PSReadLine#upgrading) for instructions. Please let us know if you run into the same issue with the latest version. "@ @@ -46,7 +46,7 @@ foreach ($item in $issues) $body -match 'PSReadLine: 2\.2\.0-beta[12]') { $comment = @' -This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.2.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.3). +This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.2.6 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.6). To upgrade, simply run `Install-Module PSReadLine -AllowPrerelease -Force` from your PowerShell console. -------- From 0ac6c076755e6f58fbf7fca6d62653e3d3986c48 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Jun 2022 11:04:10 -0700 Subject: [PATCH 009/127] Handle multi-line description for parameter help content (#3358) --- PSReadLine/DynamicHelp.cs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 94657688e..a7cdb3927 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -202,28 +202,40 @@ private void WriteParameterHelp(dynamic helpContent) { helpBlock = new Collection() { - String.Empty, + string.Empty, PSReadLineResources.NeedsUpdateHelp }; } else { string syntax = $"-{helpContent.name} <{helpContent.type.name}>"; - string desc = "DESC: " + helpContent.Description[0].Text; - - // trim new line characters as some help content has it at the end of the first list on the description. - desc = desc.Trim('\r', '\n'); - - string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; - helpBlock = new Collection { string.Empty, syntax, - string.Empty, - desc, - details + string.Empty }; + + string text = helpContent.Description[0].Text; + if (text.Contains("\n")) + { + string[] lines = text.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < lines.Length; i++) + { + string prefix = i == 0 ? "DESC: " : " "; + string s = prefix + lines[i].Trim('\r'); + helpBlock.Add(s); + } + } + else + { + string desc = "DESC: " + text; + // trim new line characters as some help content has it at the end of the first list on the description. + helpBlock.Add(desc.Trim('\r', '\n')); + } + + string details = $"Required: {helpContent.required}, Position: {helpContent.position}, Default Value: {helpContent.defaultValue}, Pipeline Input: {helpContent.pipelineInput}, WildCard: {helpContent.globbing}"; + helpBlock.Add(details); } WriteDynamicHelpBlock(helpBlock); From 36893ee0f3143e7f490cb64009a3b3ab83de693c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 5 Jul 2022 16:11:26 -0700 Subject: [PATCH 010/127] Fix parameter dynamic help when the help content is specified in ParameterAttribute (#3370) --- PSReadLine/DynamicHelp.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index a7cdb3927..6334c59b6 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -198,7 +198,12 @@ private void WriteParameterHelp(dynamic helpContent) { Collection helpBlock; - if (string.IsNullOrEmpty(helpContent?.Description?[0]?.Text)) + if (helpContent?.Description is not string descriptionText) + { + descriptionText = helpContent?.Description?[0]?.Text; + } + + if (descriptionText is null) { helpBlock = new Collection() { @@ -216,7 +221,7 @@ private void WriteParameterHelp(dynamic helpContent) string.Empty }; - string text = helpContent.Description[0].Text; + string text = descriptionText; if (text.Contains("\n")) { string[] lines = text.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); From fbaba3e63bea3861dd0c2269489c9032f6f2b31a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 8 Jul 2022 15:30:50 -0700 Subject: [PATCH 011/127] Fix `ViModeIndicator = Cursor` for Windows Terminal (#3374) --- PSReadLine/ConsoleLib.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/PSReadLine/ConsoleLib.cs b/PSReadLine/ConsoleLib.cs index ff8c1e6d9..7cc53b341 100644 --- a/PSReadLine/ConsoleLib.cs +++ b/PSReadLine/ConsoleLib.cs @@ -31,7 +31,7 @@ public int CursorTop set => Console.CursorTop = value; } - // .NET doesn't implement this API, so we fake it with a commonly supported escape sequence. + // .NET doesn't fully implement this API on all platforms, so we fake it with a commonly supported escape sequence. protected int _unixCursorSize = 25; public virtual int CursorSize { @@ -45,9 +45,15 @@ public virtual int CursorSize else { _unixCursorSize = value; - // Solid blinking block or blinking vertical bar - Write(value > 50 ? "\x1b[2 q" : "\x1b[5 q"); } + + // See the cursor ANSI codes at https://www.real-world-systems.com/docs/ANSIcode.html, searching for 'blinking block'. + // We write out the ANSI escape sequence even if we are on Windows, where the 'Console.CursorSize' API is supported by .NET. + // This is because this API works fine in console host, but not in Windows Terminal. The escape sequence will configure the + // cursor as expected in Windows Terminal, while in console host, the escape sequence works after it's written out, but then + // will be overwritten by 'CursorSize' when the user continues typing. + // We use blinking block and blinking underscore, so as to mimic the cursor size 100 and 25 in console host. + Write(value > 50 ? "\x1b[1 q" : "\x1b[3 q"); } } From e9d42424bf626bdd175542c4b10dd25408989315 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 8 Jul 2022 15:32:07 -0700 Subject: [PATCH 012/127] Fix wrong cursor position in menu completion (#3373) --- PSReadLine/Completion.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 6506f24c9..0e7f8108f 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -561,6 +561,7 @@ public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips // Determine if showing the tooltip would scroll the top of our buffer off the screen. int lineLength = 0; + bool fullLine = false; for (var i = 0; i < toolTip.Length; i++) { char c = toolTip[i]; @@ -572,8 +573,13 @@ public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips if (c == '\r' || c == '\n') { - toolTipLines += 1; - lineLength = 0; + // If we happened to have a full line right before the newline character, then we + // skip this newline character because we already increased the line count. + if (!fullLine) + { + toolTipLines += 1; + lineLength = 0; + } } else { @@ -582,8 +588,14 @@ public void UpdateMenuSelection(int selectedItem, bool select, bool showTooltips { toolTipLines += 1; lineLength = 0; + + // Indicate that we just had a full line. + fullLine = true; + continue; } } + + fullLine = false; } // The +1 is for the blank line between the menu and tooltips. From bbbe43bde346c443e236ac39fff1103cd3b2ccd0 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 8 Jul 2022 15:32:37 -0700 Subject: [PATCH 013/127] No list view prediction when the first line was scrolled up off the buffer (#3372) --- PSReadLine/Prediction.Views.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index e61d396cd..ce1eec3e8 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -275,6 +275,13 @@ internal PredictionListView(PSConsoleReadLine singleton) internal override void GetSuggestion(string userInput) { + if (_singleton._initialY < 0) + { + // Do not trigger list view prediction when the first line has already been scrolled up off the buffer. + // See https://github.com/PowerShell/PSReadLine/issues/3347 for an example where this may happen. + return; + } + bool inputUnchanged = string.Equals(_inputText, userInput, _singleton._options.HistoryStringComparison); if (!inputUnchanged && _selectedIndex > -1) { From 6808c18762347ddb71a238cdad4d03f2f38fdc7b Mon Sep 17 00:00:00 2001 From: Enan Ajmain <3nan.ajmain@gmail.com> Date: Mon, 18 Jul 2022 23:26:40 +0600 Subject: [PATCH 014/127] Add support for upcasing, downcasing, and capitalizing word (#3365) --- PSReadLine/BasicEditing.cs | 66 ++++++++++++++++++++++ PSReadLine/KeyBindings.cs | 13 ++++- PSReadLine/PSReadLineResources.Designer.cs | 33 +++++++++++ PSReadLine/PSReadLineResources.resx | 9 +++ test/BasicEditingTest.cs | 31 ++++++++++ 5 files changed, 150 insertions(+), 2 deletions(-) diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index a59bdf4ef..33445ed36 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -246,6 +246,72 @@ public static void DeleteCharOrExit(ConsoleKeyInfo? key = null, object arg = nul _singleton.DeleteCharImpl(1, orExit: true); } + /// + /// A helper function to change the case of the current word. + /// + private static void UpdateWordCase(bool toUpper) + { + if (_singleton._current >= _singleton._buffer.Length) + { + Ding(); + return; + } + + int endOfWord = _singleton.FindForwardWordPoint(_singleton.Options.WordDelimiters); + int wordlen = endOfWord - _singleton._current; + + string word = _singleton._buffer.ToString(_singleton._current, wordlen); + word = toUpper ? word.ToUpper() : word.ToLower(); + + Replace(_singleton._current, wordlen, word); + + _singleton.MoveCursor(endOfWord); + _singleton.Render(); + } + + /// + /// Upcase the current word and move to the next one. + /// + public static void UpcaseWord(ConsoleKeyInfo? key = null, object arg = null) + { + UpdateWordCase(toUpper: true); + } + + /// + /// Downcase the current word and move to the next one. + /// + public static void DowncaseWord(ConsoleKeyInfo? key = null, object arg = null) + { + UpdateWordCase(toUpper: false); + } + + /// + /// Capitalize the current word and move to the next one. + /// + public static void CapitalizeWord(ConsoleKeyInfo? key = null, object arg = null) + { + if (_singleton._current >= _singleton._buffer.Length) + { + Ding(); + return; + } + + int endOfWord = _singleton.FindForwardWordPoint(_singleton.Options.WordDelimiters); + int wordlen = endOfWord - _singleton._current; + + char[] word = _singleton._buffer.ToString(_singleton._current, wordlen).ToLower().ToCharArray(); + int firstLetterIdx = Array.FindIndex(word, static x => char.IsLetter(x)); + + if (firstLetterIdx >= 0) + { + word[firstLetterIdx] = char.ToUpper(word[firstLetterIdx]); + Replace(_singleton._current, wordlen, new string(word)); + } + + _singleton.MoveCursor(endOfWord); + _singleton.Render(); + } + private bool AcceptLineImpl(bool validate) { using var _ = _prediction.DisableScoped(); diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 9315cd560..c758f8e8d 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -339,6 +339,9 @@ void SetDefaultEmacsBindings() { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, + { Keys.AltU, MakeKeyHandler(UpcaseWord, "UpcaseWord") }, + { Keys.AltL, MakeKeyHandler(DowncaseWord, "DowncaseWord") }, + { Keys.AltC, MakeKeyHandler(CapitalizeWord, "CapitalizeWord") }, }; // Some bindings are not available on certain platforms @@ -371,6 +374,9 @@ void SetDefaultEmacsBindings() { Keys.F, MakeKeyHandler(ForwardWord, "ForwardWord")}, { Keys.R, MakeKeyHandler(RevertLine, "RevertLine")}, { Keys.Y, MakeKeyHandler(YankPop, "YankPop")}, + { Keys.U, MakeKeyHandler(UpcaseWord, "UpcaseWord") }, + { Keys.L, MakeKeyHandler(DowncaseWord, "DowncaseWord") }, + { Keys.C, MakeKeyHandler(CapitalizeWord, "CapitalizeWord") }, { Keys.CtrlY, MakeKeyHandler(YankNthArg, "YankNthArg")}, { Keys.Backspace, MakeKeyHandler(BackwardKillWord, "BackwardKillWord")}, { Keys.Period, MakeKeyHandler(YankLastArg, "YankLastArg")}, @@ -399,14 +405,15 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(AcceptAndGetNext): case nameof(AcceptLine): case nameof(AddLine): - case nameof(BackwardDeleteInput): case nameof(BackwardDeleteChar): + case nameof(BackwardDeleteInput): case nameof(BackwardDeleteLine): case nameof(BackwardDeleteWord): case nameof(BackwardKillInput): case nameof(BackwardKillLine): case nameof(BackwardKillWord): case nameof(CancelLine): + case nameof(CapitalizeWord): case nameof(Copy): case nameof(CopyOrCancelLine): case nameof(Cut): @@ -414,13 +421,14 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(DeleteCharOrExit): case nameof(DeleteEndOfBuffer): case nameof(DeleteEndOfWord): - case nameof(DeleteRelativeLines): case nameof(DeleteLine): case nameof(DeleteLineToFirstChar): case nameof(DeleteNextLines): case nameof(DeletePreviousLines): + case nameof(DeleteRelativeLines): case nameof(DeleteToEnd): case nameof(DeleteWord): + case nameof(DowncaseWord): case nameof(ForwardDeleteInput): case nameof(ForwardDeleteLine): case nameof(InsertLineAbove): @@ -442,6 +450,7 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(Undo): case nameof(UndoAll): case nameof(UnixWordRubout): + case nameof(UpcaseWord): case nameof(ValidateAndAcceptLine): case nameof(ViAcceptLine): case nameof(ViAcceptLineOrExit): diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index d107632a6..5366f4793 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2202,5 +2202,38 @@ internal static string FailedToConvertPointToRenderDataOffset return ResourceManager.GetString("FailedToConvertPointToRenderDataOffset", resourceCulture); } } + + /// + /// Looks up a localized string similar to: Find the next word starting from the current position and then make it Pascal case. + /// + internal static string CapitalizeWordDescription + { + get + { + return ResourceManager.GetString("CapitalizeWordDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to: Find the next word starting from the current position and then make it lower case. + /// + internal static string DowncaseWordDescription + { + get + { + return ResourceManager.GetString("DowncaseWordDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to: Find the next word starting from the current position and then make it upper case. + /// + internal static string UpcaseWordDescription + { + get + { + return ResourceManager.GetString("UpcaseWordDescription", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index f57835029..d66cd13f3 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -858,4 +858,13 @@ Or not saving history with: Cannot locate the offset in the rendered text that was pointed by the original cursor. Initial Coord: ({0}, {1}) Buffer: ({2}, {3}) Cursor: ({4}, {5}) + + Find the next word starting from the current position and then make it Pascal case. + + + Find the next word starting from the current position and then make it lower case. + + + Find the next word starting from the current position and then make it upper case. + diff --git a/test/BasicEditingTest.cs b/test/BasicEditingTest.cs index dd5bc357e..16d8d277d 100644 --- a/test/BasicEditingTest.cs +++ b/test/BasicEditingTest.cs @@ -200,6 +200,37 @@ public void DeleteCharOrExit() Test("exit", Keys("foo", _.Home, Enumerable.Repeat(_.Ctrl_d, 4), InputAcceptedNow)); } + [SkippableFact] + public void UpcaseWord() + { + TestSetup(KeyMode.Emacs); + Test("FOO", Keys("foo", _.Alt_b, _.Alt_u)); + Test("FOO bar", Keys("foo bar", _.Home, _.Alt_u)); + Test("foo BAR", Keys("foo bar", _.Alt_b, _.Alt_u)); + Test("FOO BAR", Keys("foo bar", _.Home, Enumerable.Repeat(_.Alt_u, 2))); + } + + [SkippableFact] + public void DowncaseWord() + { + TestSetup(KeyMode.Emacs); + Test("foo", Keys("fOO", _.Alt_b, _.Alt_l)); + Test("FOO bar", Keys("FOO BAR", _.Alt_b, _.Alt_l)); + Test("foo BAR", Keys("FOO BAR", _.Home, _.Alt_l)); + Test("foo bar", Keys("FOO BAR", _.Home, Enumerable.Repeat(_.Alt_l, 2))); + } + + [SkippableFact] + public void CapitalizeWord() + { + TestSetup(KeyMode.Emacs); + Test("Foo", Keys("fOO", _.Alt_b, _.Alt_c)); + Test("fOO Bar", Keys("fOO bAR", _.Alt_b, _.Alt_c)); + Test("Foo BAR", Keys("fOO BAR", _.Home, _.Alt_c)); + Test("Foo Bar", Keys("fOO BAR", _.Home, Enumerable.Repeat(_.Alt_c, 2))); + Test("Foo ^&*() Bar", Keys("Foo ^&*() bar", _.Home, Enumerable.Repeat(_.Alt_c, 2))); + } + [SkippableFact] public void SelectAndDelete() { From 8d1e3c1ee7da9f40cb28cee68d840884b512a73a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 18 Jul 2022 11:39:40 -0700 Subject: [PATCH 015/127] Fix the description of `CapitalizeWord` (#3384) --- PSReadLine/PSReadLineResources.Designer.cs | 2 +- PSReadLine/PSReadLineResources.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 5366f4793..4a8e905e6 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2204,7 +2204,7 @@ internal static string FailedToConvertPointToRenderDataOffset } /// - /// Looks up a localized string similar to: Find the next word starting from the current position and then make it Pascal case. + /// Looks up a localized string similar to: Find the next word starting from the current position and then upcase the first character and downcase the remaining characters. /// internal static string CapitalizeWordDescription { diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index d66cd13f3..f90b510c0 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -859,7 +859,7 @@ Or not saving history with: Cannot locate the offset in the rendered text that was pointed by the original cursor. Initial Coord: ({0}, {1}) Buffer: ({2}, {3}) Cursor: ({4}, {5}) - Find the next word starting from the current position and then make it Pascal case. + Find the next word starting from the current position and then upcase the first character and downcase the remaining characters. Find the next word starting from the current position and then make it lower case. From c5d11871795be065372c036faf4d064c0df9b7d4 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 29 Aug 2022 10:29:21 -0700 Subject: [PATCH 016/127] Place 'ViDGChord' in the right group (#3422) --- PSReadLine/KeyBindings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index c758f8e8d..7b3f0c1c9 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -585,6 +585,7 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(ViEditVisually): case nameof(ViExit): case nameof(ViInsertMode): + case nameof(ViDGChord): case nameof(WhatIsKey): case nameof(ShowCommandHelp): case nameof(ShowParameterHelp): From cb0e0d47355d62cb2e0e6aac6b3405c7a7e218f0 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 29 Aug 2022 14:11:21 -0700 Subject: [PATCH 017/127] Fix the 'SmartInsertQuote' example in README.md (#3424) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a76c9b03f..ce80599c4 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Set-PSReadLineKeyHandler -Key Tab -Function Complete Here is a more interesting example of what is possible: ```powershell -Set-PSReadLineKeyHandler -Chord 'Oem7','Shift+Oem7' ` +Set-PSReadLineKeyHandler -Chord '"',"'" ` -BriefDescription SmartInsertQuote ` -LongDescription "Insert paired quotes if not already on a quote" ` -ScriptBlock { From ea25286b864897c3202ae32da9f4a960ce21513a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 22 Sep 2022 10:39:01 -0700 Subject: [PATCH 018/127] Update the sample in README.md to work in strict mode (#3440) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce80599c4..9f2c69804 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ Set-PSReadLineKeyHandler -Chord '"',"'" ` $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) - if ($line[$cursor] -eq $key.KeyChar) { + if ($line.Length -gt $cursor -and $line[$cursor] -eq $key.KeyChar) { # Just move the cursor [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1) } From 3bc6561270794c056dc869a3da52ac9bb99019c6 Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Tue, 18 Oct 2022 16:44:22 -0700 Subject: [PATCH 019/127] Fix to use the default member color for members (#3450) --- PSReadLine/Cmdlets.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 762f39b70..1e635a914 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -526,7 +526,7 @@ internal void ResetColors() ParameterColor = DefaultParameterColor; TypeColor = DefaultTypeColor; NumberColor = DefaultNumberColor; - MemberColor = DefaultNumberColor; + MemberColor = DefaultMemberColor; EmphasisColor = DefaultEmphasisColor; ErrorColor = DefaultErrorColor; InlinePredictionColor = DefaultInlinePredictionColor; From c8db4f587a17a9e8eaf7cbb9ca988d3af5b17925 Mon Sep 17 00:00:00 2001 From: "James Truher [MSFT]" Date: Fri, 21 Oct 2022 15:04:29 -0700 Subject: [PATCH 020/127] Do not filter out duplicate completions on non-Windows systems (#3454) --- PSReadLine/Completion.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 0e7f8108f..caa291fe7 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -10,6 +10,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using System.Management.Automation; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; @@ -299,7 +300,8 @@ private CommandCompletion GetCompletions() if (start < 0 || start > _singleton._buffer.Length) return null; if (length < 0 || length > (_singleton._buffer.Length - start)) return null; - if (_tabCompletions.CompletionMatches.Count > 1) + // Only filter out duplicates on Windows. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _tabCompletions.CompletionMatches.Count > 1) { // Filter out apparent duplicates var hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); From 4bfc4e94d9d4e30801d57f4e59e71d3f57d83a19 Mon Sep 17 00:00:00 2001 From: Dominik Kaszewski Date: Mon, 24 Oct 2022 20:56:09 +0200 Subject: [PATCH 021/127] Make tab completion show results whose `ListItemText` are different by case only (#3456) --- PSReadLine/Completion.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index caa291fe7..4523e5244 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -10,7 +10,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Runtime.InteropServices; using System.Management.Automation; using System.Management.Automation.Runspaces; using Microsoft.PowerShell.Internal; @@ -300,17 +299,27 @@ private CommandCompletion GetCompletions() if (start < 0 || start > _singleton._buffer.Length) return null; if (length < 0 || length > (_singleton._buffer.Length - start)) return null; - // Only filter out duplicates on Windows. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _tabCompletions.CompletionMatches.Count > 1) + if (_tabCompletions.CompletionMatches.Count > 1) { - // Filter out apparent duplicates - var hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); + // Filter out apparent duplicates -- the 'ListItemText' is exactly the same. + var hashSet = new HashSet(); + var matches = _tabCompletions.CompletionMatches; + List indices = null; - foreach (var match in _tabCompletions.CompletionMatches.ToArray()) + for (int i = 0; i < matches.Count; i++) { - if (!hashSet.Add(match.ListItemText)) + if (!hashSet.Add(matches[i].ListItemText)) { - _tabCompletions.CompletionMatches.Remove(match); + indices ??= new List(); + indices.Add(i); + } + } + + if (indices is not null) + { + for (int i = indices.Count - 1; i >= 0; i--) + { + matches.RemoveAt(indices[i]); } } } From 6c7ccc933fa5cea3dc092541f5a042639c930b55 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Wed, 23 Nov 2022 14:54:01 -0800 Subject: [PATCH 022/127] Change default color for inline prediction to `dim` (#3493) --- PSReadLine/Cmdlets.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 1e635a914..f6afe3275 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -95,10 +95,10 @@ public class PSConsoleReadLineOptions // Find the most suitable color using https://stackoverflow.com/a/33206814 // Default prediction color settings: - // - use FG color 'dark black' for the inline-view suggestion text + // - use FG color 'dim white italic' for the inline-view suggestion text // - use FG color 'yellow' for the list-view suggestion text // - use BG color 'dark black' for the selected list-view suggestion text - public const string DefaultInlinePredictionColor = "\x1b[38;5;238m"; + public const string DefaultInlinePredictionColor = "\x1b[97;2;3m"; public const string DefaultListPredictionColor = "\x1b[33m"; public const string DefaultListPredictionSelectedColor = "\x1b[48;5;238m"; From 96737bf59e50433eeda81579335cf726e29df334 Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Wed, 18 Jan 2023 17:20:36 -0800 Subject: [PATCH 023/127] Updating Fabric bot (#3540) --- .github/fabricbot.json | 102 ++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 33 deletions(-) diff --git a/.github/fabricbot.json b/.github/fabricbot.json index 493058bb4..4abf7dd9e 100644 --- a/.github/fabricbot.json +++ b/.github/fabricbot.json @@ -50,8 +50,7 @@ "issues", "project_card" ] - }, - "id": "i3BHh5Qe9F" + } }, { "taskType": "trigger", @@ -107,8 +106,7 @@ "eventNames": [ "issue_comment" ] - }, - "id": "x_h9ia7zwG" + } }, { "taskType": "trigger", @@ -117,8 +115,7 @@ "version": "1.0", "config": { "taskName": "Add a CodeFlow link to new pull requests" - }, - "id": "z2P6L1OkC4" + } }, { "taskType": "trigger", @@ -156,8 +153,7 @@ "eventNames": [ "pull_request_review" ] - }, - "id": "s_Q6W352PU" + } }, { "taskType": "trigger", @@ -210,8 +206,7 @@ "issues", "project_card" ] - }, - "id": "2rzxLdRUQ7h" + } }, { "taskType": "trigger", @@ -251,8 +246,7 @@ "eventNames": [ "issue_comment" ] - }, - "id": "p7EkOxE_g3T" + } }, { "taskType": "trigger", @@ -292,8 +286,7 @@ "eventNames": [ "pull_request_review" ] - }, - "id": "xIoNg4bVKyA" + } }, { "taskType": "trigger", @@ -338,8 +331,7 @@ "issues", "project_card" ] - }, - "id": "o_mQ1sO0zdO" + } }, { "taskType": "trigger", @@ -371,8 +363,7 @@ "eventNames": [ "issue_comment" ] - }, - "id": "ibqxdSfreaD" + } }, { "taskType": "trigger", @@ -404,8 +395,7 @@ "eventNames": [ "pull_request_review" ] - }, - "id": "7_EH-4ffdtY" + } }, { "taskType": "scheduled", @@ -520,8 +510,7 @@ "parameters": {} } ] - }, - "id": "X3R7HwIUme_" + } }, { "taskType": "scheduled", @@ -644,8 +633,7 @@ } } ] - }, - "id": "zYmFzaIHtT8" + } }, { "taskType": "trigger", @@ -661,15 +649,13 @@ "allowAutoMergeInstructionsWithoutLabel": false, "deleteBranches": true, "removeLabelOnPush": true - }, - "id": "BPA0tvDiHBN" + } }, { "taskType": "trigger", "capabilityId": "IssueResponder", "subCapability": "IssuesOnlyResponder", "version": "1.0", - "id": "kvB9kCm1d", "config": { "conditions": { "operator": "and", @@ -709,7 +695,6 @@ "capabilityId": "ScheduledSearch", "subCapability": "ScheduledSearch", "version": "1.1", - "id": "28wu6aj_J", "config": { "frequency": [ { @@ -796,6 +781,12 @@ "parameters": { "days": 7 } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Repro" + } } ], "taskName": "Close stale issues", @@ -818,7 +809,6 @@ "capabilityId": "InPrLabel", "subCapability": "InPrLabel", "version": "1.0", - "id": "dfYD-29Up", "config": { "taskName": "Add 'In-PR' label to issue", "label_inPr": "In-PR", @@ -831,7 +821,6 @@ "capabilityId": "ReleaseAnnouncement", "subCapability": "ReleaseAnnouncement", "version": "1.0", - "id": "_vafvxO3x", "config": { "taskName": "Release announcement for Issue/PR", "prReply": ":tada: [`${version}`](https://github.com/PowerShell/PSReadLine/releases/tag/${version}) has been released which incorporates this pull request. :tada:\n", @@ -846,7 +835,6 @@ "capabilityId": "ScheduledSearch", "subCapability": "ScheduledSearch", "version": "1.1", - "id": "HDx9Yd09Z1iue7fv7A22t", "config": { "frequency": [ { @@ -926,6 +914,54 @@ "parameters": { "days": 1 } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Answered" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Duplicate" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-External" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-By Design" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Declined" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Fixed" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Wont Fix" + } + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Not Repro" + } } ], "taskName": "Close answered issues", @@ -933,7 +969,7 @@ { "name": "addReply", "parameters": { - "comment": "This issue has been marked as answered and has not had any activity for **1 day**. It has been closed for housekeeping purposes." + "comment": "This issue has been marked as answered or resolved and has not had any activity for **1 day**. It has been closed for housekeeping purposes." } }, { @@ -945,4 +981,4 @@ } ], "userGroups": [] -} +} \ No newline at end of file From 659c8282cd9acd943e0cca15ca20676bbb8e0ecb Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 23 Jan 2023 11:04:40 -0800 Subject: [PATCH 024/127] De-duplicate prediction results with the history results (#3543) --- PSReadLine/Prediction.Views.cs | 64 +++++++++++++--- test/ListPredictionTest.cs | 135 +++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 12 deletions(-) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index ce1eec3e8..de04c4e10 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -144,12 +144,9 @@ protected List GetHistorySuggestions(string input, int count) continue; } - if (results == null) - { - results = new List(capacity: count); - } - _cacheHistorySet.Add(line); + results ??= new List(capacity: count); + if (matchIndex == 0) { results.Add(new SuggestionEntry(line, matchIndex)); @@ -224,8 +221,11 @@ private class PredictionListView : PredictionViewBase private bool _updatePending; // Caches re-used when aggregating the suggestion results from predictors and history. + // Those caches help us avoid allocation on tons of short-lived collections. private List _cacheList1; private List _cacheList2; + private HashSet _cachedHistorySet; + private StringComparer _cachedComparer; /// /// Gets whether the current window size meets the minimum requirement for the List view to work. @@ -376,7 +376,7 @@ private void AggregateSuggestions() // Assign the results of each plugin to the average slots. // Note that it's possible a plugin may return less results than the average slots, - // and in that case, the unused slots will be come remaining slots that are to be + // and in that case, the unused slots will become remaining slots which are to be // distributed again. for (int i = 0; i < pCount; i++) { @@ -419,6 +419,18 @@ private void AggregateSuggestions() if (hCount > 0) { _listItems.RemoveRange(hCount, _listItems.Count - hCount); + + if (_cachedComparer != _singleton._options.HistoryStringComparer) + { + // Create the cached history set if not yet, or re-create the set if case-sensitivity was changed by the user. + _cachedComparer = _singleton._options.HistoryStringComparer; + _cachedHistorySet = new HashSet(_cachedComparer); + } + + foreach (SuggestionEntry entry in _listItems) + { + _cachedHistorySet.Add(entry.SuggestionText); + } } int index = -1; @@ -435,19 +447,46 @@ private void AggregateSuggestions() break; } + int skipCount = 0; int num = _cacheList2[index]; - for (int i = 0; i < num; i++) + foreach (PredictiveSuggestion suggestion in item.Suggestions) { - string sugText = item.Suggestions[i].SuggestionText ?? string.Empty; + string sugText = suggestion.SuggestionText ?? string.Empty; + if (_cachedHistorySet?.Contains(sugText) == true) + { + // Skip the prediction result that is exactly the same as one of the history results. + skipCount++; + continue; + } + int matchIndex = sugText.IndexOf(_inputText, comparison); _listItems.Add(new SuggestionEntry(item.Name, item.Id, item.Session, sugText, matchIndex)); + + if (--num == 0) + { + // Break after we've added the desired number of prediction results. + break; + } } - if (item.Session.HasValue) + // Get the number of prediction results that were actually put in the list after filtering out the duplicate ones. + int count = _cacheList2[index] - num; + if (item.Session.HasValue && count > 0) { - // Send feedback only if the mini-session id is specified. - // When it's not specified, we consider the predictor doesn't accept feedback. - _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, num); + // Send feedback only if the mini-session id is specified and we truely have its results in the list to be rendered. + // When the mini-session id is not specified, we consider the predictor doesn't accept feedback. + // + // NOTE: when any duplicate results were skipped, the 'count' passed in here won't be accurate as it still includes + // those skipped ones. This is due to the limitation of the 'OnSuggestionDisplayed' interface method, which didn't + // assume any prediction results from a predictor could be filtered out at the initial design time. We will have to + // change the predictor interface to pass in accurate information, such as: + // void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex, int[] skippedIndices) + // + // However, an interface change has huge impacts. At least, a newer version of PSReadLine will stop working on the + // existing PowerShell 7+ versions. For this particular issue, the chance that it could happen is low and the impact + // of the inaccurate feedback is also low, so we should delay this interface change until another highly-demanded + // change to the interface is required in future (e.g. changes related to supporting OpenAI models). + _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, count + skipCount); } } } @@ -456,6 +495,7 @@ private void AggregateSuggestions() { _cacheList1.Clear(); _cacheList2.Clear(); + _cachedHistorySet?.Clear(); } } diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 1a4ee2f9a..3788fca1f 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -37,6 +37,16 @@ private Disposable SetPrediction(PredictionSource source, PredictionViewStyle vi new SetPSReadLineOption { PredictionSource = oldSource, PredictionViewStyle = oldView })); } + private Disposable SetHistorySearchCaseSensitive(bool caseSensitive) + { + var options = PSConsoleReadLine.GetOptions(); + var oldValue = options.HistorySearchCaseSensitive; + + PSConsoleReadLine.SetOptions(new SetPSReadLineOption { HistorySearchCaseSensitive = caseSensitive }); + return new Disposable(() => PSConsoleReadLine.SetOptions( + new SetPSReadLineOption { HistorySearchCaseSensitive = oldValue })); + } + private void AssertDisplayedSuggestions(int count, Guid predictorId, uint session, int countOrIndex) { Assert.Equal(count, _mockedMethods.displayedSuggestions.Count); @@ -1711,6 +1721,131 @@ public void List_HistoryAndPluginSource_Acceptance() Assert.Equal("SOME NEW TEX SOME TEXT AFTER", _mockedMethods.commandHistory[3]); } + [SkippableFact] + public void List_HistoryAndPluginSource_Deduplication() + { + TestSetup(KeyMode.Cmd); + int listWidth = CheckWindowSize(); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. + using var disp1 = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + // The 1st result from 'predictorId_1' is the same as the 1st entry in history with case-insensitive comparison, + // which is the default comparison. So, that result will be filtered out due to the de-duplication logic. + SetHistory("some TEXT BEFORE de-dup", "de-dup -of"); + Test("de-dup", Keys( + "de-dup", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "de-dup", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "de-dup", + TokenClassification.None, " -of", + TokenClassification.None, new string(' ', listWidth - 21), // 21 is the length of '> de-dup -of' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " some TEXT BEFORE ", + emphasisColors, "de-dup", + TokenClassification.None, new string(' ', listWidth - 34), // 34 is the length of '> SOME TEXT BEFORE de-dup' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "de-dup", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 39), // 35 is the length of '> de-dup SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + // `OnSuggestionDisplayed` should be fired for both predictors. + // For 'predictorId_1', the reported 'countOrIndex' from feedback is still 2 even though its 1st result was filtered out due to duplication. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "de-dup", + NextLine, + NextLine)) + )); + + // Change the setting to be case sensitive, and check the list view content. + using var disp2 = SetHistorySearchCaseSensitive(caseSensitive: true); + _mockedMethods.ClearPredictionFields(); + + // The 1st result from 'predictorId_1' is not the same as the 2nd entry in history with the case-sensitive comparison. + // But the 2nd result from 'predictorId_1' is the same as teh 1st entry in history with the case-sensitive comparison, + // so, that result will be filtered out due to the de-duplication logic. + SetHistory("de-dup SOME TEXT AFTER", "some TEXT BEFORE de-dup"); + Test("de-dup", Keys( + "de-dup", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "de-dup", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "de-dup", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 33), // 33 is the length of '> de-dup SOME TEXT AFTER' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " some TEXT BEFORE ", + emphasisColors, "de-dup", + TokenClassification.None, new string(' ', listWidth - 34), // 34 is the length of '> some TEXT BEFORE de-dup' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "de-dup", + TokenClassification.None, new string(' ', listWidth - 40), // 40 is the length of '> SOME TEXT BEFORE de-dup' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + // `OnSuggestionDisplayed` should be fired for both predictors. + // For 'predictorId_1', the reported 'countOrIndex' from feedback is still 2 even though its 2nd result was filtered out due to duplication. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "de-dup", + NextLine, + NextLine)) + )); + } + [SkippableFact] public void List_NoneSource_ExecutionStatus() { From 486388c59fda9ec200bc801ab948d19dbd810c3f Mon Sep 17 00:00:00 2001 From: spaette <111918424+spaette@users.noreply.github.com> Date: Mon, 23 Jan 2023 16:22:59 -0600 Subject: [PATCH 025/127] Fix some typos in this repository (#3547) --- PSReadLine/Changes.txt | 6 ++--- PSReadLine/Cmdlets.cs | 2 +- PSReadLine/Completion.cs | 2 +- PSReadLine/DisplayBlockBase.cs | 2 +- PSReadLine/Keys.cs | 4 +-- PSReadLine/Movement.cs | 30 +++++++++++----------- PSReadLine/PSReadLineResources.Designer.cs | 4 +-- PSReadLine/PSReadLineResources.resx | 4 +-- PSReadLine/Prediction.Views.cs | 2 +- PSReadLine/ReadLine.cs | 2 +- PSReadLine/Render.Helper.cs | 2 +- PSReadLine/Render.cs | 2 +- PSReadLine/YankPaste.vi.cs | 2 +- build.ps1 | 2 +- test/CompletionTest.cs | 6 ++--- test/MockConsole.cs | 2 +- test/RenderTest.cs | 2 +- tools/CheckHelp.ps1 | 2 +- tools/releaseTools.psm1 | 2 +- 19 files changed, 40 insertions(+), 40 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index a3822e36d..18ffcc3c5 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -6,7 +6,7 @@ ### [2.2.5] - 2022-05-03 -- Re-package the `2.2.4-beta1` version to `2.2.5` as an offical servicing release. +- Re-package the `2.2.4-beta1` version to `2.2.5` as an official servicing release. [2.2.5]: https://github.com/PowerShell/PSReadLine/compare/v2.2.4-beta1...v2.2.5 @@ -385,7 +385,7 @@ Bug fixes: * Fix InvokePrompt when the prompt is > 1 line. * Fix YankToPercent off by 1 error. * Fix error reported when running in container. -* Catch and ignore execptions in InvokePrompt (#583) +* Catch and ignore exceptions in InvokePrompt (#583) * Get new completions on 2nd tab if 1st had 1 result (#238) * Tab replaced with 4 spaces during paste (#144) * Fix rendering after buffer resize (#418) @@ -670,7 +670,7 @@ New features: * Add ETW event source for demo mode, key logger, macro recorder etc. * Undo/redo * Get-PSReadLineOption cmdlet -* Make specifying key handlers for builtins simpler +* Make specifying key handlers for built-ins simpler * Current un-entered line is saved and recalled when cycling through history * Support syntax coloring of member names diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index f6afe3275..ee323db57 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -853,7 +853,7 @@ public class SetPSReadLineKeyHandlerCommand : ChangePSReadLineKeyHandlerCommandB public string BriefDescription { get; set; } [Parameter(ParameterSetName = "ScriptBlock")] - [Alias("LongDescription")] // Alias to stay comptible with previous releases + [Alias("LongDescription")] // Alias to stay compatible with previous releases public string Description { get; set; } private const string FunctionParameter = "Function"; diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 4523e5244..5a8dd9b02 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -1079,7 +1079,7 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions) processingKeys = false; prependNextKey = true; - // without this branch experience doesnt look naturally + // without this branch experience doesn't look naturally if (_dispatchTable.TryGetValue(nextKey, out var handler) && ( handler.Action == CopyOrCancelLine || diff --git a/PSReadLine/DisplayBlockBase.cs b/PSReadLine/DisplayBlockBase.cs index c4da53247..e84be99a5 100644 --- a/PSReadLine/DisplayBlockBase.cs +++ b/PSReadLine/DisplayBlockBase.cs @@ -49,7 +49,7 @@ protected void MoveCursorToStartDrawingPosition(IConsole console) { // Calculate the coord to place the cursor at the end of current input. Point bufferEndPoint = Singleton.ConvertOffsetToPoint(Singleton._buffer.Length); - // Top must be initialized before any possible adjustion by 'AdjustForPossibleScroll' or 'AdjustForActualScroll', + // Top must be initialized before any possible adjustment by 'AdjustForPossibleScroll' or 'AdjustForActualScroll', // otherwise its value would be corrupted and cause rendering issue. Top = bufferEndPoint.Y + 1; diff --git a/PSReadLine/Keys.cs b/PSReadLine/Keys.cs index c9ed230e1..4a621bb2a 100644 --- a/PSReadLine/Keys.cs +++ b/PSReadLine/Keys.cs @@ -139,7 +139,7 @@ internal static void TryGetCharFromConsoleKey(ConsoleKeyInfo key, ref char resul // get corresponding scan code uint scanCode = MapVirtualKey(virtualKey, 0x0 /*MAPVK_VK_TO_VSC*/); - // get corresponding character - maybe be 0, 1 or 2 in length (diacriticals) + // get corresponding character - may be 0, 1 or 2 in length (diacriticals) var chars = toUnicodeBuffer.Value; var flags = 0u; /* If bit 0 is set, a menu is active. */ var osVersion = Environment.OSVersion.Version; @@ -238,7 +238,7 @@ void AppendPart(string str) { // A heuristic to check for dead keys -- // We got an 'OemXXX' ConsoleKey, '\0' key char, and no 'Ctrl' modifier. It's very likely generated by a dead key. - // We check for 'Ctrl' modifier because it's easy to generate '\0' KeyChar and 'OemXXX' by combinding 'Ctrl' with + // We check for 'Ctrl' modifier because it's easy to generate '\0' KeyChar and 'OemXXX' by combining 'Ctrl' with // another special key, such as 'Ctrl+?' and 'Ctrl+;'. isDeadKey = (c == '\0') && (consoleKey >= ConsoleKey.Oem1 && consoleKey <= ConsoleKey.Oem102) && !isCtrl; diff --git a/PSReadLine/Movement.cs b/PSReadLine/Movement.cs index 074ae4b10..da52230e6 100644 --- a/PSReadLine/Movement.cs +++ b/PSReadLine/Movement.cs @@ -527,16 +527,16 @@ private static char TryGetArgAsChar(object arg) } /// - /// Read a character and search forward for the next occurence of that character. + /// Read a character and search forward for the next occurrence of that character. /// If an argument is specified, search forward (or backward if negative) for the - /// nth occurence. + /// nth occurrence. /// public static void CharacterSearch(ConsoleKeyInfo? key = null, object arg = null) { - int occurence = arg as int? ?? 1; - if (occurence < 0) + int occurrence = arg as int? ?? 1; + if (occurrence < 0) { - CharacterSearchBackward(key, -occurence); + CharacterSearchBackward(key, -occurrence); return; } @@ -550,31 +550,31 @@ public static void CharacterSearch(ConsoleKeyInfo? key = null, object arg = null { if (_singleton._buffer[i] == toFind) { - occurence -= 1; - if (occurence == 0) + occurrence -= 1; + if (occurrence == 0) { _singleton.MoveCursor(i); break; } } } - if (occurence > 0) + if (occurrence > 0) { Ding(); } } /// - /// Read a character and search backward for the next occurence of that character. + /// Read a character and search backward for the next occurrence of that character. /// If an argument is specified, search backward (or forward if negative) for the - /// nth occurence. + /// nth occurrence. /// public static void CharacterSearchBackward(ConsoleKeyInfo? key = null, object arg = null) { - int occurence = arg as int? ?? 1; - if (occurence < 0) + int occurrence = arg as int? ?? 1; + if (occurrence < 0) { - CharacterSearch(key, -occurence); + CharacterSearch(key, -occurrence); return; } @@ -588,8 +588,8 @@ public static void CharacterSearchBackward(ConsoleKeyInfo? key = null, object ar { if (_singleton._buffer[i] == toFind) { - occurence -= 1; - if (occurence == 0) + occurrence -= 1; + if (occurrence == 0) { _singleton.MoveCursor(i); return; diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 4a8e905e6..925355f72 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -241,7 +241,7 @@ internal static string CaptureScreenDescription { } /// - /// Looks up a localized string similar to Read a character and move the cursor to the previous occurence of that character. + /// Looks up a localized string similar to Read a character and move the cursor to the previous occurrence of that character. /// internal static string CharacterSearchBackwardDescription { get { @@ -250,7 +250,7 @@ internal static string CharacterSearchBackwardDescription { } /// - /// Looks up a localized string similar to Read a character and move the cursor to the next occurence of that character. + /// Looks up a localized string similar to Read a character and move the cursor to the next occurrence of that character. /// internal static string CharacterSearchDescription { get { diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index f90b510c0..7dc66be28 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -292,10 +292,10 @@ Move the text from the cursor to the start of the current or previous whitespace delimited word to the kill ring - Read a character and move the cursor to the previous occurence of that character + Read a character and move the cursor to the previous occurrence of that character - Read a character and move the cursor to the next occurence of that character + Read a character and move the cursor to the next occurrence of that character Start or accumulate a numeric argument to other functions diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index de04c4e10..ddfbd073d 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -742,7 +742,7 @@ internal override void RenderSuggestion(List consoleBufferLines, // The whole suggestion text cannot fit in the console buffer without having part of it scrolled up off the buffer. // We truncate the end part and append ellipsis. - // We need to truncate 4 buffer cells ealier (just to be safe), so we have enough room to add the ellipsis. + // We need to truncate 4 buffer cells earlier (just to be safe), so we have enough room to add the ellipsis. int lenFromEnd = SubstringLengthByCellsFromEnd(_suggestionText, length - 1, countOfCells: 4); totalLength = length - lenFromEnd; if (totalLength <= inputLength) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index e38b05c55..a6010bb9d 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -777,7 +777,7 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) private void DelayedOneTimeInitialize() { // Delayed initialization is needed so that options can be set - // after the constuctor but have an affect before the user starts + // after the constructor but have an affect before the user starts // editing their first command line. For example, if the user // specifies a custom history save file, we don't want to try reading // from the default one. diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 9fd67159d..0c041e735 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -93,7 +93,7 @@ internal static int LengthInBufferCells(char c) (c >= 0xff00 && c <= 0xff60) || /* Fullwidth Forms */ (c >= 0xffe0 && c <= 0xffe6)); // We can ignore these ranges because .Net strings use surrogate pairs - // for this range and we do not handle surrogage pairs. + // for this range and we do not handle surrogate pairs. // (c >= 0x20000 && c <= 0x2fffd) || // (c >= 0x30000 && c <= 0x3fffd) return 1 + (isWide ? 1 : 0); diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 94df96316..ff5649fe3 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -1162,7 +1162,7 @@ private void MoveCursor(int newCursor) _previousRender.initialY = _initialY; } - // While waiting to render, and a keybinding has occured that is moving the cursor, + // While waiting to render, and a keybinding has occurred that is moving the cursor, // converting offset to point could potentially result in an invalid screen position, // but the insertion point should reflect the move. _current = newCursor; diff --git a/PSReadLine/YankPaste.vi.cs b/PSReadLine/YankPaste.vi.cs index 727fec872..d59fdd5cc 100644 --- a/PSReadLine/YankPaste.vi.cs +++ b/PSReadLine/YankPaste.vi.cs @@ -80,7 +80,7 @@ private void SaveLinesToClipboard(int lineIndex, int lineCount) /// /// /// Use 'false' as the default value because this method is used a lot by VI operations, - /// and for VI opeartions, we do NOT want to move the cursor to the end when undoing a + /// and for VI operations, we do NOT want to move the cursor to the end when undoing a /// deletion. /// private void RemoveTextToViRegister( diff --git a/build.ps1 b/build.ps1 index 0c303bc99..bb41830ba 100644 --- a/build.ps1 +++ b/build.ps1 @@ -57,7 +57,7 @@ if ($Clean) { Import-Module "$PSScriptRoot/tools/helper.psm1" if ($Bootstrap) { - Write-Log "Validate and install missing prerequisits for building ..." + Write-Log "Validate and install missing prerequisites for building ..." Install-Dotnet if (-not (Get-Module -Name InvokeBuild -ListAvailable)) { diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index c2f6bad1e..eeec6a162 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -975,7 +975,7 @@ public void MenuCompletions_WorkWithListView() using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); _console.Clear(); - SetHistory("Get-Mocha -AddMilk -AddSugur -ExtraCup", "Get-MoreBook -Kind Fiction -FlatCover"); + SetHistory("Get-Mocha -AddMilk -AddSugar -ExtraCup", "Get-MoreBook -Kind Fiction -FlatCover"); Test("Get-Module", Keys( "Get-Mo", @@ -994,8 +994,8 @@ public void MenuCompletions_WorkWithListView() TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "Get-Mo", - TokenClassification.None, "cha -AddMilk -AddSugur -ExtraCup", - TokenClassification.None, new string(' ', listWidth - 49), // 49 is the length of '> Get-Mocha -AddMilk -AddSugur -ExtraCup' plus '[History]'. + TokenClassification.None, "cha -AddMilk -AddSugar -ExtraCup", + TokenClassification.None, new string(' ', listWidth - 49), // 49 is the length of '> Get-Mocha -AddMilk -AddSugar -ExtraCup' plus '[History]'. TokenClassification.None, '[', TokenClassification.ListPrediction, "History", TokenClassification.None, ']')), diff --git a/test/MockConsole.cs b/test/MockConsole.cs index 372a6fff0..dc218e774 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -102,7 +102,7 @@ protected TestConsole(int width, int height, bool mimicScrolling) _bufferWidth = _windowWidth = width; _bufferHeight = _windowHeight = height; - // Use a big enough buffer when we are mimicing scrolling. + // Use a big enough buffer when we are mimicking scrolling. int bufferSize = mimicScrolling ? BufferWidth * 1000 : BufferWidth * BufferHeight; buffer = new CHAR_INFO[bufferSize]; ClearBuffer(); diff --git a/test/RenderTest.cs b/test/RenderTest.cs index 647fac673..a1222c115 100644 --- a/test/RenderTest.cs +++ b/test/RenderTest.cs @@ -307,7 +307,7 @@ public void InvokePrompt() Tuple.Create(_console.ForegroundColor, _console.BackgroundColor), "PSREADLINE> ", TokenClassification.Command, "dir")))); - // Tricky prompt - writes to console directly with colors, uses ^H trick to eliminate trailng space. + // Tricky prompt - writes to console directly with colors, uses ^H trick to eliminate trailing space. using (var ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) { ps.AddCommand("New-Variable").AddParameter("Name", "__console").AddParameter("Value", _console).Invoke(); diff --git a/tools/CheckHelp.ps1 b/tools/CheckHelp.ps1 index 2a3caa62c..bc37b540d 100644 --- a/tools/CheckHelp.ps1 +++ b/tools/CheckHelp.ps1 @@ -13,7 +13,7 @@ Import-Module $PSScriptRoot/helper.psm1 $t ='Microsoft.PowerShell.PSConsoleReadLine' -as [type] if ($null -ne $t) { - # Make sure we're runnning in a non-interactive session by relaunching + # Make sure we're running in a non-interactive session by relaunching $psExePath = Get-PSExePath & $psExePath -NoProfile -NonInteractive -File $PSCommandPath $Configuration exit $LASTEXITCODE diff --git a/tools/releaseTools.psm1 b/tools/releaseTools.psm1 index 3437e93a7..8fab92e1b 100644 --- a/tools/releaseTools.psm1 +++ b/tools/releaseTools.psm1 @@ -168,7 +168,7 @@ function Get-ChangeLog ## but not reachable from the last release tag. Instead, we need to exclude the commits that were cherry-picked, ## and only include the commits that are not in the last release into the change log. - # Find the commits that were only in the orginal master, excluding those that were cherry-picked to release branch. + # Find the commits that were only in the original master, excluding those that were cherry-picked to release branch. $new_commits_from_other_parent = git --no-pager log --first-parent --cherry-pick --right-only "$tag_hash...$other_parent_hash" --format=$format | New-CommitNode # Find the commits that were only in the release branch, excluding those that were cherry-picked from master branch. $new_commits_from_last_release = git --no-pager log --first-parent --cherry-pick --left-only "$tag_hash...$other_parent_hash" --format=$format | New-CommitNode From 9957183ff2eb128a8f9df9e077c22acf21b72fdd Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Tue, 24 Jan 2023 16:24:26 -0800 Subject: [PATCH 026/127] Updating README with clearer build instructions (#3550) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9f2c69804..ef73f216a 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,9 @@ The build script `build.ps1` can be used to bootstrap, build and test the projec * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework netcoreapp2.1` After build, the produced artifacts can be found at `/bin/Debug`. +In order to isolate your imported module to the one locally built, be sure to run +`pwsh -NonInteractive -NoProfile` to not automatically load the default PSReadLine module installed. +Then, load the locally built PSReadLine module by `Import-Module /bin/Debug/PSReadLine/PSReadLine.psd1`. [Contribution Guide]: https://github.com/PowerShell/PSReadLine/blob/master/.github/CONTRIBUTING.md From c1e87ae55bf29dd14ce96a534d3034603d2a912f Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Thu, 9 Feb 2023 09:27:38 -0800 Subject: [PATCH 027/127] Updating fabric bot to separate out each label instead of all in one (#3576) --- .github/fabricbot.json | 926 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 828 insertions(+), 98 deletions(-) diff --git a/.github/fabricbot.json b/.github/fabricbot.json index 4abf7dd9e..c172e91fe 100644 --- a/.github/fabricbot.json +++ b/.github/fabricbot.json @@ -298,22 +298,17 @@ "conditions": { "operator": "and", "operands": [ - { - "operator": "not", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, { "name": "hasLabel", "parameters": { "label": "Status-No Recent Activity" } + }, + { + "name": "isAction", + "parameters": { + "action": "closed" + } } ] }, @@ -491,12 +486,6 @@ "label": "Needs-Author Feedback" } }, - { - "name": "hasLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - }, { "name": "noActivitySince", "parameters": { @@ -508,6 +497,12 @@ { "name": "closeIssue", "parameters": {} + }, + { + "name": "addReply", + "parameters": { + "comment": "This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." + } } ] } @@ -781,12 +776,6 @@ "parameters": { "days": 7 } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Repro" - } } ], "taskName": "Close stale issues", @@ -840,65 +829,68 @@ { "weekDay": 0, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 1, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 2, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 3, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 4, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 5, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] }, { "weekDay": 6, "hours": [ - 7, - 19 - ], - "timezoneOffset": -7 + 0, + 6, + 12, + 18 + ] } ], "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, { "name": "isOpen", "parameters": {} @@ -906,78 +898,816 @@ { "name": "hasLabel", "parameters": { - "label": "Question-Answered" + "label": "Resolution-Answered" } - }, + } + ], + "taskName": "Closing if Resolution Answered", + "actions": [ { - "name": "noActivitySince", - "parameters": { - "days": 1 - } - }, + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Answered" - } + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-External" - } + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-By Design" - } + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Declined" - } + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fixed" - } + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] }, { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Wont Fix" - } + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} }, { "name": "hasLabel", "parameters": { - "label": "Resolution-Not Repro" + "label": "Resolution-By Design" } } ], - "taskName": "Close answered issues", + "taskName": "Closing if Resolution By Design", "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "This issue has been marked as answered or resolved and has not had any activity for **1 day**. It has been closed for housekeeping purposes." - } - }, { "name": "closeIssue", "parameters": {} } ] } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Declined" + } + } + ], + "taskName": "Closing if Resolution Declined", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Duplicate" + } + } + ], + "taskName": "Closing if Resolution Dup", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-External" + } + } + ], + "taskName": "Closing if Resolution External", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Fixed" + } + } + ], + "taskName": "Closing if Resolution Fixed", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Not Repro" + } + } + ], + "taskName": "Closing if Resolution Not Repro", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Resolution-Wont Fix" + } + } + ], + "taskName": "Closing if Resolution Wont Fix", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + } + ] + } + }, + { + "taskType": "scheduled", + "capabilityId": "ScheduledSearch", + "subCapability": "ScheduledSearch", + "version": "1.1", + "config": { + "frequency": [ + { + "weekDay": 0, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 1, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 2, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 3, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 4, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 5, + "hours": [ + 0, + 6, + 12, + 18 + ] + }, + { + "weekDay": 6, + "hours": [ + 0, + 6, + 12, + 18 + ] + } + ], + "searchTerms": [ + { + "name": "isOpen", + "parameters": {} + }, + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Repro" + } + }, + { + "name": "noActivitySince", + "parameters": { + "days": 7 + } + } + ], + "taskName": "Closing if Stale Needs Repro", + "actions": [ + { + "name": "closeIssue", + "parameters": {} + }, + { + "name": "addReply", + "parameters": { + "comment": "This issue is closed because it has been marked as requiring repro steps but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." + } + } + ] + } + }, + { + "taskType": "trigger", + "capabilityId": "IssueResponder", + "subCapability": "IssueCommentResponder", + "version": "1.0", + "config": { + "conditions": { + "operator": "and", + "operands": [ + { + "name": "hasLabel", + "parameters": { + "label": "Needs-Repro" + } + }, + { + "name": "isActivitySender", + "parameters": { + "user": { + "type": "author" + } + } + } + ] + }, + "eventType": "issue", + "eventNames": [ + "issue_comment" + ], + "taskName": "", + "actions": [ + { + "name": "reopenIssue", + "parameters": {} + }, + { + "name": "removeLabel", + "parameters": { + "label": "Needs-Repro" + } + }, + { + "name": "addLabel", + "parameters": { + "label": "Needs-Attention :wave:" + } + } + ] + } } ], "userGroups": [] From 16c7ee99884ee7acd56bea667a02488daa34a017 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 23 Feb 2023 17:38:49 -0800 Subject: [PATCH 028/127] Use 'Visual Studio 2022' as the image for `appveyor` CI (#3594) --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1b0df3017..3c6dfa26c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ -image: Visual Studio 2017 +image: Visual Studio 2022 environment: POWERSHELL_TELEMETRY_OPTOUT: 1 From 513aaff0d90d78eb3da01e1618fb0a62e9c3e373 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 24 Feb 2023 14:41:51 -0800 Subject: [PATCH 029/127] Minor fix to the `appveyor` YAML file to remove the extra sub-expression (#3595) --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3c6dfa26c..0caf57baa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ cache: install: - pwsh: | - Write-Host "PS Version: $($($PSVersionTable.PSVersion))" + Write-Host "PS Version: $($PSVersionTable.PSVersion)" ./build.ps1 -Bootstrap build_script: From 372212e5ca6d00a67f4a5ee00c8243cfaa266562 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Feb 2023 10:48:00 -0800 Subject: [PATCH 030/127] Improve the list view to be scrollable and auto-adjust the list view height (#3583) --- PSReadLine/KeyBindings.cs | 4 +- PSReadLine/PSReadLineResources.Designer.cs | 11 + PSReadLine/PSReadLineResources.resx | 3 + PSReadLine/Prediction.Entry.cs | 106 ++- PSReadLine/Prediction.Views.cs | 530 ++++++++++-- PSReadLine/Prediction.cs | 36 + PSReadLine/Render.Helper.cs | 21 + PSReadLine/Render.cs | 57 +- test/CompletionTest.cs | 7 +- test/InlinePredictionTest.cs | 12 +- test/KeyInfo-en-US-windows.json | 2 +- test/ListPredictionTest.cs | 451 ++++++++-- test/ListScrollableViewTest.cs | 908 +++++++++++++++++++++ test/MockConsole.cs | 14 + 14 files changed, 1992 insertions(+), 170 deletions(-) create mode 100644 test/ListScrollableViewTest.cs diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 7b3f0c1c9..69daaa0c9 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -664,7 +664,7 @@ public static void ShowKeyBindings(ConsoleKeyInfo? key = null, object arg = null } // Don't overwrite any of the line - so move to first line after the end of our buffer. - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); console.SetCursorPosition(point.X, point.Y); console.Write("\n"); @@ -721,7 +721,7 @@ public static void WhatIsKey(ConsoleKeyInfo? key = null, object arg = null) var console = _singleton._console; // Don't overwrite any of the line - so move to first line after the end of our buffer. - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); console.SetCursorPosition(point.X, point.Y); console.Write("\n"); diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 925355f72..35dd5a337 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2070,6 +2070,17 @@ internal static string WindowSizeTooSmallForListView } } + /// + /// Looks up a localized string similar to "! terminal size too small to show the list view". + /// + internal static string WindowSizeTooSmallWarning + { + get + { + return ResourceManager.GetString("WindowSizeTooSmallWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Copy the text from the current kill ring position to the input. /// diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 7dc66be28..28d6e5d4f 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -840,6 +840,9 @@ Or not saving history with: The prediction 'ListView' is temporarily disabled because the current window size of the console is too small. To use the 'ListView', please make sure the 'WindowWidth' is not less than '{0}' and the 'WindowHeight' is not less than '{1}'. + + ! terminal size too small to show the list view + No help content available. Please use Update-Help to download the latest help content. diff --git a/PSReadLine/Prediction.Entry.cs b/PSReadLine/Prediction.Entry.cs index a60d05e2e..f576f62a6 100644 --- a/PSReadLine/Prediction.Entry.cs +++ b/PSReadLine/Prediction.Entry.cs @@ -9,19 +9,52 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { + /// + /// Represents a prediction source. + /// + private readonly struct SourceInfo + { + internal readonly string SourceName; + internal readonly int EndIndex; + internal readonly int PrevSourceEndIndex; + internal readonly int ItemCount; + + internal SourceInfo(string sourceName, int endIndex, int prevSourceEndIndex) + { + SourceName = sourceName; + int sourceWidth = LengthInBufferCells(SourceName); + if (sourceWidth > PredictionListView.SourceMaxWidth) + { + sourceWidth = PredictionListView.SourceMaxWidth - 1; + int sourceStrLen = SubstringLengthByCells(sourceName, sourceWidth); + SourceName = sourceName.Substring(0, sourceStrLen) + SuggestionEntry.Ellipsis; + } + + EndIndex = endIndex; + PrevSourceEndIndex = prevSourceEndIndex; + ItemCount = EndIndex - PrevSourceEndIndex; + } + } + /// /// This type represents an individual suggestion entry. /// private struct SuggestionEntry { + internal const char Ellipsis = '\u2026'; + internal const string HistorySource = "History"; + internal readonly Guid PredictorId; internal readonly uint? PredictorSession; internal readonly string Source; internal readonly string SuggestionText; internal readonly int InputMatchIndex; + private string _listItemTextRegular; + private string _listItemTextSelected; + internal SuggestionEntry(string suggestion, int matchIndex) - : this(source: "History", predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) + : this(source: HistorySource, predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) { } @@ -32,6 +65,8 @@ internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession PredictorSession = predictorSession; SuggestionText = suggestion; InputMatchIndex = matchIndex; + + _listItemTextRegular = _listItemTextSelected = null; } /// @@ -57,8 +92,19 @@ private static int DivideAndRoundUp(int dividend, int divisor) /// The highlighting sequences for a selected list item. internal string GetListItemText(int width, string input, string selectionHighlighting) { - const string ellipsis = "..."; - const int ellipsisLength = 3; + const int ellipsisLength = 1; + + if (selectionHighlighting is null) + { + if (_listItemTextRegular is not null) + { + return _listItemTextRegular; + } + } + else if (_listItemTextSelected is not null) + { + return _listItemTextSelected; + } // Calculate the 'SOURCE' portion to be rendered. int sourceStrLen = Source.Length; @@ -119,7 +165,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig // The suggestion text doesn't contain the user input. int length = SubstringLengthByCells(SuggestionText, textWidth - ellipsisLength); line.Append(SuggestionText, 0, length) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -136,7 +182,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig .Append(SuggestionText, 0, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, length - input.Length) - .Append(ellipsis); + .Append(Ellipsis); } else { @@ -149,7 +195,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig int remainingLenInCells = textWidth - ellipsisLength - rightLenInCells; int length = SubstringLengthByCellsFromEnd(SuggestionText, input.Length - 1, remainingLenInCells); line.Append(_singleton._options.EmphasisColor) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, input.Length - length, length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, SuggestionText.Length - input.Length); @@ -162,11 +208,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig int startIndex = input.Length - leftStrLen; int totalStrLen = SubstringLengthByCells(SuggestionText, startIndex, textWidth - ellipsisLength * 2); line.Append(_singleton._options.EmphasisColor) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, startIndex, leftStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, input.Length, totalStrLen - leftStrLen) - .Append(ellipsis); + .Append(Ellipsis); } } @@ -192,7 +238,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -201,7 +247,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig { // Otherwise, if the (mid+right) portions take up to 2/3 of the text width, we just truncate the suggestion text at the beginning. int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, textWidth - midRightLenInCells - ellipsisLength); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) @@ -223,13 +269,13 @@ internal string GetListItemText(int width, string input, string selectionHighlig int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, leftCellLen - ellipsisLength); int rightStrLen = SubstringLengthByCells(SuggestionText, rightStartindex, rigthCellLen - ellipsisLength); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, input.Length) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -249,7 +295,7 @@ internal string GetListItemText(int width, string input, string selectionHighlig line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); @@ -277,11 +323,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig line.Append(SuggestionText, 0, InputMatchIndex) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis); + .Append(Ellipsis); break; } @@ -298,11 +344,11 @@ internal string GetListItemText(int width, string input, string selectionHighlig int midRightStrLen = SubstringLengthByCellsFromEnd(SuggestionText, rightStartindex - 1, midRightCellLen); int leftStrLen = SubstringLengthByCellsFromEnd(SuggestionText, InputMatchIndex - 1, midRemainingLenInCells); - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, SuggestionText.Length - rightStartindex); @@ -324,15 +370,15 @@ internal string GetListItemText(int width, string input, string selectionHighlig int spacesNeeded = textWidth - midRemainingLenInCells * 3 - ellipsisLength * 3; string spaces = spacesNeeded > 0 ? Spaces(spacesNeeded) : string.Empty; - line.Append(ellipsis) + line.Append(Ellipsis) .Append(SuggestionText, InputMatchIndex - leftStrLen, leftStrLen) .Append(_singleton._options.EmphasisColor) .Append(SuggestionText, InputMatchIndex, midLeftStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(SuggestionText, rightStartindex - midRightStrLen, midRightStrLen) .EndColorSection(selectionHighlighting) .Append(SuggestionText, rightStartindex, rightStrLen) - .Append(ellipsis) + .Append(Ellipsis) .Append(spaces); break; } @@ -351,13 +397,29 @@ internal string GetListItemText(int width, string input, string selectionHighlig else { line.Append(Source, 0, sourceStrLen) - .Append(ellipsis); + .Append(Ellipsis); } line.EndColorSection(selectionHighlighting) .Append(']'); - return line.ToString(); + if (selectionHighlighting is not null) + { + // Need to reset at the end if the selection highlighting is being applied. + line.Append(VTColorUtils.AnsiReset); + } + + string textForRendering = line.ToString(); + if (selectionHighlighting is null) + { + _listItemTextRegular = textForRendering; + } + else + { + _listItemTextSelected = textForRendering; + } + + return textForRendering; } } } diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index ddfbd073d..91ef84377 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using System.Management.Automation.Subsystem.Prediction; using Microsoft.PowerShell.Internal; +using Microsoft.PowerShell.PSReadLine; namespace Microsoft.PowerShell { @@ -207,19 +208,48 @@ protected List GetPredictionResults() /// private class PredictionListView : PredictionViewBase { - internal const int ListMaxCount = 10; - internal const int ListMaxWidth = 100; + // Item count constants. + internal const int ListMaxCount = 50; + internal const int HistoryMaxCount = 10; + + // List view constants. + internal const int ListViewMaxHeight = 10; + internal const int ListViewMaxWidth = 100; internal const int SourceMaxWidth = 15; - internal const int MinWindowWidth = 54; - internal const int MinWindowHeight = 15; + // Minimal window size. + internal const int MinWindowWidth = 50; + internal const int MinWindowHeight = 5; + // The items to be displayed in the list view. private List _listItems; - private int _listItemWidth; - private int _listItemHeight; + // Information about the sources of those items. + private List _sources; + // The index that is currently selected by user. private int _selectedIndex; + // Indicates to have the list view starts at the selected index. + private bool _renderFromSelected; + // Indicates a navigation update within the list view is pending. private bool _updatePending; + // The max list height to be used for rendering, which is auto-adjusted based on terminal height. + private int _maxViewHeight; + // The actual height of the list view that is currently rendered. + private int _listViewHeight; + // The actual width of the list view that is currently rendered. + private int _listViewWidth; + // An index pointing to the item that is shown in the first slot of the list view. + private int _listViewTop; + // An index pointing to the item right AFTER the one that is shown in the last slot of the list view. + private int _listViewEnd; + + // Indicates if we need to check on the height for each navigation in the list view. + private bool _checkOnHeight; + // To warn about that the terminal size is too small to display the list view. + private bool _warnAboutSize; + // Indicates if a warning message was displayed. + private bool _warningPrinted; + // Caches re-used when aggregating the suggestion results from predictors and history. // Those caches help us avoid allocation on tons of short-lived collections. private List _cacheList1; @@ -273,6 +303,36 @@ internal PredictionListView(PSConsoleReadLine singleton) internal override bool HasPendingUpdate => _updatePending; internal override bool HasActiveSuggestion => _listItems != null; + /// + /// Calculate the max width and height of the list view based on the current terminal size. + /// + private (int maxWidth, int maxHeight, bool checkOnHeight) RefreshMaxViewSize() + { + var console = _singleton._console; + int maxWidth = Math.Min(console.BufferWidth, ListViewMaxWidth); + + (int maxHeight, bool moreCheck) = console.BufferHeight switch + { + > ListViewMaxHeight * 2 => (ListViewMaxHeight, false), + > ListViewMaxHeight => (ListViewMaxHeight / 2, false), + _ => (ListViewMaxHeight / 3, true) + }; + + return (maxWidth, maxHeight, moreCheck); + } + + /// + /// Check if the height becomes too small for the current rendering. + /// + private bool HeightIsTooSmall() + { + int physicalLineCountForBuffer = _singleton.EndOfBufferPosition().Y - _singleton._initialY + 1; + return _singleton._console.BufferHeight < physicalLineCountForBuffer + _maxViewHeight + 1 /* one metadata line */; + } + + /// + /// Get suggestion results. + /// internal override void GetSuggestion(string userInput) { if (_singleton._initialY < 0) @@ -295,14 +355,16 @@ internal override void GetSuggestion(string userInput) if (!WindowSizeMeetsMinRequirement) { - // If the window size is too small for the list view to work, we just disable the list view. - Reset(); + // If the window size is too small to show the list view, we disable the list view and show a warning. + _warnAboutSize = true; return; } _inputText = userInput; + // Reset the list item selection. _selectedIndex = -1; - _listItemWidth = Math.Min(_singleton._console.BufferWidth, ListMaxWidth); + // Refresh the list view width and height in case the terminal was resized. + (_listViewWidth, _maxViewHeight, _checkOnHeight) = RefreshMaxViewSize(); if (inputUnchanged) { @@ -313,6 +375,7 @@ internal override void GetSuggestion(string userInput) } _listItems?.Clear(); + _sources?.Clear(); try { @@ -323,7 +386,11 @@ internal override void GetSuggestion(string userInput) if (UseHistory) { - _listItems = GetHistorySuggestions(userInput, ListMaxCount); + _listItems = GetHistorySuggestions(userInput, HistoryMaxCount); + if (_listItems?.Count > 0) + { + _sources = new List() { new SourceInfo(SuggestionEntry.HistorySource, _listItems.Count - 1, -1) }; + } } } catch @@ -348,6 +415,7 @@ private void AggregateSuggestions() try { _listItems ??= new List(); + _sources ??= new List(); _cacheList1 ??= new List(); // This list holds the total number of suggestions from each of the predictors. _cacheList2 ??= new List(); // This list holds the final number of suggestions that will be rendered for each of the predictors. @@ -412,13 +480,18 @@ private void AggregateSuggestions() break; } - more = _cacheList1[i] > 0; + more |= _cacheList1[i] > 0; } } if (hCount > 0) { - _listItems.RemoveRange(hCount, _listItems.Count - hCount); + if (hCount < _listItems.Count) + { + _listItems.RemoveRange(hCount, _listItems.Count - hCount); + _sources.Clear(); + _sources.Add(new SourceInfo(SuggestionEntry.HistorySource, hCount - 1, prevSourceEndIndex: -1)); + } if (_cachedComparer != _singleton._options.HistoryStringComparer) { @@ -471,22 +544,29 @@ private void AggregateSuggestions() // Get the number of prediction results that were actually put in the list after filtering out the duplicate ones. int count = _cacheList2[index] - num; - if (item.Session.HasValue && count > 0) + if (count > 0) { - // Send feedback only if the mini-session id is specified and we truely have its results in the list to be rendered. - // When the mini-session id is not specified, we consider the predictor doesn't accept feedback. - // - // NOTE: when any duplicate results were skipped, the 'count' passed in here won't be accurate as it still includes - // those skipped ones. This is due to the limitation of the 'OnSuggestionDisplayed' interface method, which didn't - // assume any prediction results from a predictor could be filtered out at the initial design time. We will have to - // change the predictor interface to pass in accurate information, such as: - // void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex, int[] skippedIndices) - // - // However, an interface change has huge impacts. At least, a newer version of PSReadLine will stop working on the - // existing PowerShell 7+ versions. For this particular issue, the chance that it could happen is low and the impact - // of the inaccurate feedback is also low, so we should delay this interface change until another highly-demanded - // change to the interface is required in future (e.g. changes related to supporting OpenAI models). - _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, count + skipCount); + int prevEndIndex = _sources.Count > 0 ? _sources[_sources.Count - 1].EndIndex : -1; + int endIndex = _listItems.Count - 1; + _sources.Add(new SourceInfo(_listItems[endIndex].Source, endIndex, prevEndIndex)); + + if (item.Session.HasValue && count > 0) + { + // Send feedback only if the mini-session id is specified and we truely have its results in the list to be rendered. + // When the mini-session id is not specified, we consider the predictor doesn't accept feedback. + // + // NOTE: when any duplicate results were skipped, the 'count' passed in here won't be accurate as it still includes + // those skipped ones. This is due to the limitation of the 'OnSuggestionDisplayed' interface method, which didn't + // assume any prediction results from a predictor could be filtered out at the initial design time. We will have to + // change the predictor interface to pass in accurate information, such as: + // void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex, int[] skippedIndices) + // + // However, an interface change has huge impacts. At least, a newer version of PSReadLine will stop working on the + // existing PowerShell 7+ versions. For this particular issue, the chance that it could happen is low and the impact + // of the inaccurate feedback is also low, so we should delay this interface change until another highly-demanded + // change to the interface is required in future (e.g. changes related to supporting OpenAI models). + _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, count + skipCount); + } } } } @@ -501,7 +581,10 @@ private void AggregateSuggestions() if (_listItems?.Count > 0) { - _listItemHeight = Math.Min(_listItems.Count, ListMaxCount); + // Initialize the view window position here. + _listViewTop = 0; + _listViewEnd = Math.Min(_listItems.Count, _maxViewHeight); + _listViewHeight = _listViewEnd - _listViewTop; } else { @@ -509,8 +592,24 @@ private void AggregateSuggestions() } } + /// + /// Generate the rendering text for the list view. + /// internal override void RenderSuggestion(List consoleBufferLines, ref int currentLogicalLine) { + if (_warnAboutSize || (_checkOnHeight && HeightIsTooSmall())) + { + _warningPrinted = true; + RenderWarningLine(NextBufferLine(consoleBufferLines, ref currentLogicalLine)); + + Reset(); + return; + } + else + { + _warningPrinted = false; + } + if (_updatePending) { _updatePending = false; @@ -520,36 +619,254 @@ internal override void RenderSuggestion(List consoleBufferLines, AggregateSuggestions(); } - if (_listItems == null) + if (_listItems is null) { return; } - for (int i = 0; i < _listItemHeight; i++) + // Create the metadata line. + RenderMetadataLine(NextBufferLine(consoleBufferLines, ref currentLogicalLine)); + + if (_selectedIndex >= 0) { - currentLogicalLine += 1; - if (currentLogicalLine == consoleBufferLines.Count) + // An item was selected, so update the view window accrodingly. + if (_renderFromSelected) { - consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + // Render from the selected index if there are enough items left for a page. + // If not, then render all remaining items plus a few from above the selected one, so as to render a full page. + _renderFromSelected = false; + int offset = _maxViewHeight - Math.Min(_listItems.Count - _selectedIndex, _maxViewHeight); + _listViewTop = offset > 0 ? Math.Max(0, _selectedIndex - offset) : _selectedIndex; + _listViewEnd = Math.Min(_listItems.Count, _listViewTop + _maxViewHeight); + } + else + { + // - if the selected item is within the current top/end, then no need to move the list view window. + // - if the selected item is before the current top, then move the top to the selected item. + // - if the selected item is after the current end, then move the end to one beyond the selected item. + if (_selectedIndex < _listViewTop) + { + _listViewTop = _selectedIndex; + _listViewEnd = Math.Min(_listItems.Count, _selectedIndex + _maxViewHeight); + } + else if (_selectedIndex >= _listViewEnd) + { + _listViewEnd = _selectedIndex + 1; + _listViewTop = Math.Max(0, _listViewEnd - _maxViewHeight); + } } - bool itemSelected = i == _selectedIndex; - StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine]; + _listViewHeight = _listViewEnd - _listViewTop; + } + for (int i = _listViewTop; i < _listViewEnd; i++) + { + bool itemSelected = i == _selectedIndex; string selectionColor = itemSelected ? _singleton._options._listPredictionSelectedColor : null; - currentLineBuffer.Append( - _listItems[i].GetListItemText( - _listItemWidth, + + NextBufferLine(consoleBufferLines, ref currentLogicalLine) + .Append(_listItems[i].GetListItemText( + _listViewWidth, _inputText, selectionColor)); + } + } + + /// + /// Generate the rendering text for the warning message. + /// + private void RenderWarningLine(StringBuilder buffer) + { + // Add italic text effect to the highlight color. + string highlightStyle = _singleton._options._listPredictionColor + "\x1b[3m"; + + buffer.Append(highlightStyle) + .Append(PSReadLineResources.WindowSizeTooSmallWarning) + .Append(VTColorUtils.AnsiReset); + } - if (itemSelected) + /// + /// Calculate the height of the list when warning was displayed. + /// + private int GetPesudoListHeightForWarningRendering() + { + int bufferWidth = _singleton._console.BufferWidth; + int lengthInCells = LengthInBufferCells(PSReadLineResources.WindowSizeTooSmallWarning); + int pesudoListHeight = lengthInCells / bufferWidth; + + if (lengthInCells % bufferWidth == 0) + { + pesudoListHeight--; + } + + return pesudoListHeight; + } + + /// + /// Generate the rendering text for the metadata line. + /// + private void RenderMetadataLine(StringBuilder buffer) + { + // Add italic text effect to the highlight color. + string highlightStyle = _singleton._options._listPredictionColor + "\x1b[3m"; + string dimmedStyle = PSConsoleReadLineOptions.DefaultInlinePredictionColor; + string activeStyle = null; + + // Render the quick indicator. + buffer.Append(highlightStyle) + .Append('<') + .Append(_selectedIndex > -1 ? _selectedIndex + 1 : "-") + .Append('/') + .Append(_listItems.Count) + .Append('>') + .Append(VTColorUtils.AnsiReset); + + if (_listViewWidth < 60) + { + // We don't render the additional information about sources when the list view width is less than 60. + // Adjust the position of quick indicator a little bit in this case and call it done. + buffer.Insert(0, VTColorUtils.AnsiReset); + buffer.Insert(VTColorUtils.AnsiReset.Length, " ", count: 2); + return; + } + + /// + /// A helper function to avoid appending extra color VT sequences unnecessarily. + /// + static StringBuilder AppendColor(StringBuilder buffer, string colorToUse, ref string activeColor, out int nextCharPos) + { + if (activeColor is null) { - currentLineBuffer.Append(VTColorUtils.AnsiReset); + buffer.Append(colorToUse); } + else if (activeColor != colorToUse) + { + buffer.Append(VTColorUtils.AnsiReset).Append(colorToUse); + } + + activeColor = colorToUse; + nextCharPos = buffer.Length; + return buffer; } + + // The list view width decides how to render the source information: + // - when width >= 80, we render upto 3 sources, + // - when width >= 60, we render upto 2 sources. + // The reason to select '80' and '60' here is because: + // - To render upto 3 sources, the maximum cell length that could be taken by both the total-count part and the extra-info part + // will be 75 (7+68), so we choose '80' as the minimal requirement for rendering 3 sources. + // - To render upto 2 sources, the maximum cell length that could be taken by both the total-count part and the extra-info part + // will be 55 (7+48), so we choose '60' as the minimal requirement for rendering 2 sources. + int maxSourceCount = _listViewWidth >= 80 ? 3 : 2; + int charPosition = buffer.Length; + int totalCountPartLength = buffer.Length - highlightStyle.Length - VTColorUtils.AnsiReset.Length; + int additionalPartLength = 0; + + int selected = -1; + int startFrom = 0; + + // If a list item was selected, calculate which source the list item belongs to and which source + // to start render for the additional information part. + if (_selectedIndex > -1) + { + for (int i = 0; i < _sources.Count; i++) + { + if (_selectedIndex <= _sources[i].EndIndex) + { + selected = i; + break; + } + } + + if (selected == 0) + { + startFrom = 0; + } + else if (selected == _sources.Count - 1) + { + startFrom = Math.Max(0, selected - (maxSourceCount - 1)); + } + else + { + startFrom = maxSourceCount == 3 ? selected - 1 : selected; + } + } + + // Start the extra information about the sources -- add the opening arrow bracket. + AppendColor(buffer, dimmedStyle, ref activeStyle, out _).Append('<'); + additionalPartLength++; + + // Add the prefix, continue to use dimmed color. + if (startFrom > 0) + { + buffer.Append(SuggestionEntry.Ellipsis).Append(' '); + additionalPartLength += 2; + } + + // Add the sources. + for (int i = 0; i < maxSourceCount; i++) + { + int index = startFrom + i; + if (index == _sources.Count) + { + break; + } + + if (i > 0) + { + // Add the separator. + buffer.Append(' '); + additionalPartLength++; + } + + int nextCharPos; + SourceInfo info = _sources[index]; + if (selected == index) + { + AppendColor(buffer, highlightStyle, ref activeStyle, out nextCharPos) + .Append(info.SourceName) + .Append('(') + .Append(_selectedIndex - info.PrevSourceEndIndex) + .Append('/') + .Append(info.ItemCount) + .Append(')'); + } + else + { + AppendColor(buffer, dimmedStyle, ref activeStyle, out nextCharPos) + .Append(info.SourceName) + .Append('(') + .Append(info.ItemCount) + .Append(')'); + } + + // Need to take into account multi-cell characters when calculating length. + additionalPartLength += LengthInBufferCells(buffer, nextCharPos, buffer.Length); + } + + // Add the suffix. + if (startFrom + maxSourceCount < _sources.Count) + { + AppendColor(buffer, dimmedStyle, ref activeStyle, out _) + .Append(' ') + .Append(SuggestionEntry.Ellipsis); + additionalPartLength += 2; + } + + // Add the closing arrow bracket. + AppendColor(buffer, dimmedStyle, ref activeStyle, out _) + .Append('>') + .Append(VTColorUtils.AnsiReset); + additionalPartLength++; + + // Lastly, insert the padding spaces. + int padding = _listViewWidth - additionalPartLength - totalCountPartLength; + buffer.Insert(charPosition, " ", padding); } + /// + /// Trigger the feedback about a suggestion was accepted. + /// internal override void OnSuggestionAccepted() { if (!UsePlugin) @@ -569,24 +886,40 @@ internal override void OnSuggestionAccepted() } } + /// + /// Clear the list view. + /// internal override void Clear(bool cursorAtEol) { - if (_listItems == null) { return; } + if (_listItems == null && !_warningPrinted) + { + return; + } + + int listHeight = _warningPrinted + ? GetPesudoListHeightForWarningRendering() + : _listViewHeight; int top = cursorAtEol ? _singleton._console.CursorTop - : _singleton.ConvertOffsetToPoint(_inputText.Length).Y; + : _singleton.EndOfBufferPosition().Y; - _singleton.WriteBlankLines(top + 1, _listItemHeight); + _warningPrinted = false; + _singleton.WriteBlankLines(top + 1, listHeight + 1 /* plus 1 to include the metadata line */); Reset(); } + /// + /// Reset all the list view states. + /// internal override void Reset() { base.Reset(); + + _sources = null; _listItems = null; - _listItemWidth = _listItemHeight = _selectedIndex = -1; - _updatePending = false; + _maxViewHeight = _listViewTop = _listViewEnd = _listViewWidth = _listViewHeight = _selectedIndex = -1; + _warnAboutSize = _checkOnHeight = _updatePending = _renderFromSelected = false; } /// @@ -595,8 +928,12 @@ internal override void Reset() /// internal void UpdateListSelection(int move) { + // While moving around the list, we want to go back to the original input when we move one down + // after the last item, or move one up before the first item in the list. + // So, we can imagine a virtual list constructed by inserting the original input at the index 0 + // of the real list. int virtualItemIndex = _selectedIndex + 1; - int virtualItemCount = _listItemHeight + 1; + int virtualItemCount = _listItems.Count + 1; _updatePending = true; virtualItemIndex += move; @@ -614,6 +951,107 @@ internal void UpdateListSelection(int move) _selectedIndex = virtualItemIndex % virtualItemCount + virtualItemCount - 1; } } + + /// + /// Page up/down within the list view. + /// Update the index of the selected item based on and . + /// + internal bool UpdateListByPaging(bool pageUp, int num) + { + if (_selectedIndex == -1) + { + return false; + } + + int oldSelectedIndex = _selectedIndex; + int lastItemIndex = _listItems.Count - 1; + + for (int i = 0; i < num; i++) + { + if (pageUp) + { + if (_selectedIndex == 0) + { + break; + } + + // Do one page up. + _selectedIndex = _selectedIndex == _listViewEnd - 1 + ? _listViewTop + : Math.Max(0, _selectedIndex - (_maxViewHeight - 1)); + } + else + { + if (_selectedIndex == lastItemIndex) + { + break; + } + + // Do one page down. + _selectedIndex = _selectedIndex == _listViewTop + ? _listViewEnd - 1 + : Math.Min(lastItemIndex, _selectedIndex + (_maxViewHeight - 1)); + } + } + + if (_selectedIndex != oldSelectedIndex) + { + // The selected item is changed, so we need to update the rendering. + _updatePending = true; + return true; + } + + return false; + } + + /// + /// Loop up/down through the sources rendered in the list view. + /// Update the index of the selected item based on and . + /// + internal bool UpdateListByLoopingSources(bool jumpUp, int num) + { + if (_selectedIndex == -1) + { + return false; + } + + int selectedSource = -1; + for (int i = 0; i < _sources.Count; i++) + { + if (_selectedIndex <= _sources[i].EndIndex) + { + selectedSource = i; + break; + } + } + + int oldSelectedIndex = _selectedIndex; + for (int i = 0; i < num; i++) + { + if (jumpUp) + { + _selectedIndex = selectedSource == 0 + ? _sources[_sources.Count - 1].PrevSourceEndIndex + 1 + : _sources[selectedSource - 1].PrevSourceEndIndex + 1; + } + else + { + _selectedIndex = selectedSource == _sources.Count - 1 + ? 0 + : _sources[selectedSource].EndIndex + 1; + } + } + + if (_selectedIndex != oldSelectedIndex) + { + // The selected item is changed, so we need to update the rendering. + _updatePending = true; + _renderFromSelected = true; + return true; + } + + return false; + } } /// diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index 53f6b8437..dfa1f1cce 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -163,6 +163,42 @@ private static bool UpdateListSelection(int numericArg) return false; } + private static bool UpdateListByPaging(bool pageUp, int numericArg) + { + if (_singleton._prediction.ActiveView is PredictionListView listView && listView.HasActiveSuggestion) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + if (listView.UpdateListByPaging(pageUp, numericArg)) + { + ReplaceSelection(listView.SelectedItemText); + } + + return true; + } + + return false; + } + + private static bool UpdateListByLoopingSources(bool jumpUp, int numericArg) + { + if (_singleton._prediction.ActiveView is PredictionListView listView && listView.HasActiveSuggestion) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + if (listView.UpdateListByLoopingSources(jumpUp, numericArg)) + { + ReplaceSelection(listView.SelectedItemText); + } + + return true; + } + + return false; + } + /// /// Replace current buffer with the selected list item text. /// The replacement is done in a way that allows further selection updates for the same list view diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 0c041e735..8fe8de071 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -3,6 +3,7 @@ --********************************************************************/ using System; +using System.Text; namespace Microsoft.PowerShell { @@ -70,6 +71,26 @@ internal static int LengthInBufferCells(string str, int start, int end) return sum; } + internal static int LengthInBufferCells(StringBuilder sb, int start, int end) + { + var sum = 0; + for (var i = start; i < end; i++) + { + var c = sb[i]; + if (c == 0x1b && (i + 1) < end && sb[i + 1] == '[') + { + // Simple escape sequence skipping + i += 2; + while (i < end && sb[i] != 'm') + i++; + + continue; + } + sum += LengthInBufferCells(c); + } + return sum; + } + internal static int LengthInBufferCells(char c) { if (c < 256) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index ff5649fe3..d08b65971 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -294,11 +294,7 @@ void RenderOneChar(char charToRender, bool toEmphasize) _consoleBufferLines[currentLogicalLine].Append(VTColorUtils.AnsiReset); } - currentLogicalLine += 1; - if (currentLogicalLine == _consoleBufferLines.Count) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } + NextBufferLine(_consoleBufferLines, ref currentLogicalLine); // Reset the color for continuation prompt so the color sequence will always be explicitly // specified for continuation prompt in the generated render strings. @@ -458,11 +454,7 @@ void RenderOneChar(char charToRender, bool toEmphasize) if (_statusLinePrompt != null) { - currentLogicalLine += 1; - if (currentLogicalLine > _consoleBufferLines.Count - 1) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } + NextBufferLine(_consoleBufferLines, ref currentLogicalLine); color = _statusIsErrorMessage ? Options._errorColor : defaultColor; UpdateColorsIfNecessary(color); @@ -478,6 +470,20 @@ void RenderOneChar(char charToRender, bool toEmphasize) return currentLogicalLine + 1; } + /// + /// Return the next logical line buffer, and create a new one if we are at the end. + /// + private static StringBuilder NextBufferLine(List consoleBufferLines, ref int current) + { + current += 1; + if (current == consoleBufferLines.Count) + { + consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + } + + return consoleBufferLines[current]; + } + /// /// Flip the color on the prompt if the error state changed. /// @@ -1168,6 +1174,11 @@ private void MoveCursor(int newCursor) _current = newCursor; } + internal Point EndOfBufferPosition() + { + return ConvertOffsetToPoint(_buffer.Length); + } + internal Point ConvertOffsetToPoint(int offset) { int x = _initialX; @@ -1670,6 +1681,12 @@ private bool PromptYesOrNo(string s) public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByPaging(pageUp: true, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop - (numericArg * console.WindowHeight); if (newTop < 0) @@ -1685,6 +1702,12 @@ public static void ScrollDisplayUp(ConsoleKeyInfo? key = null, object arg = null public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByLoopingSources(jumpUp: true, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop - numericArg; if (newTop < 0) @@ -1700,6 +1723,12 @@ public static void ScrollDisplayUpLine(ConsoleKeyInfo? key = null, object arg = public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByPaging(pageUp: false, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop + (numericArg * console.WindowHeight); if (newTop > (console.BufferHeight - console.WindowHeight)) @@ -1715,6 +1744,12 @@ public static void ScrollDisplayDown(ConsoleKeyInfo? key = null, object arg = nu public static void ScrollDisplayDownLine(ConsoleKeyInfo? key = null, object arg = null) { TryGetArgAsInt(arg, out var numericArg, +1); + + if (UpdateListByLoopingSources(jumpUp: false, numericArg)) + { + return; + } + var console = _singleton._console; var newTop = console.WindowTop + numericArg; if (newTop > (console.BufferHeight - console.WindowHeight)) @@ -1738,7 +1773,7 @@ public static void ScrollDisplayTop(ConsoleKeyInfo? key = null, object arg = nul public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg = null) { // Ideally, we'll put the last input line at the bottom of the window - var point = _singleton.ConvertOffsetToPoint(_singleton._buffer.Length); + var point = _singleton.EndOfBufferPosition(); var console = _singleton._console; var newTop = point.Y - console.WindowHeight + 1; diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index eeec6a162..238006c07 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -971,6 +971,7 @@ public void MenuCompletions_WorkWithListView() TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); @@ -979,9 +980,13 @@ public void MenuCompletions_WorkWithListView() Test("Get-Module", Keys( "Get-Mo", - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "Get-Mo", NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "Get-Mo", diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 9c0edd752..7dedb2c5f 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -397,6 +397,7 @@ public void ViDefect2408() private const uint MiniSessionId = 56; private static readonly Guid predictorId_1 = Guid.Parse("b45b5fbe-90fa-486c-9c87-e7940fdd6273"); private static readonly Guid predictorId_2 = Guid.Parse("74a86463-033b-44a3-b386-41ee191c94be"); + private static readonly Guid predictorId_3 = Guid.Parse("19e98622-99e0-41f7-9ee0-a8bed92cde51"); /// /// Mocked implementation of 'PredictInput'. @@ -423,13 +424,22 @@ internal static List MockedPredictInput(Ast ast, Token[] token new PredictiveSuggestion($"SOME NEW TEXT"), }; - return new List + var result = new List { (PredictionResult)ctor.Invoke( new object[] { predictorId_1, "TestPredictor", MiniSessionId, suggestions_1 }), (PredictionResult)ctor.Invoke( new object[] { predictorId_2, "LongNamePredictor", MiniSessionId, suggestions_2 }), }; + + // Return an extra source if it's for testing the metadata line. + if (input == "metadata-line") + { + result.Add((PredictionResult)ctor.Invoke( + new object[] { predictorId_3, "Metadata", MiniSessionId, suggestions_2 })); + } + + return result; } [SkippableFact] diff --git a/test/KeyInfo-en-US-windows.json b/test/KeyInfo-en-US-windows.json index 53243ab27..c0fcc65db 100644 --- a/test/KeyInfo-en-US-windows.json +++ b/test/KeyInfo-en-US-windows.json @@ -831,7 +831,7 @@ "Key": "Ctrl+PageUp", "KeyChar": "\u0000", "ConsoleKey": "PageUp", - "Modifiers": "0" + "Modifiers": "Control" }, { "Key": "Ctrl+q", diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 3788fca1f..4931867a5 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -8,7 +8,7 @@ public partial class ReadLine { // The source of truth is defined in 'Microsoft.PowerShell.PSConsoleReadLine+PredictionListView'. // Make sure the values are in sync. - private const int MinWindowWidth = 54; + private const int MinWindowWidth = 50; private const int MinWindowHeight = 15; private const int ListMaxWidth = 100; private const int SourceMaxWidth = 15; @@ -19,8 +19,8 @@ private int CheckWindowSize() // This is a precaution check, just in case that things change. int winWidth = _console.WindowWidth; int winHeight = _console.WindowHeight; - Assert.True(winWidth >= 54, $"list-view prediction requires minimum window width {MinWindowWidth}. Make sure the TestConsole's width is set properly."); - Assert.True(winHeight >= 15, $"list-view prediction requires minimum window height {MinWindowHeight}. Make sure the TestConsole's height is set properly."); + Assert.True(winWidth >= MinWindowWidth, $"list-view prediction requires minimum window width {MinWindowWidth}. Make sure the TestConsole's width is set properly."); + Assert.True(winHeight >= MinWindowHeight, $"list-view prediction requires minimum window height {MinWindowHeight}. Make sure the TestConsole's height is set properly."); int listWidth = winWidth > ListMaxWidth ? ListMaxWidth : winWidth; return listWidth; @@ -98,15 +98,22 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + // The font effect sequences of the dimmed color used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Different matches as more input coming SetHistory("echo -bar", "eca -zoo"); Test("ech", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -125,9 +132,13 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() TokenClassification.ListPrediction, "History", TokenClassification.None, ']' )), - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ec", @@ -146,9 +157,13 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() TokenClassification.ListPrediction, "History", TokenClassification.None, ']' )), - 'h', CheckThat(() => AssertScreenIs(2, + 'h', CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "ech", NextLine, + TokenClassification.ListPrediction, "<-/1>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/1>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ech", @@ -171,15 +186,20 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Navigate up and down in the list SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -199,11 +219,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -223,11 +249,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -247,9 +279,13 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.ListPredictionSelected, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -269,11 +305,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -293,11 +335,17 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.ListPredictionSelected, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -317,9 +365,13 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() TokenClassification.None, ']' )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -353,15 +405,20 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() new KeyHandler("Ctrl+p", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("Ctrl+l", PSConsoleReadLine.HistorySearchForward)); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Navigate up and down in the list SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -381,11 +438,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -405,11 +468,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -429,9 +498,13 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.ListPredictionSelected, ']' )), _.Ctrl_l, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -451,11 +524,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -475,11 +554,17 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.ListPredictionSelected, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -499,9 +584,13 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() TokenClassification.None, ']' )), _.Ctrl_p, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -533,15 +622,20 @@ public void List_RenderSuggestion_Escape() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); // Press 'Escape' without selecting an item. SetHistory("echo -bar", "eca -zoo"); Test("echo -bar", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -568,9 +662,13 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, new string(' ', listWidth) )), // Keep typing will trigger the list view again - 'h', CheckThat(() => AssertScreenIs(2, + 'h', CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "ch", NextLine, + TokenClassification.ListPrediction, "<-/1>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/1>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, "ch", @@ -581,11 +679,17 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(2, + CheckThat(() => AssertScreenIs(3, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<1/1>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/1>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " e", emphasisColors, "ch", @@ -607,9 +711,13 @@ public void List_RenderSuggestion_Escape() // Press 'Escape' after selecting an item. SetHistory("echo -bar", "eca -zoo"); Test("c", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -629,11 +737,17 @@ public void List_RenderSuggestion_Escape() TokenClassification.None, ']' )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " e", emphasisColors, 'c', @@ -671,14 +785,19 @@ public void List_RenderSuggestion_DigitArgument() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("c", Keys( - 'c', CheckThat(() => AssertScreenIs(3, + 'c', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -698,9 +817,13 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, ']' )), _.Alt_2, - CheckThat(() => AssertScreenIs(4, + CheckThat(() => AssertScreenIs(5, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -722,11 +845,17 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, "digit-argument: 2" )), _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -746,11 +875,17 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.ListPredictionSelected, ']' )), _.Alt_2, - CheckThat(() => AssertScreenIs(4, + CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "echo", TokenClassification.None, ' ', TokenClassification.Parameter, "-bar", NextLine, + TokenClassification.ListPrediction, "<2/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<2/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(2/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -772,9 +907,13 @@ public void List_RenderSuggestion_DigitArgument() TokenClassification.None, "digit-argument: 2" )), _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'c', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " e", emphasisColors, 'c', @@ -806,14 +945,19 @@ public void List_RenderSuggestion_CtrlZ() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("e", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -833,11 +977,17 @@ public void List_RenderSuggestion_CtrlZ() TokenClassification.None, ']' )), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -858,9 +1008,13 @@ public void List_RenderSuggestion_CtrlZ() )), // No matter how many navigation operations were done, 'Ctrl+z' (undo) reverts back to the initial list view state. _.Ctrl_z, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -881,11 +1035,17 @@ public void List_RenderSuggestion_CtrlZ() )), // After undo, you can continue to navigate in the list. _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -918,14 +1078,19 @@ public void List_RenderSuggestion_Selection() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.ListView); SetHistory("echo -bar", "eca -zoo"); Test("eca -zoo", Keys( - 'e', CheckThat(() => AssertScreenIs(3, + 'e', CheckThat(() => AssertScreenIs(4, TokenClassification.Command, 'e', NextLine, + TokenClassification.ListPrediction, "<-/2>", + TokenClassification.None, new string(' ', listWidth - 17), // 17 is the length of '<-/2>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, 'e', @@ -946,11 +1111,17 @@ public void List_RenderSuggestion_Selection() )), _.DownArrow, CheckThat(() => AssertCursorLeftIs(8)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -972,11 +1143,17 @@ public void List_RenderSuggestion_Selection() // Moving cursor won't trigger a new prediction. _.LeftArrow, _.LeftArrow, CheckThat(() => AssertCursorLeftIs(6)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -997,11 +1174,17 @@ public void List_RenderSuggestion_Selection() )), _.Ctrl_LeftArrow, CheckThat(() => AssertCursorLeftIs(5)), - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1022,12 +1205,18 @@ public void List_RenderSuggestion_Selection() )), // Text selection won't trigger a new prediction. _.Shift_LeftArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Selection, '-', TokenClassification.Parameter, "zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1047,10 +1236,16 @@ public void List_RenderSuggestion_Selection() TokenClassification.None, ']' )), _.Ctrl_Shift_LeftArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Selection, "eca -", TokenClassification.Parameter, "zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1083,6 +1278,7 @@ public void List_HistorySource_NoAcceptanceCallback() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'History' source will not trigger 'acceptance' callbacks. @@ -1092,11 +1288,17 @@ public void List_HistorySource_NoAcceptanceCallback() SetHistory("echo -bar", "eca -zoo"); Test("eca -zooa", Keys( 'e', _.DownArrow, - CheckThat(() => AssertScreenIs(3, + CheckThat(() => AssertScreenIs(4, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo", NextLine, + TokenClassification.ListPrediction, "<1/2>", + TokenClassification.None, new string(' ', listWidth - 19), // 19 is the length of '<1/2>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, 'e', @@ -1140,6 +1342,7 @@ public void List_PluginSource_Acceptance() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'Plugin' source will make PSReadLine get prediction from the plugin only. @@ -1148,9 +1351,13 @@ public void List_PluginSource_Acceptance() SetHistory("echo -bar", "eca -zoo"); Test("SOME NEW TEX SOME TEXT AFTER", Keys( - "ec", CheckThat(() => AssertScreenIs(5, + "ec", CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1170,9 +1377,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1183,10 +1390,16 @@ public void List_PluginSource_Acceptance() CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " TEXT BEFORE ec", NextLine, + TokenClassification.ListPrediction, "<1/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(1/2) ", + dimmedColors, "LongNamePredic…(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1206,9 +1419,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1217,9 +1430,15 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Shift_Home, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Selection, "SOME TEXT BEFORE ec", NextLine, + TokenClassification.ListPrediction, "<1/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(1/2) ", + dimmedColors, "LongNamePredic…(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", emphasisColors, "ec", @@ -1239,9 +1458,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1250,9 +1469,13 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when selecting the input. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), "j", - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "j", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "j", @@ -1272,9 +1495,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1290,10 +1513,16 @@ public void List_PluginSource_Acceptance() _.DownArrow, _.DownArrow, _.DownArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEXT", NextLine, + TokenClassification.ListPrediction, "<3/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + dimmedColors, "', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "j", @@ -1313,9 +1542,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1324,10 +1553,14 @@ public void List_PluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the input. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1349,9 +1582,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1366,10 +1599,16 @@ public void List_PluginSource_Acceptance() CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(2/2) ", + dimmedColors, "LongNamePredic…(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1391,9 +1630,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1424,6 +1663,7 @@ public void List_HistoryAndPluginSource_Acceptance() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. @@ -1432,9 +1672,13 @@ public void List_HistoryAndPluginSource_Acceptance() SetHistory("echo -bar", "java", "eca -zoo"); Test("SOME NEW TEX SOME TEXT AFTER", Keys( - "ec", CheckThat(() => AssertScreenIs(7, + "ec", CheckThat(() => AssertScreenIs(8, TokenClassification.Command, "ec", NextLine, + TokenClassification.ListPrediction, "<-/5>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/5>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "ec", @@ -1472,9 +1716,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1485,9 +1729,15 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, _.Shift_Home, - CheckThat(() => AssertScreenIs(7, + CheckThat(() => AssertScreenIs(8, TokenClassification.Selection, "eca -zoo", NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 38), // 38 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/2) ", + dimmedColors, "TestPredictor(2) …>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', emphasisColors, "ec", @@ -1525,9 +1775,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1535,9 +1785,13 @@ public void List_HistoryAndPluginSource_Acceptance() )), // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), - 'j', CheckThat(() => AssertScreenIs(6, + 'j', CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "j", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "j", @@ -1566,9 +1820,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1583,10 +1837,16 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, - CheckThat(() => AssertScreenIs(6, + CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEXT", NextLine, + TokenClassification.ListPrediction, "<4/4>", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '<4/4>' plus '<… TestPredictor(2) LongNamePredic…(1/1)>'. + dimmedColors, "<… TestPredictor(2) ", + TokenClassification.ListPrediction, "LongNamePredic…(1/1)", + dimmedColors, '>', + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "j", @@ -1615,9 +1875,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1626,10 +1886,14 @@ public void List_HistoryAndPluginSource_Acceptance() // `OnSuggestionDisplayed` should not be fired when navigating the list. CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX", NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1651,9 +1915,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1668,10 +1932,16 @@ public void List_HistoryAndPluginSource_Acceptance() CheckThat(() => _mockedMethods.ClearPredictionFields()), _.UpArrow, _.UpArrow, - CheckThat(() => AssertScreenIs(5, + CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "SOME", TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "TestPredictor(2/2) ", + dimmedColors, "LongNamePredic…(1)>", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", emphasisColors, "SOME NEW TEX", @@ -1693,9 +1963,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1726,6 +1996,7 @@ public void List_HistoryAndPluginSource_Deduplication() { TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. @@ -1736,9 +2007,13 @@ public void List_HistoryAndPluginSource_Deduplication() // which is the default comparison. So, that result will be filtered out due to the de-duplication logic. SetHistory("some TEXT BEFORE de-dup", "de-dup -of"); Test("de-dup", Keys( - "de-dup", CheckThat(() => AssertScreenIs(6, + "de-dup", CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "de-dup", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "de-dup", @@ -1767,9 +2042,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1796,9 +2071,13 @@ public void List_HistoryAndPluginSource_Deduplication() // so, that result will be filtered out due to the de-duplication logic. SetHistory("de-dup SOME TEXT AFTER", "some TEXT BEFORE de-dup"); Test("de-dup", Keys( - "de-dup", CheckThat(() => AssertScreenIs(6, + "de-dup", CheckThat(() => AssertScreenIs(7, TokenClassification.Command, "de-dup", NextLine, + TokenClassification.ListPrediction, "<-/4>", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", + NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', emphasisColors, "de-dup", @@ -1826,9 +2105,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePred...]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePred...", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, diff --git a/test/ListScrollableViewTest.cs b/test/ListScrollableViewTest.cs new file mode 100644 index 000000000..11452bb22 --- /dev/null +++ b/test/ListScrollableViewTest.cs @@ -0,0 +1,908 @@ +using System; +using Microsoft.PowerShell; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + [SkippableFact] + public void List_MetaLine_And_Paging_Navigation() + { + int listWidth = 100; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 15), KeyMode.Cmd); + + // The font effect sequences of the dimmed color used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("metadata-line -zoo"); + Test("SOME TEXT BEFORE metadata-line", Keys( + "metadata-line", CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.ListPrediction, "<-/5>", + TokenClassification.None, new string(' ', listWidth - 55), // 55 is the length of '<-/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, _.PageDown, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageUp, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1) ", + dimmedColors, "TestPredictor(2) LongNamePredic…(1) …>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.ListPrediction, "<2/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<4/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<4/5>' plus '<… TestPredictor(2) LongNamePredic…(1/1) Metadata(1)>'. + dimmedColors, "<… TestPredictor(2) ", + TokenClassification.ListPrediction, "LongNamePredic…(1/1) ", + dimmedColors, "Metadata(1)>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageDown, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.ListPrediction, "<1/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1) ", + dimmedColors, "TestPredictor(2) LongNamePredic…(1) …>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.ListPrediction, "<5/5>", + TokenClassification.None, new string(' ', listWidth - 58), // 58 is the length of '<5/5>' plus '<… TestPredictor(2) LongNamePredic…(1) Metadata(1/1)>'. + dimmedColors, "<… TestPredictor(2) LongNamePredic…(1) ", + TokenClassification.ListPrediction, "Metadata(1/1)", + dimmedColors, '>', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, _.Ctrl_PageUp, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.ListPrediction, "<2/5>", + TokenClassification.None, new string(' ', listWidth - 57), // 57 is the length of '<1/5>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void ListView_AdapteTo_ConsoleSize() + { + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, + // and the metadata line will be reduced to only show the (index/total) info. + int listWidth = 50; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 6), KeyMode.Cmd); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from both history and plugin. + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("metadata-line -zoo"); + Test("metadata-line -zoo", Keys( + "metadata-line", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<-/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.UpArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<5/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.UpArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<4/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.Ctrl_PageUp, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " TEXT BEFORE metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<2/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageUp, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<1/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageDown, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<3/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " -zoo", + TokenClassification.None, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " SOME TEXT AFTER", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.PageDown, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "SOME", + TokenClassification.None, " NEW TEXT", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<5/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, " SOME NEW TEXT", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.ListPredictionSelected, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<-/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME NEW TEXT", + TokenClassification.None, new string(' ', listWidth - 25), // 25 is the length of '> SOME NEW TEXT' plus '[Metadata]' + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Metadata", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + TokenClassification.None, " ", + TokenClassification.ListPrediction, "<1/5>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "metadata-line", + TokenClassification.ListPredictionSelected, " -zoo", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 29), // 29 is the length of '> metadata-line -zoo' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, " SOME TEXT BEFORE ", + emphasisColors, "metadata-line", + TokenClassification.None, new string(' ', listWidth - 47), // 47 is the length of '> SOME TEXT BEFORE metadata-line' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "metadata-line", + TokenClassification.None, " SOME TEXT AFTER", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '> metadata-line SOME TEXT AFTER' plus '[TestPredictor]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "TestPredictor", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "metadata-line", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void ListView_TermSize_Warning() + { + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, + // and the metadata line will be reduced to only show the (index/total) info. + int listWidth = 40; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 4), KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); + + Test("git", Keys( + _.F2, // Switch to the list view, then test the warning message. + 'g', CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "g", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + 'i', CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "gi", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Escape should clear the warning as well. + _.Escape, CheckThat(() => AssertScreenIs(3, + NextLine, + NextLine, + NextLine + )), + "git", CheckThat(() => AssertScreenIs(4, + TokenClassification.Command, "git", + NextLine, + TokenClassification.ListPrediction, "! terminal size too small to show the li", + NextLine, + TokenClassification.ListPrediction, "st view", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "git", + NextLine, + NextLine, + NextLine)) + )); + } + } +} diff --git a/test/MockConsole.cs b/test/MockConsole.cs index dc218e774..f827d4691 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -235,6 +235,13 @@ public virtual void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { + if (subsequence is "2" or "3") + { + // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // They are used in the metadata line of the list view. + continue; + } + EscapeSequenceActions[subsequence](this); } i = endSequence; @@ -444,6 +451,13 @@ public override void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { + if (subsequence is "2" or "3") + { + // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // They are used in the metadata line of the list view. + continue; + } + EscapeSequenceActions[subsequence](this); } i = endSequence; From f1b204348186929207362bca6ae0c5cbb9d3e3ef Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Feb 2023 11:01:48 -0800 Subject: [PATCH 031/127] Fix the menu completion to better handle the backspace key (#3574) --- PSReadLine/Completion.cs | 21 ++++++--- test/CompletionTest.cs | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 5a8dd9b02..f35c7cd52 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -983,14 +983,25 @@ private void MenuCompleteImpl(Menu menu, CommandCompletion completions) // TODO: Shift + Backspace does not fail here? if (menuStack.Count > 1) { - var newMenu = menuStack.Pop(); - - newMenu.DrawMenu(menu, menuSelect: true); previousSelection = -1; + userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); - menu = newMenu; + Menu newMenu = menuStack.Peek(); + int pos = FindUserCompletionTextPosition(newMenu.CurrentMenuItem, userCompletionText); + if (pos >= 0) + { + newMenu = menuStack.Pop(); + newMenu.DrawMenu(menu, menuSelect: true); - userCompletionText = userCompletionText.Substring(0, userCompletionText.Length - 1); + menu = newMenu; + } + // else { + // We should not pop the stack yet. The updated user completion text contains characters + // that are not included in the selected item of the menu at the top of stack. This may + // happen when the user pressed a 'Tab' before this 'Backspace', which updated the user + // completion text to include the unambiguous common prefix of the available completion + // candidates. In this case, we should stay in the current menu. + // } } else if (menuStack.Count == 1) { diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index 238006c07..7a3ea5152 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -1324,6 +1324,95 @@ public void DirectoryCompletion() _.Ctrl_c, InputAcceptedNow)); } + [SkippableFact] + public void MenuCompletions_Backspace() + { + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); + + _console.Clear(); + char separator = Path.DirectorySeparatorChar; + + Test("cd stro", Keys( + "cd stron", _.Ctrl_Spacebar, + CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.h, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + // Tab will update the user completion text to include the unambiguous common prefix. + _.Tab, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}stronghol", + TokenClassification.Selection, $"d{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongho", + TokenClassification.Selection, $"ld{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.h, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strongh", + TokenClassification.Selection, $"old{separator}", + NextLine, + TokenClassification.Selection, "stronghold ", + TokenClassification.None, "strongholp", + NextLine, + NextLine)), + _.Backspace, CheckThat(() => AssertScreenIs(3, + TokenClassification.Command, "cd", + TokenClassification.None, $" .{separator}strong", + TokenClassification.Selection, separator, + NextLine, + TokenClassification.Selection, "strong ", + TokenClassification.None, "stronghold strongholp", + NextLine, + NextLine)), + _.Backspace, + _.Backspace, CheckThat(() => AssertLineIs("cd stro")), + _.Enter + )); + } + internal static CommandCompletion MockedCompleteInput(string input, int cursor, Hashtable options, PowerShell powerShell) { var ctor = typeof (CommandCompletion).GetConstructor( @@ -1424,6 +1513,14 @@ internal static CommandCompletion MockedCompleteInput(string input, int cursor, break; case "none": break; + case "cd stron": + replacementIndex = 3; + replacementLength = 5; + char separator = Path.DirectorySeparatorChar; + completions.Add(new CompletionResult($".{separator}strong", "strong", CompletionResultType.ProviderContainer, $".{separator}strong")); + completions.Add(new CompletionResult($".{separator}stronghold", "stronghold", CompletionResultType.ProviderContainer, $".{separator}stronghold")); + completions.Add(new CompletionResult($".{separator}strongholp", "strongholp", CompletionResultType.ProviderContainer, $".{separator}strongholp")); + break; default: if (input.EndsWith("Get-Mo", StringComparison.OrdinalIgnoreCase)) From 259d82ed65c215af00be2bf473360e7c65aab199 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 27 Feb 2023 16:11:59 -0800 Subject: [PATCH 032/127] Address feedback to the scrollable list view from Andy (#3600) --- PSReadLine/Prediction.Views.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index 91ef84377..8def76e38 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -389,7 +389,7 @@ internal override void GetSuggestion(string userInput) _listItems = GetHistorySuggestions(userInput, HistoryMaxCount); if (_listItems?.Count > 0) { - _sources = new List() { new SourceInfo(SuggestionEntry.HistorySource, _listItems.Count - 1, -1) }; + _sources = new List() { new SourceInfo(SuggestionEntry.HistorySource, _listItems.Count - 1, prevSourceEndIndex: -1) }; } } } @@ -546,6 +546,7 @@ private void AggregateSuggestions() int count = _cacheList2[index] - num; if (count > 0) { + // If we had at least one source, we take the end index of the last source in the list. int prevEndIndex = _sources.Count > 0 ? _sources[_sources.Count - 1].EndIndex : -1; int endIndex = _listItems.Count - 1; _sources.Add(new SourceInfo(_listItems[endIndex].Source, endIndex, prevEndIndex)); From 465da62ac53c5a7426ad6c0fb8f6309bccbb2940 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 8 Mar 2023 09:31:45 -0800 Subject: [PATCH 033/127] Prepare for the `2.3.0-beta0` release of PSReadLine (#3612) --- PSReadLine/Changes.txt | 24 ++++++++++++++++++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 18ffcc3c5..a00ad4578 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,27 @@ +### [2.3.0-beta0] - 2023-03-07 + +- Fix the menu completion to better handle the backspace key (#3574) +- Improve the list view to be scrollable and auto-adjust the list view height (#3583) +- Use 'Visual Studio 2022' as the image for `appveyor` CI (#3594) +- Fix some typos in this repository (#3547) (Thanks @spaette!) +- De-duplicate prediction results with the history results (#3543) +- Updating Fabric bot (#3540, #3576) +- Change default color for inline prediction to `dim` (#3493) +- Make tab completion show results whose `ListItemText` are different by case only (#3456) (Thanks @dkaszews!) +- Fix to use the default member color for members (#3450) +- Update the samples in README.md (#3440, #3424) +- Place 'ViDGChord' in the right group (#3422) +- Fix the description of `CapitalizeWord` (#3384) +- Add support for upcasing, downcasing, and capitalizing word (#3365) (Thanks @3N4N!) +- No list view prediction when the first line was scrolled up off the buffer (#3372) +- Fix wrong cursor position in menu completion (#3373) +- Fix `ViModeIndicator = Cursor` for Windows Terminal (#3374) +- Fix parameter dynamic help when the help content is specified in ParameterAttribute (#3370) +- Handle multi-line description for parameter help content (#3358) +- Update module version in bot messages (#3361) + +[2.3.0-beta0]: https://github.com/PowerShell/PSReadLine/compare/v2.2.6...v2.3.0-beta0 + ### [2.2.6] - 2022-06-27 - Enable Predictive Intellisense by default (#3351) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 7ce0148ed..b88afdcd0 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.2.6.0 - 2.2.6 - 2.2.6 + 2.3.0.0 + 2.3.0 + 2.3.0-beta0 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index befc6e11a..f237efc2e 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.2.6' +ModuleVersion = '2.3.0' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 84858553682a19c2ae213a4bbe19c69f1e51945b Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 21 Mar 2023 12:58:14 -0700 Subject: [PATCH 034/127] Make PSReadLine script hidden from debugger (#3629) --- PSReadLine/PSReadLine.psm1 | 3 +++ PSReadLine/ReadLine.cs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PSReadLine/PSReadLine.psm1 b/PSReadLine/PSReadLine.psm1 index 74c271419..572aee80f 100644 --- a/PSReadLine/PSReadLine.psm1 +++ b/PSReadLine/PSReadLine.psm1 @@ -1,5 +1,8 @@ function PSConsoleHostReadLine { + [System.Diagnostics.DebuggerHidden()] + param() + ## Get the execution status of the last accepted user input. ## This needs to be done as the first thing because any script run will flush $?. $lastRunStatus = $? diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index a6010bb9d..33b65d06d 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -238,7 +238,7 @@ internal static PSKeyInfo ReadKey() if (ps == null) { ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); - ps.AddScript("0", useLocalScope: true); + ps.AddScript("[System.Diagnostics.DebuggerHidden()]param() 0", useLocalScope: true); } // To detect output during possible event processing, see if the cursor moved @@ -674,7 +674,7 @@ private PSConsoleReadLine() { try { - var results = ps.AddScript("$Host", useLocalScope: true).Invoke(); + var results = ps.AddScript("[System.Diagnostics.DebuggerHidden()]param() $Host", useLocalScope: true).Invoke(); PSHost host = results.Count == 1 ? results[0] : null; hostName = host?.Name; } From 1e84d68248e429448d2427d34d6efc2a2806d824 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 22 Mar 2023 15:47:14 -0700 Subject: [PATCH 035/127] Improve the default sensitive history scrubbing to allow safe property access (#3630) --- PSReadLine/History.cs | 33 +++++++++++++++++++++++++++------ test/HistoryTest.cs | 16 +++++++++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 637db8dd0..8c71b084f 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -525,6 +525,22 @@ private static bool IsSecretMgmtCommand(StringConstantExpressionAst strConst, ou return result; } + private static bool IsSafePropertyUsage(Ast member) + { + bool result = false; + + if (member.Parent is MemberExpressionAst memberExpr) + { + // - If the property is NOT on the left side of an assignment, then it's safe. + // - Otherwise, if the right-hand side is a pipeline or a variable, then we consider it safe. + result = !IsOnLeftSideOfAnAssignment(memberExpr, out Ast rhs) + || rhs is PipelineAst + || (rhs is CommandExpressionAst cmdExpr && cmdExpr.Expression is VariableExpressionAst); + } + + return result; + } + private static ExpressionAst GetArgumentForParameter(CommandParameterAst param) { if (param.Argument is not null) @@ -612,15 +628,20 @@ public static AddToHistoryOption GetDefaultAddToHistoryOption(string line) break; case StringConstantExpressionAst strConst: - // If it's not a command name, or it's not one of the secret management commands that - // we can ignore, we consider it sensitive. - isSensitive = !IsSecretMgmtCommand(strConst, out CommandAst command); - - if (!isSensitive) + isSensitive = true; + if (IsSecretMgmtCommand(strConst, out CommandAst command)) { - // We can safely skip the whole command text. + // If it's one of the secret management commands that we can ignore, we consider it safe. + isSensitive = false; + // And we can safely skip the whole command text in this case. match = s_sensitivePattern.Match(line, command.Extent.EndOffset); } + else if (IsSafePropertyUsage(strConst)) + { + isSensitive = false; + match = match.NextMatch(); + } + break; case CommandParameterAst param: diff --git a/test/HistoryTest.cs b/test/HistoryTest.cs index 6d724e005..0b669c710 100644 --- a/test/HistoryTest.cs +++ b/test/HistoryTest.cs @@ -202,7 +202,16 @@ public void SensitiveHistoryDefaultBehavior_Two() "Set-SecretInfo -Name apikey; Set-SecretVaultDefault; Test-SecretVault; Unlock-SecretVault -password $pwd; Unregister-SecretVault -SecretVault vaultInfo", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 $secret2", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 sdv87ysdfayf798hfasd8f7ha", // '-Secret2' has expr-value argument. Not saved to file. - "$environment -brand $brand -userBitWardenEmail $bwuser -userBitWardenPassword $bwpass" // '-userBitWardenPassword' matches sensitive pattern and it has parsing error. Not save to file. + "$environment -brand $brand -userBitWardenEmail $bwuser -userBitWardenPassword $bwpass", // '-userBitWardenPassword' matches sensitive pattern and it has parsing error. Not save to file. + "(Import-Clixml \"${Env:HOME}\\credential.clixml\").GetNetworkCredential().Password | Set-Clipboard", // 'Password' is a property not in assignment. + "$a.Password = 'abcd'", // setting the 'Password' property with string value. Not saved to file. + "$a.Password.Value = 'abcd'", // indirectly setting the 'Password' property with string value. Not saved to file. + "$a.Secret = Get-Secret -Name github-token -Vault MySecret", + "$a.Secret = $secret", + "$a.Password = 'ab' + 'cd'", // setting the 'Password' property with string values. Not saved to file. + "$a.Password.Secret | Set-Value", + "Write-Host $a.Password.Secret", + "($a.Password, $b) = ('aa', 'bb')", // setting the 'Password' property with string value. Not saved to file. }; string[] expectedSavedItems = new[] { @@ -218,6 +227,11 @@ public void SensitiveHistoryDefaultBehavior_Two() "Get-SecretInfo -Name mytoken; Get-SecretVault; Register-SecretVault; Remove-Secret apikey", "Set-SecretInfo -Name apikey; Set-SecretVaultDefault; Test-SecretVault; Unlock-SecretVault -password $pwd; Unregister-SecretVault -SecretVault vaultInfo", "Get-ResultFromTwo -Secret1 (Get-Secret -Name blah -AsPlainText) -Secret2 $secret2", + "(Import-Clixml \"${Env:HOME}\\credential.clixml\").GetNetworkCredential().Password | Set-Clipboard", + "$a.Secret = Get-Secret -Name github-token -Vault MySecret", + "$a.Secret = $secret", + "$a.Password.Secret | Set-Value", + "Write-Host $a.Password.Secret", }; try From d6efcabcc14c044e285a12eb9b4a6ef11fa2a657 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 29 Mar 2023 11:27:55 -0700 Subject: [PATCH 036/127] Set the current location in `PredictionClient` when it's supported (#3639) --- PSReadLine/Prediction.cs | 40 ++++++++++++++++++++++++++++++++++++++-- PSReadLine/ReadLine.cs | 10 +++++----- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index dfa1f1cce..fc790f948 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using System.Management.Automation; +using System.Management.Automation.Runspaces; using System.Management.Automation.Language; using System.Management.Automation.Subsystem.Prediction; using System.Diagnostics.CodeAnalysis; @@ -16,8 +18,42 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { - private const string PSReadLine = "PSReadLine"; - private static PredictionClient s_predictionClient = new(PSReadLine, PredictionClientKind.Terminal); + private const string DefaultName = "PSReadLine"; + private static readonly PredictionClient s_predictionClient = new(DefaultName, PredictionClientKind.Terminal); + private static PropertyInfo s_pCurrentLocation = null; + + /// + /// Initialize the objects for those public settable properties newly added to + /// . + /// + private static void InitializePropertyInfo() + { + Version ver = typeof(PSObject).Assembly.GetName().Version; + if (ver.Major < 7 || ver.Minor < 4) + { + return; + } + + Type pcType = typeof(PredictionClient); + // Property added in 7.4 + s_pCurrentLocation = pcType.GetProperty("CurrentLocation"); + } + + /// + /// New public settable properties may be added to the type as it evolves to + /// offer more helpful context information. We dynamically set those properties here to avoid any backward + /// compatibility issues. + /// + private static void UpdatePredictionClient(Runspace runspace, EngineIntrinsics engineIntrinsics) + { + // Set the current location if the 'CurrentLocation' property exists. + if (s_pCurrentLocation is not null) + { + // Set the current location if it's a local Runspace. Otherwise, set it to null. + object path = runspace.RunspaceIsRemote ? null : engineIntrinsics.SessionState.Path.CurrentLocation; + s_pCurrentLocation.SetValue(s_predictionClient, path); + } + } // Stub helper methods so prediction can be mocked [ExcludeFromCodeCoverage] diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 33b65d06d..3b042a448 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -652,6 +652,7 @@ static PSConsoleReadLine() { _singleton = new PSConsoleReadLine(); _viRegister = new ViRegister(_singleton); + InitializePropertyInfo(); } private PSConsoleReadLine() @@ -682,13 +683,9 @@ private PSConsoleReadLine() { } } - if (hostName == null) - { - hostName = PSReadLine; - } bool usingLegacyConsole = _console is PlatformWindows.LegacyWin32Console; - _options = new PSConsoleReadLineOptions(hostName, usingLegacyConsole); + _options = new PSConsoleReadLineOptions(hostName ?? DefaultName, usingLegacyConsole); _prediction = new Prediction(this); SetDefaultBindings(_options.EditMode); } @@ -698,6 +695,9 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _engineIntrinsics = engineIntrinsics; _runspace = runspace; + // Update the client instance per every call to PSReadLine. + UpdatePredictionClient(runspace, engineIntrinsics); + if (!_delayedOneTimeInitCompleted) { DelayedOneTimeInitialize(); From 3d20df783134bf97ecc1c784e7b02a94862fc5ef Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 4 Apr 2023 11:02:46 -0700 Subject: [PATCH 037/127] Improve the sensitive history scrubbing to allow retrieving token from `az`, `gcloud`, and `kubectl` (#3641) --- PSReadLine/History.cs | 73 ++++++++++++++++++++++++++++++++++++++----- test/HistoryTest.cs | 16 ++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 8c71b084f..246df21e0 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -128,7 +128,8 @@ public class HistoryItem "Set-SecretVaultDefault", "Test-SecretVault", "Unlock-SecretVault", - "Unregister-SecretVault" + "Unregister-SecretVault", + "Get-AzAccessToken", }; private void ClearSavedCurrentLine() @@ -511,15 +512,32 @@ private static bool IsOnLeftSideOfAnAssignment(Ast ast, out Ast rhs) return result; } + private static bool IsRightSideOfAnAssignmentSafe(Ast rhs) + { + if (rhs is PipelineAst) + { + // Right hand side is a pipeline. + return true; + } + + if (rhs is CommandExpressionAst cmdExprAst && cmdExprAst.Expression is MemberExpressionAst or InvokeMemberExpressionAst) + { + // Right hand side is a member access, or method invocation. + return true; + } + + return false; + } + private static bool IsSecretMgmtCommand(StringConstantExpressionAst strConst, out CommandAst command) { + command = null; bool result = false; - command = strConst.Parent as CommandAst; - if (command is not null) + if (strConst.Parent is CommandAst cmdAst && ReferenceEquals(cmdAst.CommandElements[0], strConst) && s_SecretMgmtCommands.Contains(strConst.Value)) { - result = ReferenceEquals(command.CommandElements[0], strConst) - && s_SecretMgmtCommands.Contains(strConst.Value); + result = true; + command = cmdAst; } return result; @@ -568,6 +586,45 @@ private static ExpressionAst GetArgumentForParameter(CommandParameterAst param) return null; } + private static bool IsCloudTokenOrSecretAccess(StringConstantExpressionAst arg2Ast, out CommandAst command) + { + bool result = false; + command = arg2Ast.Parent as CommandAst; + + if (command is not null && command.CommandElements.Count >= 3 + && command.CommandElements[0] is StringConstantExpressionAst nameAst + && command.CommandElements[1] is StringConstantExpressionAst arg1Ast + && command.CommandElements[2] == arg2Ast) + { + string name = nameAst.Value; + string arg1 = arg1Ast.Value; + string arg2 = arg2Ast.Value; + + if (string.Equals(name, "gcloud", StringComparison.OrdinalIgnoreCase)) + { + result = string.Equals(arg1, "auth", StringComparison.OrdinalIgnoreCase) && + string.Equals(arg2, "print-access-token", StringComparison.OrdinalIgnoreCase); + } + else if (string.Equals(name, "az", StringComparison.OrdinalIgnoreCase)) + { + result = string.Equals(arg1, "account", StringComparison.OrdinalIgnoreCase) && + string.Equals(arg2, "get-access-token", StringComparison.OrdinalIgnoreCase); + } + else if (string.Equals(name, "kubectl", StringComparison.OrdinalIgnoreCase)) + { + result = (string.Equals(arg1, "get", StringComparison.OrdinalIgnoreCase) || string.Equals(arg1, "describe", StringComparison.OrdinalIgnoreCase)) + && (string.Equals(arg2, "secrets", StringComparison.OrdinalIgnoreCase) || string.Equals(arg2, "secret", StringComparison.OrdinalIgnoreCase)); + } + } + + if (!result) + { + command = null; + } + + return result; + } + public static AddToHistoryOption GetDefaultAddToHistoryOption(string line) { if (string.IsNullOrEmpty(line)) @@ -618,8 +675,7 @@ public static AddToHistoryOption GetDefaultAddToHistoryOption(string line) // If it appears on the left-hand-side of an assignment, and the right-hand-side is // not a command invocation, we consider it sensitive. // e.g. `$token = Get-Secret` vs. `$token = 'token-text'` or `$token, $url = ...` - isSensitive = IsOnLeftSideOfAnAssignment(innerAst, out Ast rhs) - && rhs is not PipelineAst; + isSensitive = IsOnLeftSideOfAnAssignment(innerAst, out Ast rhs) && !IsRightSideOfAnAssignmentSafe(rhs); if (!isSensitive) { @@ -629,7 +685,8 @@ public static AddToHistoryOption GetDefaultAddToHistoryOption(string line) case StringConstantExpressionAst strConst: isSensitive = true; - if (IsSecretMgmtCommand(strConst, out CommandAst command)) + if (IsSecretMgmtCommand(strConst, out CommandAst command) + || IsCloudTokenOrSecretAccess(strConst, out command)) { // If it's one of the secret management commands that we can ignore, we consider it safe. isSensitive = false; diff --git a/test/HistoryTest.cs b/test/HistoryTest.cs index 0b669c710..ad6b95f0c 100644 --- a/test/HistoryTest.cs +++ b/test/HistoryTest.cs @@ -212,6 +212,14 @@ public void SensitiveHistoryDefaultBehavior_Two() "$a.Password.Secret | Set-Value", "Write-Host $a.Password.Secret", "($a.Password, $b) = ('aa', 'bb')", // setting the 'Password' property with string value. Not saved to file. + "kubectl get secrets", + "kubectl get secret db-user-pass -o jsonpath='{.data.password}' | base64 --decode", + "kubectl describe secret db-user-pass", + "(Get-AzAccessToken -ResourceUrl 'https://abc.com').Token", + "$token = (Get-AzAccessToken -ResourceUrl 'abc').Token", + "az account get-access-token --resource=https://abc.com --query accessToken --output tsv", + "curl -X GET --header \"Authorization: Bearer $token\" https://abc.com", + "$env:PGPASS = gcloud auth print-access-token", }; string[] expectedSavedItems = new[] { @@ -232,6 +240,14 @@ public void SensitiveHistoryDefaultBehavior_Two() "$a.Secret = $secret", "$a.Password.Secret | Set-Value", "Write-Host $a.Password.Secret", + "kubectl get secrets", + "kubectl get secret db-user-pass -o jsonpath='{.data.password}' | base64 --decode", + "kubectl describe secret db-user-pass", + "(Get-AzAccessToken -ResourceUrl 'https://abc.com').Token", + "$token = (Get-AzAccessToken -ResourceUrl 'abc').Token", + "az account get-access-token --resource=https://abc.com --query accessToken --output tsv", + "curl -X GET --header \"Authorization: Bearer $token\" https://abc.com", + "$env:PGPASS = gcloud auth print-access-token", }; try From 2fb3c330b89f681f3430300be230b63943ba2a65 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 6 Apr 2023 22:07:16 -0700 Subject: [PATCH 038/127] Force refreshing suggestion in the inline view when plugin is in use (#3644) --- PSReadLine/Prediction.Views.cs | 35 +++++++++++++++++++++------- test/InlinePredictionTest.cs | 42 ++++++++++++++++++++++++++++++++++ test/UnitTestReadLine.cs | 2 +- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index 8def76e38..7ab89e65b 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -1082,9 +1082,29 @@ internal override void GetSuggestion(string userInput) { _inputText = userInput; - if (_suggestionText == null || _suggestionText.Length <= userInput.Length || - _lastInputText.Length > userInput.Length || - !_suggestionText.StartsWith(userInput, _singleton._options.HistoryStringComparison)) + string currentSugText = null; + bool needToRefresh = _suggestionText == null + || _suggestionText.Length <= userInput.Length + || _lastInputText.Length > userInput.Length + || !_suggestionText.StartsWith(userInput, _singleton._options.HistoryStringComparison); + + + // The current suggestion was from history and it still applies to the new input. However, the plugin is in use, + // so we may need to force refreshing in case the plugin gives more relevant suggestion for the new input. This + // is because we favor plugin over history in the inline view. + if (!needToRefresh && _predictorId == Guid.Empty && UsePlugin) + { + // We generally want to force refreshing in this case, with only one exception -- the user accepted the next + // word from the current history suggestion. That means the user is interested in the current suggestion and + // thus we should keep on using. + needToRefresh = !_alreadyAccepted; + _alreadyAccepted = false; + + // We can reuse the current suggestion text for history to avoid an unnecessary search. + currentSugText = _suggestionText; + } + + if (needToRefresh) { _alreadyAccepted = false; _suggestionText = null; @@ -1097,7 +1117,7 @@ internal override void GetSuggestion(string userInput) if (UseHistory) { - _suggestionText = GetOneHistorySuggestion(userInput); + _suggestionText = currentSugText ?? GetOneHistorySuggestion(userInput); _predictorId = Guid.Empty; _predictorSession = null; } @@ -1215,15 +1235,14 @@ internal override void RenderSuggestion(List consoleBufferLines, internal override void OnSuggestionAccepted() { - if (!UsePlugin) + if (_alreadyAccepted) { return; } - if (!_alreadyAccepted && _suggestionText != null && _predictorSession.HasValue) + _alreadyAccepted = true; + if (_suggestionText != null && _predictorSession.HasValue) { - _alreadyAccepted = true; - // Send feedback only if the mini-session id is specified. // When it's not specified, we consider the predictor doesn't accept feedback. _singleton._mockableMethods.OnSuggestionAccepted(_predictorId, _predictorSession.Value, _suggestionText); diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 7dedb2c5f..f67125581 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -605,6 +605,48 @@ public void Inline_HistoryAndPluginSource_Acceptance() Assert.NotNull(_mockedMethods.commandHistory); Assert.Equal(1, _mockedMethods.commandHistory.Count); Assert.Equal("netsh show me", _mockedMethods.commandHistory[0]); + + _mockedMethods.ClearPredictionFields(); + SetHistory("netsh show me"); + Test("netsh SOME TEXT AFTER", Keys( + "netsh", CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "netsh", + TokenClassification.InlinePrediction, " show me")), + // Yeah, we still have `OnSuggestionDisplayed` fired, from the typing of each character of `nets`. + CheckThat(() => AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, countOrIndex: -1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), + + // Now mimic pressing a space key. This will trigger the refreshing of suggestions even though the + // current history suggestion still applies to the new input, because plugin is in use and we favor + // plugin over history results. + ' ', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "netsh", + TokenClassification.None, " ", + TokenClassification.InlinePrediction, " SOME TEXT AFTER")), + CheckThat(() => AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, countOrIndex: -1)), + CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), + CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), + CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), + + CheckThat(() => _mockedMethods.ClearPredictionFields()), + // 'RightArrow' will trigger 'OnSuggestionAccepted' as the suggestion is now from plugin. + _.RightArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "netsh", + TokenClassification.None, " SOME TEXT AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), + CheckThat(() => Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId)), + CheckThat(() => Assert.Equal("netsh SOME TEXT AFTER", _mockedMethods.acceptedSuggestion)), + CheckThat(() => Assert.Null(_mockedMethods.commandHistory)) + )); + + Assert.Empty(_mockedMethods.displayedSuggestions); + Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId); + Assert.Equal("netsh SOME TEXT AFTER", _mockedMethods.acceptedSuggestion); + Assert.NotNull(_mockedMethods.commandHistory); + Assert.Equal(2, _mockedMethods.commandHistory.Count); + Assert.Equal("netsh show me", _mockedMethods.commandHistory[0]); + Assert.Equal("netsh SOME TEXT AFTER", _mockedMethods.commandHistory[1]); + _mockedMethods.ClearPredictionFields(); } [SkippableFact] diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 2399fb47e..ad248096b 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -27,7 +27,7 @@ internal class MockedMethods : IPSConsoleReadLineMockableMethods internal Guid acceptedPredictorId; internal string acceptedSuggestion; internal string helpContentRendered; - internal Dictionary> displayedSuggestions = new Dictionary>(); + internal Dictionary> displayedSuggestions = new(); internal void ClearPredictionFields() { From 59fbc46a3c5da9fd0e3680d7a72fe25b9b6828db Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 6 Apr 2023 22:09:02 -0700 Subject: [PATCH 039/127] Avoid running `AddToHistoryHandler` on command lines loaded from history file (#3643) --- PSReadLine/History.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 246df21e0..1d501e1a3 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -140,7 +140,7 @@ private void ClearSavedCurrentLine() _savedCurrentLine._editGroupStart = -1; } - private AddToHistoryOption GetAddToHistoryOption(string line) + private AddToHistoryOption GetAddToHistoryOption(string line, bool fromHistoryFile) { // Whitespace only is useless, never add. if (string.IsNullOrWhiteSpace(line)) @@ -155,7 +155,7 @@ private AddToHistoryOption GetAddToHistoryOption(string line) return AddToHistoryOption.SkipAdding; } - if (Options.AddToHistoryHandler != null) + if (!fromHistoryFile && Options.AddToHistoryHandler != null) { if (Options.AddToHistoryHandler == PSConsoleReadLineOptions.DefaultAddToHistoryHandler) { @@ -204,10 +204,11 @@ private string MaybeAddToHistory( bool fromDifferentSession = false, bool fromInitialRead = false) { - var addToHistoryOption = GetAddToHistoryOption(result); + bool fromHistoryFile = fromDifferentSession || fromInitialRead; + var addToHistoryOption = GetAddToHistoryOption(result, fromHistoryFile); + if (addToHistoryOption != AddToHistoryOption.SkipAdding) { - var fromHistoryFile = fromDifferentSession || fromInitialRead; _previousHistoryItem = new HistoryItem { CommandLine = result, From 23882b7719792aa7888bd214232ea85728b88013 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 10 Apr 2023 11:42:12 -0700 Subject: [PATCH 040/127] Add a sample for transforming Unicode code point to Unicode char by `Alt+x` (#3652) --- PSReadLine/SamplePSReadLineProfile.ps1 | 35 +++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/PSReadLine/SamplePSReadLineProfile.ps1 b/PSReadLine/SamplePSReadLineProfile.ps1 index 8ccb8c1ee..69177499b 100644 --- a/PSReadLine/SamplePSReadLineProfile.ps1 +++ b/PSReadLine/SamplePSReadLineProfile.ps1 @@ -603,7 +603,7 @@ Set-PSReadLineKeyHandler -Key RightArrow ` # Cycle through arguments on current line and select the text. This makes it easier to quickly change the argument if re-running a previously run command from the history # or if using a psreadline predictor. You can also use a digit argument to specify which argument you want to select, i.e. Alt+1, Alt+a selects the first argument -# on the command line. +# on the command line. Set-PSReadLineKeyHandler -Key Alt+a ` -BriefDescription SelectCommandArguments ` -LongDescription "Set current selection to next command argument in the command line. Use of digit argument selects argument by position" ` @@ -656,3 +656,36 @@ Set-PSReadLineKeyHandler -Key Alt+a ` [Microsoft.PowerShell.PSConsoleReadLine]::SetMark($null, $null) [Microsoft.PowerShell.PSConsoleReadLine]::SelectForwardChar($null, ($nextAst.Extent.EndOffset - $nextAst.Extent.StartOffset) - $endOffsetAdjustment) } + +# Allow you to type a Unicode code point, then pressing `Alt+x` to transform it into a Unicode char. +Set-PSReadLineKeyHandler -Chord 'Alt+x' ` + -BriefDescription ToUnicodeChar ` + -LongDescription "Transform Unicode code point into a UTF-16 encoded string" ` + -ScriptBlock { + $buffer = $null + $cursor = 0 + [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $buffer, [ref] $cursor) + if ($cursor -lt 4) { + return + } + + $number = 0 + $isNumber = [int]::TryParse( + $buffer.Substring($cursor - 4, 4), + [System.Globalization.NumberStyles]::AllowHexSpecifier, + $null, + [ref] $number) + + if (-not $isNumber) { + return + } + + try { + $unicode = [char]::ConvertFromUtf32($number) + } catch { + return + } + + [Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 4, 4) + [Microsoft.PowerShell.PSConsoleReadLine]::Insert($unicode) +} From 18b5614545dede57b39bcb087ad80e910675966a Mon Sep 17 00:00:00 2001 From: vimode <39148877+vimode@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:22:02 +0000 Subject: [PATCH 041/127] Fix the broken doc link about `PowerShellGet` (#3657) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef73f216a..ea4826610 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ There are multiple ways to install `PSReadLine`. ### Install from PowerShellGallery (preferred) -You will need the `1.6.0` or a higher version of [`PowerShellGet`](https://docs.microsoft.com/powershell/scripting/gallery/installing-psget) to install the latest prerelease version of `PSReadLine`. +You will need the `1.6.0` or a higher version of [`PowerShellGet`](https://learn.microsoft.com/en-us/powershell/gallery/powershellget/install-powershellget) to install the latest prerelease version of `PSReadLine`. Windows PowerShell 5.1 ships an older version of `PowerShellGet` which doesn't support installing prerelease modules, so Windows PowerShell users need to install the latest `PowerShellGet` (if not yet) by running the following commands from an elevated Windows PowerShell session: From ae08ca6caf33585516cbb1f98aa5aa38beacba99 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 2 May 2023 16:52:19 -0700 Subject: [PATCH 042/127] Support tooltip rendering in the prediction list view (#3667) --- PSReadLine/Cmdlets.cs | 10 + PSReadLine/DynamicHelp.cs | 6 +- PSReadLine/KeyBindings.cs | 3 + PSReadLine/KeyBindings.vi.cs | 2 + PSReadLine/PSReadLine.format.ps1xml | 4 + PSReadLine/PSReadLineResources.Designer.cs | 11 + PSReadLine/PSReadLineResources.resx | 3 + PSReadLine/Prediction.Entry.cs | 6 +- PSReadLine/Prediction.Views.cs | 220 +++++++++- PSReadLine/Prediction.cs | 15 + PSReadLine/Render.Helper.cs | 25 ++ test/InlinePredictionTest.cs | 17 +- test/ListViewTooltipTest.cs | 484 +++++++++++++++++++++ test/MockConsole.cs | 8 +- 14 files changed, 789 insertions(+), 25 deletions(-) create mode 100644 test/ListViewTooltipTest.cs diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index ee323db57..bc89cc1f9 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -101,6 +101,7 @@ public class PSConsoleReadLineOptions public const string DefaultInlinePredictionColor = "\x1b[97;2;3m"; public const string DefaultListPredictionColor = "\x1b[33m"; public const string DefaultListPredictionSelectedColor = "\x1b[48;5;238m"; + public const string DefaultListPredictionTooltipColor = "\x1b[97;2;3m"; public static EditMode DefaultEditMode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? EditMode.Windows @@ -495,6 +496,12 @@ public object ListPredictionSelectedColor set => _listPredictionSelectedColor = VTColorUtils.AsEscapeSequence(value); } + public object ListPredictionTooltipColor + { + get => _listPredictionTooltipColor; + set => _listPredictionTooltipColor = VTColorUtils.AsEscapeSequence(value); + } + internal string _defaultTokenColor; internal string _commentColor; internal string _keywordColor; @@ -512,6 +519,7 @@ public object ListPredictionSelectedColor internal string _inlinePredictionColor; internal string _listPredictionColor; internal string _listPredictionSelectedColor; + internal string _listPredictionTooltipColor; internal void ResetColors() { @@ -532,6 +540,7 @@ internal void ResetColors() InlinePredictionColor = DefaultInlinePredictionColor; ListPredictionColor = DefaultListPredictionColor; ListPredictionSelectedColor = DefaultListPredictionSelectedColor; + ListPredictionTooltipColor = DefaultListPredictionTooltipColor; var bg = Console.BackgroundColor; if (fg == VTColorUtils.UnknownColor || bg == VTColorUtils.UnknownColor) @@ -571,6 +580,7 @@ internal void SetColor(string property, object value) {"InlinePrediction", (o, v) => o.InlinePredictionColor = v}, {"ListPrediction", (o, v) => o.ListPredictionColor = v}, {"ListPredictionSelected", (o, v) => o.ListPredictionSelectedColor = v}, + {"ListPredictionTooltip", (o, v) => o.ListPredictionTooltipColor = v}, }; Interlocked.CompareExchange(ref ColorSetters, setters, null); diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index 6334c59b6..bbd6bf70e 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -19,6 +19,7 @@ public partial class PSConsoleReadLine [ExcludeFromCodeCoverage] void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo) { + _pager ??= new Pager(); _pager.Write(content, regexPatternToScrollTo); } @@ -142,11 +143,6 @@ private void WriteDynamicHelpContent(string commandName, string parameterName, b private void DynamicHelpImpl(bool isFullHelp) { - if (isFullHelp) - { - _pager ??= new Pager(); - } - int cursor = _singleton._current; string commandName = null; string parameterName = null; diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 69daaa0c9..89564228d 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -235,6 +235,7 @@ void SetDefaultWindowsBindings() { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, { Keys.F3, MakeKeyHandler(CharacterSearch, "CharacterSearch") }, { Keys.ShiftF3, MakeKeyHandler(CharacterSearchBackward, "CharacterSearchBackward") }, + { Keys.F4, MakeKeyHandler(ShowFullPredictionTooltip, "ShowFullPredictionTooltip") }, { Keys.F8, MakeKeyHandler(HistorySearchBackward, "HistorySearchBackward") }, { Keys.ShiftF8, MakeKeyHandler(HistorySearchForward, "HistorySearchForward") }, // Added for xtermjs-based terminals that send different key combinations. @@ -339,6 +340,7 @@ void SetDefaultEmacsBindings() { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, + { Keys.F4, MakeKeyHandler(ShowFullPredictionTooltip, "ShowFullPredictionTooltip") }, { Keys.AltU, MakeKeyHandler(UpcaseWord, "UpcaseWord") }, { Keys.AltL, MakeKeyHandler(DowncaseWord, "DowncaseWord") }, { Keys.AltC, MakeKeyHandler(CapitalizeWord, "CapitalizeWord") }, @@ -566,6 +568,7 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(NextSuggestion): case nameof(PreviousSuggestion): case nameof(SwitchPredictionView): + case nameof(ShowFullPredictionTooltip): return KeyHandlerGroup.Prediction; case nameof(CaptureScreen): diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs index e7498c166..9d051ec02 100644 --- a/PSReadLine/KeyBindings.vi.cs +++ b/PSReadLine/KeyBindings.vi.cs @@ -91,6 +91,8 @@ private void SetDefaultViBindings() { Keys.CtrlG, MakeKeyHandler(Abort, "Abort") }, { Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") }, { Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") }, + { Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") }, + { Keys.F4, MakeKeyHandler(ShowFullPredictionTooltip, "ShowFullPredictionTooltip") }, }; // Some bindings are not available on certain platforms diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 3446753f2..5b20c58af 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -204,6 +204,10 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ListPredictionSelectedColor) + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ListPredictionTooltipColor) + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.MemberColor) diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 35dd5a337..7d3435945 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2192,6 +2192,17 @@ internal static string SwitchPredictionViewDescription } } + /// + /// Looks up a localized string similar to Show the full tooltip of the selected list-view item in the terminal's alternate screen buffer. + /// + internal static string ShowFullPredictionTooltipDescription + { + get + { + return ResourceManager.GetString("ShowFullPredictionTooltipDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Make visual selection of the command arguments. /// diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 28d6e5d4f..618ef22b9 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -837,6 +837,9 @@ Or not saving history with: Switch between the inline and list prediction views. + + Show the full tooltip of the selected list-view item in the terminal's alternate screen buffer. + The prediction 'ListView' is temporarily disabled because the current window size of the console is too small. To use the 'ListView', please make sure the 'WindowWidth' is not less than '{0}' and the 'WindowHeight' is not less than '{1}'. diff --git a/PSReadLine/Prediction.Entry.cs b/PSReadLine/Prediction.Entry.cs index f576f62a6..643afa5d3 100644 --- a/PSReadLine/Prediction.Entry.cs +++ b/PSReadLine/Prediction.Entry.cs @@ -47,6 +47,7 @@ private struct SuggestionEntry internal readonly Guid PredictorId; internal readonly uint? PredictorSession; internal readonly string Source; + internal readonly string ToolTip; internal readonly string SuggestionText; internal readonly int InputMatchIndex; @@ -54,16 +55,17 @@ private struct SuggestionEntry private string _listItemTextSelected; internal SuggestionEntry(string suggestion, int matchIndex) - : this(source: HistorySource, predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) + : this(source: HistorySource, predictorId: Guid.Empty, predictorSession: null, suggestion, tooltip: null, matchIndex) { } - internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession, string suggestion, int matchIndex) + internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession, string suggestion, string tooltip, int matchIndex) { Source = source; PredictorId = predictorId; PredictorSession = predictorSession; SuggestionText = suggestion; + ToolTip = tooltip; InputMatchIndex = matchIndex; _listItemTextRegular = _listItemTextSelected = null; diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index 7ab89e65b..e4ad73024 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -215,6 +215,7 @@ private class PredictionListView : PredictionViewBase // List view constants. internal const int ListViewMaxHeight = 10; internal const int ListViewMaxWidth = 100; + internal const int TooltipMaxHeight = 4; internal const int SourceMaxWidth = 15; // Minimal window size. @@ -236,6 +237,11 @@ private class PredictionListView : PredictionViewBase private int _maxViewHeight; // The actual height of the list view that is currently rendered. private int _listViewHeight; + // The max number of physical lines for rendering tooltip. + private int _maxTooltipHeight; + // The actual number of lines used for rendering the tooltip. + private int _tooltipHeight; + // The actual width of the list view that is currently rendered. private int _listViewWidth; // An index pointing to the item that is shown in the first slot of the list view. @@ -294,6 +300,23 @@ internal string SelectedItemText } } + /// + /// The tooltip of the currently selected item. + /// + internal string SelectedItemTooltip + { + get + { + if (_listItems == null || _selectedIndex == -1) + return null; + + if (_selectedIndex >= 0) + return _listItems[_selectedIndex].ToolTip; + + throw new InvalidOperationException("Unexpected '_selectedIndex' value: " + _selectedIndex); + } + } + internal PredictionListView(PSConsoleReadLine singleton) : base(singleton) { @@ -306,19 +329,19 @@ internal PredictionListView(PSConsoleReadLine singleton) /// /// Calculate the max width and height of the list view based on the current terminal size. /// - private (int maxWidth, int maxHeight, bool checkOnHeight) RefreshMaxViewSize() + private (int, int, int, bool) RefreshMaxViewSize() { var console = _singleton._console; - int maxWidth = Math.Min(console.BufferWidth, ListViewMaxWidth); + int maxListWidth = Math.Min(console.BufferWidth, ListViewMaxWidth); - (int maxHeight, bool moreCheck) = console.BufferHeight switch + (int maxListHeight, int maxTooltipHeigth, bool moreCheck) = console.BufferHeight switch { - > ListViewMaxHeight * 2 => (ListViewMaxHeight, false), - > ListViewMaxHeight => (ListViewMaxHeight / 2, false), - _ => (ListViewMaxHeight / 3, true) + > ListViewMaxHeight * 2 => (ListViewMaxHeight, TooltipMaxHeight, false), + > ListViewMaxHeight => (ListViewMaxHeight / 2, TooltipMaxHeight / 2, false), + _ => (ListViewMaxHeight / 3, TooltipMaxHeight / 3, true) }; - return (maxWidth, maxHeight, moreCheck); + return (maxListWidth, maxListHeight, maxTooltipHeigth, moreCheck); } /// @@ -326,8 +349,9 @@ internal PredictionListView(PSConsoleReadLine singleton) /// private bool HeightIsTooSmall() { + int tooltipLineCount = GetToolTipLineCountForHeightCheck(); int physicalLineCountForBuffer = _singleton.EndOfBufferPosition().Y - _singleton._initialY + 1; - return _singleton._console.BufferHeight < physicalLineCountForBuffer + _maxViewHeight + 1 /* one metadata line */; + return _singleton._console.BufferHeight < physicalLineCountForBuffer + _maxViewHeight + tooltipLineCount + 1 /* one metadata line */; } /// @@ -364,7 +388,7 @@ internal override void GetSuggestion(string userInput) // Reset the list item selection. _selectedIndex = -1; // Refresh the list view width and height in case the terminal was resized. - (_listViewWidth, _maxViewHeight, _checkOnHeight) = RefreshMaxViewSize(); + (_listViewWidth, _maxViewHeight, _maxTooltipHeight, _checkOnHeight) = RefreshMaxViewSize(); if (inputUnchanged) { @@ -533,7 +557,7 @@ private void AggregateSuggestions() } int matchIndex = sugText.IndexOf(_inputText, comparison); - _listItems.Add(new SuggestionEntry(item.Name, item.Id, item.Session, sugText, matchIndex)); + _listItems.Add(new SuggestionEntry(item.Name, item.Id, item.Session, sugText, suggestion.ToolTip, matchIndex)); if (--num == 0) { @@ -586,6 +610,7 @@ private void AggregateSuggestions() _listViewTop = 0; _listViewEnd = Math.Min(_listItems.Count, _maxViewHeight); _listViewHeight = _listViewEnd - _listViewTop; + _tooltipHeight = 0; } else { @@ -658,18 +683,25 @@ internal override void RenderSuggestion(List consoleBufferLines, } _listViewHeight = _listViewEnd - _listViewTop; + _tooltipHeight = 0; } for (int i = _listViewTop; i < _listViewEnd; i++) { bool itemSelected = i == _selectedIndex; string selectionColor = itemSelected ? _singleton._options._listPredictionSelectedColor : null; + SuggestionEntry entry = _listItems[i]; NextBufferLine(consoleBufferLines, ref currentLogicalLine) - .Append(_listItems[i].GetListItemText( + .Append(entry.GetListItemText( _listViewWidth, _inputText, selectionColor)); + + if (_singleton._options.ShowToolTips && itemSelected && !string.IsNullOrWhiteSpace(entry.ToolTip)) + { + _tooltipHeight = RenderTooltip(entry.ToolTip, consoleBufferLines, ref currentLogicalLine); + } } } @@ -703,6 +735,26 @@ private int GetPesudoListHeightForWarningRendering() return pesudoListHeight; } + /// + /// Calculate the number of tooltip lines rendered in the list view. + /// + private int GetToolTipLineCountForHeightCheck() + { + int tooltipLineCount = 0; + if (_singleton._options.ShowToolTips && _selectedIndex >= 0) + { + // When '_selectedIndex >= 0', this is an update to the list view triggered by navigation + // within the list, and thus '_listItems' is guaranteed to be not null. + string tooltip = _listItems[_selectedIndex].ToolTip; + if (!string.IsNullOrWhiteSpace(tooltip)) + { + tooltipLineCount = _maxTooltipHeight; + } + } + + return tooltipLineCount; + } + /// /// Generate the rendering text for the metadata line. /// @@ -865,6 +917,146 @@ static StringBuilder AppendColor(StringBuilder buffer, string colorToUse, ref st buffer.Insert(charPosition, " ", padding); } + /// + /// Generate the rendering text for the tooltip. + /// + private int RenderTooltip(string tooltip, List consoleBufferLines, ref int currentLogicalLine) + { + const int LengthOfLeadingPart = 6; + const string IndicatorSymbol = ">>"; + const string MsgForViewAll = "( to view all)"; + const string MoreTextIndicator1 = " \u2026 "; + const string MoreTextIndicator2 = "\u2026 "; + const string BoldDimItalicStyle = "\x1b[1;2;3m"; + + bool first = true; + int newlineIndex = -1; + int cellCount = 0; + int start, end; + + StringBuilder buff = null; + bool moreToCome = false; + int windowWidth = _singleton._console.BufferWidth; + int linesLeft = _maxTooltipHeight; + + string tooltipStyle = _singleton._options._listPredictionTooltipColor; + if (tooltipStyle != PSConsoleReadLineOptions.DefaultInlinePredictionColor) + { + tooltipStyle += BoldDimItalicStyle; + } + + do + { + int startIndex = newlineIndex + 1; + + // This may happen when the tooltip ends with a newline character. + if (startIndex == tooltip.Length) + { + break; + } + + // Get the range of the current non-whitespace substring line. + newlineIndex = tooltip.IndexOf('\n', startIndex); + (start, end) = TrimSubstringInPlace(tooltip, startIndex, newlineIndex is -1 ? tooltip.Length - 1 : newlineIndex); + + // This may happen when the current substring contains whitespace characters only. + if (start is -1) + { + if (newlineIndex is -1) + { + // If we have reached the end, then we are done. + break; + } + + // Otherwise, we need to continue processing the next substring line. + continue; + } + + if (linesLeft is 0) + { + // More non-empty substring lines, but no more space for rendering. + moreToCome = true; + break; + } + + if (buff is not null) + { + // Append reset for the previous substring line. + buff.Append(VTColorUtils.AnsiReset); + } + + // Create a new buffer line for the current substring line. + linesLeft--; + buff = NextBufferLine(consoleBufferLines, ref currentLogicalLine); + + if (first) + { + first = false; + buff.Append(tooltipStyle) + .Append(' ', 3) + .Append(IndicatorSymbol) + .Append(' '); + } + else + { + buff.Append(tooltipStyle) + .Append(' ', LengthOfLeadingPart); + } + + cellCount = LengthOfLeadingPart; + for (; start <= end; start++) + { + char ch = tooltip[start]; + int charInCells = LengthInBufferCells(ch); + + cellCount += charInCells; + if (cellCount > windowWidth) + { + linesLeft--; + if (linesLeft is -1) + { + // More text from the current substring line, but no more space for rendering. + moreToCome = true; + cellCount -= charInCells; + break; + } + + cellCount = charInCells; + } + + buff.Append(ch); + } + } + while (linesLeft >= 0 && newlineIndex >= 0); + + if (moreToCome) + { + // Append the "( to view all)" at the end of the last line + string highlightStyle = _singleton._options._listPredictionColor + BoldDimItalicStyle; + int remainingCells = windowWidth - cellCount; + + if (remainingCells >= MsgForViewAll.Length + MoreTextIndicator1.Length) + { + buff.Append(MoreTextIndicator1); + } + else + { + int length = MsgForViewAll.Length + MoreTextIndicator2.Length - remainingCells; + int buffIndex = buff.Length - length; + + buff.Remove(buffIndex, length); + buff.Append(MoreTextIndicator2); + } + + buff.Append(VTColorUtils.AnsiReset) + .Append(highlightStyle) + .Append(MsgForViewAll); + } + + buff.Append(VTColorUtils.AnsiReset); + return _maxTooltipHeight - linesLeft > 0 ? linesLeft : 0; + } + /// /// Trigger the feedback about a suggestion was accepted. /// @@ -899,7 +1091,7 @@ internal override void Clear(bool cursorAtEol) int listHeight = _warningPrinted ? GetPesudoListHeightForWarningRendering() - : _listViewHeight; + : _listViewHeight + _tooltipHeight; int top = cursorAtEol ? _singleton._console.CursorTop @@ -919,7 +1111,9 @@ internal override void Reset() _sources = null; _listItems = null; - _maxViewHeight = _listViewTop = _listViewEnd = _listViewWidth = _listViewHeight = _selectedIndex = -1; + _maxViewHeight = _maxTooltipHeight = -1; + _listViewWidth = _listViewHeight = _tooltipHeight = -1; + _listViewTop = _listViewEnd = _selectedIndex = -1; _warnAboutSize = _checkOnHeight = _updatePending = _renderFromSelected = false; } diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index fc790f948..7ece2ed1b 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -180,6 +180,21 @@ public static void PreviousSuggestion(ConsoleKeyInfo? key = null, object arg = n UpdateListSelection(numericArg); } + /// + /// Show the tooltip of the currently selected list item in the full view. + /// + public static void ShowFullPredictionTooltip(ConsoleKeyInfo? key = null, object arg = null) + { + if (_singleton._prediction.ActiveView is PredictionListView listView && listView.HasActiveSuggestion) + { + string tooltip = listView.SelectedItemTooltip; + if (!string.IsNullOrWhiteSpace(tooltip)) + { + _singleton._mockableMethods.RenderFullHelp(tooltip, regexPatternToScrollTo: null); + } + } + } + /// /// Implementation for updating the selected item in list view. /// diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 8fe8de071..8fc56bea1 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -190,5 +190,30 @@ private static int SubstringLengthByCellsFromEnd(string text, int start, int cou return charLength; } + + private static (int newStart, int newEnd) TrimSubstringInPlace(string text, int start, int end) + { + int newStart = start; + int newEnd = end; + + for (; newStart <= end; newStart++) + { + if (!char.IsWhiteSpace(text[newStart])) + { + break; + } + } + + for (; newEnd > newStart; newEnd--) + { + if (!char.IsWhiteSpace(text[newEnd])) + { + break; + } + } + + // Return the new start/end after triming, or (-1, -1) if the substring only consists of whitespaces. + return newStart > newEnd ? (-1, -1) : (newStart, newEnd); + } } } diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index f67125581..d9729c675 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -395,6 +395,7 @@ public void ViDefect2408() } private const uint MiniSessionId = 56; + private static readonly Guid predictorId_0 = Guid.Parse("c69fa9fc-6157-4877-8cf4-a9c83a0128af"); private static readonly Guid predictorId_1 = Guid.Parse("b45b5fbe-90fa-486c-9c87-e7940fdd6273"); private static readonly Guid predictorId_2 = Guid.Parse("74a86463-033b-44a3-b386-41ee191c94be"); private static readonly Guid predictorId_3 = Guid.Parse("19e98622-99e0-41f7-9ee0-a8bed92cde51"); @@ -409,10 +410,24 @@ internal static List MockedPredictInput(Ast ast, Token[] token new[] { typeof(Guid), typeof(string), typeof(uint), typeof(List) }, null); var input = ast.Extent.Text; - if (input == "netsh") + if (input is "netsh") { return null; } + else if (input is "tooltip") + { + var suggestions_0 = new List + { + new PredictiveSuggestion("tooltip NO1", "Hello\nBinary\nWorld\nPowerShell is a task automation and configuration management program from Microsoft"), + new PredictiveSuggestion("tooltip NO2", "Hello\nWorld"), + }; + + return new List + { + (PredictionResult)ctor.Invoke( + new object[] { predictorId_0, "Tooltip", MiniSessionId, suggestions_0 }), + }; + } var suggestions_1 = new List { diff --git a/test/ListViewTooltipTest.cs b/test/ListViewTooltipTest.cs new file mode 100644 index 000000000..624ec0c99 --- /dev/null +++ b/test/ListViewTooltipTest.cs @@ -0,0 +1,484 @@ +using System; +using Microsoft.PowerShell; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + [SkippableFact] + public void List_Item_Tooltip_4_Lines() + { + // Set the terminal height to 22 and width to 60, so the metadata line will be fully rendered + // and maximum 4 lines can be used for tooltip for a selected list item. + int listWidth = 60; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 22), KeyMode.Cmd); + + // The font effect sequences of 'dim' and 'italic' used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("tooltip -history"); + Test("tooltip NO2", Keys( + "tooltip", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "tooltip", + NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 28), // 28 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, + CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "tooltip", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-history", + NextLine, + TokenClassification.ListPrediction, "<1/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<1/3>' plus ''. + dimmedColors, '<', + TokenClassification.ListPrediction, "History(1/1) ", + dimmedColors, "Tooltip(2)>", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " -history", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.ListPredictionSelected, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, + CheckThat(() => AssertScreenIs(10, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO1", + NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<2/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO1", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello", NextLine, + dimmedColors, " Binary", NextLine, + dimmedColors, " World", NextLine, + dimmedColors, " PowerShell is a task automation an… ", + TokenClassification.ListPrediction, "( to view all)", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + TokenClassification.ListPrediction, "<3/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<3/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO2", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello", NextLine, + dimmedColors, " World", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void List_Item_Tooltip_2_Lines() + { + // Set the terminal height to 15 and width to 60, so the metadata line will be fully rendered + // and maximum 2 lines can be used for tooltip for a selected list item. + int listWidth = 60; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 15), KeyMode.Cmd); + + // The font effect sequences of 'dim' and 'italic' used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("tooltip -history"); + Test("tooltip NO2", Keys( + "tooltip", CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "tooltip", + NextLine, + TokenClassification.ListPrediction, "<-/3>", + TokenClassification.None, new string(' ', listWidth - 28), // 28 is the length of '<-/3>' plus ''. + dimmedColors, "", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.DownArrow, _.DownArrow, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO1", + NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<2/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO1", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello", NextLine, + dimmedColors, " Binary … ", + TokenClassification.ListPrediction, "( to view all)", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine, + NextLine + )), + _.F4, + CheckThat(() => Assert.Equal( + "Hello\nBinary\nWorld\nPowerShell is a task automation and configuration management program from Microsoft", + _mockedMethods.helpContentRendered)), + _.DownArrow, + CheckThat(() => AssertScreenIs(8, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + TokenClassification.ListPrediction, "<3/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<3/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO2", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello", NextLine, + dimmedColors, " World", + // List view is done, no more list item following. + NextLine, + NextLine + )), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + NextLine)) + )); + } + + [SkippableFact] + public void List_Item_Tooltip_1_Line() + { + // Set the terminal height to 6 and width to 60, so the metadata line will be fully rendered + // and maximum 2 lines can be used for tooltip for a selected list item. + int listWidth = 60; + TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 6), KeyMode.Cmd); + + // The font effect sequences of 'dim' and 'italic' used in list view metadata line + // are ignored in the mock console, so only the white color will be left. + var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); + var emphasisColors = Tuple.Create(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor); + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.ListView); + _mockedMethods.ClearPredictionFields(); + + SetHistory("tooltip -history"); + Test("tooltip NO2", Keys( + "tooltip", _.DownArrow, _.DownArrow, + CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO1", + NextLine, + TokenClassification.ListPrediction, "<2/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<2/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO1", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello … ", + TokenClassification.ListPrediction, "( to view all)", + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO2", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + // List view is done, no more list item following. + NextLine + )), + _.F4, + CheckThat(() => Assert.Equal( + "Hello\nBinary\nWorld\nPowerShell is a task automation and configuration management program from Microsoft", + _mockedMethods.helpContentRendered)), + _.DownArrow, + CheckThat(() => AssertScreenIs(6, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + TokenClassification.ListPrediction, "<3/3>", + TokenClassification.None, new string(' ', listWidth - 30), // 30 is the length of '<3/3>' plus ''. + dimmedColors, "', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " -history", + TokenClassification.None, new string(' ', listWidth - 27), // 27 is the length of '> tooltip -history' plus '[History]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "History", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.None, ' ', + emphasisColors, "tooltip", + TokenClassification.None, " NO1", + TokenClassification.None, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO1' plus '[Tooltip]'. + TokenClassification.None, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.None, ']', + NextLine, + TokenClassification.ListPrediction, '>', + TokenClassification.ListPredictionSelected, ' ', + emphasisColors, "tooltip", + TokenClassification.ListPredictionSelected, " NO2", + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 22), // 22 is the length of '> tooltip NO2' plus '[Tooltip]'. + TokenClassification.ListPredictionSelected, '[', + TokenClassification.ListPrediction, "Tooltip", + TokenClassification.ListPredictionSelected, ']', + NextLine, + dimmedColors, " >> Hello … ", + TokenClassification.ListPrediction, "( to view all)", + // List view is done, no more list item following. + NextLine + )), + _.F4, + CheckThat(() => Assert.Equal( + "Hello\nWorld", + _mockedMethods.helpContentRendered)), + + // Once accepted, the list should be cleared. + _.Enter, CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "tooltip", + TokenClassification.None, " NO2", + NextLine, + NextLine)) + )); + } + } +} diff --git a/test/MockConsole.cs b/test/MockConsole.cs index f827d4691..ee0a7c363 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -235,9 +235,9 @@ public virtual void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { - if (subsequence is "2" or "3") + if (subsequence is "1" or "2" or "3") { - // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // Ignore the font effect sequence: 1 - bold; 2 - dimmed color; 3 - italics // They are used in the metadata line of the list view. continue; } @@ -451,9 +451,9 @@ public override void Write(string s) var escapeSequence = s.Substring(i + 2, len); foreach (var subsequence in escapeSequence.Split(';')) { - if (subsequence is "2" or "3") + if (subsequence is "1" or "2" or "3") { - // Ignore the font effect sequence: 2 - dimmed color; 3 - italics + // Ignore the font effect sequence: 1 - bold; 2 - dimmed color; 3 - italics // They are used in the metadata line of the list view. continue; } From 0032acbeafbe8c3d9507c1a2fbba592dd31fe2bc Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 2 May 2023 22:20:10 -0700 Subject: [PATCH 043/127] Append reset VT sequence before rendering the ineline prediction (#3669) --- PSReadLine/Prediction.Views.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index e4ad73024..f64e6f5bb 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -1416,6 +1416,7 @@ internal override void RenderSuggestion(List consoleBufferLines, StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine]; currentLineBuffer + .Append(VTColorUtils.AnsiReset) .Append(_singleton._options._inlinePredictionColor) .Append(_suggestionText, inputLength, _renderedLength - inputLength); From 6b7c48a279423a37d39f4295f1ab82051ab1cbc3 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 3 May 2023 14:24:05 -0700 Subject: [PATCH 044/127] Fix a bug in tooltip rendering that caused incorrect calculation of the tooltip height (#3671) --- PSReadLine/Prediction.Views.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index f64e6f5bb..a2770eca4 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -1054,7 +1054,7 @@ private int RenderTooltip(string tooltip, List consoleBufferLines } buff.Append(VTColorUtils.AnsiReset); - return _maxTooltipHeight - linesLeft > 0 ? linesLeft : 0; + return _maxTooltipHeight - (linesLeft > 0 ? linesLeft : 0); } /// From 9e7b1fe5249588a3814ae8f8e8ac3a96df48210c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 3 May 2023 15:19:11 -0700 Subject: [PATCH 045/127] Prepare for the v2.3.1-beta1 release of PSReadLine (#3672) --- PSReadLine/Changes.txt | 15 +++++++++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index a00ad4578..27ccd6f6e 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,18 @@ +### [2.3.1-beta1] - 2023-05-03 + +- Append reset VT sequence before rendering the ineline prediction (#3669) +- Support tooltip rendering in the prediction list view (#3667, #3671) +- Fix the broken doc link about `PowerShellGet` (#3657) (Thanks @vimode!) +- Add a sample for transforming Unicode code point to Unicode char by `Alt+x` (#3652) +- Avoid running `AddToHistoryHandler` on command lines loaded from history file (#3643) +- Force refreshing suggestion in the inline view when plugin is in use (#3644) +- Improve the sensitive history scrubbing to allow retrieving token from `az`, `gcloud`, and `kubectl` (#3641) +- Set the current location in `PredictionClient` when it's supported (#3639) +- Improve the default sensitive history scrubbing to allow safe property access (#3630) +- Make PSReadLine script hidden from debugger (#3629) + +[2.3.1-beta1]: https://github.com/PowerShell/PSReadLine/compare/v2.3.0-beta0...v2.3.1-beta1 + ### [2.3.0-beta0] - 2023-03-07 - Fix the menu completion to better handle the backspace key (#3574) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index b88afdcd0..abe887a7a 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.3.0.0 - 2.3.0 - 2.3.0-beta0 + 2.3.1.0 + 2.3.1 + 2.3.1-beta1 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index f237efc2e..11299abf0 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.3.0' +ModuleVersion = '2.3.1' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From c48f77dee7aacf2490dfa2df22893f0c612cd74a Mon Sep 17 00:00:00 2001 From: "microsoft-github-policy-service[bot]" <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:50:16 -0700 Subject: [PATCH 046/127] FabricBot: Onboarding to GitOps.ResourceManagement because of FabricBot decommissioning (#3738) --- .github/fabricbot.json | 1714 ----------------------- .github/policies/resourceManagement.yml | 290 ++++ 2 files changed, 290 insertions(+), 1714 deletions(-) delete mode 100644 .github/fabricbot.json create mode 100644 .github/policies/resourceManagement.yml diff --git a/.github/fabricbot.json b/.github/fabricbot.json deleted file mode 100644 index c172e91fe..000000000 --- a/.github/fabricbot.json +++ /dev/null @@ -1,1714 +0,0 @@ -{ - "version": "1.0", - "tasks": [ - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "taskName": "Add needs-triage label to new issues", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "opened" - } - }, - { - "operator": "not", - "operands": [ - { - "name": "isPartOfProject", - "parameters": {} - } - ] - }, - { - "operator": "not", - "operands": [ - { - "name": "isAssignedToSomeone", - "parameters": {} - } - ] - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Triage :mag:" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "config": { - "taskName": "Replace needs author feedback label with needs attention label when the author comments on an issue", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "created" - } - }, - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - }, - { - "name": "isOpen", - "parameters": {} - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Attention :wave:" - } - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ], - "eventType": "issue", - "eventNames": [ - "issue_comment" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "CodeFlowLink", - "subCapability": "CodeFlowLink", - "version": "1.0", - "config": { - "taskName": "Add a CodeFlow link to new pull requests" - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Add needs author feedback label to pull requests when changes are requested", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "submitted" - } - }, - { - "name": "isReviewState", - "parameters": { - "state": "changes_requested" - } - } - ] - }, - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author responds to a pull request", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "operator": "not", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestCommentResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author comments on a pull request", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "issue_comment" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Remove needs author feedback label when the author responds to a pull request review comment", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label from pull requests", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - }, - { - "name": "isAction", - "parameters": { - "action": "closed" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request", - "issues", - "project_card" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestCommentResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label when a pull request is commented on", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "issue_comment" - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "PullRequestReviewResponder", - "version": "1.0", - "config": { - "taskName": "Remove no recent activity label when a pull request is reviewed", - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ] - }, - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ], - "eventType": "pull_request", - "eventNames": [ - "pull_request_review" - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Close stale pull requests", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isPr", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "actions": [ - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "addReply", - "parameters": { - "comment": "This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." - } - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "taskName": "Add no recent activity label to pull requests", - "frequency": [ - { - "weekDay": 0, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 1, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 2, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 3, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 4, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 5, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - }, - { - "weekDay": 6, - "hours": [ - 3, - 9, - 15, - 21 - ], - "timezoneOffset": -8 - } - ], - "searchTerms": [ - { - "name": "isPr", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 14 - } - }, - { - "name": "noLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - } - ], - "actions": [ - { - "name": "addLabel", - "parameters": { - "label": "Status-No Recent Activity" - } - }, - { - "name": "addReply", - "parameters": { - "comment": "This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **14 days**. It will be closed if no further activity occurs **within 7 days of this comment**." - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "AutoMerge", - "subCapability": "AutoMerge", - "version": "1.0", - "config": { - "taskName": "Automatically merge pull requests", - "label": "Auto Merge", - "silentMode": false, - "minMinutesOpen": "1440", - "mergeType": "squash", - "allowAutoMergeInstructionsWithoutLabel": false, - "deleteBranches": true, - "removeLabelOnPush": true - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssuesOnlyResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "isAction", - "parameters": { - "action": "closed" - } - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Triage :mag:" - } - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issues", - "project_card" - ], - "taskName": "Remove needs-triage label when an issue is closed", - "actions": [ - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Triage :mag:" - } - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isIssue", - "parameters": {} - }, - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Author Feedback" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "taskName": "Close stale issues", - "actions": [ - { - "name": "addReply", - "parameters": { - "comment": "This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." - } - }, - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "InPrLabel", - "subCapability": "InPrLabel", - "version": "1.0", - "config": { - "taskName": "Add 'In-PR' label to issue", - "label_inPr": "In-PR", - "fixedLabelEnabled": true, - "label_fixed": "Resolution-Fixed" - } - }, - { - "taskType": "trigger", - "capabilityId": "ReleaseAnnouncement", - "subCapability": "ReleaseAnnouncement", - "version": "1.0", - "config": { - "taskName": "Release announcement for Issue/PR", - "prReply": ":tada: [`${version}`](https://github.com/PowerShell/PSReadLine/releases/tag/${version}) has been released which incorporates this pull request. :tada:\n", - "issueReply": ":tada: This issue was addressed in ${prNumber}, which has now been successfully released in [`${version}`](https://github.com/PowerShell/PSReadLine/releases/tag/${version}). :tada:", - "packageRegex": "(v\\d+\\.\\d+\\.\\d+(-\\w+)?)", - "packageVersionGroup": 0, - "referencedPrsRegex": "\\(#(\\d+)\\)" - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Answered" - } - } - ], - "taskName": "Closing if Resolution Answered", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-By Design" - } - } - ], - "taskName": "Closing if Resolution By Design", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Declined" - } - } - ], - "taskName": "Closing if Resolution Declined", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Duplicate" - } - } - ], - "taskName": "Closing if Resolution Dup", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-External" - } - } - ], - "taskName": "Closing if Resolution External", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Fixed" - } - } - ], - "taskName": "Closing if Resolution Fixed", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Not Repro" - } - } - ], - "taskName": "Closing if Resolution Not Repro", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Resolution-Wont Fix" - } - } - ], - "taskName": "Closing if Resolution Wont Fix", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - } - ] - } - }, - { - "taskType": "scheduled", - "capabilityId": "ScheduledSearch", - "subCapability": "ScheduledSearch", - "version": "1.1", - "config": { - "frequency": [ - { - "weekDay": 0, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 1, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 2, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 3, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 4, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 5, - "hours": [ - 0, - 6, - 12, - 18 - ] - }, - { - "weekDay": 6, - "hours": [ - 0, - 6, - 12, - 18 - ] - } - ], - "searchTerms": [ - { - "name": "isOpen", - "parameters": {} - }, - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Repro" - } - }, - { - "name": "noActivitySince", - "parameters": { - "days": 7 - } - } - ], - "taskName": "Closing if Stale Needs Repro", - "actions": [ - { - "name": "closeIssue", - "parameters": {} - }, - { - "name": "addReply", - "parameters": { - "comment": "This issue is closed because it has been marked as requiring repro steps but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback." - } - } - ] - } - }, - { - "taskType": "trigger", - "capabilityId": "IssueResponder", - "subCapability": "IssueCommentResponder", - "version": "1.0", - "config": { - "conditions": { - "operator": "and", - "operands": [ - { - "name": "hasLabel", - "parameters": { - "label": "Needs-Repro" - } - }, - { - "name": "isActivitySender", - "parameters": { - "user": { - "type": "author" - } - } - } - ] - }, - "eventType": "issue", - "eventNames": [ - "issue_comment" - ], - "taskName": "", - "actions": [ - { - "name": "reopenIssue", - "parameters": {} - }, - { - "name": "removeLabel", - "parameters": { - "label": "Needs-Repro" - } - }, - { - "name": "addLabel", - "parameters": { - "label": "Needs-Attention :wave:" - } - } - ] - } - } - ], - "userGroups": [] -} \ No newline at end of file diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml new file mode 100644 index 000000000..f10d1216d --- /dev/null +++ b/.github/policies/resourceManagement.yml @@ -0,0 +1,290 @@ +id: +name: GitOps.PullRequestIssueManagement +description: GitOps.PullRequestIssueManagement primitive +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + scheduledSearches: + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isPullRequest + - isOpen + - hasLabel: + label: Needs-Author Feedback + - noActivitySince: + days: 7 + actions: + - closeIssue + - addReply: + reply: This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback. + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isPullRequest + - isOpen + - hasLabel: + label: Needs-Author Feedback + - noActivitySince: + days: 14 + - isNotLabeledWith: + label: Status-No Recent Activity + actions: + - addLabel: + label: Status-No Recent Activity + - addReply: + reply: This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **14 days**. It will be closed if no further activity occurs **within 7 days of this comment**. + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isIssue + - isOpen + - hasLabel: + label: Needs-Author Feedback + - noActivitySince: + days: 7 + actions: + - addReply: + reply: This issue is closed because it has been marked as requiring author feedback but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback. + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Answered + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-By Design + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Declined + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Duplicate + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-External + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Fixed + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Not Repro + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-Wont Fix + actions: + - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Needs-Repro + - noActivitySince: + days: 7 + actions: + - closeIssue + - addReply: + reply: This issue is closed because it has been marked as requiring repro steps but has not had any activity for **7 days**. If you think the issue is still relevant, please reopen and provide your feedback. + eventResponderTasks: + - if: + - payloadType: Issue_Comment + - isAction: + action: Created + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author Feedback + - isOpen + then: + - addLabel: + label: 'Needs-Attention :wave:' + - removeLabel: + label: Needs-Author Feedback + description: + - if: + - payloadType: Pull_Request + - isAction: + action: Opened + then: + - addCodeFlowLink + description: + - if: + - payloadType: Pull_Request_Review + - isAction: + action: Submitted + - isReviewState: + reviewState: Changes_requested + then: + - addLabel: + label: Needs-Author Feedback + description: + - if: + - payloadType: Pull_Request + - isActivitySender: + issueAuthor: True + - not: + isAction: + action: Closed + - hasLabel: + label: Needs-Author Feedback + then: + - removeLabel: + label: Needs-Author Feedback + description: + - if: + - payloadType: Issue_Comment + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author Feedback + then: + - removeLabel: + label: Needs-Author Feedback + description: + - if: + - payloadType: Pull_Request_Review + - isActivitySender: + issueAuthor: True + - hasLabel: + label: Needs-Author Feedback + then: + - removeLabel: + label: Needs-Author Feedback + description: + - if: + - payloadType: Pull_Request + - hasLabel: + label: Status-No Recent Activity + - isAction: + action: Closed + then: + - removeLabel: + label: Status-No Recent Activity + description: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: Status-No Recent Activity + then: + - removeLabel: + label: Status-No Recent Activity + description: + - if: + - payloadType: Pull_Request_Review + - hasLabel: + label: Status-No Recent Activity + then: + - removeLabel: + label: Status-No Recent Activity + description: + - if: + - payloadType: Pull_Request + - hasLabel: + label: Auto Merge + then: + - enableAutoMerge: + mergeMethod: Squash + description: + - if: + - payloadType: Pull_Request + - labelRemoved: + label: Auto Merge + then: + - disableAutoMerge + description: + - if: + - payloadType: Issues + - isAction: + action: Closed + - hasLabel: + label: 'Needs-Triage :mag:' + then: + - removeLabel: + label: 'Needs-Triage :mag:' + description: + - if: + - payloadType: Pull_Request + then: + - inPrLabel: + label: In-PR + description: + - if: + - payloadType: Issue_Comment + - hasLabel: + label: Needs-Repro + - isActivitySender: + issueAuthor: True + then: + - reopenIssue + - removeLabel: + label: Needs-Repro + - addLabel: + label: 'Needs-Attention :wave:' + description: +onFailure: +onSuccess: From e5537d814bb460c3d6736e43fbf2da7f21fff162 Mon Sep 17 00:00:00 2001 From: Dan Thompson Date: Thu, 27 Jul 2023 11:47:06 -0700 Subject: [PATCH 047/127] Work around `InvalidOperationException` from Console API (#3755) --- PSReadLine/ConsoleLib.cs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/PSReadLine/ConsoleLib.cs b/PSReadLine/ConsoleLib.cs index 7cc53b341..dc1d1b455 100644 --- a/PSReadLine/ConsoleLib.cs +++ b/PSReadLine/ConsoleLib.cs @@ -113,8 +113,40 @@ public Encoding OutputEncoding set { try { Console.OutputEncoding = value; } catch { } } } - public ConsoleKeyInfo ReadKey() => _readKeyMethod.Value(true); - public bool KeyAvailable => Console.KeyAvailable; + private static T _TryIgnoreIOE(Func f) + { + int triesLeft = 10; + while (true) + { + try + { + triesLeft--; + return f(); + } + catch (InvalidOperationException) + { + // Ignore it. An IOE could be thrown if the "application does not have a + // console or when console input has been redirected"... but we don't + // expect PSReadLine to be involved in such a situation. So we are + // actually probably running into this Issue (wherein another process + // attached to the same console terminated at just the right/wrong time): + // + // https://github.com/dotnet/runtime/issues/88697 + // + // In the event there is some *other* pathological situation + // happening, we have limited the number of times we will + // swallow/retry this exception/operation. + + if (triesLeft <= 0) + { + throw; + } + } + } + } + + public ConsoleKeyInfo ReadKey() => _TryIgnoreIOE(() => _readKeyMethod.Value(true)); + public bool KeyAvailable => _TryIgnoreIOE(() => Console.KeyAvailable); public void SetWindowPosition(int left, int top) => Console.SetWindowPosition(left, top); public void SetCursorPosition(int left, int top) => Console.SetCursorPosition(left, top); public virtual void Write(string value) => Console.Write(value); From 9ffba6ba3dc8d9de8eb172668c89848e6203e3cc Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Mon, 7 Aug 2023 15:23:35 -0700 Subject: [PATCH 048/127] Fix bot to add `needs-triage` label to newly opened issue (#3772) --- .github/policies/resourceManagement.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index f10d1216d..f6cff8991 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -286,5 +286,17 @@ configuration: - addLabel: label: 'Needs-Attention :wave:' description: + - if: + - payloadType: Issues + - and: + - isOpen + - not: + and: + - isAssignedToSomeone + - isLabeled + then: + - addLabel: + label: 'Needs-Triage :mag:' + description: 'Adding needs triage label to newly opened issues' onFailure: onSuccess: From 33c96af55da248353def04a35337a469b900190c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 7 Aug 2023 16:06:34 -0700 Subject: [PATCH 049/127] Update `actions/checkout` to v3 (#3773) --- .github/workflows/IssuePreTriage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/IssuePreTriage.yml b/.github/workflows/IssuePreTriage.yml index e7107f3ae..19e6ed684 100644 --- a/.github/workflows/IssuePreTriage.yml +++ b/.github/workflows/IssuePreTriage.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: do-work run: | From 4d78ce153dc0e58dd9e88d6509544f9690bdd5a5 Mon Sep 17 00:00:00 2001 From: Dan Thompson Date: Wed, 9 Aug 2023 10:51:26 -0700 Subject: [PATCH 050/127] Add the `TerminateOrphanedConsoleApps` option on Windows to kill orphaned console-attached process that may mess up reading from Console input (#3764) --- PSReadLine/Cmdlets.cs | 10 + PSReadLine/Options.cs | 18 + PSReadLine/PSReadLine.format.ps1xml | 3 + PSReadLine/PSReadLineResources.Designer.cs | 11 + PSReadLine/PSReadLineResources.resx | 3 + PSReadLine/PlatformWindows.cs | 454 +++++++++++++++++++-- 6 files changed, 474 insertions(+), 25 deletions(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index bc89cc1f9..7771fa920 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -502,6 +502,8 @@ public object ListPredictionTooltipColor set => _listPredictionTooltipColor = VTColorUtils.AsEscapeSequence(value); } + public bool TerminateOrphanedConsoleApps { get; set; } + internal string _defaultTokenColor; internal string _commentColor; internal string _keywordColor; @@ -808,6 +810,14 @@ public PredictionViewStyle PredictionViewStyle [Parameter] public Hashtable Colors { get; set; } + [Parameter] + public SwitchParameter TerminateOrphanedConsoleApps + { + get => _terminateOrphanedConsoleApps.GetValueOrDefault(); + set => _terminateOrphanedConsoleApps = value; + } + internal SwitchParameter? _terminateOrphanedConsoleApps; + [ExcludeFromCodeCoverage] protected override void EndProcessing() { diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 19f366513..7485154b4 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -6,8 +6,10 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Management.Automation; using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; using Microsoft.PowerShell.PSReadLine; @@ -167,6 +169,22 @@ private void SetOptionsInternal(SetPSReadLineOption options) } } } + if (options._terminateOrphanedConsoleApps.HasValue) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Options.TerminateOrphanedConsoleApps = options.TerminateOrphanedConsoleApps; + PlatformWindows.SetTerminateOrphanedConsoleApps(Options.TerminateOrphanedConsoleApps); + } + else + { + throw new PlatformNotSupportedException( + string.Format( + CultureInfo.CurrentUICulture, + PSReadLineResources.OptionNotSupportedOnNonWindows, + nameof(Options.TerminateOrphanedConsoleApps))); + } + } } private void SetKeyHandlerInternal(string[] keys, Action handler, string briefDescription, string longDescription, ScriptBlock scriptBlock) diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 5b20c58af..24f70799a 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -164,6 +164,9 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) PredictionViewStyle + + TerminateOrphanedConsoleApps + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.CommandColor) diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 7d3435945..ac672d3f7 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2257,5 +2257,16 @@ internal static string UpcaseWordDescription return ResourceManager.GetString("UpcaseWordDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to: The '{0}' option is not supported on non-Windows platforms. + /// + internal static string OptionNotSupportedOnNonWindows + { + get + { + return ResourceManager.GetString("OptionNotSupportedOnNonWindows", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 618ef22b9..e619e7c06 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -873,4 +873,7 @@ Or not saving history with: Find the next word starting from the current position and then make it upper case. + + The '{0}' option is not supported on non-Windows platforms. + diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index b28077ada..86b4a73c5 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using Microsoft.PowerShell; using Microsoft.PowerShell.Internal; @@ -201,6 +202,15 @@ internal static void Init(ref ICharMap charMap) EnableAnsiInput(ref charMap); } + // Is the TerminateOrphanedConsoleApps feature enabled? + if (_allowedPids != null) + { + // We are about to disable Ctrl+C signals... so if there are still any + // console-attached children, the shell will be broken until they are + // gone, so we'll get rid of them: + TerminateStragglers(); + } + SetOurInputMode(); } } @@ -289,11 +299,11 @@ internal static void CallUsingOurInputMode(Action a) } } - private static readonly Lazy _inputHandle = new Lazy(() => + private static SafeFileHandle OpenConsoleHandle(string name) { // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles var handle = CreateFile( - "CONIN$", + name, (uint)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), (uint)ShareModes.ShareWrite, (IntPtr)0, @@ -305,33 +315,14 @@ internal static void CallUsingOurInputMode(Action a) { int err = Marshal.GetLastWin32Error(); Win32Exception innerException = new Win32Exception(err); - throw new Exception("Failed to retrieve the input console handle.", innerException); + throw new Exception($"Failed to retrieve the console handle ({name}).", innerException); } return new SafeFileHandle(handle, true); - }); - - private static readonly Lazy _outputHandle = new Lazy(() => - { - // We use CreateFile here instead of GetStdWin32Handle, as GetStdWin32Handle will return redirected handles - var handle = CreateFile( - "CONOUT$", - (uint)(AccessQualifiers.GenericRead | AccessQualifiers.GenericWrite), - (uint)ShareModes.ShareWrite, - (IntPtr)0, - (uint)CreationDisposition.OpenExisting, - 0, - (IntPtr)0); - - if (handle == INVALID_HANDLE_VALUE) - { - int err = Marshal.GetLastWin32Error(); - Win32Exception innerException = new Win32Exception(err); - throw new Exception("Failed to retrieve the input console handle.", innerException); - } + } - return new SafeFileHandle(handle, true); - }); + private static readonly Lazy _inputHandle = new Lazy(() => OpenConsoleHandle("CONIN$")); + private static readonly Lazy _outputHandle = new Lazy(() => OpenConsoleHandle("CONOUT$")); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool GetConsoleMode(IntPtr hConsole, out uint dwMode); @@ -343,6 +334,13 @@ private static uint GetConsoleInputMode() return result; } + private static uint GetConsoleOutputMode() + { + var handle = _outputHandle.Value.DangerousGetHandle(); + GetConsoleMode(handle, out var result); + return result; + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool SetConsoleMode(IntPtr hConsole, uint dwMode); @@ -614,4 +612,410 @@ public override void BlankRestOfLine() [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni); + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_BASIC_INFORMATION + { + public IntPtr ExitStatus; + public IntPtr PebBaseAddress; + public IntPtr AffinityMask; + public IntPtr BasePriority; + public IntPtr UniqueProcessId; + public IntPtr InheritedFromUniqueProcessId; + } + + [DllImport("ntdll.dll")] + internal static extern int NtQueryInformationProcess( + IntPtr processHandle, + int processInformationClass, + out PROCESS_BASIC_INFORMATION processInformation, + int processInformationLength, + out int returnLength); + + internal const int InvalidProcessId = -1; + + internal static int GetParentPid(Process process) + { + // (This is how ProcessCodeMethods in pwsh does it.) + PROCESS_BASIC_INFORMATION pbi; + int size; + var res = NtQueryInformationProcess(process.Handle, 0, out pbi, Marshal.SizeOf(), out size); + + return res != 0 ? InvalidProcessId : pbi.InheritedFromUniqueProcessId.ToInt32(); + } + + [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "GetConsoleProcessList")] + private static extern uint native_GetConsoleProcessList([In, Out] uint[] lpdwProcessList, uint dwProcessCount); + + private static uint[] GetConsoleProcessList() + { + int size = 100; + uint[] pids = new uint[size]; + uint numPids = native_GetConsoleProcessList(pids, (uint) size); + + if (numPids > size) + { + size = (int) numPids + 10; // a bit extra, since we may be racing attaches. + pids = new uint[size]; + numPids = native_GetConsoleProcessList(pids, (uint) size); + } + + if (0 == numPids || numPids > size) + { + return null; // no TerminateOrphanedConsoleApps for you, sorry + } + + Array.Resize(ref pids, (int) numPids); + return pids; + } + + // If the TerminateOrphanedConsoleApps option is enabled, this is the list of PIDs + // that are allowed to stay attached to the console (effectively the current process + // plus ancestors). + private static uint[] _allowedPids; + + internal static void SetTerminateOrphanedConsoleApps(bool enabled) + { + if (enabled) + { + _allowedPids = GetConsoleProcessList(); + } + else + { + _allowedPids = null; + } + } + + private static bool ItLooksLikeWeAreInTerminal() + { + return !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("WT_SESSION")); + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetConsoleWindow(); + + internal enum TaskbarStates + { + NoProgress = 0, + Indeterminate = 0x1, + Normal = 0x2, + Error = 0x4, + Paused = 0x8, + } + + internal static class TaskbarProgress + { + [ComImport()] + [Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface ITaskbarList3 + { + // ITaskbarList + [PreserveSig] + int HrInit(); + + [PreserveSig] + int AddTab(IntPtr hwnd); + + [PreserveSig] + int DeleteTab(IntPtr hwnd); + + [PreserveSig] + int ActivateTab(IntPtr hwnd); + + [PreserveSig] + int SetActiveAlt(IntPtr hwnd); + + // ITaskbarList2 + [PreserveSig] + int MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fFullscreen); + + // ITaskbarList3 + [PreserveSig] + int SetProgressValue(IntPtr hwnd, UInt64 ullCompleted, UInt64 ullTotal); + + [PreserveSig] + int SetProgressState(IntPtr hwnd, TaskbarStates state); + + // N.B. for copy/pasters: we've left out the rest of the ITaskbarList3 methods... + } + + [ComImport()] + [Guid("56fdf344-fd6d-11d0-958a-006097c9a090")] + [ClassInterface(ClassInterfaceType.None)] + private class TaskbarInstance + { + } + + private static Lazy _taskbarInstance = new Lazy(() => (ITaskbarList3) new TaskbarInstance()); + + public static int SetProgressState(IntPtr windowHandle, TaskbarStates taskbarState) + { + return _taskbarInstance.Value.SetProgressState(windowHandle, taskbarState); + } + + public static int SetProgressValue(IntPtr windowHandle, int progressValue, int progressMax) + { + return _taskbarInstance.Value.SetProgressValue(windowHandle, (ulong) progressValue, (ulong) progressMax); + } + } + + private static readonly Lazy _myPid = new(() => + { + using var me = Process.GetCurrentProcess(); + return (uint)me.Id; + }); + + // Calculates what processes need to be terminated (populated into procsToTerminate), + // and returns the count. A "straggler" is a console-attached process (so GUI + // processes don't count) that is not in the _allowedPids list. + private static int GatherStragglers(List procsToTerminate) + { + procsToTerminate.Clear(); + + // These are the processes currently attached to this console. Note that GUI + // processes will not be attached to the console. + uint[] currentPids = GetConsoleProcessList(); + + foreach (var pid in currentPids) + { + if (!_allowedPids.Contains(pid)) + { + Process proc = null; + try + { + proc = Process.GetProcessById((int) pid); + } + catch (ArgumentException) + { + // Ignore it: process could be gone, or something else that we + // likely can't do anything about it. + } + + if (proc != null) + { + // Q: Why the check against the parent pid (below)? + // + // A: The idea is that a user could do something like this: + // + // $p = Start-Process pwsh -ArgumentList '-c Write-Host start $pid; sleep -seconds 30; Write-Host stop' -NoNewWindow -passThru + // + // Such a process *is* indeed _capable_ of wrecking the interactive prompt (all it has to do is to attempt to read input; and any output + // will be interleaved with your interactive session)... but MAYBE it won't. So the idea with letting such processes live is that perhaps + // the user did this on purpose, to do some sort of "background work" (even though it may not seem like the best way to do that); and we + // only want to kill *actually-orphaned* processes: processes whose parent is gone, so they should be gone, too. + // + // We only check the immediate children processes here for simplicity. However, an immediate child process may have children that accidentally + // derive the standard input (which technically is a wrong thing to do), so ideally we should check if the parent of a console-attached process + // is still alive -- the parent process id points to an alive process that was created earlier. + // We will wait for feedback to see if this check needs to be updated. + + if (GetParentPid(proc) != _myPid.Value) + { + procsToTerminate.Add(proc); + } + else + { + proc.Dispose(); + } + } + } + } + return procsToTerminate.Count; + } + + [DllImport("kernel32.dll")] + internal static extern ulong GetTickCount64(); + + private static int MillisLeftUntilDeadline(ulong deadline) + { + long diff = (long) (deadline - GetTickCount64()); + + if (diff < 0) + { + diff = 0; + } + else if (diff >= (long) Int32.MaxValue) + { + // Should not ever actually happen... + diff = DefaultGraceMillis; + } + + return (int) diff; + } + + private const int DefaultGraceMillis = 1000; + private const int MaxRounds = 2; + + // + // TerminateOrphanedConsoleApps + // + // This feature works around a bad interaction on Windows between: + // * a race condition between ctrl+c and console attachment, and + // * poor behavior when multiple processes want console input. + // + // This bad interaction is most likely to happen when the user has launched a process + // that is launching many, MANY more child processes (imagine a build system, for + // example): if the user types ctrl+c to cancel, all processes *currently attached* to + // the console will receive the ctrl+c signal (and presumably exit). However, there + // *may* have been some processes that had been created, but are not yet attached to + // the console--these grandchildren will have missed the ctrl+c signal (that's the + // race condition). If those grandchildren do not somehow figure out on their own that + // they should exit, the console enters a highly problematic state ("the borked + // state"): because pwsh's immediate child has exited, the shell will return to the + // prompt and wait for input. But those straggler granchildren are ALSO attached to + // the console... so when the user starts typing, who gets the input? + // + // It turns out that the console will just sort of randomly distribute pieces of input + // between all processes who want input--a straggler grandchild process might get a + // "key down" record, and then PSReadLine might get the corresponding "key up". This + // is obviously untenable; it makes the shell totally unusable. (The console team has + // been made aware, and there are several ideas of how to Do Better, but who knows + // when any of those will come to fruition.) + // + // To make matters worse: when returning to the prompt, PSReadLine disables ctrl+c + // signals (we prefer to handle those keys specially ourselves). So if you hit this + // situation with cmd.exe as your shell, you can just mash on ctrl+c for a while and + // kill all the stragglers manually; but if you have PSReadLine loaded, your shell is + // borked, and you are stuck. You CAN recover, IF you can track down and kill all the + // straggler processes manually. + // + // So when enabled, this feature does that for you: it kills all those straggler + // processes, right before we disable ctrl+c signals and wait for user input, ensuring + // that the user has a usable shell. + // + // Note that GUI processes do not attach to the console, so if you have launched + // notepad, for example, TerminateOrphanedConsoleApps will never even "see" it; they + // are immune from getting terminated. + // + // Q: But isn't terminating processes that we know nothing about kind of risky and + // extreme? + // + // A: Perhaps so... but consider the alternative: by definition, if you get into a + // situation where the TerminateOrphanedConsoleApps feature would actually kill + // anything, your shell will be Completely Broken. It's "them or us": allow the + // stragglers to live, but leave the user without their shell; or kill the + // stragglers and give the user their shell back. There is no middle ground. So + // when the TerminateOrphanedConsoleApps feature is enabled, that means the user + // has opted for "give me back my shell". + // + // Note that we do give stragglers a small grace period before terminating them, in + // case they are somehow just slow shutting down. But if you're wondering "should + // we make that grace period longer?", remember that another way to think of that + // period is "how long do I want the shell to potentially be unusable after + // displaying the prompt?" + // + // Q: What if the user *didn't* type ctrl+c? + // + // A: We don't care. When TerminateOrphanedConsoleApps is called, all we know is that + // the shell has displayed the prompt and believes it is time to wait for user + // input. Whether this situation came about because of a ctrl+c, or some other + // situation (for example, if the shell's immediate child crashed or was manually + // killed), if there are leftover straggler processes (console-attached + // grandchildren), the shell will be broken until they are gone, and thus we must + // take action (if the feature is enabled). + // + // Q: Should this really be baked into PSReadLine, or could we leave it to some other + // module to implement? (See: https://github.com/jazzdelightsme/ConsoleBouncer) + // + // A: We should have the option in PSReadLine. An external module can do something + // very *similar* to what we do here in PSReadLine, but not quite the same, and is + // strictly inferior. An external module would have to rely on receiving a ctrl+c + // signal, but "there was a ctrl+c signal" is NOT equivalent to "the shell is about + // to wait for input". For example, some child processes may depend on handling + // ctrl+c signals, *without* exiting (kd.exe / cdb.exe, for example). In such a + // case, control would not return to the shell, but an external module would have + // no way to know that (hence it is inferior). That could be worked around, but + // only clumsily--the user would have to have a way to tell the module "hey BTW + // please don't kill these ones, even though they will *look* like stragglers". + // + // And in fact, an external module solution may still be attractive to some users + // (and could safely be used with TerminateOrphanedConsoleApps enabled). Because + // the (external solution) ConsoleBouncer module reacts to ctrl+c signals, that + // makes it a bit more aggressive than what we do here: + // TerminateOrphanedConsoleApps only comes into play when control has returned to + // the shell, which might not be right away after the user types ctrl+c--there + // might be "Terminate batch job (Y/N)?" messages, etc. So if the user understands + // the limitations of the ConsoleBouncer module and has an environment where it + // would be suitable, they could still opt to use it to get much more responsive + // ctrl+c behavior. (A metaphor with a club: the PSReadLine built-in feature + // patiently waits for the host of a private party to leave before kicking the rest + // of the guests out; whereas the ConsoleBouncer, upon receipt of a ctrl+c signal, + // just clears the whole place out right away (which *might* not be the right thing + // to do, but you're paying them to be tough, not smart).) + // + private static void TerminateStragglers() + { + var procsToTerminate = new List(); + + // The theory for why more than one round might be needed is that the same race + // between process creation and console attachment that could cause lingering + // processes in the first place could cause us to need a second round of + // cleanup... but I've never actually seen more than one round be needed. Probably + // because in my specific scenario the process that was spawning processes got + // taken out with the original ctrl+c signal. + // + // If it takes more than a few rounds of cleanup, we may be in some kind of + // pathological situation, and we'll bow out. + int round = 0; + int killAttempts = 0; + + while (round++ < MaxRounds && + GatherStragglers(procsToTerminate) > 0) + { + // We'll give them up to GracePeriodMillis for them to exit on their + // own, in case they actually did receive the original ctrl+c, and are + // just a tad slow shutting down. + ulong deadline = GetTickCount64() + (ulong) DefaultGraceMillis; + + var notDeadYet = procsToTerminate.Where( + (p) => !p.WaitForExit(MillisLeftUntilDeadline(deadline))); + + foreach (var process in notDeadYet) + { + try + { + killAttempts++; + process.Kill(); + } + // Ignore problems; maybe it's gone already, maybe something else; + // whatever. + catch (InvalidOperationException) { } + catch (Win32Exception) { } + } + + foreach (var process in procsToTerminate) + { + process.Dispose(); + } + } // end retry loop + + // In forcible termination scenarios, if there was a child updating the terminal's + // progress state, it may be left stuck that way... we can clear that out. + // + // The preferred way to do that is with a VT sequence, but there's no way to know + // if the console we are attached to supports that sequence. If we are in Windows + // Terminal, we know we can use the VT sequence; else we'll fall back to the old + // (Win7-era?) COM API (which does the same thing). + uint consoleMode = GetConsoleOutputMode(); + if (ItLooksLikeWeAreInTerminal()) + { + // We can use the [semi-]standard OSC sequence: + // https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC + if (0 != (consoleMode & (uint) ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + // Use "bell" if we actually tried to whack anything. + string final = (killAttempts > 0) ? "\a" : "\x001b\\"; + Console.Write("\x001b]9;4;0;0" + final); + } + } + else + { + IntPtr hwnd = GetConsoleWindow(); + if (hwnd != IntPtr.Zero) + { + int ret = TaskbarProgress.SetProgressState(hwnd, TaskbarStates.NoProgress); + } + } + } } From d7b9f82ba07f94d2e3ec008c8be6a00322177364 Mon Sep 17 00:00:00 2001 From: Maxime Labelle Date: Tue, 15 Aug 2023 00:56:46 +0200 Subject: [PATCH 051/127] [vi-mode] Supports the text-object command `diw` (#2059) --- PSReadLine/Cmdlets.cs | 2 +- PSReadLine/KeyBindings.vi.cs | 8 + PSReadLine/Position.cs | 13 +- PSReadLine/Prediction.Views.cs | 6 +- .../StringBuilderCharacterExtensions.cs | 78 ++++++++ ....cs => StringBuilderLinewiseExtensions.cs} | 20 ++ .../StringBuilderTextObjectExtensions.cs | 113 +++++++++++ PSReadLine/TextObjects.Vi.cs | 181 ++++++++++++++++++ PSReadLine/Words.cs | 8 +- PSReadLine/Words.vi.cs | 7 +- test/StringBuilderCharacterExtensionsTests.cs | 46 +++++ .../StringBuilderTextObjectExtensionsTests.cs | 77 ++++++++ test/TextObjects.Vi.Tests.cs | 176 +++++++++++++++++ 13 files changed, 709 insertions(+), 26 deletions(-) create mode 100644 PSReadLine/StringBuilderCharacterExtensions.cs rename PSReadLine/{StringBuilderExtensions.cs => StringBuilderLinewiseExtensions.cs} (72%) create mode 100644 PSReadLine/StringBuilderTextObjectExtensions.cs create mode 100644 PSReadLine/TextObjects.Vi.cs create mode 100644 test/StringBuilderCharacterExtensionsTests.cs create mode 100644 test/StringBuilderTextObjectExtensionsTests.cs create mode 100644 test/TextObjects.Vi.Tests.cs diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 7771fa920..222185602 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -142,7 +142,7 @@ public class PSConsoleReadLineOptions public const int DefaultCompletionQueryItems = 100; // Default includes all characters PowerShell treats like a dash - em dash, en dash, horizontal bar - public const string DefaultWordDelimiters = @";:,.[]{}()/\|^&*-=+'""" + "\u2013\u2014\u2015"; + public const string DefaultWordDelimiters = @";:,.[]{}()/\|!?^&*-=+'""" + "\u2013\u2014\u2015"; /// /// When ringing the bell, what should be done? diff --git a/PSReadLine/KeyBindings.vi.cs b/PSReadLine/KeyBindings.vi.cs index 9d051ec02..5a47c6608 100644 --- a/PSReadLine/KeyBindings.vi.cs +++ b/PSReadLine/KeyBindings.vi.cs @@ -45,6 +45,8 @@ internal static ConsoleColor AlternateBackground(ConsoleColor bg) private static Dictionary _viChordYTable; private static Dictionary _viChordDGTable; + private static Dictionary _viChordTextObjectsTable; + private static Dictionary> _viCmdChordTable; private static Dictionary> _viInsChordTable; @@ -238,6 +240,7 @@ private void SetDefaultViBindings() { Keys.ucG, MakeKeyHandler( DeleteEndOfBuffer, "DeleteEndOfBuffer") }, { Keys.ucE, MakeKeyHandler( ViDeleteEndOfGlob, "ViDeleteEndOfGlob") }, { Keys.H, MakeKeyHandler( BackwardDeleteChar, "BackwardDeleteChar") }, + { Keys.I, MakeKeyHandler( ViChordDeleteTextObject, "ChordViTextObject") }, { Keys.J, MakeKeyHandler( DeleteNextLines, "DeleteNextLines") }, { Keys.K, MakeKeyHandler( DeletePreviousLines, "DeletePreviousLines") }, { Keys.L, MakeKeyHandler( DeleteChar, "DeleteChar") }, @@ -296,6 +299,11 @@ private void SetDefaultViBindings() { Keys.Percent, MakeKeyHandler( ViYankPercent, "ViYankPercent") }, }; + _viChordTextObjectsTable = new Dictionary + { + { Keys.W, MakeKeyHandler(ViHandleTextObject, "WordTextObject")}, + }; + _viChordDGTable = new Dictionary { { Keys.G, MakeKeyHandler( DeleteRelativeLines, "DeleteRelativeLines") }, diff --git a/PSReadLine/Position.cs b/PSReadLine/Position.cs index 32068da91..2aa32039c 100644 --- a/PSReadLine/Position.cs +++ b/PSReadLine/Position.cs @@ -102,23 +102,14 @@ private static int GetFirstNonBlankOfLogicalLinePos(int current) var beginningOfLine = GetBeginningOfLinePos(current); var newCurrent = beginningOfLine; + var buffer = _singleton._buffer; - while (newCurrent < _singleton._buffer.Length && IsVisibleBlank(newCurrent)) + while (newCurrent < buffer.Length && buffer.IsVisibleBlank(newCurrent)) { newCurrent++; } return newCurrent; } - - private static bool IsVisibleBlank(int newCurrent) - { - var c = _singleton._buffer[newCurrent]; - - // [:blank:] of vim's pattern matching behavior - // defines blanks as SPACE and TAB characters. - - return c == ' ' || c == '\t'; - } } } diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index a2770eca4..a9145c432 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -1513,12 +1513,12 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit } int i = currentIndex; - if (!_singleton.InWord(_suggestionText[i], wordDelimiters)) + if (!Character.IsInWord(_suggestionText[i], wordDelimiters)) { // Scan to end of current non-word region while (++i < _suggestionText.Length) { - if (_singleton.InWord(_suggestionText[i], wordDelimiters)) + if (Character.IsInWord(_suggestionText[i], wordDelimiters)) { break; } @@ -1529,7 +1529,7 @@ internal int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimit { while (++i < _suggestionText.Length) { - if (!_singleton.InWord(_suggestionText[i], wordDelimiters)) + if (!Character.IsInWord(_suggestionText[i], wordDelimiters)) { if (_suggestionText[i] == ' ') { diff --git a/PSReadLine/StringBuilderCharacterExtensions.cs b/PSReadLine/StringBuilderCharacterExtensions.cs new file mode 100644 index 000000000..ab3faaea3 --- /dev/null +++ b/PSReadLine/StringBuilderCharacterExtensions.cs @@ -0,0 +1,78 @@ +using System.Text; + +namespace Microsoft.PowerShell +{ + internal static class StringBuilderCharacterExtensions + { + /// + /// Returns true if the character at the specified position is a visible whitespace character. + /// A blank character is defined as a SPACE or a TAB. + /// + /// + /// + /// + public static bool IsVisibleBlank(this StringBuilder buffer, int i) + { + var c = buffer[i]; + + // [:blank:] of vim's pattern matching behavior + // defines blanks as SPACE and TAB characters. + + return c == ' ' || c == '\t'; + } + + /// + /// Returns true if the character at the specified position is + /// not present in a list of word-delimiter characters. + /// + /// + /// + /// + /// + public static bool InWord(this StringBuilder buffer, int i, string wordDelimiters) + { + return Character.IsInWord(buffer[i], wordDelimiters); + } + + /// + /// Returns true if the character at the specified position is + /// at the end of the buffer + /// + /// + /// + /// + public static bool IsAtEndOfBuffer(this StringBuilder buffer, int i) + { + return i >= (buffer.Length - 1); + } + + /// + /// Returns true if the character at the specified position is + /// a unicode whitespace character. + /// + /// + /// + /// + public static bool IsWhiteSpace(this StringBuilder buffer, int i) + { + // Treat just beyond the end of buffer as whitespace because + // it looks like whitespace to the user even though they haven't + // entered a character yet. + return i >= buffer.Length || char.IsWhiteSpace(buffer[i]); + } + } + + public static class Character + { + /// + /// Returns true if the character not present in a list of word-delimiter characters. + /// + /// + /// + /// + public static bool IsInWord(char c, string wordDelimiters) + { + return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; + } + } +} diff --git a/PSReadLine/StringBuilderExtensions.cs b/PSReadLine/StringBuilderLinewiseExtensions.cs similarity index 72% rename from PSReadLine/StringBuilderExtensions.cs rename to PSReadLine/StringBuilderLinewiseExtensions.cs index 08deef333..40320a97d 100644 --- a/PSReadLine/StringBuilderExtensions.cs +++ b/PSReadLine/StringBuilderLinewiseExtensions.cs @@ -72,6 +72,26 @@ internal static Range GetRange(this StringBuilder buffer, int lineIndex, int lin endPosition - startPosition + 1 ); } + + /// + /// Returns true if the specified position is on an empty logical line. + /// + /// + /// + /// + public static bool IsLogigalLineEmpty(this StringBuilder buffer, int cursor) + { + // the cursor is on a logical line considered empty if... + return + // the entire buffer is empty (by definition), + buffer.Length == 0 || + // or the cursor sits at the start of the empty last line, + // meaning that it is past the end of the buffer and the + // last character in the buffer is a newline character, + (cursor == buffer.Length && buffer[cursor - 1] == '\n') || + // or if the cursor is on a newline character. + (cursor > 0 && buffer[cursor] == '\n'); + } } internal static class StringBuilderPredictionExtensions diff --git a/PSReadLine/StringBuilderTextObjectExtensions.cs b/PSReadLine/StringBuilderTextObjectExtensions.cs new file mode 100644 index 000000000..421ab3454 --- /dev/null +++ b/PSReadLine/StringBuilderTextObjectExtensions.cs @@ -0,0 +1,113 @@ +using System; +using System.Text; + +namespace Microsoft.PowerShell +{ + internal static class StringBuilderTextObjectExtensions + { + private const string WhiteSpace = " \n\t"; + + /// + /// Returns the position of the beginning of the current word as delimited by white space and delimiters + /// This method differs from : + /// - When the cursor location is on the first character of a word, + /// returns the position of the previous word, whereas this method returns the cursor location. + /// - When the cursor location is in a word, both methods return the same result. + /// This method supports VI "iw" text object. + /// + public static int ViFindBeginningOfWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters) + { + // Cursor may be past the end of the buffer when calling this method + // this may happen if the cursor is at the beginning of a new line. + var i = Math.Min(position, buffer.Length - 1); + + // If starting on a word consider a text object as a sequence of characters excluding the delimiters, + // otherwise, consider a word as a sequence of delimiters. + var delimiters = wordDelimiters; + var isInWord = buffer.InWord(i, wordDelimiters); + + if (isInWord) + { + // For the purpose of this method, whitespace character is considered a delimiter. + delimiters += WhiteSpace; + } + else + { + char c = buffer[i]; + if ((wordDelimiters + '\n').IndexOf(c) == -1 && char.IsWhiteSpace(c)) + { + // Current position points to a whitespace that is not a newline. + delimiters = WhiteSpace; + } + else + { + delimiters += '\n'; + } + } + + var isTextObjectChar = isInWord + ? (Func)(c => delimiters.IndexOf(c) == -1) + : c => delimiters.IndexOf(c) != -1; + + var beginning = i; + while (i >= 0 && isTextObjectChar(buffer[i])) + { + beginning = i--; + } + + return beginning; + } + + /// + /// Finds the position of the beginning of the next word object starting from the specified position. + /// If positioned on the last word in the buffer, returns buffer length + 1. + /// This method supports VI "iw" text-object. + /// iw: "inner word", select words. White space between words is counted too. + /// + public static int ViFindBeginningOfNextWordObjectBoundary(this StringBuilder buffer, int position, string wordDelimiters) + { + // Cursor may be past the end of the buffer when calling this method + // this may happen if the cursor is at the beginning of a new line. + var i = Math.Min(position, buffer.Length - 1); + + // Always skip the first newline character. + if (buffer[i] == '\n' && i < buffer.Length - 1) + { + ++i; + } + + // If starting on a word consider a text object as a sequence of characters excluding the delimiters, + // otherwise, consider a word as a sequence of delimiters. + var delimiters = wordDelimiters; + var isInWord = buffer.InWord(i, wordDelimiters); + + if (isInWord) + { + delimiters += WhiteSpace; + } + else if (char.IsWhiteSpace(buffer[i])) + { + delimiters = " \t"; + } + + var isTextObjectChar = isInWord + ? (Func)(c => delimiters.IndexOf(c) == -1) + : c => delimiters.IndexOf(c) != -1; + + // Try to skip a second newline characters to replicate vim behaviour. + if (buffer[i] == '\n' && i < buffer.Length - 1) + { + ++i; + } + + // Skip to next non-word characters. + while (i < buffer.Length && isTextObjectChar(buffer[i])) + { + ++i; + } + + // Make sure end includes the starting position. + return Math.Max(i, position); + } + } +} diff --git a/PSReadLine/TextObjects.Vi.cs b/PSReadLine/TextObjects.Vi.cs new file mode 100644 index 000000000..ea9810fbc --- /dev/null +++ b/PSReadLine/TextObjects.Vi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + internal enum TextObjectOperation + { + None, + Change, + Delete, + } + + internal enum TextObjectSpan + { + None, + Around, + Inner, + } + + private TextObjectOperation _textObjectOperation = TextObjectOperation.None; + private TextObjectSpan _textObjectSpan = TextObjectSpan.None; + + private readonly Dictionary> _textObjectHandlers = new() + { + [TextObjectOperation.Delete] = new() { [TextObjectSpan.Inner] = MakeKeyHandler(ViDeleteInnerWord, "ViDeleteInnerWord") }, + }; + + private void ViChordDeleteTextObject(ConsoleKeyInfo? key = null, object arg = null) + { + _textObjectOperation = TextObjectOperation.Delete; + ViChordTextObject(key, arg); + } + + private void ViChordTextObject(ConsoleKeyInfo? key = null, object arg = null) + { + if (!key.HasValue) + { + ResetTextObjectState(); + throw new ArgumentNullException(nameof(key)); + } + + _textObjectSpan = GetRequestedTextObjectSpan(key.Value); + + // Handle text object + var textObjectKey = ReadKey(); + if (_viChordTextObjectsTable.TryGetValue(textObjectKey, out _)) + { + _singleton.ProcessOneKey(textObjectKey, _viChordTextObjectsTable, ignoreIfNoAction: true, arg: arg); + } + else + { + ResetTextObjectState(); + Ding(); + } + } + + private TextObjectSpan GetRequestedTextObjectSpan(ConsoleKeyInfo key) + { + if (key.KeyChar == 'i') + { + return TextObjectSpan.Inner; + } + else if (key.KeyChar == 'a') + { + return TextObjectSpan.Around; + } + else + { + System.Diagnostics.Debug.Assert(false); + throw new NotSupportedException(); + } + } + + private static void ViHandleTextObject(ConsoleKeyInfo? key = null, object arg = null) + { + if (!_singleton._textObjectHandlers.TryGetValue(_singleton._textObjectOperation, out var textObjectHandler) || + !textObjectHandler.TryGetValue(_singleton._textObjectSpan, out var handler)) + { + ResetTextObjectState(); + Ding(); + return; + } + + handler.Action(key, arg); + } + + private static void ResetTextObjectState() + { + _singleton._textObjectOperation = TextObjectOperation.None; + _singleton._textObjectSpan = TextObjectSpan.None; + } + + private static void ViDeleteInnerWord(ConsoleKeyInfo? key = null, object arg = null) + { + var delimiters = _singleton.Options.WordDelimiters; + + if (!TryGetArgAsInt(arg, out var numericArg, 1)) + { + return; + } + + if (_singleton._buffer.Length == 0) + { + if (numericArg > 1) + { + Ding(); + } + return; + } + + // Unless at the end of the buffer a single delete word should not delete backwards + // so if the cursor is on an empty line, do nothing. + if (numericArg == 1 && + _singleton._current < _singleton._buffer.Length && + _singleton._buffer.IsLogigalLineEmpty(_singleton._current)) + { + return; + } + + var start = _singleton._buffer.ViFindBeginningOfWordObjectBoundary(_singleton._current, delimiters); + var end = _singleton._current; + + // Attempting to find a valid position for multiple words. + // If no valid position is found, this is a no-op + { + while (numericArg-- > 0 && end < _singleton._buffer.Length) + { + end = _singleton._buffer.ViFindBeginningOfNextWordObjectBoundary(end, delimiters); + } + + // Attempting to delete too many words should ding. + if (numericArg > 0) + { + Ding(); + return; + } + } + + if (end > 0 && _singleton._buffer.IsAtEndOfBuffer(end - 1) && _singleton._buffer.InWord(end - 1, delimiters)) + { + _singleton._shouldAppend = true; + } + + _singleton.RemoveTextToViRegister(start, end - start); + _singleton.AdjustCursorPosition(start); + _singleton.Render(); + } + + /// + /// Attempt to set the cursor at the specified position. + /// + /// + /// + private int AdjustCursorPosition(int position) + { + // This method might prove useful in a more general case. + if (_buffer.Length == 0) + { + _current = 0; + return 0; + } + + var maxPosition = _buffer[_buffer.Length - 1] == '\n' + ? _buffer.Length + : _buffer.Length - 1; + + var newCurrent = Math.Min(position, maxPosition); + var beginning = GetBeginningOfLinePos(newCurrent); + + if (newCurrent < _buffer.Length && _buffer[newCurrent] == '\n' && (newCurrent + ViEndOfLineFactor > beginning)) + { + newCurrent += ViEndOfLineFactor; + } + + _current = newCurrent; + return newCurrent; + } + } +} diff --git a/PSReadLine/Words.cs b/PSReadLine/Words.cs index 5c4c09f67..7bdc34a88 100644 --- a/PSReadLine/Words.cs +++ b/PSReadLine/Words.cs @@ -90,13 +90,7 @@ private Token FindToken(int current, FindTokenMode mode) private bool InWord(int index, string wordDelimiters) { - char c = _buffer[index]; - return InWord(c, wordDelimiters); - } - - private bool InWord(char c, string wordDelimiters) - { - return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; + return _buffer.InWord(index, wordDelimiters); } /// diff --git a/PSReadLine/Words.vi.cs b/PSReadLine/Words.vi.cs index 8ba987bae..5a475c19f 100644 --- a/PSReadLine/Words.vi.cs +++ b/PSReadLine/Words.vi.cs @@ -2,6 +2,8 @@ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ +using System; + namespace Microsoft.PowerShell { public partial class PSConsoleReadLine @@ -106,10 +108,7 @@ private int ViFindNextWordFromWord(int i, string wordDelimiters) /// private bool IsWhiteSpace(int i) { - // Treat just beyond the end of buffer as whitespace because - // it looks like whitespace to the user even though they haven't - // entered a character yet. - return i >= _buffer.Length || char.IsWhiteSpace(_buffer[i]); + return _buffer.IsWhiteSpace(i); } /// diff --git a/test/StringBuilderCharacterExtensionsTests.cs b/test/StringBuilderCharacterExtensionsTests.cs new file mode 100644 index 000000000..064477a93 --- /dev/null +++ b/test/StringBuilderCharacterExtensionsTests.cs @@ -0,0 +1,46 @@ +using Microsoft.PowerShell; +using System.Text; +using Xunit; + +namespace Test +{ + public sealed class StringBuilderCharacterExtensionsTests + { + [Fact] + public void StringBuilderCharacterExtensions_IsVisibleBlank() + { + var buffer = new StringBuilder(" \tn"); + + // system under test + + Assert.True(buffer.IsVisibleBlank(0)); + Assert.True(buffer.IsVisibleBlank(1)); + Assert.False(buffer.IsVisibleBlank(2)); + } + + [Fact] + public void StringBuilderCharacterExtensions_InWord() + { + var buffer = new StringBuilder("hello, world!"); + const string wordDelimiters = " "; + + // system under test + + Assert.True(buffer.InWord(2, wordDelimiters)); + Assert.True(buffer.InWord(5, wordDelimiters)); + } + + [Fact] + public void StringBuilderCharacterExtensions_IsWhiteSpace() + { + var buffer = new StringBuilder("a c"); + + + // system under test + + Assert.False(buffer.IsWhiteSpace(0)); + Assert.True(buffer.IsWhiteSpace(1)); + Assert.False(buffer.IsWhiteSpace(2)); + } + } +} diff --git a/test/StringBuilderTextObjectExtensionsTests.cs b/test/StringBuilderTextObjectExtensionsTests.cs new file mode 100644 index 000000000..66bd590de --- /dev/null +++ b/test/StringBuilderTextObjectExtensionsTests.cs @@ -0,0 +1,77 @@ +using Microsoft.PowerShell; +using System.Text; +using Xunit; + +namespace Test +{ + public sealed class StringBuilderTextObjectExtensionsTests + { + [Fact] + public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary() + { + const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters; + + var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four."); + Assert.Equal(0, buffer.ViFindBeginningOfWordObjectBoundary(1, wordDelimiters)); + } + + [Fact] + public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_whitespace() + { + const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters; + + var buffer = new StringBuilder("Hello, world!"); + Assert.Equal(6, buffer.ViFindBeginningOfWordObjectBoundary(7, wordDelimiters)); + } + + [Fact] + public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_backwards() + { + const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters; + + var buffer = new StringBuilder("Hello!\nworld!"); + Assert.Equal(5, buffer.ViFindBeginningOfWordObjectBoundary(6, wordDelimiters)); + } + + [Fact] + public void StringBuilderTextObjectExtensions_ViFindBeginningOfWordObjectBoundary_end_of_buffer() + { + const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters; + + var buffer = new StringBuilder("Hello, world!"); + Assert.Equal(12, buffer.ViFindBeginningOfWordObjectBoundary(buffer.Length, wordDelimiters)); + } + + [Fact] + public void StringBuilderTextObjectExtensions_ViFindBeginningOfNextWordObjectBoundary() + { + const string wordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters; + + var buffer = new StringBuilder("Hello, world!\ncruel world.\none\n\n\n\n\ntwo\n three four."); + + // Words |Hello|,| |world|!|\n|cruel |world|.|\n|one\n\n|\n\n|\n|two|\n |three| |four|.| + // Pos 01234 5 6 78901 2 _3 456789 01234 5 _6 789_0_1 _2_3 _4 567 _89 01234 5 6789 0 + // Pos 0 1 2 3 4 5 + + // system under test + + Assert.Equal(5, buffer.ViFindBeginningOfNextWordObjectBoundary(0, wordDelimiters)); + Assert.Equal(6, buffer.ViFindBeginningOfNextWordObjectBoundary(5, wordDelimiters)); + Assert.Equal(7, buffer.ViFindBeginningOfNextWordObjectBoundary(6, wordDelimiters)); + Assert.Equal(12, buffer.ViFindBeginningOfNextWordObjectBoundary(7, wordDelimiters)); + Assert.Equal(13, buffer.ViFindBeginningOfNextWordObjectBoundary(12, wordDelimiters)); + Assert.Equal(19, buffer.ViFindBeginningOfNextWordObjectBoundary(13, wordDelimiters)); + Assert.Equal(20, buffer.ViFindBeginningOfNextWordObjectBoundary(19, wordDelimiters)); + Assert.Equal(25, buffer.ViFindBeginningOfNextWordObjectBoundary(20, wordDelimiters)); + Assert.Equal(26, buffer.ViFindBeginningOfNextWordObjectBoundary(25, wordDelimiters)); + Assert.Equal(30, buffer.ViFindBeginningOfNextWordObjectBoundary(26, wordDelimiters)); + Assert.Equal(32, buffer.ViFindBeginningOfNextWordObjectBoundary(30, wordDelimiters)); + Assert.Equal(34, buffer.ViFindBeginningOfNextWordObjectBoundary(32, wordDelimiters)); + Assert.Equal(38, buffer.ViFindBeginningOfNextWordObjectBoundary(34, wordDelimiters)); + Assert.Equal(40, buffer.ViFindBeginningOfNextWordObjectBoundary(38, wordDelimiters)); + Assert.Equal(45, buffer.ViFindBeginningOfNextWordObjectBoundary(40, wordDelimiters)); + Assert.Equal(46, buffer.ViFindBeginningOfNextWordObjectBoundary(45, wordDelimiters)); + Assert.Equal(50, buffer.ViFindBeginningOfNextWordObjectBoundary(46, wordDelimiters)); + } + } +} diff --git a/test/TextObjects.Vi.Tests.cs b/test/TextObjects.Vi.Tests.cs new file mode 100644 index 000000000..f819b3879 --- /dev/null +++ b/test/TextObjects.Vi.Tests.cs @@ -0,0 +1,176 @@ +using Microsoft.PowerShell; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + [SkippableFact] + public void ViTextObject_diw() + { + TestSetup(KeyMode.Vi); + + Test("\"hello, \ncruel world!\"", Keys( + _.DQuote, + "hello, world!", _.Enter, + "cruel world!", _.DQuote, + _.Escape, + + // move cursor to the 'o' in 'world' + "gg9l", + + // delete text object + "diw", + CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")), + CheckThat(() => AssertCursorLeftIs(8)), + + // delete + "diw", + CheckThat(() => AssertLineIs("\"hello, \ncruel world!\"")), + CheckThat(() => AssertCursorLeftIs(7)) + )); + } + + [SkippableFact] + public void ViTextObject_diw_digit_arguments() + { + TestSetup(KeyMode.Vi); + + Test("\"hello, world!\"", Keys( + _.DQuote, + "hello, world!", _.Enter, + "cruel world!", _.DQuote, + _.Escape, + + // move cursor to the 'o' in 'world' + "gg9l", + + // delete text object + "diw", + CheckThat(() => AssertLineIs("\"hello, !\ncruel world!\"")), + CheckThat(() => AssertCursorLeftIs(8)), + + // delete multiple text objects (spans multiple lines) + "3diw", + CheckThat(() => AssertLineIs("\"hello, world!\"")), + CheckThat(() => AssertCursorLeftIs(8)) + )); + } + + + [SkippableFact] + public void ViTextObject_diw_noop() + { + TestSetup(KeyMode.Vi); + + TestMustDing("\"hello, world!\ncruel world!\"", Keys( + _.DQuote, + "hello, world!", _.Enter, + "cruel world!", _.DQuote, + _.Escape, + + // move cursor to the 'o' in 'world' + "gg9l", + + // attempting to delete too many words must ding + "1274diw" + )); + } + + [SkippableFact] + public void ViTextObject_diw_empty_line() + { + TestSetup(KeyMode.Vi); + + var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; + + Test("\"\nhello, world!\n\noh, bitter world!\n\"", Keys( + _.DQuote, _.Enter, + "hello, world!", _.Enter, + _.Enter, + "oh, bitter world!", _.Enter, + _.DQuote, _.Escape, + + // move cursor to the second line + "ggjj", + + // deleting single word cannot move backwards to previous line (noop) + "diw", + CheckThat(() => AssertLineIs("\"\nhello, world!\n\noh, bitter world!\n\"")) + )); + } + + [SkippableFact] + public void ViTextObject_diw_end_of_buffer() + { + TestSetup(KeyMode.Vi); + + var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; + + Test("", Keys( + _.DQuote, + "hello, world!", _.Enter, + "cruel world!", _.DQuote, + _.Escape, + + // move to end of buffer + "G$", + + // delete text object (deletes backwards) + "diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel world")), + "diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel ")), + "diw", CheckThat(() => AssertLineIs("\"hello, world!\ncruel")), + "diw", CheckThat(() => AssertLineIs("\"hello, world!\n")), + "diw", CheckThat(() => AssertLineIs("\"hello, world")), + "diw", CheckThat(() => AssertLineIs("\"hello, ")), + "diw", CheckThat(() => AssertLineIs("\"hello,")), + "diw", CheckThat(() => AssertLineIs("\"hello")), + "diw", CheckThat(() => AssertLineIs("\"")), + "diw", CheckThat(() => AssertLineIs("")) + )); + } + + [SkippableFact] + public void ViTextObject_diw_empty_buffer() + { + TestSetup(KeyMode.Vi); + Test("", Keys(_.Escape, "diw")); + TestMustDing("", Keys(_.Escape, "d2iw")); + } + + [SkippableFact] + public void ViTextObject_diw_new_lines() + { + TestSetup(KeyMode.Vi); + + var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; + + Test("\"\ntwo\n\"", Keys( + _.DQuote, _.Enter, + "one", _.Enter, + _.Enter, _.Enter, + _.Enter, _.Enter, + _.Enter, + "two", _.Enter, _.DQuote, + _.Escape, + + // move to the beginning of 'one' + "gg0j", + + // delete text object + "2diw", + CheckThat(() => AssertLineIs("\"\n\n\n\n\ntwo\n\"")), + + "ugg0j", // currently undo does not move the cursor to the correct position + // delete multiple text objects (spans multiple lines) + "3diw", + CheckThat(() => AssertLineIs("\"\n\n\ntwo\n\"")), + + "ugg0j", // currently undo does not move the cursor to the correct position + // delete multiple text objects (spans multiple lines) + "4diw", + CheckThat(() => AssertLineIs("\"\ntwo\n\"")) + )); + } + } +} From 4e456f020579dc8ac9d48bde3a29e4ffbe5d1d7f Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Aug 2023 09:37:47 -0700 Subject: [PATCH 052/127] Fix `NullReferenceException` when processing event subscribers (#3781) --- PSReadLine/ReadLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 3b042a448..2da14ff75 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -210,7 +210,7 @@ internal static PSKeyInfo ReadKey() bool runPipelineForEventProcessing = false; foreach (var sub in eventSubscribers) { - if (sub.SourceIdentifier.Equals(PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(sub.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) { // If the buffer is not empty, let's not consider we are idle because the user is in the middle of typing something. if (_singleton._buffer.Length > 0) From fbff10ccdd863e898c51a0e72ea55413c6d1a914 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Aug 2023 09:59:57 -0700 Subject: [PATCH 053/127] Point to `F7History` in the comment of the `F7` sample (#3782) --- PSReadLine/SamplePSReadLineProfile.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PSReadLine/SamplePSReadLineProfile.ps1 b/PSReadLine/SamplePSReadLineProfile.ps1 index 69177499b..0956dc2f3 100644 --- a/PSReadLine/SamplePSReadLineProfile.ps1 +++ b/PSReadLine/SamplePSReadLineProfile.ps1 @@ -24,6 +24,9 @@ Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward # typed text is used as the substring pattern for filtering. A selected command # is inserted to the command line without invoking. Multiple command selection # is supported, e.g. selected by Ctrl + Click. +# As another example, the module 'F7History' does something similar but uses the +# console GUI instead of Out-GridView. Details about this module can be found at +# PowerShell Gallery: https://www.powershellgallery.com/packages/F7History. Set-PSReadLineKeyHandler -Key F7 ` -BriefDescription History ` -LongDescription 'Show command history' ` From 423cb47324dc0e245afb0f2a461891f450e1117b Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Aug 2023 14:10:13 -0700 Subject: [PATCH 054/127] Prepare for the v2.3.2-beta2 release of PSReadLine (#3783) --- MockPSConsole/MockPSConsole.csproj | 2 +- PSReadLine/Changes.txt | 12 ++++++++++++ PSReadLine/PSReadLine.csproj | 8 ++++---- PSReadLine/PSReadLine.psd1 | 2 +- Polyfill/Polyfill.csproj | 2 +- test/PSReadLine.Tests.csproj | 4 ++-- 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index e934b2446..73766831e 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -18,7 +18,7 @@ - + diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 27ccd6f6e..f28a71322 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,15 @@ +### [2.3.2-beta2] - 2023-08-17 + +- Work around `InvalidOperationException` from Console API (#3755) (Thanks @jazzdelightsme!) +- Add the `TerminateOrphanedConsoleApps` option on Windows to kill orphaned console-attached process that may mess up reading from Console input (#3764) (Thanks @jazzdelightsme!) +- Fix bot to add `needs-triage` label to newly opened issue (#3772) +- Update `actions/checkout` used in GitHub action to v3 (#3773) +- Supports the text-object command `diw` in the VI edit mode (#2059) (Thanks @springcomp!) +- Fix `NullReferenceException` when processing event subscribers (#3781) +- Point to `F7History` in the comment of the `F7` key-binding sample (#3782) + +[2.3.2-beta2]: https://github.com/PowerShell/PSReadLine/compare/v2.3.1-beta1...v2.3.2-beta2 + ### [2.3.1-beta1] - 2023-05-03 - Append reset VT sequence before rendering the ineline prediction (#3669) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index abe887a7a..38b8f6430 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.3.1.0 - 2.3.1 - 2.3.1-beta1 + 2.3.2.0 + 2.3.2 + 2.3.2-beta2 true net462;net6.0 true @@ -22,7 +22,7 @@ - + diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 11299abf0..3f900233a 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.3.1' +ModuleVersion = '2.3.2' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index c6b7ee481..bcc2fcb24 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index e79fa34e8..b6c4625f0 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -19,12 +19,12 @@ - + - + From 33b74db80d142df7ba3b1302cc4919b5caaf9c76 Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Mon, 21 Aug 2023 10:42:56 -0700 Subject: [PATCH 055/127] Fix bot to only put the `needs-triage` label when opening an issue (#3788) --- .github/policies/resourceManagement.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index f6cff8991..2e731b5ab 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -289,7 +289,8 @@ configuration: - if: - payloadType: Issues - and: - - isOpen + - isAction: + action: Opened - not: and: - isAssignedToSomeone From 97f85b93a5e1099cc49d169afba1689fb5587132 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 18 Sep 2023 16:37:17 -0700 Subject: [PATCH 056/127] Prepare for the v2.3.3 release of PSReadLine (#3802) --- PSReadLine/Changes.txt | 6 ++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index f28a71322..031c43d7c 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,9 @@ +### [2.3.3] - 2023-09-18 + +- Re-package the `2.3.2-beta2` version to `2.3.3` as an official stable release. + +[2.3.3]: https://github.com/PowerShell/PSReadLine/compare/v2.3.2-beta2...v2.3.3 + ### [2.3.2-beta2] - 2023-08-17 - Work around `InvalidOperationException` from Console API (#3755) (Thanks @jazzdelightsme!) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 38b8f6430..8fc372155 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.3.2.0 - 2.3.2 - 2.3.2-beta2 + 2.3.3.0 + 2.3.3 + 2.3.3 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 3f900233a..8ca5d55d3 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.3.2' +ModuleVersion = '2.3.3' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 238c710a0312b79f9aeade6f8dc14108a9749d50 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 18 Sep 2023 17:10:28 -0700 Subject: [PATCH 057/127] Update `Compliance_Job` to add a new variable group for APIScan (#3803) --- .vsts-ci/releaseBuild.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.vsts-ci/releaseBuild.yml b/.vsts-ci/releaseBuild.yml index e30050035..f11c870d4 100644 --- a/.vsts-ci/releaseBuild.yml +++ b/.vsts-ci/releaseBuild.yml @@ -143,6 +143,7 @@ stages: displayName: PSReadLine Compliance variables: - group: APIScan + - group: ApiScanMeta # APIScan can take a long time timeoutInMinutes: 240 From d98e1e6833679a0ca0088109b12d451b88af30f9 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 18 Sep 2023 17:27:11 -0700 Subject: [PATCH 058/127] Update the stable version of PSReadLine used in the auto triage messages (#3804) --- tools/issue-mgmt/CloseDupIssues.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/issue-mgmt/CloseDupIssues.ps1 b/tools/issue-mgmt/CloseDupIssues.ps1 index 65587a733..28dad7d02 100644 --- a/tools/issue-mgmt/CloseDupIssues.ps1 +++ b/tools/issue-mgmt/CloseDupIssues.ps1 @@ -11,7 +11,7 @@ class issue $repo_name = "PowerShell/PSReadLine" $root_url = "https://github.com/PowerShell/PSReadLine/issues" $msg_upgrade = @" -Please upgrade to the [2.2.6 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.6) from PowerShell Gallery. +Please upgrade to the [2.3.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.3) from PowerShell Gallery. See the [upgrading section](https://github.com/PowerShell/PSReadLine#upgrading) for instructions. Please let us know if you run into the same issue with the latest version. "@ @@ -46,7 +46,7 @@ foreach ($item in $issues) $body -match 'PSReadLine: 2\.2\.0-beta[12]') { $comment = @' -This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.2.6 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.2.6). +This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.3.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.3). To upgrade, simply run `Install-Module PSReadLine -AllowPrerelease -Force` from your PowerShell console. -------- From d045b508f41e2c3fe9763768a5adfc58a5439907 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 26 Sep 2023 15:51:45 -0700 Subject: [PATCH 059/127] Choose the inline prediction color based on the environment (#3808) --- PSReadLine/Cmdlets.cs | 55 ++++++++++++++++++++++++++--------- PSReadLine/PlatformWindows.cs | 5 +--- PSReadLine/ReadLine.cs | 2 -- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 222185602..7fecf4b4e 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -95,17 +95,14 @@ public class PSConsoleReadLineOptions // Find the most suitable color using https://stackoverflow.com/a/33206814 // Default prediction color settings: - // - use FG color 'dim white italic' for the inline-view suggestion text // - use FG color 'yellow' for the list-view suggestion text // - use BG color 'dark black' for the selected list-view suggestion text - public const string DefaultInlinePredictionColor = "\x1b[97;2;3m"; public const string DefaultListPredictionColor = "\x1b[33m"; public const string DefaultListPredictionSelectedColor = "\x1b[48;5;238m"; - public const string DefaultListPredictionTooltipColor = "\x1b[97;2;3m"; - public static EditMode DefaultEditMode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? EditMode.Windows - : EditMode.Emacs; + public static readonly string DefaultInlinePredictionColor; + public static readonly string DefaultListPredictionTooltipColor; + public static readonly EditMode DefaultEditMode; public const string DefaultContinuationPrompt = ">> "; @@ -166,6 +163,40 @@ public class PSConsoleReadLineOptions /// public const int DefaultAnsiEscapeTimeout = 100; + static PSConsoleReadLineOptions() + { + // For inline-view suggestion text, we use the new FG color 'dim white italic' when possible, because it provides + // sufficient contrast in terminals that don't use a pure black background (like VSCode terminal). + // However, on Windows 10 and Windows Server, the ConHost doesn't support font effect VT sequences, such as 'dim' + // and 'italic', so we need to use the old FG color 'dark black' as in the v2.2.6. + const string newInlinePredictionColor = "\x1b[97;2;3m"; + const string oldInlinePredictionColor = "\x1b[38;5;238m"; + + ColorSetters = null; + DefaultEditMode = EditMode.Emacs; + DefaultInlinePredictionColor = newInlinePredictionColor; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + DefaultEditMode = EditMode.Windows; + + // Our tests expect that the default inline-view color is set to the new color, so we configure + // the color based on system environment only if we are not in test runs. + if (AppDomain.CurrentDomain.FriendlyName is not "PSReadLine.Tests") + { + DefaultInlinePredictionColor = + Environment.OSVersion.Version.Build >= 22621 // on Windows 11 22H2 or newer versions + || Environment.GetEnvironmentVariable("WT_SESSION") is not null // in Windows Terminal + ? newInlinePredictionColor + : oldInlinePredictionColor; + } + } + + // Use the same color for the list prediction tooltips. + DefaultListPredictionTooltipColor = DefaultInlinePredictionColor; + DefaultAddToHistoryHandler = s => PSConsoleReadLine.GetDefaultAddToHistoryOption(s); + } + public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) { ResetColors(); @@ -285,8 +316,7 @@ public object ContinuationPromptColor /// or added to memory only, or added to both memory and history file. /// public Func AddToHistoryHandler { get; set; } - public static readonly Func DefaultAddToHistoryHandler = - s => PSConsoleReadLine.GetDefaultAddToHistoryOption(s); + public static readonly Func DefaultAddToHistoryHandler; /// /// This handler is called from ValidateAndAcceptLine. @@ -305,7 +335,6 @@ public object ContinuationPromptColor /// odd things with script blocks, we create a white-list of commands /// that do invoke the script block - this covers the most useful cases. /// - [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public HashSet CommandsToValidateScriptBlockArguments { get; set; } /// @@ -555,7 +584,7 @@ internal void ResetColors() SelectionColor = VTColorUtils.AsEscapeSequence(bg, fg); } - private static Dictionary> ColorSetters = null; + private static Dictionary> ColorSetters; internal void SetColor(string property, object value) { @@ -830,7 +859,6 @@ public class ChangePSReadLineKeyHandlerCommandBase : PSCmdlet [Parameter(Position = 0, Mandatory = true)] [Alias("Key")] [ValidateNotNullOrEmpty] - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public string[] Chord { get; set; } [Parameter] @@ -903,8 +931,7 @@ protected override void EndProcessing() } } - private readonly Lazy _dynamicParameters = - new Lazy(CreateDynamicParametersResult); + private readonly Lazy _dynamicParameters = new(CreateDynamicParametersResult); private static RuntimeDefinedParameterDictionary CreateDynamicParametersResult() { @@ -1027,7 +1054,7 @@ public static class VTColorUtils public const ConsoleColor UnknownColor = (ConsoleColor) (-1); private static readonly Dictionary ConsoleColors = - new Dictionary(StringComparer.OrdinalIgnoreCase) + new(StringComparer.OrdinalIgnoreCase) { {"Black", ConsoleColor.Black}, {"DarkBlue", ConsoleColor.DarkBlue}, diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index 86b4a73c5..ef3a7eae5 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -637,10 +637,7 @@ internal static extern int NtQueryInformationProcess( internal static int GetParentPid(Process process) { // (This is how ProcessCodeMethods in pwsh does it.) - PROCESS_BASIC_INFORMATION pbi; - int size; - var res = NtQueryInformationProcess(process.Handle, 0, out pbi, Marshal.SizeOf(), out size); - + var res = NtQueryInformationProcess(process.Handle, 0, out PROCESS_BASIC_INFORMATION pbi, Marshal.SizeOf(), out _); return res != 0 ? InvalidProcessId : pbi.InheritedFromUniqueProcessId.ToInt32(); } diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 2da14ff75..c06120dbf 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -25,8 +25,6 @@ namespace Microsoft.PowerShell { - [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")] - [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable")] class ExitException : Exception { } public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods From 3b215825257f086bdc6459c78b8c8731901bf6a5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 2 Oct 2023 17:33:59 -0700 Subject: [PATCH 060/127] Prepare for the v2.3.4 release of PSReadLine (#3819) --- PSReadLine/Changes.txt | 8 ++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 031c43d7c..e2c673023 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,11 @@ +### [2.3.4] - 2023-10-02 + +- Choose the inline prediction color based on the environment (#3808) +- Update the stable version of PSReadLine used in the auto triage messages (#3804) +- Update `Compliance_Job` to add a new variable group for APIScan (#3803) + +[2.3.4]: https://github.com/PowerShell/PSReadLine/compare/v2.3.3...v2.3.4 + ### [2.3.3] - 2023-09-18 - Re-package the `2.3.2-beta2` version to `2.3.3` as an official stable release. diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 8fc372155..2b2946921 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.3.3.0 - 2.3.3 - 2.3.3 + 2.3.4.0 + 2.3.4 + 2.3.4 true net462;net6.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 8ca5d55d3..836c87022 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.3.3' +ModuleVersion = '2.3.4' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 3c433c8a05415a6ea95ea4a860dfad09a6ff80e9 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 2 Oct 2023 18:20:51 -0700 Subject: [PATCH 061/127] Update the auto-triage message to suggest PSReadLine v2.3.4 (#3820) --- tools/issue-mgmt/CloseDupIssues.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/issue-mgmt/CloseDupIssues.ps1 b/tools/issue-mgmt/CloseDupIssues.ps1 index 28dad7d02..c29ffcd09 100644 --- a/tools/issue-mgmt/CloseDupIssues.ps1 +++ b/tools/issue-mgmt/CloseDupIssues.ps1 @@ -11,7 +11,7 @@ class issue $repo_name = "PowerShell/PSReadLine" $root_url = "https://github.com/PowerShell/PSReadLine/issues" $msg_upgrade = @" -Please upgrade to the [2.3.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.3) from PowerShell Gallery. +Please upgrade to the [2.3.4 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.4) from PowerShell Gallery. See the [upgrading section](https://github.com/PowerShell/PSReadLine#upgrading) for instructions. Please let us know if you run into the same issue with the latest version. "@ @@ -46,7 +46,7 @@ foreach ($item in $issues) $body -match 'PSReadLine: 2\.2\.0-beta[12]') { $comment = @' -This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.3.3 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.3). +This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.3.4 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.4). To upgrade, simply run `Install-Module PSReadLine -AllowPrerelease -Force` from your PowerShell console. -------- From 5f9e6e84c4f66868cc645ea8bd528c4c606e524c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 2 Oct 2023 18:29:07 -0700 Subject: [PATCH 062/127] Update build script to always include the `ProjectUri` info (#3821) --- PSReadLine.build.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index 7cb98f2da..366ca4940 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -165,10 +165,14 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { if ($matches[1] -ne $version) { throw "AssemblyFileVersion mismatch with AssemblyInformationalVersion" } $prerelease = $matches[2] - # Put the prerelease tag in private data - $moduleManifestContent = [regex]::Replace($moduleManifestContent, "}", "PrivateData = @{ PSData = @{ Prerelease = '$prerelease'; ProjectUri = 'https://github.com/PowerShell/PSReadLine' } }$([System.Environment]::Newline)}") + # Put the prerelease tag in private data, along with the project URI. + $privateDataSection = "PrivateData = @{ PSData = @{ Prerelease = '$prerelease'; ProjectUri = 'https://github.com/PowerShell/PSReadLine' } }" + } else { + # Put the project URI in private data. + $privateDataSection = "PrivateData = @{ PSData = @{ ProjectUri = 'https://github.com/PowerShell/PSReadLine' } }" } + $moduleManifestContent = [regex]::Replace($moduleManifestContent, "}", "${privateDataSection}$([System.Environment]::Newline)}") $moduleManifestContent = [regex]::Replace($moduleManifestContent, "ModuleVersion = '.*'", "ModuleVersion = '$version'") $moduleManifestContent | Set-Content -Path $targetDir/PSReadLine.psd1 From ac69010fec57df06827aebb711d243706ec24e9b Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 2 Oct 2023 18:33:42 -0700 Subject: [PATCH 063/127] Handle large history file properly by reading lines in the streaming way (#3810) --- PSReadLine/History.cs | 51 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 1d501e1a3..c1490a230 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -457,12 +457,61 @@ private void ReadHistoryFile() { WithHistoryFileMutexDo(1000, () => { - var historyLines = File.ReadAllLines(Options.HistorySavePath); + var historyLines = ReadHistoryLinesImpl(Options.HistorySavePath, Options.MaximumHistoryCount); UpdateHistoryFromFile(historyLines, fromDifferentSession: false, fromInitialRead: true); var fileInfo = new FileInfo(Options.HistorySavePath); _historyFileLastSavedSize = fileInfo.Length; }); } + + static 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, + }; + + 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); + + // 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) + { + if (!line.EndsWith("`", StringComparison.Ordinal)) + { + // 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(); + } + } } void UpdateHistoryFromFile(IEnumerable historyLines, bool fromDifferentSession, bool fromInitialRead) From 46ed9d51a10bcfdd99c811d5739be9d66a707934 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 16 Oct 2023 22:27:44 -0700 Subject: [PATCH 064/127] Update the documentation issue template to point to the PowerShell-Doc repo (#3839) --- .../ISSUE_TEMPLATE/Documentation_Issue.yaml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml index f7388737e..e91d98d3f 100644 --- a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml +++ b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml @@ -1,16 +1,10 @@ name: Documentation Issue 📚 -description: Report issues in our documentation. +description: Report documentation issues at https://github.com/MicrosoftDocs/PowerShell-Docs/issues/new/choose labels: Issue-Docs body: -- type: checkboxes +- type: markdown attributes: - label: Prerequisites - options: - - label: Write a descriptive title. - required: true -- type: textarea - attributes: - label: Issue summary - description: Briefly describe which document needs to be corrected and why. - validations: - required: true + value: | + Thanks for taking the time to fill out this documentation Issue! | + Documents for PSReadLine are hosted in the [PowerShell-Docs](https://github.com/MicrosoftDocs/PowerShell-Docs) repo, | + so, please submit the issue directly in that repo for a quicker response and fix. From 090b7c2341afa037c84c64eff62ebeb1dcda8a43 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 16 Oct 2023 22:34:56 -0700 Subject: [PATCH 065/127] Fix the document issue template to make it valid (#3840) --- .github/ISSUE_TEMPLATE/Documentation_Issue.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml index e91d98d3f..2ac1b2d59 100644 --- a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml +++ b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml @@ -8,3 +8,9 @@ body: Thanks for taking the time to fill out this documentation Issue! | Documents for PSReadLine are hosted in the [PowerShell-Docs](https://github.com/MicrosoftDocs/PowerShell-Docs) repo, | so, please submit the issue directly in that repo for a quicker response and fix. +- type: checkboxes + attributes: + label: Prerequisites + options: + - label: Write a descriptive title. + required: true From bfd88e4d093e2446b9758d575d2fd66550bc633e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 16 Oct 2023 22:37:58 -0700 Subject: [PATCH 066/127] Fix the markdown message for the document issue template (#3841) --- .github/ISSUE_TEMPLATE/Documentation_Issue.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml index 2ac1b2d59..feee51172 100644 --- a/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml +++ b/.github/ISSUE_TEMPLATE/Documentation_Issue.yaml @@ -5,9 +5,8 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this documentation Issue! | - Documents for PSReadLine are hosted in the [PowerShell-Docs](https://github.com/MicrosoftDocs/PowerShell-Docs) repo, | - so, please submit the issue directly in that repo for a quicker response and fix. + Thanks for taking the time to fill out this documentation Issue! + Documents for PSReadLine are hosted in the [PowerShell-Docs](https://github.com/MicrosoftDocs/PowerShell-Docs) repo. So, please submit the issue directly in that repo for a quicker response and fix. - type: checkboxes attributes: label: Prerequisites From fbdf7faf0bf5c7eaa6917649efeea0c7806a3472 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 24 Oct 2023 09:46:18 -0700 Subject: [PATCH 067/127] Fix a few VI key handlers to close edit group properly (#3845) --- PSReadLine/Completion.cs | 2 +- PSReadLine/ReadLine.vi.cs | 8 +++- PSReadLine/Replace.vi.cs | 28 +++++++++---- test/BasicEditingTest.VI.cs | 83 +++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 9 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index f35c7cd52..6b3bb89b8 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -205,7 +205,7 @@ private void CompleteImpl(bool menuSelect) if (InViInsertMode()) // must close out the current edit group before engaging menu completion { ViCommandMode(); - ViInsertWithAppend(); + ViInsertWithAppendImpl(); } // Do not show suggestion text during tab completion. diff --git a/PSReadLine/ReadLine.vi.cs b/PSReadLine/ReadLine.vi.cs index c39932c75..571f06f5c 100644 --- a/PSReadLine/ReadLine.vi.cs +++ b/PSReadLine/ReadLine.vi.cs @@ -572,6 +572,12 @@ public static void ViInsertAtEnd(ConsoleKeyInfo? key = null, object arg = null) /// Append from the current line position. /// public static void ViInsertWithAppend(ConsoleKeyInfo? key = null, object arg = null) + { + _singleton._groupUndoHelper.StartGroup(ViInsertWithAppend, arg); + ViInsertWithAppendImpl(key, arg); + } + + private static void ViInsertWithAppendImpl(ConsoleKeyInfo? key = null, object arg = null) { ViInsertMode(key, arg); ForwardChar(key, arg); @@ -1304,7 +1310,7 @@ public static void ViAppendLine(ConsoleKeyInfo? key = null, object arg = null) } _singleton.SaveEditItem(EditItemInsertChar.Create('\n', insertPoint)); _singleton.Render(); - ViInsertWithAppend(); + ViInsertWithAppendImpl(); } private void MoveToEndOfPhrase() diff --git a/PSReadLine/Replace.vi.cs b/PSReadLine/Replace.vi.cs index efd23e469..8617f79c0 100644 --- a/PSReadLine/Replace.vi.cs +++ b/PSReadLine/Replace.vi.cs @@ -161,7 +161,7 @@ private static void ViReplaceWord(ConsoleKeyInfo? key, object arg) && !_singleton.IsDelimiter(_singleton._lastWordDelimiter, _singleton.Options.WordDelimiters) && _singleton._shouldAppend) { - ViInsertWithAppend(key, arg); + ViInsertWithAppendImpl(key, arg); } else { @@ -180,7 +180,7 @@ private static void ViReplaceGlob(ConsoleKeyInfo? key, object arg) } if (_singleton._current == _singleton._buffer.Length - 1) { - ViInsertWithAppend(key, arg); + ViInsertWithAppendImpl(key, arg); } else { @@ -194,7 +194,7 @@ private static void ViReplaceEndOfWord(ConsoleKeyInfo? key, object arg) DeleteEndOfWord(key, arg); if (_singleton._current == _singleton._buffer.Length - 1) { - ViInsertWithAppend(key, arg); + ViInsertWithAppendImpl(key, arg); } else { @@ -208,7 +208,7 @@ private static void ViReplaceEndOfGlob(ConsoleKeyInfo? key, object arg) ViDeleteEndOfGlob(key, arg); if (_singleton._current == _singleton._buffer.Length - 1) { - ViInsertWithAppend(key, arg); + ViInsertWithAppendImpl(key, arg); } else { @@ -270,13 +270,17 @@ private static void ViReplaceToChar(char keyChar, ConsoleKeyInfo? key = null, ob { if (_singleton._current < initialCurrent || _singleton._current >= _singleton._buffer.Length) { - ViInsertWithAppend(key, arg); + ViInsertWithAppendImpl(key, arg); } else { ViInsertMode(key, arg); } } + else + { + _singleton._groupUndoHelper.EndGroup(); + } } /// @@ -295,6 +299,10 @@ private static void ViReplaceToCharBack(char keyChar, ConsoleKeyInfo? key = null { ViInsertMode(key, arg); } + else + { + _singleton._groupUndoHelper.EndGroup(); + } } /// @@ -314,6 +322,10 @@ private static void ViReplaceToBeforeChar(char keyChar, ConsoleKeyInfo? key = nu { ViInsertMode(key, arg); } + else + { + _singleton._groupUndoHelper.EndGroup(); + } } /// @@ -332,8 +344,10 @@ private static void ViReplaceToBeforeCharBack(char keyChar, ConsoleKeyInfo? key { ViInsertMode(key, arg); } + else + { + _singleton._groupUndoHelper.EndGroup(); + } } - - } } diff --git a/test/BasicEditingTest.VI.cs b/test/BasicEditingTest.VI.cs index a41b6b078..1ab2bca51 100644 --- a/test/BasicEditingTest.VI.cs +++ b/test/BasicEditingTest.VI.cs @@ -1121,5 +1121,88 @@ public void ViInsertModeMoveCursor() _.RightArrow, // 'RightArrow' again does nothing, but doesn't crash "c")); } + + [SkippableFact] + public void ViDefect1281_1() + { + TestSetup(KeyMode.Vi); + + Test("bcd", Keys( + "abcdabcd", _.Escape, + + // return to the [B]eginning of the word, + // then [c]hange text un[t]il just before the [2]nd [b] character + // this leaves the cursor at the current position (0) but erases + // the "abcda" text portion, / switches to edit mode and + // positions the cursor just before the "bcd" text portion. + + "Bc2tb", + + // going back to normal mode again without having modified the buffer further + // even though the [c] command started an edit group, going back to normal + // mode closes the pending edit group. + + _.Escape, CheckThat(() => AssertCursorLeftIs(0)), + + // attempt to [c]hange text un[t]il just before the [2]nd [b] character again + // because the [b] character only appears once further down in the buffer + // relative to where the cursor position is – currently set to 0 - the command + // fails. Therefore, we are still in normal mode. + // + // as the command failed, the current edit group is now correctly closed. + + "c2tb", CheckThat(() => AssertLineIs("bcd")), + + // attempt to [c]hange text un[t]il just before the [2]nd [b] character a third time. + // this exercises a code path where starting a edit group while another + // pending edit group was previously started crashed PSRL. + // + // this should no longer crash as any started pending group is now properly closed if + // the command fails + "c2tb" + )); + } + + [SkippableFact] + public void ViDefect1281_2() + { + TestSetup(KeyMode.Vi); + + Test("abc", Keys( + "abc", _.Escape, + + // 'cff' triggers `ViReplaceToChar` to delete until the character 'f'. But 'abc' doesn't + // have the letter 'f', so the started edit group should be ended and cursor is not moved. + "cff", + CheckThat(() => AssertCursorLeftIs(2)), + + // the subsequent 'cc' calls `ViReplaceLine` to replace the current line with 'i', which + // starts a new edit group. + "ccip", + CheckThat(() => AssertLineIs("ip")), + + // now we undo the 'cci' step, and accept the current command line, which should be 'abc'. + _.Escape, "u" + )); + } + + [SkippableFact] + public void ViDefect1281_3() + { + TestSetup(KeyMode.Vi); + + Test("bcd", Keys( + "bcd", _.Escape, _.LeftArrow, _.LeftArrow, + + // 'a' triggers `ViInsertWithAppend` and we append 'iii' after 'b'. + "aiii", + CheckThat(() => AssertCursorLeftIs(4)), + CheckThat(() => AssertLineIs("biiicd")), + + // now we undo the 'aiii' step, and accept the current command line, + // which should be 'bcd'. + _.Escape, "u" + )); + } } } From 5e9ea88a9e3691015ca33ac59deac0f4d5b555da Mon Sep 17 00:00:00 2001 From: Steven Bucher Date: Mon, 30 Oct 2023 11:20:49 -0700 Subject: [PATCH 068/127] adding resolution no activity to bot close list (#3852) * adding resolution no activity to close list * add response --- .github/policies/resourceManagement.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 2e731b5ab..d2bb10360 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -136,6 +136,18 @@ configuration: label: Resolution-Wont Fix actions: - closeIssue + - description: + frequencies: + - hourly: + hour: 6 + filters: + - isOpen + - hasLabel: + label: Resolution-No Activity + actions: + - closeIssue + - addReply: + reply: This issue is closed because it has had no activity and on older unsupported versions of PowerShell or PSReadLine. Please try again on latest versions of both and if its still an issue please submit a new issue. - description: frequencies: - hourly: From ff4bbd5ee0e2dea7d72e0adb43d64a3f07c0e7e1 Mon Sep 17 00:00:00 2001 From: Friedrich von Never Date: Sat, 4 Nov 2023 01:40:25 +0700 Subject: [PATCH 069/127] Windows keyboard layout handling: get the current layout from the parent terminal process (#3786) --- PSReadLine/Keys.cs | 9 ++- PSReadLine/PlatformWindows.cs | 102 ++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/PSReadLine/Keys.cs b/PSReadLine/Keys.cs index 4a621bb2a..70cd80e47 100644 --- a/PSReadLine/Keys.cs +++ b/PSReadLine/Keys.cs @@ -115,13 +115,14 @@ public override int GetHashCode() public static extern uint MapVirtualKey(ConsoleKey uCode, uint uMapType); [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern int ToUnicode( + public static extern int ToUnicodeEx( ConsoleKey uVirtKey, uint uScanCode, byte[] lpKeyState, [MarshalAs(UnmanagedType.LPArray)] [Out] char[] chars, int charMaxCount, - uint flags); + uint flags, + IntPtr dwhkl); static readonly ThreadLocal toUnicodeBuffer = new ThreadLocal(() => new char[2]); static readonly ThreadLocal toUnicodeStateBuffer = new ThreadLocal(() => new byte[256]); @@ -147,7 +148,9 @@ internal static void TryGetCharFromConsoleKey(ConsoleKeyInfo key, ref char resul { flags |= (1 << 2); /* If bit 2 is set, keyboard state is not changed (Windows 10, version 1607 and newer) */ } - int charCount = ToUnicode(virtualKey, scanCode, state, chars, chars.Length, flags); + + IntPtr layout = PlatformWindows.GetConsoleKeyboardLayout(); + int charCount = ToUnicodeEx(virtualKey, scanCode, state, chars, chars.Length, flags, layout); if (charCount == 1) { diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index ef3a7eae5..c7e0313b9 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -140,6 +140,7 @@ internal static IConsole OneTimeInit(PSConsoleReadLine singleton) var breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(OnBreak)); SetConsoleCtrlHandler((BreakHandler)breakHandlerGcHandle.Target, true); _enableVtOutput = !Console.IsOutputRedirected && SetConsoleOutputVirtualTerminalProcessing(); + _terminalOwnerThreadId = GetTerminalOwnerThreadId(); return _enableVtOutput ? new VirtualTerminal() : new LegacyWin32Console(); } @@ -1015,4 +1016,105 @@ private static void TerminateStragglers() } } } + + private static uint _terminalOwnerThreadId; + + /// + /// This method helps to find the owner thread of the terminal window used by this pwsh instance, + /// by looking for a parent process whose ) is visible. + /// + /// The terminal process is not always the direct parent of the current process, but may be higher + /// in the process tree in case this pwsh process is a child of some other console process. + /// + /// This works well in Windows Terminal (with profile), IntelliJ and VSCode. + /// It doesn't work when PowerShell runs in conhost, or when it gets started from Start Menu with + /// Windows Terminal as the default terminal application (without profile). + /// + private static uint GetTerminalOwnerThreadId() + { + try + { + // The window handle returned by `GetConsoleWindow` is not the correct terminal/console window for us + // to query about the keyboard layout change. It's the window created for a console application, such + // as `cmd` or `pwsh`, so its owner process in those cases will be `cmd` or `pwsh`. + // + // When we are running with conhost, this window is visible, but it's not what we want and needs to be + // filtered out. When running with conhost, we want the window owned by the conhost. But unfortunately, + // there is no reliable way to get the conhost process that is associated with the current pwsh, since + // it's not in the parent chain of the process tree. + // So, this method is supposed to always fail when running with conhost. + IntPtr wrongHandle = GetConsoleWindow(); + + // Limit for parent process walk-up for not getting stuck in a loop (possible in case pid reuse). + const int iterationLimit = 20; + var process = Process.GetCurrentProcess(); + + for (int i = 0; i < iterationLimit; ++i) + { + if (process.ProcessName is "explorer") + { + // We've reached the root of the process tree. This can happen when PowerShell was started + // from Start Menu with Windows Terminal as the default terminal application. + // The `explorer` process has a visible window, but it doesn't help for getting the layout + // change. Again, we need to find the terminal window owner. + break; + } + + IntPtr mainWindowHandle = process.MainWindowHandle; + if (mainWindowHandle == wrongHandle) + { + // This can only happen when we are running with conhost. + // Break early because the terminal owner process is not in the parent chain in this scenario. + break; + } + + if (mainWindowHandle != IntPtr.Zero && IsWindowVisible(mainWindowHandle)) + { + // The window is visible, so it's likely the terminal window. + return GetWindowThreadProcessId(process.MainWindowHandle, out _); + } + + // When reaching here, the main window of the process: + // - doesn't exist, or + // - exists but invisible + // So, this is likely not a terminal process. + // Now we get its parent process and continue with the check. + int parentId = GetParentPid(process); + process = Process.GetProcessById(parentId); + } + } + catch (Exception) + { + // No access to the process, or the process is already dead. + // Either way, we cannot determine the owner thread of the terminal window. + } + + // We could not find the owner thread/process of the terminal window in following scenarios: + // 1. pwsh is running with conhost. + // This happens when conhost is set as the default terminal application, and a user starts pwsh + // from the Start Menu, or with `win+r` (run code) and etc. + // + // 2. pwsh is running with Windows Terminal, but was not started from a Windows Terminal profile. + // This happens when Windows Terminal is set as the default terminal application, and a user + // starts pwsh from the Start Menu, or with `win+r` (run code) and etc. + // The `WindowsTerminal` process is not in the parent process chain in this case. + // + // 3. pwsh's parent process chain is broken -- a parent was terminated so we cannot walk up the chain. + return 0; + } + + internal static IntPtr GetConsoleKeyboardLayout() + { + return GetKeyboardLayout(_terminalOwnerThreadId); + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("User32.dll", SetLastError = true)] + private static extern IntPtr GetKeyboardLayout(uint idThread); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint proccess); } From f2207f42593eee6cf80fbf7c1e70f96596be738e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 11 Dec 2023 10:44:20 -0800 Subject: [PATCH 070/127] Stop trying to de-duplicate completion results (#3897) --- PSReadLine/Completion.cs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 6b3bb89b8..d1b3399eb 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -298,31 +298,6 @@ private CommandCompletion GetCompletions() var length = _tabCompletions.ReplacementLength; if (start < 0 || start > _singleton._buffer.Length) return null; if (length < 0 || length > (_singleton._buffer.Length - start)) return null; - - if (_tabCompletions.CompletionMatches.Count > 1) - { - // Filter out apparent duplicates -- the 'ListItemText' is exactly the same. - var hashSet = new HashSet(); - var matches = _tabCompletions.CompletionMatches; - List indices = null; - - for (int i = 0; i < matches.Count; i++) - { - if (!hashSet.Add(matches[i].ListItemText)) - { - indices ??= new List(); - indices.Add(i); - } - } - - if (indices is not null) - { - for (int i = indices.Count - 1; i >= 0; i--) - { - matches.RemoveAt(indices[i]); - } - } - } } catch (Exception) { From e57f7d691d8df8c1121fddf47084f96aea74a688 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 11 Dec 2023 10:55:13 -0800 Subject: [PATCH 071/127] Little code style cleanup for the `GetCompletions()` method (#3898) --- PSReadLine/Completion.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index d1b3399eb..ac6e9260d 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -288,16 +288,27 @@ private CommandCompletion GetCompletions() ps = System.Management.Automation.PowerShell.Create(); ps.Runspace = _runspace; } - _tabCompletions = _mockableMethods.CompleteInput(_buffer.ToString(), _current, null, ps); - if (_tabCompletions.CompletionMatches.Count == 0) return null; + _tabCompletions = _mockableMethods.CompleteInput(_buffer.ToString(), _current, null, ps); + if (_tabCompletions.CompletionMatches.Count == 0) + { + return null; + } // Validate the replacement index/length - if we can't do // the replacement, we'll ignore the completions. var start = _tabCompletions.ReplacementIndex; var length = _tabCompletions.ReplacementLength; - if (start < 0 || start > _singleton._buffer.Length) return null; - if (length < 0 || length > (_singleton._buffer.Length - start)) return null; + + if (start < 0 || start > _singleton._buffer.Length) + { + return null; + } + + if (length < 0 || length > (_singleton._buffer.Length - start)) + { + return null; + } } catch (Exception) { From d4ef9312359ff9bfc8bb1ceb47d6240107ea6553 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 31 Jan 2024 13:41:59 -0800 Subject: [PATCH 072/127] Update the minimal PS version required to be 5.1 (#3936) --- PSReadLine/PSReadLine.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 836c87022..972931c4c 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -7,7 +7,7 @@ Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' Copyright = '(c) Microsoft Corporation. All rights reserved.' Description = 'Great command line editing in the PowerShell console host' -PowerShellVersion = '5.0' +PowerShellVersion = '5.1' DotNetFrameworkVersion = '4.6.2' CLRVersion = '4.0.0' FormatsToProcess = 'PSReadLine.format.ps1xml' From 5fa2a21abd5804e1ff28c15fbace2fdf2c00c523 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 31 Jan 2024 21:46:13 -0800 Subject: [PATCH 073/127] Use the correct directory separator for tab completion based on the platform we are working with (#3935) --- PSReadLine/Completion.cs | 31 ++++++++++++++++++++++++------- PSReadLine/ReadLine.cs | 7 +++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index ac6e9260d..c6654f924 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -23,6 +23,7 @@ public partial class PSConsoleReadLine private int _tabCommandCount; private CommandCompletion _tabCompletions; private Runspace _runspace; + private string _directorySeparator; private static readonly Dictionary KeysEndingCompletion = new Dictionary @@ -40,7 +41,7 @@ public partial class PSConsoleReadLine private static readonly char[] EolChars = {'\r', '\n'}; // String helper for directory paths - private static readonly string DirectorySeparatorString = System.IO.Path.DirectorySeparatorChar.ToString(); + private static readonly string DefaultDirectorySeparator = System.IO.Path.DirectorySeparatorChar.ToString(); // Stub helper method so completion can be mocked [ExcludeFromCodeCoverage] @@ -281,10 +282,26 @@ private CommandCompletion GetCompletions() System.Management.Automation.PowerShell ps; if (!_mockableMethods.RunspaceIsRemote(_runspace)) { + _directorySeparator ??= DefaultDirectorySeparator; ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); } else { + if (_directorySeparator is null) + { + // Use the default separator by default. + _directorySeparator = DefaultDirectorySeparator; + PSPrimitiveDictionary dict = _runspace.GetApplicationPrivateData(); + + if (dict["PSVersionTable"] is PSPrimitiveDictionary versionTable) + { + // If the 'Platform' key is available and its value is not 'Win*', then the server side is macOS or Linux. + // In that case, we use the forward slash '/' as the directory separator. + // Otherwise, the server side is Windows and we use the backward slash '\' instead. + _directorySeparator = versionTable["Platform"] is string platform && !platform.StartsWith("Win", StringComparison.Ordinal) ? "/" : @"\"; + } + } + ps = System.Management.Automation.PowerShell.Create(); ps.Runspace = _runspace; } @@ -375,12 +392,12 @@ private void DoReplacementForCompletion(CompletionResult completionResult, Comma completions.ReplacementLength = replacementText.Length; } - private static string GetReplacementTextForDirectory(string replacementText, ref int cursorAdjustment) + private string GetReplacementTextForDirectory(string replacementText, ref int cursorAdjustment) { - if (!replacementText.EndsWith(DirectorySeparatorString , StringComparison.Ordinal)) + if (!replacementText.EndsWith(_directorySeparator , StringComparison.Ordinal)) { - if (replacementText.EndsWith(String.Format("{0}\'", DirectorySeparatorString), StringComparison.Ordinal) || - replacementText.EndsWith(String.Format("{0}\"", DirectorySeparatorString), StringComparison.Ordinal)) + if (replacementText.EndsWith(string.Format("{0}\'", _directorySeparator), StringComparison.Ordinal) || + replacementText.EndsWith(string.Format("{0}\"", _directorySeparator), StringComparison.Ordinal)) { cursorAdjustment = -1; } @@ -388,12 +405,12 @@ private static string GetReplacementTextForDirectory(string replacementText, ref replacementText.EndsWith("\"", StringComparison.Ordinal)) { var len = replacementText.Length; - replacementText = replacementText.Substring(0, len - 1) + System.IO.Path.DirectorySeparatorChar + replacementText[len - 1]; + replacementText = replacementText.Substring(0, len - 1) + _directorySeparator + replacementText[len - 1]; cursorAdjustment = -1; } else { - replacementText = replacementText + System.IO.Path.DirectorySeparatorChar; + replacementText += _directorySeparator; } } return replacementText; diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index c06120dbf..951349c51 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -693,6 +693,13 @@ private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) _engineIntrinsics = engineIntrinsics; _runspace = runspace; + // The directory separator to be used for tab completion may change depending on + // whether we are working with a remote Runspace. + // So, we always set it to null for every call into 'PSConsoleReadLine.ReadLine', + // and do the real initialization when tab completion is triggered for the first + // time during that call. + _directorySeparator = null; + // Update the client instance per every call to PSReadLine. UpdatePredictionClient(runspace, engineIntrinsics); From 826f73e5080d10bb7b7911439ec0261fb2758a31 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 1 Feb 2024 16:14:22 -0800 Subject: [PATCH 074/127] Fix copying text to system clipboard on Linux using xclip (#3937) --- PSReadLine/Clipboard.cs | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/PSReadLine/Clipboard.cs b/PSReadLine/Clipboard.cs index a91586b9f..60e6d9392 100644 --- a/PSReadLine/Clipboard.cs +++ b/PSReadLine/Clipboard.cs @@ -16,22 +16,22 @@ static class Clipboard // This is useful for testing in CI as well. private static string _internalClipboard; - private static string StartProcess( - string tool, - string args, - string stdin = "" - ) + private static string StartProcess(bool collectOutput, string tool, string args, string stdin = null) { - ProcessStartInfo startInfo = new ProcessStartInfo(); - startInfo.UseShellExecute = false; - startInfo.RedirectStandardInput = true; - startInfo.RedirectStandardOutput = true; - startInfo.RedirectStandardError = true; - startInfo.FileName = tool; - startInfo.Arguments = args; string stdout; + bool redirectInput = !string.IsNullOrEmpty(stdin); - using (Process process = new Process()) + ProcessStartInfo startInfo = new() + { + UseShellExecute = false, + RedirectStandardInput = redirectInput, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = tool, + Arguments = args + }; + + using (Process process = new()) { process.StartInfo = startInfo; try @@ -42,15 +42,16 @@ private static string StartProcess( { _clipboardSupported = false; PSConsoleReadLine.Ding(); - return ""; + return string.Empty; } - if (stdin != "") + if (redirectInput) { process.StandardInput.Write(stdin); process.StandardInput.Close(); } - stdout = process.StandardOutput.ReadToEnd(); + + stdout = collectOutput ? process.StandardOutput.ReadToEnd() : string.Empty; process.WaitForExit(250); _clipboardSupported = process.ExitCode == 0; @@ -91,7 +92,7 @@ public static string GetText() return ""; } - return StartProcess(tool, args); + return StartProcess(collectOutput: true, tool, args); } public static void SetText(string text) @@ -128,7 +129,7 @@ public static void SetText(string text) return; } - StartProcess(tool, args, text); + StartProcess(collectOutput: false, tool, args, text); if (_clipboardSupported == false) { _internalClipboard = text; From d6cc8ad74c6f401a8bb290ad716a8c19b3c80d3c Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Feb 2024 11:03:29 -0800 Subject: [PATCH 075/127] Update `actions/checkout` to v4 (#3944) --- .github/workflows/IssuePreTriage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/IssuePreTriage.yml b/.github/workflows/IssuePreTriage.yml index 19e6ed684..d6c73cd22 100644 --- a/.github/workflows/IssuePreTriage.yml +++ b/.github/workflows/IssuePreTriage.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: do-work run: | From 5c69ba0951675fb288095440a48def3f79f9465a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Feb 2024 11:55:02 -0800 Subject: [PATCH 076/127] Add needed permission to the workflow (#3945) --- .github/workflows/IssuePreTriage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/IssuePreTriage.yml b/.github/workflows/IssuePreTriage.yml index d6c73cd22..b906e7189 100644 --- a/.github/workflows/IssuePreTriage.yml +++ b/.github/workflows/IssuePreTriage.yml @@ -21,6 +21,10 @@ jobs: name: Process new issues timeout-minutes: 20 runs-on: ubuntu-latest + permissions: + contents: read + issues: write + metadata: read steps: - name: checkout uses: actions/checkout@v4 From d332e214157bb9f44bef3f12af57b10df4049247 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Feb 2024 11:58:26 -0800 Subject: [PATCH 077/127] Add needed permission to the workflow (attempt 2) (#3946) --- .github/workflows/IssuePreTriage.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/IssuePreTriage.yml b/.github/workflows/IssuePreTriage.yml index b906e7189..2bf3e29cb 100644 --- a/.github/workflows/IssuePreTriage.yml +++ b/.github/workflows/IssuePreTriage.yml @@ -24,7 +24,6 @@ jobs: permissions: contents: read issues: write - metadata: read steps: - name: checkout uses: actions/checkout@v4 From ce2302c52635ec4e9ce3b3017e48f59b6b9ecf51 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 26 Feb 2024 13:29:54 -0800 Subject: [PATCH 078/127] Fix the null-reference exception when running `Debug-Job` on a thread job (#3957) --- PSReadLine/Prediction.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index 7ece2ed1b..bd1448b9a 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -50,7 +50,9 @@ private static void UpdatePredictionClient(Runspace runspace, EngineIntrinsics e if (s_pCurrentLocation is not null) { // Set the current location if it's a local Runspace. Otherwise, set it to null. - object path = runspace.RunspaceIsRemote ? null : engineIntrinsics.SessionState.Path.CurrentLocation; + object path = runspace is null || runspace.RunspaceIsRemote + ? null + : engineIntrinsics?.SessionState.Path.CurrentLocation; s_pCurrentLocation.SetValue(s_predictionClient, path); } } From 5efe2ef55f85bbac9c8a8f39825ad62b3049b0a5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 1 Mar 2024 16:24:34 -0800 Subject: [PATCH 079/127] Prepare for the v2.4.0-beta0 release of PSReadLine (#3962) --- .vsts-ci/releaseBuild.yml | 2 +- MockPSConsole/MockPSConsole.csproj | 2 +- PSReadLine.build.ps1 | 3 ++- PSReadLine/Changes.txt | 18 ++++++++++++++++++ PSReadLine/PSReadLine.csproj | 8 ++++---- PSReadLine/PSReadLine.psd1 | 2 +- Polyfill/Polyfill.csproj | 2 +- test/PSReadLine.Tests.csproj | 2 +- 8 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.vsts-ci/releaseBuild.yml b/.vsts-ci/releaseBuild.yml index f11c870d4..dd150aa9a 100644 --- a/.vsts-ci/releaseBuild.yml +++ b/.vsts-ci/releaseBuild.yml @@ -42,7 +42,7 @@ stages: Write-Host "PS Version: $($($PSVersionTable.PSVersion))" Set-Location -Path '$(Build.SourcesDirectory)\PSReadLine' .\build.ps1 -Bootstrap - .\build.ps1 -Configuration Release -Framework net462 -CheckHelpContent + .\build.ps1 -Configuration Release -Framework net462 # Set target folder paths New-Item -Path .\bin\Release\NuGetPackage -ItemType Directory > $null diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 73766831e..8c66078ce 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -18,7 +18,7 @@ - + diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index 366ca4940..49a397c21 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -160,7 +160,8 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { $version = $versionInfo.FileVersion $semVer = $versionInfo.ProductVersion - if ($semVer -match "(.*)-(.*)") { + # dotnet build may add the Git commit hash to the 'ProductVersion' attribute with this format: +. + if ($semVer -match "(.*)-([^\+]*)(?:\+.*)?") { # Make sure versions match if ($matches[1] -ne $version) { throw "AssemblyFileVersion mismatch with AssemblyInformationalVersion" } $prerelease = $matches[2] diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index e2c673023..ab68c56c1 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,21 @@ +### [2.4.0-beta0] - 2024-03-01 + +- Fix the null-reference exception when running `Debug-Job` on a thread job (#3957) +- Add needed permission to the workflow (#3944, #3945, #3946) +- Fix copying text to system clipboard on Linux using xclip (#3937) +- Use the correct directory separator for tab completion based on the platform we are working with (#3935) +- Update the minimal PS version required to be 5.1 (#3936) +- Little code style cleanup for the `GetCompletions()` method (#3898) +- Stop trying to de-duplicate completion results (#3897) +- Windows keyboard layout handling: get the current layout from the parent terminal process (#3786) (Thanks @ForNeVeR!) +- Add "resolution no activity" label to the bot-close list (#3852) +- Fix a few VI key handlers to close edit group properly (#3845) +- Update the documentation issue template to point to the PowerShell-Doc repo (#3839, #3840, #3841) +- Handle large history file properly by reading lines in the streaming way (#3810) +- Update build script to always include the `ProjectUri` info (#3821) + +[2.4.0-beta0]: https://github.com/PowerShell/PSReadLine/compare/v2.3.4...v2.4.0-beta0 + ### [2.3.4] - 2023-10-02 - Choose the inline prediction color based on the environment (#3808) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 2b2946921..2c7fa7e47 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 $(NoWarn);CA1416 - 2.3.4.0 - 2.3.4 - 2.3.4 + 2.4.0.0 + 2.4.0 + 2.4.0-beta0 true net462;net6.0 true @@ -22,7 +22,7 @@ - + diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 972931c4c..560df39b5 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") -ModuleVersion = '2.3.4' +ModuleVersion = '2.4.0' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index bcc2fcb24..a1a1693c6 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index b6c4625f0..f7d69cd70 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -24,7 +24,7 @@ - + From 5e63f147f5c13fd4838904243dfc98980e67f307 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 20 Mar 2024 16:06:19 -0700 Subject: [PATCH 080/127] Migrate PSReadLine release build pipeline to OneBranch (#3975) --- .config/tsaoptions.json | 9 ++ .pipelines/PSReadLine-Official.yml | 237 +++++++++++++++++++++++++++++ .vsts-ci/releaseBuild.yml | 192 ----------------------- 3 files changed, 246 insertions(+), 192 deletions(-) create mode 100644 .config/tsaoptions.json create mode 100644 .pipelines/PSReadLine-Official.yml delete mode 100644 .vsts-ci/releaseBuild.yml diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json new file mode 100644 index 000000000..0f1ded2de --- /dev/null +++ b/.config/tsaoptions.json @@ -0,0 +1,9 @@ +{ + "instanceUrl": "https://msazure.visualstudio.com", + "projectName": "One", + "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core", + "notificationAliases": [ + "dongbow@microsoft.com", + "slee@microsoft.com" + ] +} diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml new file mode 100644 index 000000000..ef4897ed7 --- /dev/null +++ b/.pipelines/PSReadLine-Official.yml @@ -0,0 +1,237 @@ +################################################################################# +# OneBranch Pipelines # +# This pipeline was created by EasyStart from a sample located at: # +# https://aka.ms/obpipelines/easystart/samples # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +# Retail Tasks: https://aka.ms/obpipelines/tasks # +# Support: https://aka.ms/onebranchsup # +################################################################################# + +name: PSReadLine-ModuleBuild-$(Build.BuildId) +trigger: none +pr: none + +variables: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest + +resources: + repositories: + - repository: onebranchTemplates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates + parameters: + featureFlags: + WindowsHostVersion: '1ESWindows2022' + globalSdl: + disableLegacyManifest: true + sbom: + enabled: true + packageName: PSReadLine + codeql: + compiled: + enabled: true + asyncSdl: # https://aka.ms/obpipelines/asyncsdl + enabled: true + forStages: [Build] + credscan: + enabled: true + scanFolder: $(Build.SourcesDirectory)\PSReadLine\PSReadLine + binskim: + enabled: true + apiscan: + enabled: false + + stages: + - stage: buildstage + displayName: Build and Sign PSReadLine + jobs: + - job: buildjob + displayName: Build PSReadLine Files + variables: + - name: ob_outputDirectory + value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' + - name: repoRoot + value: $(Build.SourcesDirectory)\PSReadLine + - name: ob_sdl_tsa_configFile + value: $(repoRoot)\.config\tsaoptions.json + - name: signSrcPath + value: $(repoRoot)\bin\Release\PSReadLine + - name: ob_sdl_sbom_enabled + value: true + - name: ob_signing_setup_enabled + value: true + #CodeQL tasks added manually to workaround signing failures + - name: ob_sdl_codeql_compiled_enabled + value: false + + pool: + type: windows + steps: + - checkout: self + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + + - pwsh: | + if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { + throw "tsaoptions.json does not exist under $(repoRoot)/.config" + } + displayName: Test if tsaoptions.json exists + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + + - pwsh: | + Write-Host "PS Version: $($PSVersionTable.PSVersion)" + Set-Location -Path '$(repoRoot)' + .\build.ps1 -Bootstrap + displayName: Bootstrap + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + + # Add CodeQL Init task right before your 'Build' step. + - task: CodeQL3000Init@0 + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + inputs: + Enabled: true + AnalyzeInPipeline: true + Language: csharp + + - pwsh: | + Write-Host "PS Version: $($($PSVersionTable.PSVersion))" + Set-Location -Path '$(repoRoot)' + .\build.ps1 -Configuration Release -Framework net462 + displayName: Build + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + + # Add CodeQL Finalize task right after your 'Build' step. + - task: CodeQL3000Finalize@0 + condition: always() + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + + - task: onebranch.pipeline.signing@1 + displayName: Sign 1st party files + inputs: + command: 'sign' + signing_profile: external_distribution + files_to_sign: '*.psd1;*.psm1;*.ps1;*.ps1xml;**\Microsoft*.dll;!Microsoft.PowerShell.Pager.dll' + search_root: $(signSrcPath) + + # Verify the signatures + - pwsh: | + $HasInvalidFiles = $false + $WrongCert = @{} + Get-ChildItem -Path $(signSrcPath) -Recurse -Include "*.dll","*.ps*1*" | ` + Get-AuthenticodeSignature | ForEach-Object { + Write-Host "$($_.Path): $($_.Status)" + if ($_.Status -ne 'Valid') { $HasInvalidFiles = $true } + if ($_.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation.*') { + $WrongCert.Add($_.Path, $_.SignerCertificate.Subject) + } + } + + if ($HasInvalidFiles) { throw "Authenticode verification failed. There is one or more invalid files." } + if ($WrongCert.Count -gt 0) { + $WrongCert + throw "Certificate should have the subject starts with 'Microsoft Corporation'" + } + + Write-Host "Display files in the folder ..." -ForegroundColor Yellow + Get-ChildItem -Path $(signSrcPath) -Recurse | Out-String -Width 120 + displayName: 'Verify the signed files' + + - task: CopyFiles@2 + displayName: "Copy signed files to ob_outputDirectory - '$(ob_outputDirectory)'" + inputs: + SourceFolder: $(signSrcPath) + Contents: '**\*' + TargetFolder: $(ob_outputDirectory) + + - pwsh: | + $versionInfo = Get-Item "$(signSrcPath)\Microsoft.PowerShell.PSReadLine2.dll" | ForEach-Object VersionInfo + $moduleVersion = $versionInfo.ProductVersion.Split('+')[0] + $vstsCommandString = "vso[task.setvariable variable=ob_sdl_sbom_packageversion]${moduleVersion}" + + Write-Host "sending $vstsCommandString" + Write-Host "##$vstsCommandString" + displayName: Setup SBOM Package Version + + - job: nupkg + dependsOn: buildjob + displayName: Package PSReadLine module + variables: + - name: ob_outputDirectory + value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' + - name: repoRoot + value: $(Build.SourcesDirectory)\PSReadLine + - name: ob_sdl_tsa_configFile + value: $(repoRoot)\.config\tsaoptions.json + # Disable because SBOM was already built in the previous job + - name: ob_sdl_sbom_enabled + value: false + - name: signOutPath + value: $(repoRoot)\signed\PSReadLine + - name: nugetPath + value: $(repoRoot)\signed\NuGetPackage + - name: ob_signing_setup_enabled + value: true + # This job is not compiling code, so disable codeQL + - name: ob_sdl_codeql_compiled_enabled + value: false + + pool: + type: windows + steps: + - checkout: self + + - task: DownloadPipelineArtifact@2 + displayName: 'Download build files' + inputs: + targetPath: $(signOutPath) + artifact: drop_buildstage_buildjob + + - pwsh: | + Get-ChildItem $(signOutPath) -Recurse + New-Item -Path $(nugetPath) -ItemType Directory > $null + displayName: Capture artifacts structure + + - pwsh: | + try { + $RepoName = "PSRLLocal" + Register-PSRepository -Name $RepoName -SourceLocation $(nugetPath) -PublishLocation $(nugetPath) -InstallationPolicy Trusted + Publish-Module -Repository $RepoName -Path $(signOutPath) + } finally { + Unregister-PSRepository -Name $RepoName -ErrorAction SilentlyContinue + } + Get-ChildItem -Path $(nugetPath) + displayName: 'Create the NuGet package' + + - task: onebranch.pipeline.signing@1 + displayName: Sign nupkg + inputs: + command: 'sign' + signing_profile: external_default + files_to_sign: '*.nupkg' + search_root: $(nugetPath) + + - task: CopyFiles@2 + displayName: "Copy nupkg to ob_outputDirectory - '$(ob_outputDirectory)'" + inputs: + SourceFolder: $(nugetPath) + Contents: '**\*' + TargetFolder: $(ob_outputDirectory) diff --git a/.vsts-ci/releaseBuild.yml b/.vsts-ci/releaseBuild.yml deleted file mode 100644 index dd150aa9a..000000000 --- a/.vsts-ci/releaseBuild.yml +++ /dev/null @@ -1,192 +0,0 @@ -name: PSReadLine-ModuleBuild-$(Build.BuildId) -trigger: none -pr: none - -variables: - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - POWERSHELL_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - SBOMGenerator_Formats: 'spdx:2.2' - -resources: - repositories: - - repository: ComplianceRepo - type: github - endpoint: ComplianceGHRepo - name: PowerShell/compliance - -stages: -- stage: Build - displayName: Build and Sign - pool: - name: PowerShell1ES - demands: - - ImageOverride -equals PSMMS2019-Secure - jobs: - - job: build_windows - displayName: Build PSReadLine - variables: - - group: ESRP - - steps: - - - checkout: self - clean: true - persistCredentials: true - - - pwsh: | - function Send-VstsCommand ($vstsCommandString) { - Write-Host ("sending: " + $vstsCommandString) - Write-Host "##$vstsCommandString" - } - Write-Host "PS Version: $($($PSVersionTable.PSVersion))" - Set-Location -Path '$(Build.SourcesDirectory)\PSReadLine' - .\build.ps1 -Bootstrap - .\build.ps1 -Configuration Release -Framework net462 - - # Set target folder paths - New-Item -Path .\bin\Release\NuGetPackage -ItemType Directory > $null - Send-VstsCommand "vso[task.setvariable variable=NuGetPackage]$(Build.SourcesDirectory)\PSReadLine\bin\Release\NuGetPackage" - Send-VstsCommand "vso[task.setvariable variable=PSReadLine]$(Build.SourcesDirectory)\PSReadLine\bin\Release\PSReadLine" - Send-VstsCommand "vso[task.setvariable variable=Signed]$(Build.SourcesDirectory)\PSReadLine\bin\Release\Signed" - displayName: Bootstrap & Build - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Governance Detection' - inputs: - sourceScanPath: '$(Build.SourcesDirectory)\PSReadLine' - snapshotForceEnabled: true - scanType: 'Register' - failOnAlert: true - - - checkout: ComplianceRepo - - # Sign the module files - - template: EsrpSign.yml@ComplianceRepo - parameters: - # the folder which contains the binaries to sign - buildOutputPath: $(PSReadLine) - # the location to put the signed output - signOutputPath: $(Signed) - # the certificate ID to use - certificateId: "CP-230012" - pattern: | - *.psd1 - *.psm1 - *.ps1 - *.ps1xml - **\*.dll - !System.Runtime.InteropServices.RuntimeInformation.dll - !Microsoft.PowerShell.Pager.dll - useMinimatch: true - - # Replace the *.psm1, *.ps1, *.psd1, *.dll files with the signed ones - - pwsh: | - # Show the signed files - Get-ChildItem -Path $(Signed) - Copy-Item -Path $(Signed)\* -Destination $(PSReadLine) -Recurse -Force - displayName: 'Replace unsigned files with signed ones' - - # Verify the signatures - - pwsh: | - $HasInvalidFiles = $false - $WrongCert = @{} - Get-ChildItem -Path $(PSReadLine) -Recurse -Include "*.dll","*.ps*1*" | ` - Get-AuthenticodeSignature | ForEach-Object { - $_ | Select-Object Path, Status - if ($_.Status -ne 'Valid') { $HasInvalidFiles = $true } - if ($_.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation.*') { - $WrongCert.Add($_.Path, $_.SignerCertificate.Subject) - } - } - - if ($HasInvalidFiles) { throw "Authenticode verification failed. There is one or more invalid files." } - if ($WrongCert.Count -gt 0) { - $WrongCert - throw "Certificate should have the subject starts with 'Microsoft Corporation'" - } - displayName: 'Verify the signed files' - - # Generate a Software Bill of Materials (SBOM) - - template: Sbom.yml@ComplianceRepo - parameters: - BuildDropPath: '$(PSReadLine)' - Build_Repository_Uri: 'https://github.com/PowerShell/PSReadLine.git' - displayName: Generate SBOM - - - pwsh: | - try { - $RepoName = "PSRLLocal" - Register-PSRepository -Name $RepoName -SourceLocation $(NuGetPackage) -PublishLocation $(NuGetPackage) -InstallationPolicy Trusted - Publish-Module -Repository $RepoName -Path $(PSReadLine) - } finally { - Unregister-PSRepository -Name $RepoName -ErrorAction SilentlyContinue - } - Get-ChildItem -Path $(NuGetPackage) - displayName: 'Create the NuGet package' - - - pwsh: | - Get-ChildItem -Path $(PSReadLine), $(NuGetPackage) - Write-Host "##vso[artifact.upload containerfolder=PSReadLine;artifactname=PSReadLine]$(PSReadLine)" - Write-Host "##vso[artifact.upload containerfolder=NuGetPackage;artifactname=NuGetPackage]$(NuGetPackage)" - displayName: 'Upload artifacts' - -- stage: compliance - displayName: Compliance - dependsOn: Build - pool: - name: PowerShell1ES - demands: - - ImageOverride -equals PSMMS2019-Secure - jobs: - - job: Compliance_Job - displayName: PSReadLine Compliance - variables: - - group: APIScan - - group: ApiScanMeta - # APIScan can take a long time - timeoutInMinutes: 240 - - steps: - - checkout: self - - checkout: ComplianceRepo - - download: current - artifact: PSReadLine - - - pwsh: | - Get-ChildItem -Path "$(Pipeline.Workspace)\PSReadLine" -Recurse - displayName: Capture downloaded artifacts - - - pwsh: | - function Send-VstsCommand ($vstsCommandString) { - Write-Host ("sending: " + $vstsCommandString) - Write-Host "##$vstsCommandString" - } - - # Get module version - $psd1Data = Import-PowerShellDataFile -Path "$(Pipeline.Workspace)\PSReadLine\PSReadLine.psd1" - $moduleVersion = $psd1Data.ModuleVersion - $prerelease = $psd1Data.PrivateData.PSData.Prerelease - if ($prerelease) { $moduleVersion = "$moduleVersion-$prerelease" } - Send-VstsCommand "vso[task.setvariable variable=ModuleVersion]$moduleVersion" - displayName: Get Module Version - - - template: assembly-module-compliance.yml@ComplianceRepo - parameters: - # binskim - AnalyzeTarget: '$(Pipeline.Workspace)\PSReadLine\*.dll' - AnalyzeSymPath: 'SRV*' - # component-governance - sourceScanPath: '' - # credscan - suppressionsFile: '' - # TermCheck - optionsRulesDBPath: '' - optionsFTPath: '' - # tsa-upload - codeBaseName: 'PSReadLine_201912' - # apiscan - softwareFolder: '$(Pipeline.Workspace)\PSReadLine' - softwareName: 'PSReadLine' - softwareVersion: '$(ModuleVersion)' - connectionString: 'RunAs=App;AppId=$(APIScanClient);TenantId=$(APIScanTenant);AppKey=$(APIScanSecret)' From b162aef91eff57a3898d6cd1b250ed7aa5413443 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 21 Mar 2024 16:08:57 -0700 Subject: [PATCH 081/127] Change back to 'external_distribution' for nupkg signing (#3977) --- .pipelines/PSReadLine-Official.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index ef4897ed7..ab49deeee 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -225,7 +225,7 @@ extends: displayName: Sign nupkg inputs: command: 'sign' - signing_profile: external_default + signing_profile: external_distribution files_to_sign: '*.nupkg' search_root: $(nugetPath) From d7fe398d5b70e767328628559eb918402e06b029 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 2 Apr 2024 11:24:22 -0700 Subject: [PATCH 082/127] Add the release stage to the pipeline and exclude test folders from Component Governance (#3982) --- .pipelines/PSReadLine-Official.yml | 61 +++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index ab49deeee..79ed79286 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -32,9 +32,12 @@ extends: WindowsHostVersion: '1ESWindows2022' globalSdl: disableLegacyManifest: true + cg: # Component Governance parameters. Ignore test components. + ignoreDirectories: $(Build.SourcesDirectory)\PSReadLine\MockPSConsole,$(Build.SourcesDirectory)\PSReadLine\test sbom: enabled: true packageName: PSReadLine + buildComponentPath: $(Build.SourcesDirectory)\PSReadLine\PSReadLine codeql: compiled: enabled: true @@ -43,7 +46,7 @@ extends: forStages: [Build] credscan: enabled: true - scanFolder: $(Build.SourcesDirectory)\PSReadLine\PSReadLine + scanFolder: $(Build.SourcesDirectory)\PSReadLine\PSReadLine binskim: enabled: true apiscan: @@ -235,3 +238,59 @@ extends: SourceFolder: $(nugetPath) Contents: '**\*' TargetFolder: $(ob_outputDirectory) + + - stage: release + dependsOn: buildstage + displayName: Release PSReadLine + + jobs: + - job: validation + displayName: Manual validation + pool: + type: agentless + timeoutInMinutes: 1440 + + steps: + - task: ManualValidation@0 + displayName: Wait 24 hours for validation + inputs: + instructions: Please validate the release + timeoutInMinutes: 1440 + + - job: publish + dependsOn: validation + displayName: Publish to AzFeed and PSGallery + variables: + - name: ob_outputDirectory + value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' + - name: nugetPath + value: $(Pipeline.Workspace)\NuGetPackage + pool: + type: windows + + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download nupkg artifact' + inputs: + targetPath: $(nugetPath) + artifact: drop_buildstage_nupkg + + - pwsh: | + Get-ChildItem $(nugetPath) -Recurse + displayName: Find signed Nupkg + + - task: NuGetCommand@2 + displayName: Push PSReadLine module to Azure feed + inputs: + command: push + packagesToPush: $(nugetPath)\PSReadLine.*.nupkg + nuGetFeedType: internal + publishVstsFeed: AzArtifactsFeed + + - task: NuGetCommand@2 + displayName: Push PSReadLine module to PSGallery feed + inputs: + command: push + packagesToPush: $(nugetPath)\PSReadLine.*.nupkg + nuGetFeedType: external + publishFeedCredentials: PowerShellGalleryFeed From 789bb848f7bf567b97e09df37ece07166f5cf9e1 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 3 Apr 2024 09:59:59 -0700 Subject: [PATCH 083/127] Fix the release stage and update the changelog for v2.3.5 servicing release (#3984) --- .pipelines/PSReadLine-Official.yml | 4 ++-- PSReadLine/Changes.txt | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 79ed79286..b84c00137 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -284,8 +284,8 @@ extends: inputs: command: push packagesToPush: $(nugetPath)\PSReadLine.*.nupkg - nuGetFeedType: internal - publishVstsFeed: AzArtifactsFeed + nuGetFeedType: external + publishFeedCredentials: AzArtifactsFeed - task: NuGetCommand@2 displayName: Push PSReadLine module to PSGallery feed diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index ab68c56c1..2c4ce39f2 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -16,6 +16,18 @@ [2.4.0-beta0]: https://github.com/PowerShell/PSReadLine/compare/v2.3.4...v2.4.0-beta0 +### [2.3.5] - 2024-04-02 + +This is a servicing release that excludes test components from SBOM generation. + +- Add the release stage to the pipeline and exclude test folders from Component Governance (#3982) +- Change back to 'external_distribution' for nupkg signing (#3977) +- Migrate PSReadLine release build pipeline to OneBranch (#3975) +- Fix the null-reference exception when running `Debug-Job` on a thread job (#3957) +- Update build script to always include the `ProjectUri` info (#3821) + +[2.3.5]: https://github.com/PowerShell/PSReadLine/compare/v2.3.4...v2.3.5 + ### [2.3.4] - 2023-10-02 - Choose the inline prediction color based on the environment (#3808) From fdac7def4084094c29adfd0adf35688b55d95196 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 3 Apr 2024 10:20:45 -0700 Subject: [PATCH 084/127] Update triage messages to use the latest stable version (#3985) --- tools/issue-mgmt/CloseDupIssues.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/issue-mgmt/CloseDupIssues.ps1 b/tools/issue-mgmt/CloseDupIssues.ps1 index c29ffcd09..38648f72f 100644 --- a/tools/issue-mgmt/CloseDupIssues.ps1 +++ b/tools/issue-mgmt/CloseDupIssues.ps1 @@ -11,7 +11,7 @@ class issue $repo_name = "PowerShell/PSReadLine" $root_url = "https://github.com/PowerShell/PSReadLine/issues" $msg_upgrade = @" -Please upgrade to the [2.3.4 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.4) from PowerShell Gallery. +Please upgrade to the [2.3.5 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.5) from PowerShell Gallery. See the [upgrading section](https://github.com/PowerShell/PSReadLine#upgrading) for instructions. Please let us know if you run into the same issue with the latest version. "@ @@ -46,7 +46,7 @@ foreach ($item in $issues) $body -match 'PSReadLine: 2\.2\.0-beta[12]') { $comment = @' -This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.3.4 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.4). +This issue was fixed in 2.2.0-beta3 version of PSReadLine. You can fix this by upgrading to the latest [2.3.5 version of PSReadLine](https://www.powershellgallery.com/packages/PSReadLine/2.3.5). To upgrade, simply run `Install-Module PSReadLine -AllowPrerelease -Force` from your PowerShell console. -------- From 4023f0607d917caffa4bfbf436bef124f874127d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 3 Apr 2024 15:32:55 -0700 Subject: [PATCH 085/127] Disable SBOM, signing, and codeQL for the publish job (#3986) --- .pipelines/PSReadLine-Official.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index b84c00137..289a34c1a 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -265,6 +265,13 @@ extends: value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' - name: nugetPath value: $(Pipeline.Workspace)\NuGetPackage + # Disable SBOM, signing, and codeQL for this job + - name: ob_sdl_sbom_enabled + value: false + - name: ob_signing_setup_enabled + value: false + - name: ob_sdl_codeql_compiled_enabled + value: false pool: type: windows From 61f598d8a733eba35810a4de6dc76f17433bbefc Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 14 May 2024 13:34:37 -0700 Subject: [PATCH 086/127] Update `SelectCommandArgument` to properly handle POSIX style options for CLI commands (#4016) --- PSReadLine/KillYank.cs | 24 +++++++++++++++++++- test/KillYankTest.cs | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/PSReadLine/KillYank.cs b/PSReadLine/KillYank.cs index f05fc8ba0..77fe33f1b 100644 --- a/PSReadLine/KillYank.cs +++ b/PSReadLine/KillYank.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Management.Automation.Language; +using System.Text.RegularExpressions; using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell @@ -29,6 +30,10 @@ class YankLastArgState private YankLastArgState _yankLastArgState; private int _visualSelectionCommandCount; + // Pattern to check for CLI parameters like '--json'. + // Valid characters are 'a-z', 'A-Z', '0-9', '_' (all covered by '\w'), and '-'. + private static readonly Regex s_cliOptionPattern = new(@"^--[\w-]+$", RegexOptions.Compiled); + /// /// Mark the current location of the cursor for use in a subsequent editing command. /// @@ -480,7 +485,7 @@ public static void SelectCommandArgument(ConsoleKeyInfo? key = null, object arg var argument = cmdAst.CommandElements[j] switch { CommandParameterAst paramAst => paramAst.Argument, - ExpressionAst expAst => expAst, + ExpressionAst exprAst => ProcessExpressionAst(exprAst), _ => null, }; @@ -614,6 +619,7 @@ public static void SelectCommandArgument(ConsoleKeyInfo? key = null, object arg _singleton.VisualSelectionCommon(() => SetCursorPosition(newEndCursor), forceSetMark: true); + // ===== Local Functions ===== // Get the script block AST's whose extent contains the cursor. bool GetScriptBlockAst(Ast ast) { @@ -639,6 +645,22 @@ bool GetScriptBlockAst(Ast ast) ? ast.Extent.EndOffset - 1 > cursor : ast.Extent.EndOffset >= cursor; } + + // Process an expression AST to check if it's a CLI posix style option. + static ExpressionAst ProcessExpressionAst(ExpressionAst exprAst) + { + if (exprAst is StringConstantExpressionAst strAst + && strAst.StringConstantType is StringConstantType.BareWord + && strAst.Value.StartsWith("--") + && s_cliOptionPattern.IsMatch(strAst.Value)) + { + // It's a CLI posix style option, like '--json' or '--machine-type', + // so we treat it as a parameter. + return null; + } + + return exprAst; + } } /// diff --git a/test/KillYankTest.cs b/test/KillYankTest.cs index 3433f7dab..8705faab6 100644 --- a/test/KillYankTest.cs +++ b/test/KillYankTest.cs @@ -619,6 +619,56 @@ public void SelectCommandArgument_VariousArgs() _.Escape)); } + [SkippableFact] + public void SelectCommandArgument_CLIArgs() + { + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "az webapp --name MyWebApp --resource-group MyResourceGroup", + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "az", + TokenClassification.None, ' ', + TokenClassification.Selection, "webapp", + TokenClassification.None, ' ', + TokenClassification.Parameter, "--name", + TokenClassification.None, " MyWebApp ", + TokenClassification.Parameter, "--resource-group", + TokenClassification.None, " MyResourceGroup ")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "az", + TokenClassification.None, " webapp ", + TokenClassification.Parameter, "--name", + TokenClassification.None, ' ', + TokenClassification.Selection, "MyWebApp", + TokenClassification.None, ' ', + TokenClassification.Parameter, "--resource-group", + TokenClassification.None, " MyResourceGroup ")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "az", + TokenClassification.None, " webapp ", + TokenClassification.Parameter, "--name", + TokenClassification.None, " MyWebApp ", + TokenClassification.Parameter, "--resource-group", + TokenClassification.None, ' ', + TokenClassification.Selection, "MyResourceGroup")), + + // Verify that we can loop through the arguments. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "az", + TokenClassification.None, ' ', + TokenClassification.Selection, "webapp", + TokenClassification.None, ' ', + TokenClassification.Parameter, "--name", + TokenClassification.None, " MyWebApp ", + TokenClassification.Parameter, "--resource-group", + TokenClassification.None, " MyResourceGroup ")), + + _.Escape)); + } + [SkippableFact] public void SelectCommandArgument_HereStringArgs() { From cb6037f41e4b2d5810befad836895d443194add6 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:17:17 -0700 Subject: [PATCH 087/127] Update "Code of Conduct" and "Security Policy" (#4037) --- .github/CODE_OF_CONDUCT.md | 10 ++++++++++ .github/CONTRIBUTING.md | 2 +- .github/SECURITY.md | 41 ++++++++++++++++++++++++++++++++++++++ README.md | 9 ++++----- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/SECURITY.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..686e5e7a0 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,10 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns +- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 218c5d543..fc3482eb7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -127,5 +127,5 @@ After build, the produced artifacts can be found at `/bin/ [platy-ps]: https://www.powershellgallery.com/packages/platyPS [using-prs]: https://help.github.com/articles/using-pull-requests/ [fork-a-repo]: https://help.github.com/articles/fork-a-repo/ -[vuln-reporting]: https://github.com/PowerShell/PowerShell/blob/master/.github/SECURITY.md +[vuln-reporting]: SECURITY.md [closing-via-message]: https://help.github.com/articles/closing-issues-via-commit-messages/ diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..f941d308b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin) and [PowerShell](https://github.com/PowerShell). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + diff --git a/README.md b/README.md index ea4826610..9d5805331 100644 --- a/README.md +++ b/README.md @@ -258,9 +258,8 @@ PSReadLine is licensed under the [2-Clause BSD License][]. ## Code of Conduct -This project has adopted the [Microsoft Open Source Code of Conduct][conduct-code]. -For more information see the [Code of Conduct FAQ][conduct-FAQ] or contact [opencode@microsoft.com][conduct-email] with any additional questions or comments. +Please see our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before participating in this project. -[conduct-code]: https://opensource.microsoft.com/codeofconduct/ -[conduct-FAQ]: https://opensource.microsoft.com/codeofconduct/faq/ -[conduct-email]: mailto:opencode@microsoft.com +## Security Policy + +For any security issues, please see our [Security Policy](.github/SECURITY.md). From 6327a1a963928aaaf2a05c91a6dd26031fc107d3 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 19 Jun 2024 15:00:04 -0700 Subject: [PATCH 088/127] Change the NuGet feed to use the governed PowerShell feed (#4044) --- nuget.config | 2 +- tools/helper.psm1 | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index 654858614..a10ce9b3d 100644 --- a/nuget.config +++ b/nuget.config @@ -2,7 +2,7 @@ - + diff --git a/tools/helper.psm1 b/tools/helper.psm1 index 29ba9f06f..b8056de65 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -339,3 +339,26 @@ function Test-XUnitTestResults throw "$($failedTests.failed) tests failed" } } + +<# +.SYNOPSIS + Run 'dotnet restore' for all the target runtime that we are interested in to + update packages on the CFS feed. + This needs to be run on a MS employee's dev machine whenever there is update + to the NuGet packages used in PSReadLine repo, so that the package and all its + dependencies can be pull into the CFS feed from upstream feed. +#> +function Update-CFSFeed +{ + $rids = @('win-x64', 'win-arm64', 'linux-x64', 'linux-arm', 'linux-arm64', 'osx-x64', 'osx-arm64') + + Write-Host "1. clear all NuGet caches on the local machine." -ForegroundColor Green + dotnet nuget locals all -c + + Write-Host "2. restore for target runtimes." -ForegroundColor Green + foreach ($rid in $rids) { + Write-Host " - $rid" -ForegroundColor Green + dotnet restore -r $rid ../test/PSReadLine.Tests.csproj + dotnet restore -r $rid ../MockPSConsole/MockPSConsole.csproj + } +} From eff1452a647a0bb5863cf5fa42062b3b1f701c10 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 20 Jun 2024 16:56:52 -0700 Subject: [PATCH 089/127] Add 'ob_restore_phase' for every task before the signing task to work around the signing issue (#4046) --- .pipelines/PSReadLine-Official.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 289a34c1a..1a2833e33 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -201,9 +201,13 @@ extends: type: windows steps: - checkout: self + env: + ob_restore_phase: true # This ensures this done in restore phase to workaround signing issue - task: DownloadPipelineArtifact@2 displayName: 'Download build files' + env: + ob_restore_phase: true # This ensures this done in restore phase to workaround signing issue inputs: targetPath: $(signOutPath) artifact: drop_buildstage_buildjob @@ -212,6 +216,8 @@ extends: Get-ChildItem $(signOutPath) -Recurse New-Item -Path $(nugetPath) -ItemType Directory > $null displayName: Capture artifacts structure + env: + ob_restore_phase: true # This ensures this done in restore phase to workaround signing issue - pwsh: | try { @@ -223,6 +229,8 @@ extends: } Get-ChildItem -Path $(nugetPath) displayName: 'Create the NuGet package' + env: + ob_restore_phase: true # This ensures this done in restore phase to workaround signing issue - task: onebranch.pipeline.signing@1 displayName: Sign nupkg From e9122d38e932614393ff61faf57d6518990d7226 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 29 Jul 2024 12:20:13 -0700 Subject: [PATCH 090/127] Make sure the `CodeQL` result from release pipeline gets uploaded (#4082) --- .pipelines/PSReadLine-Official.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 1a2833e33..cbbcb701b 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -108,7 +108,6 @@ extends: ob_restore_phase: true inputs: Enabled: true - AnalyzeInPipeline: true Language: csharp - pwsh: | From a1130b519001492969fda4c410ffe803b600825e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 2 Oct 2024 16:08:06 -0700 Subject: [PATCH 091/127] Release SBOM (#4201) --- .pipelines/PSReadLine-Official.yml | 37 ++++++++++++++---------------- MockPSConsole/MockPSConsole.csproj | 2 +- PSReadLine/PSReadLine.csproj | 2 +- Polyfill/Polyfill.csproj | 2 +- test/PSReadLine.Tests.csproj | 2 +- tools/helper.psm1 | 2 +- 6 files changed, 22 insertions(+), 25 deletions(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index cbbcb701b..1308f963c 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -15,21 +15,22 @@ pr: none variables: DOTNET_CLI_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest resources: repositories: - - repository: onebranchTemplates - type: git - name: OneBranch.Pipelines/GovernedTemplates - ref: refs/heads/main + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main extends: - template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates + template: v2/OneBranch.Official.CrossPlat.yml@templates parameters: featureFlags: - WindowsHostVersion: '1ESWindows2022' + WindowsHostVersion: + Version: 2022 globalSdl: disableLegacyManifest: true cg: # Component Governance parameters. Ignore test components. @@ -43,7 +44,7 @@ extends: enabled: true asyncSdl: # https://aka.ms/obpipelines/asyncsdl enabled: true - forStages: [Build] + forStages: [buildstage] credscan: enabled: true scanFolder: $(Build.SourcesDirectory)\PSReadLine\PSReadLine @@ -154,7 +155,7 @@ extends: } Write-Host "Display files in the folder ..." -ForegroundColor Yellow - Get-ChildItem -Path $(signSrcPath) -Recurse | Out-String -Width 120 + Get-ChildItem -Path $(signSrcPath) -Recurse | Out-String -Width 120 -Stream displayName: 'Verify the signed files' - task: CopyFiles@2 @@ -212,7 +213,11 @@ extends: artifact: drop_buildstage_buildjob - pwsh: | - Get-ChildItem $(signOutPath) -Recurse + if (Test-Path '$(signOutPath)\_manifest') { + Write-Verbose -Verbose "Delete SBOM files ..." + Remove-Item -Path '$(signOutPath)\_manifest' -Recurse -Force + } + Get-ChildItem $(signOutPath) -Recurse | Out-String -Width 120 -Stream New-Item -Path $(nugetPath) -ItemType Directory > $null displayName: Capture artifacts structure env: @@ -226,7 +231,7 @@ extends: } finally { Unregister-PSRepository -Name $RepoName -ErrorAction SilentlyContinue } - Get-ChildItem -Path $(nugetPath) + Get-ChildItem -Path $(nugetPath) | Out-String -Width 120 -Stream displayName: 'Create the NuGet package' env: ob_restore_phase: true # This ensures this done in restore phase to workaround signing issue @@ -290,17 +295,9 @@ extends: artifact: drop_buildstage_nupkg - pwsh: | - Get-ChildItem $(nugetPath) -Recurse + Get-ChildItem $(nugetPath) -Recurse | Out-String -Width 120 -Stream displayName: Find signed Nupkg - - task: NuGetCommand@2 - displayName: Push PSReadLine module to Azure feed - inputs: - command: push - packagesToPush: $(nugetPath)\PSReadLine.*.nupkg - nuGetFeedType: external - publishFeedCredentials: AzArtifactsFeed - - task: NuGetCommand@2 displayName: Push PSReadLine module to PSGallery feed inputs: diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 8c66078ce..725855d7b 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -18,7 +18,7 @@ - + diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 2c7fa7e47..7537ed984 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -22,7 +22,7 @@ - + diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index a1a1693c6..86a62ad33 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index f7d69cd70..465743ec8 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -24,7 +24,7 @@ - + diff --git a/tools/helper.psm1 b/tools/helper.psm1 index b8056de65..a3ee18a87 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -1,5 +1,5 @@ -$MinimalSDKVersion = '6.0.100' +$MinimalSDKVersion = '6.0.425' $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } From 11974f8fa45b6ad0c12d8871f165d482a4afefd6 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 3 Oct 2024 14:09:31 -0700 Subject: [PATCH 092/127] Merge the v2.3.6 changelog to GitHub (#4202) --- PSReadLine/Changes.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 2c4ce39f2..35c73813e 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -16,6 +16,18 @@ [2.4.0-beta0]: https://github.com/PowerShell/PSReadLine/compare/v2.3.4...v2.4.0-beta0 +### [2.3.6] - 2024-10-02 + +This is a servicing release that excludes SBOM files from the module. + +- Update the OneBranch pipeline to keep it compliant and remove SBOM files from module (#4201) +- Make sure the `CodeQL` result from release pipeline gets uploaded (#4082) +- Add 'ob_restore_phase' for every task before the signing task to work around the signing issue (#4046) +- Change the NuGet feed to use the governed PowerShell feed (#4044) +- Disable SBOM, signing, and codeQL for the publish job (#3986) + +[2.3.6]: https://github.com/PowerShell/PSReadLine/compare/v2.3.5...v2.3.6 + ### [2.3.5] - 2024-04-02 This is a servicing release that excludes test components from SBOM generation. From e87a265ef8d2c6c5498500deb155bf6258b34629 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 3 Oct 2024 16:30:01 -0700 Subject: [PATCH 093/127] Update the release pipeline to remove `AzFeed` from display name (#4204) --- .pipelines/PSReadLine-Official.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 1308f963c..4aee66af7 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -271,7 +271,7 @@ extends: - job: publish dependsOn: validation - displayName: Publish to AzFeed and PSGallery + displayName: Publish to PSGallery variables: - name: ob_outputDirectory value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' From 9e946af05058322041e6bce08ae269b3ba75acf0 Mon Sep 17 00:00:00 2001 From: Sean Wheeler Date: Fri, 22 Nov 2024 11:59:59 -0600 Subject: [PATCH 094/127] Update `HelpInfoUri` for 7.5 (#4284) --- PSReadLine/PSReadLine.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 560df39b5..a9f201c00 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -15,5 +15,5 @@ AliasesToExport = @() FunctionsToExport = 'PSConsoleHostReadLine' CmdletsToExport = 'Get-PSReadLineKeyHandler','Set-PSReadLineKeyHandler','Remove-PSReadLineKeyHandler', 'Get-PSReadLineOption','Set-PSReadLineOption' -HelpInfoURI = 'https://aka.ms/powershell72-help' +HelpInfoURI = 'https://aka.ms/powershell75-help' } From 1ea00df7d2f13c36e1e3fb821e5aa646ab588684 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 3 Feb 2025 17:36:27 -0800 Subject: [PATCH 095/127] Handle buffer changes made by an event handler (#4442) --- PSReadLine/PublicAPI.cs | 1 + PSReadLine/ReadLine.cs | 50 +++++++++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 3e9696686..e4e311e80 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -92,6 +92,7 @@ public static void Insert(char c) /// String to insert public static void Insert(string s) { + s = s.Replace("\r\n", "\n"); _singleton.SaveEditItem(EditItemInsertString.Create(s, _singleton._current)); // Use Append if possible because Insert at end makes StringBuilder quite slow. diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 951349c51..02a0455b0 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -203,6 +203,7 @@ internal static PSKeyInfo ReadKey() // If we timed out, check for event subscribers (which is just // a hint that there might be an event waiting to be processed.) var eventSubscribers = _singleton._engineIntrinsics?.Events.Subscribers; + int bufferLen = _singleton._buffer.Length; if (eventSubscribers?.Count > 0) { bool runPipelineForEventProcessing = false; @@ -211,16 +212,20 @@ internal static PSKeyInfo ReadKey() if (string.Equals(sub.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) { // If the buffer is not empty, let's not consider we are idle because the user is in the middle of typing something. - if (_singleton._buffer.Length > 0) + if (bufferLen > 0) { continue; } - // There is an OnIdle event subscriber and we are idle because we timed out and the buffer is empty. - // Normally PowerShell generates this event, but PowerShell assumes the engine is not idle because - // it called PSConsoleHostReadLine which isn't returning. So we generate the event instead. + // There is an 'OnIdle' event subscriber and we are idle because we timed out and the buffer is empty. + // Normally PowerShell generates this event, but now PowerShell assumes the engine is not idle because + // it called 'PSConsoleHostReadLine' which isn't returning. So we generate the event instead. runPipelineForEventProcessing = true; - _singleton._engineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, null, null, null); + _singleton._engineIntrinsics.Events.GenerateEvent( + PSEngineEvent.OnIdle, + sender: null, + args: null, + extraData: null); // Break out so we don't genreate more than one 'OnIdle' event for a timeout. break; @@ -239,15 +244,36 @@ internal static PSKeyInfo ReadKey() ps.AddScript("[System.Diagnostics.DebuggerHidden()]param() 0", useLocalScope: true); } - // To detect output during possible event processing, see if the cursor moved - // and rerender if so. - var console = _singleton._console; - var y = console.CursorTop; + // To detect output during possible event processing, see if the cursor moved and rerender if so. + int cursorTop = _singleton._console.CursorTop; + + // Start the pipeline to process events. ps.Invoke(); - if (y != console.CursorTop) + + // Check if any event handler writes console output to the best of our effort, and adjust the initial coordinates in that case. + // + // I say "to the best of our effort" because the delegate handler for an event will mostly run on a background thread, and thus + // there is no guarantee about when the delegate would finish. So in an extreme case, there could be race conditions in console + // read/write: we are reading 'CursorTop' while the delegate is writing console output on a different thread. + // There is no much we can do about that extreme case. However, our focus here is the 'OnIdle' event, and its handler is usually + // a script block, which will run within the 'ps.Invoke()' call above. + // + // We detect new console output by checking if cursor top changed, but handle a very special case: an event handler changed our + // buffer, by calling 'Insert' for example. + // I know only checking on buffer length change doesn't cover the case where buffer changed but the length is the same. However, + // we mainly want to cover buffer changes made by an 'OnIdle' event handler, and we trigger 'OnIdle' event only if the buffer is + // empty. So, this check is efficient and good enough for that main scenario. + // When our buffer was changed by an event handler, we assume that was all the event handler did and there was no direct console + // output. So, we adjust the initial coordinates only if cursor top changed but there was no buffer change. + int newCursorTop = _singleton._console.CursorTop; + int newBufferLen = _singleton._buffer.Length; + if (cursorTop != newCursorTop && bufferLen == newBufferLen) { - _singleton._initialY = console.CursorTop; - _singleton.Render(); + _singleton._initialY = newCursorTop; + if (bufferLen > 0) + { + _singleton.Render(); + } } } } From 455eebeeb8a0d0977bc7eb22d59952f615a9883f Mon Sep 17 00:00:00 2001 From: Fabrice Sanga <69244030+sangafabrice@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:40:17 +0100 Subject: [PATCH 096/127] Update documentation about the building of PSReadLine (#4286) --- .github/CONTRIBUTING.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fc3482eb7..3badafcdf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -76,10 +76,10 @@ The build script `build.ps1` can be used to bootstrap, build and test the projec * Bootstrap: `./build.ps1 -Bootstrap` * Build: * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework netcoreapp2.1` + * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework net6.0` * Test: * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Test -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework netcoreapp2.1` + * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework net6.0` After build, the produced artifacts can be found at `/bin/Debug`. diff --git a/README.md b/README.md index 9d5805331..24d962a9b 100644 --- a/README.md +++ b/README.md @@ -234,10 +234,10 @@ The build script `build.ps1` can be used to bootstrap, build and test the projec * Bootstrap: `./build.ps1 -Bootstrap` * Build: * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework netcoreapp2.1` + * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework net6.0` * Test: * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Test -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework netcoreapp2.1` + * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework net6.0` After build, the produced artifacts can be found at `/bin/Debug`. In order to isolate your imported module to the one locally built, be sure to run From a272810fc44d3f752b570dad79cbb82487ac9abd Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 4 Feb 2025 14:02:19 -0800 Subject: [PATCH 097/127] Avoid querying for cursor position when it's not necessary (#4448) --- PSReadLine/Render.cs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index d08b65971..fe688481a 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -122,6 +122,14 @@ public void UpdateConsoleInfo(IConsole console) cursorLeft = console.CursorLeft; cursorTop = console.CursorTop; } + + public void UpdateConsoleInfo(int bWidth, int bHeight, int cLeft, int cTop) + { + bufferWidth = bWidth; + bufferHeight = bHeight; + cursorLeft = cLeft; + cursorTop = cTop; + } } internal readonly struct RenderDataOffset @@ -212,9 +220,9 @@ private void RenderWithPredictionQueryPaused() private void Render() { - // If there are a bunch of keys queued up, skip rendering if we've rendered - // recently. - if (_queuedKeys.Count > 10 && (_lastRenderTime.ElapsedMilliseconds < 50)) + // If there are a bunch of keys queued up, skip rendering if we've rendered very recently. + long elapsedMs = _lastRenderTime.ElapsedMilliseconds; + if (_queuedKeys.Count > 10 && elapsedMs < 50) { // We won't render, but most likely the tokens will be different, so make // sure we don't use old tokens, also allow garbage to get collected. @@ -225,6 +233,20 @@ private void Render() return; } + // If we've rendered very recently, skip the terminal window resizing check as it's unlikely + // to happen in such a short time interval. + // We try to avoid unnecessary resizing check because it requires getting the cursor position + // which would force a network round trip in an environment where front-end xtermjs talking to + // a server-side PTY via websocket. Without querying for cursor position, content written on + // the server side could be buffered, which is much more performant. + // See the following 2 GitHub issues for more context: + // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070 + // - https://github.com/PowerShell/PowerShell/issues/24696 + if (elapsedMs < 50) + { + _handlePotentialResizing = false; + } + ForceRender(); } @@ -928,7 +950,7 @@ void UpdateColorsIfNecessary(string newColor) _console.SetCursorPosition(point.X, point.Y); _console.CursorVisible = true; - _previousRender.UpdateConsoleInfo(_console); + _previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y); _previousRender.initialY = _initialY; // TODO: set WindowTop if necessary From 49604e89ec26c0d643f37f094704c27c8df3db92 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 28 Feb 2025 11:09:08 -0800 Subject: [PATCH 098/127] Update PSReadLine build to target `netstandard2.0` (#4584) By retargeting PSReadLine to `netstandard2.0`, we can simplify the build a lot and produce the same assembly no matter building on Windows or non-Windows. - Update PSReadLine build to target `netstandard2.0`, including the CI and release pipeline YAML files. - Update the test to target `net472` and `net6.0`, so that we can run tests with both .NET Framework and .NET - Rename `Microsoft.PowerShell.PSReadLine2.dll` to `Microsoft.PowerShell.PSReadLine.dll` - Update and clean up the `README.md` with the up-to-date information --- .github/CONTRIBUTING.md | 18 +--- .pipelines/PSReadLine-Official.yml | 4 +- MockPSConsole/MockPSConsole.csproj | 9 +- PSReadLine.build.ps1 | 75 ++++++---------- PSReadLine/OnImportAndRemove.cs | 2 +- PSReadLine/PSReadLine.csproj | 17 ++-- PSReadLine/PSReadLine.psd1 | 2 +- Polyfill/Polyfill.csproj | 8 +- README.md | 139 +++++++---------------------- appveyor.yml | 4 +- build.ps1 | 45 ++++++---- test/PSReadLine.Tests.csproj | 20 ++--- 12 files changed, 115 insertions(+), 228 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3badafcdf..52fb53194 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -65,23 +65,7 @@ Additional references: ### Bootstrap, Build and Test -To build `PSReadLine` on Windows, Linux, or macOS, -you must have the following installed: - -* .NET Core SDK 2.1.802 or [a newer version](https://www.microsoft.com/net/download) -* The PowerShell modules `InvokeBuild` and `platyPS` - -The build script `build.ps1` can be used to bootstrap, build and test the project. - -* Bootstrap: `./build.ps1 -Bootstrap` -* Build: - * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework net6.0` -* Test: - * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Test -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework net6.0` - -After build, the produced artifacts can be found at `/bin/Debug`. +See the [Building](../README.md#building) section in README for details. ### Submitting Pull Request diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 4aee66af7..01d18522c 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -114,7 +114,7 @@ extends: - pwsh: | Write-Host "PS Version: $($($PSVersionTable.PSVersion))" Set-Location -Path '$(repoRoot)' - .\build.ps1 -Configuration Release -Framework net462 + .\build.ps1 -Configuration Release displayName: Build env: # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. @@ -166,7 +166,7 @@ extends: TargetFolder: $(ob_outputDirectory) - pwsh: | - $versionInfo = Get-Item "$(signSrcPath)\Microsoft.PowerShell.PSReadLine2.dll" | ForEach-Object VersionInfo + $versionInfo = Get-Item "$(signSrcPath)\Microsoft.PowerShell.PSReadLine.dll" | ForEach-Object VersionInfo $moduleVersion = $versionInfo.ProductVersion.Split('+')[0] $vstsCommandString = "vso[task.setvariable variable=ob_sdl_sbom_packageversion]${moduleVersion}" diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 725855d7b..cebd3941d 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -4,21 +4,18 @@ Exe MockPSConsole MockPSConsole - net462;net6.0 + net472;net6.0 512 Program.manifest true - - - - + - + diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index 49a397c21..e245dfae3 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -19,8 +19,8 @@ param( [ValidateSet("Debug", "Release")] [string]$Configuration = (property Configuration Release), - [ValidateSet("net462", "net6.0")] - [string]$Framework, + [ValidateSet("net472", "net6.0")] + [string]$TestFramework, [switch]$CheckHelpContent ) @@ -30,71 +30,58 @@ Import-Module "$PSScriptRoot/tools/helper.psm1" # Final bits to release go here $targetDir = "bin/$Configuration/PSReadLine" -if (-not $Framework) -{ - $Framework = if ($PSVersionTable.PSEdition -eq "Core") { "net6.0" } else { "net462" } +if (-not $TestFramework) { + $TestFramework = $IsWindows ? "net472" : "net6.0" } -Write-Verbose "Building for '$Framework'" -Verbose - function ConvertTo-CRLF([string] $text) { $text.Replace("`r`n","`n").Replace("`n","`r`n") } $polyFillerParams = @{ Inputs = { Get-ChildItem Polyfill/*.cs, Polyfill/Polyfill.csproj } - Outputs = "Polyfill/bin/$Configuration/$Framework/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" + Outputs = "Polyfill/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" } $binaryModuleParams = @{ Inputs = { Get-ChildItem PSReadLine/*.cs, PSReadLine/PSReadLine.csproj, PSReadLine/PSReadLineResources.resx, Polyfill/*.cs, Polyfill/Polyfill.csproj } - Outputs = "PSReadLine/bin/$Configuration/$Framework/Microsoft.PowerShell.PSReadLine2.dll" + Outputs = "PSReadLine/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.dll" } $xUnitTestParams = @{ Inputs = { Get-ChildItem test/*.cs, test/*.json, test/PSReadLine.Tests.csproj } - Outputs = "test/bin/$Configuration/$Framework/PSReadLine.Tests.dll" -} - -$mockPSConsoleParams = @{ - Inputs = { Get-ChildItem MockPSConsole/*.cs, MockPSConsole/Program.manifest, MockPSConsole/MockPSConsole.csproj } - Outputs = "MockPSConsole/bin/$Configuration/$Framework/MockPSConsole.dll" + Outputs = "test/bin/$Configuration/$TestFramework/PSReadLine.Tests.dll" } <# Synopsis: Build the Polyfiller assembly #> -task BuildPolyfiller @polyFillerParams -If ($Framework -eq "net462") { - ## Build both "net462" and "net6.0" - exec { dotnet publish -f "net462" -c $Configuration Polyfill } - exec { dotnet publish -f "net6.0" -c $Configuration Polyfill } +task BuildPolyfiller @polyFillerParams { + exec { dotnet publish -c $Configuration -f 'netstandard2.0' Polyfill } + exec { dotnet publish -c $Configuration -f 'net6.0' Polyfill } } <# Synopsis: Build main binary module #> task BuildMainModule @binaryModuleParams { - exec { dotnet publish -f $Framework -c $Configuration PSReadLine } + exec { dotnet publish -c $Configuration PSReadLine\PSReadLine.csproj } } <# Synopsis: Build xUnit tests #> task BuildXUnitTests @xUnitTestParams { - exec { dotnet publish -f $Framework -c $Configuration test } -} - -<# -Synopsis: Build the mock powershell console. -#> -task BuildMockPSConsole @mockPSConsoleParams { - exec { dotnet publish -f $Framework -c $Configuration MockPSConsole } + exec { dotnet publish -f $TestFramework -c $Configuration test } } <# Synopsis: Run the unit tests #> -task RunTests BuildMainModule, BuildXUnitTests, { Start-TestRun -Configuration $Configuration -Framework $Framework } +task RunTests BuildMainModule, BuildXUnitTests, { + Write-Verbose "Run tests targeting '$TestFramework' ..." + Start-TestRun -Configuration $Configuration -Framework $TestFramework +} <# Synopsis: Check if the help content is in sync. @@ -128,35 +115,27 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { Set-Content -Path (Join-Path $targetDir (Split-Path $file -Leaf)) -Value (ConvertTo-CRLF $content) -Force } - if ($Framework -eq "net462") { - if (-not (Test-Path "$targetDir/net462")) { - New-Item "$targetDir/net462" -ItemType Directory -Force > $null - } - if (-not (Test-Path "$targetDir/net6plus")) { - New-Item "$targetDir/net6plus" -ItemType Directory -Force > $null - } - - Copy-Item "Polyfill/bin/$Configuration/net462/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net462" -Force - Copy-Item "Polyfill/bin/$Configuration/net6.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net6plus" -Force + if (-not (Test-Path "$targetDir/netstd")) { + New-Item "$targetDir/netstd" -ItemType Directory -Force > $null + } + if (-not (Test-Path "$targetDir/net6plus")) { + New-Item "$targetDir/net6plus" -ItemType Directory -Force > $null } - $binPath = "PSReadLine/bin/$Configuration/$Framework/publish" - Copy-Item $binPath/Microsoft.PowerShell.PSReadLine2.dll $targetDir + 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" + Copy-Item $binPath/Microsoft.PowerShell.PSReadLine.dll $targetDir Copy-Item $binPath/Microsoft.PowerShell.Pager.dll $targetDir if ($Configuration -eq 'Debug') { Copy-Item $binPath/*.pdb $targetDir } - if (Test-Path $binPath/System.Runtime.InteropServices.RuntimeInformation.dll) { - Copy-Item $binPath/System.Runtime.InteropServices.RuntimeInformation.dll $targetDir - } else { - Write-Warning "Build using $Framework is not sufficient to be downlevel compatible" - } - # Copy module manifest, but fix the version to match what we've specified in the binary module. $moduleManifestContent = ConvertTo-CRLF (Get-Content -Path 'PSReadLine/PSReadLine.psd1' -Raw) - $versionInfo = (Get-ChildItem -Path $targetDir/Microsoft.PowerShell.PSReadLine2.dll).VersionInfo + $versionInfo = (Get-ChildItem -Path $targetDir/Microsoft.PowerShell.PSReadLine.dll).VersionInfo $version = $versionInfo.FileVersion $semVer = $versionInfo.ProductVersion diff --git a/PSReadLine/OnImportAndRemove.cs b/PSReadLine/OnImportAndRemove.cs index 10f7000fb..70208420a 100644 --- a/PSReadLine/OnImportAndRemove.cs +++ b/PSReadLine/OnImportAndRemove.cs @@ -28,7 +28,7 @@ private static Assembly ResolveAssembly(object sender, ResolveEventArgs args) } string root = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location); - string subd = (Environment.Version.Major >= 6) ? "net6plus" : "net462"; + string subd = (Environment.Version.Major >= 6) ? "net6plus" : "netstd"; string path = Path.Combine(root, subd, "Microsoft.PowerShell.PSReadLine.Polyfiller.dll"); return Assembly.LoadFrom(path); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 7537ed984..72fc8fe88 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -3,30 +3,23 @@ Library Microsoft.PowerShell.PSReadLine - Microsoft.PowerShell.PSReadLine2 + Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 2.4.0.0 2.4.0 2.4.0-beta0 true - net462;net6.0 + netstandard2.0 true + false 9.0 - + - - - - - - - - - + diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index a9f201c00..d32db386a 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSReadLine.psm1' -NestedModules = @("Microsoft.PowerShell.PSReadLine2.dll") +NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") ModuleVersion = '2.4.0' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index 86a62ad33..2cdccfbba 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -3,19 +3,19 @@ Microsoft.PowerShell.PSReadLine.Polyfiller 1.0.0.0 - net462;net6.0 + netstandard2.0;net6.0 true - + - + - + $(DefineConstants);LEGACY diff --git a/README.md b/README.md index 24d962a9b..d1ab3d11c 100644 --- a/README.md +++ b/README.md @@ -36,115 +36,34 @@ Some good resources about `PSReadLine`: - Ed Wilson (Scripting Guy) wrote a [series](https://devblogs.microsoft.com/scripting/tag/psreadline/) (2014-2015) on `PSReadLine`. - John Savill has a [video](https://www.youtube.com/watch?v=Q11sSltuTE0) (2021) covering installation, configuration, and tailoring `PSReadLine` to your liking. -## Installation +## Installation and Upgrading -There are multiple ways to install `PSReadLine`. +You will need the `1.6.0` or a higher version of [`PowerShellGet`](https://learn.microsoft.com/en-us/powershell/gallery/powershellget/install-powershellget) to install or upgrade to the latest prerelease version of `PSReadLine`. -### Install from PowerShellGallery (preferred) - -You will need the `1.6.0` or a higher version of [`PowerShellGet`](https://learn.microsoft.com/en-us/powershell/gallery/powershellget/install-powershellget) to install the latest prerelease version of `PSReadLine`. - -Windows PowerShell 5.1 ships an older version of `PowerShellGet` which doesn't support installing prerelease modules, -so Windows PowerShell users need to install the latest `PowerShellGet` (if not yet) by running the following commands from an elevated Windows PowerShell session: +PowerShell 6+ already has a higher version of `PowerShellGet` built-in. +However, Windows PowerShell 5.1 ships an older version of `PowerShellGet` which doesn't support installing prerelease modules. +So, Windows PowerShell users need to install the latest `PowerShellGet` (if not yet) by running the following commands from an elevated Windows PowerShell session: ```powershell -Install-Module -Name PowerShellGet -Force -Exit +Install-Module -Name PowerShellGet -Force; exit ``` -After installing `PowerShellGet`, you can get the latest prerelease version of `PSReadLine` by running +After installing `PowerShellGet`, you install or upgrade to the latest prerelease version of `PSReadLine` by running ```powershell -Install-Module PSReadLine -AllowPrerelease -Force +Install-Module PSReadLine -Repository PSGallery -Scope CurrentUser -AllowPrerelease -Force ``` If you only want to get the latest stable version, run: ```powershell -Install-Module PSReadLine +Install-Module PSReadLine -Repository PSGallery -Scope CurrentUser -Force ``` >[!NOTE] Prerelease versions will have newer features and bug fixes, but may also introduce new issues. -If you are using Windows PowerShell on Windows 10 or using PowerShell 6+, `PSReadLine` is already installed. -Windows PowerShell on the latest Windows 10 has version `2.0.0-beta2` of `PSReadLine`. -PowerShell 6+ versions have the newer prerelease versions of `PSReadLine`. - -### Install from GitHub (deprecated) - -With the preview release of PowerShellGet for PowerShell V3/V4, downloads from GitHub are deprecated. -We don't intend to update releases on GitHub, and may remove the release entirely from GitHub at some point. - -### Post Installation - -If you are using Windows PowerShell V5 or V5.1 versions, or using PowerShell 6+ versions, you are good to go and can skip this section. - -Otherwise, you need to edit your profile to import the module. -There are two profile files commonly used and the instructions are slightly different for each. -The file `C:\Users\[User]\Documents\WindowsPowerShell\profile.ps1` is used for all hosts (e.g. the `ISE` and `powershell.exe`). -If you already have this file, then you should add the following: - -```powershell -if ($host.Name -eq 'ConsoleHost') -{ - Import-Module PSReadLine -} -``` - -Alternatively, the file `C:\Users\[User]\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1` is for `powershell.exe` only. Using this file, you can simply add: - -```powershell -Import-Module PSReadLine -``` - -In either case, you can create the appropriate file if you don't already have one. - -## Upgrading - -When running one of the suggested commands below, be sure to exit all instances of `powershell.exe`, `pwsh.exe` or `pwsh`, -including those opened in `VSCode` terminals. - -Then, to make sure `PSReadLine` isn't loaded: -- _if you are on Windows_, run the suggested command below from `cmd.exe`, `powershell_ise.exe`, or via the `Win+R` shortcut; -- _if you are on Linux/macOS_, run the suggested command below from the default terminal (like `bash` or `zsh`). - - -If you are using the version of `PSReadLine` that ships with Windows PowerShell, -you need to run: `powershell -noprofile -command "Install-Module PSReadLine -Force -SkipPublisherCheck -AllowPrerelease"`. -Note: you will need to make sure [PowershellGet is updated](https://github.com/PowerShell/PSReadLine#install-from-powershellgallery-preferred) before running this command. - -If you are using the version of `PSReadLine` that ships with PowerShell 6+ versions, -you need to run: ` -noprofile -command "Install-Module PSReadLine -Force -SkipPublisherCheck -AllowPrerelease"`. - -If you've installed `PSReadLine` yourself from the PowerShell Gallery, -you can simply run: `powershell -noprofile -command "Update-Module PSReadLine -AllowPrerelease"` or -` -noprofile -command "Update-Module PSReadLine -AllowPrerelease"`, -depending on the version of PowerShell you are using. - -If you get an error like: - -```none -Remove-Item : Cannot remove item -C:\Users\{yourName}\Documents\WindowsPowerShell\Modules\PSReadLine\Microsoft.PowerShell.PSReadLine.dll: Access to the path -'C:\Users\{yourName}\Documents\WindowsPowerShell\Modules\PSReadLine\Microsoft.PowerShell.PSReadLine.dll' is denied. -``` - -or a warning like: - -```none -WARNING: The version '2.0.0' of module 'PSReadLine' is currently in use. Retry the operation after closing the applications. -``` - -Then you didn't kill all the processes that loaded `PSReadLine`. - ## Usage -To start using, just import the module: - -```powershell -Import-Module PSReadLine -``` - To use Emacs key bindings, you can use: ```powershell @@ -157,16 +76,19 @@ To view the current key bindings: Get-PSReadLineKeyHandler ``` -There are many configuration options, see the options to `Set-PSReadLineOption`. `PSReadLine` has help for it's cmdlets as well as an `about_PSReadLine` topic - see those topics for more detailed help. +There are many configuration options, see the options to `Set-PSReadLineOption`. +`PSReadLine` has help for its cmdlets as well as an `about_PSReadLine` topic - see those topics for more detailed help. -To set your own custom keybindings, use the cmdlet `Set-PSReadLineKeyHandler`. For example, for a better history experience, try: +To set your own custom keybindings, use the cmdlet `Set-PSReadLineKeyHandler`. +For example, for a better history experience, try: ```powershell Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward ``` -With these bindings, up arrow/down arrow will work like PowerShell/cmd if the current command line is blank. If you've entered some text though, it will search the history for commands that start with the currently entered text. +With these bindings, up arrow/down arrow will work like PowerShell/cmd if the current command line is blank. +If you've entered some text though, it will search the history for commands that start with the currently entered text. To enable bash style completion without using Emacs mode, you can use: @@ -200,7 +122,10 @@ Set-PSReadLineKeyHandler -Chord '"',"'" ` } ``` -In this example, when you type a single quote or double quote, there are two things that can happen. If the character following the cursor is not the quote typed, then a matched pair of quotes is inserted and the cursor is placed inside the the matched quotes. If the character following the cursor is the quote typed, the cursor is simply moved past the quote without inserting anything. If you use Resharper or another smart editor, this experience will be familiar. +In this example, when you type a single quote or double quote, there are two things that can happen. +If the character following the cursor is not the quote typed, then a matched pair of quotes is inserted and the cursor is placed inside the the matched quotes. +If the character following the cursor is the quote typed, the cursor is simply moved past the quote without inserting anything. +If you use `VSCode`, `Resharper`, or another smart editor, this experience will be familiar. Note that with the handler written this way, it correctly handles Undo - both quotes will be undone with one undo. @@ -211,10 +136,10 @@ See the public methods of `[Microsoft.PowerShell.PSConsoleReadLine]` to see what If you want to change the command line in some unimplmented way in your custom key binding, you can use the methods: ```powershell - [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState - [Microsoft.PowerShell.PSConsoleReadLine]::Insert - [Microsoft.PowerShell.PSConsoleReadLine]::Replace - [Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition +[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState +[Microsoft.PowerShell.PSConsoleReadLine]::Insert +[Microsoft.PowerShell.PSConsoleReadLine]::Replace +[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition ``` ## Developing and Contributing @@ -226,26 +151,23 @@ Please see the [Contribution Guide][] for how to develop and contribute. To build `PSReadLine` on Windows, Linux, or macOS, you must have the following installed: -* .NET Core SDK 2.1.802 or [a newer version](https://www.microsoft.com/net/download) +* .NET 6.0 or [a newer version](https://www.microsoft.com/net/download) * The PowerShell modules `InvokeBuild` and `platyPS` The build script `build.ps1` can be used to bootstrap, build and test the project. * Bootstrap: `./build.ps1 -Bootstrap` -* Build: - * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Configuration Debug -Framework net6.0` +* Build: `./build.ps1 -Configuration Debug` * Test: - * Targeting .NET 4.6.2 (Windows only): `./build.ps1 -Test -Configuration Debug -Framework net462` - * Targeting .NET Core: `./build.ps1 -Test -Configuration Debug -Framework net6.0` + * Targeting .NET 4.7.2 (Windows only): `./build.ps1 -Test -Configuration Debug -Framework net472` + * Targeting .NET 6.0: `./build.ps1 -Test -Configuration Debug -Framework net6.0` After build, the produced artifacts can be found at `/bin/Debug`. + In order to isolate your imported module to the one locally built, be sure to run `pwsh -NonInteractive -NoProfile` to not automatically load the default PSReadLine module installed. Then, load the locally built PSReadLine module by `Import-Module /bin/Debug/PSReadLine/PSReadLine.psd1`. -[Contribution Guide]: https://github.com/PowerShell/PSReadLine/blob/master/.github/CONTRIBUTING.md - ## Change Log The change log is available [here](https://github.com/PowerShell/PSReadLine/blob/master/PSReadLine/Changes.txt). @@ -254,8 +176,6 @@ The change log is available [here](https://github.com/PowerShell/PSReadLine/blob PSReadLine is licensed under the [2-Clause BSD License][]. -[2-Clause BSD License]: https://github.com/PowerShell/PSReadLine/blob/master/License.txt - ## Code of Conduct Please see our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before participating in this project. @@ -263,3 +183,6 @@ Please see our [Code of Conduct](.github/CODE_OF_CONDUCT.md) before participatin ## Security Policy For any security issues, please see our [Security Policy](.github/SECURITY.md). + +[Contribution Guide]: https://github.com/PowerShell/PSReadLine/blob/master/.github/CONTRIBUTING.md +[2-Clause BSD License]: https://github.com/PowerShell/PSReadLine/blob/master/License.txt diff --git a/appveyor.yml b/appveyor.yml index 0caf57baa..e989afe08 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,10 +17,10 @@ install: build_script: - pwsh: | - ./build.ps1 -Configuration Release -Framework net462 + ./build.ps1 -Configuration Release test_script: - - pwsh: ./build.ps1 -Test -Configuration Release -Framework net462 + - pwsh: ./build.ps1 -Test -Configuration Release -Framework net472 artifacts: - path: .\bin\Release\PSReadLine.zip diff --git a/build.ps1 b/build.ps1 index bb41830ba..481f2a271 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,3 +1,5 @@ +#Requires -Version 7.4 + <# .SYNOPSIS A script that provides simple entry points for bootstrapping, building and testing. @@ -7,14 +9,14 @@ PS > .\build.ps1 -Bootstrap Check and install prerequisites for the build. .EXAMPLE - PS > .\build.ps1 -Configuration Release -Framework net462 - Build the main module with 'Release' configuration and targeting 'net462'. + PS > .\build.ps1 -Configuration Release + Build the main module with 'Release' configuration targeting 'netstandard2.0'. .EXAMPLE PS > .\build.ps1 - Build the main module with the default configuration (Debug) and the default target framework (determined by the current session). + Build the main module with the default configuration (Debug) targeting 'netstandard2.0'. .EXAMPLE PS > .\build.ps1 -Test - Run xUnit tests with the default configuration (Debug) and the default target framework (determined by the current session). + Run xUnit tests with the default configuration (Debug) and the default target framework (net472 on Windows or net6.0 otherwise). .PARAMETER Clean Clean the local repo, but keep untracked files. .PARAMETER Bootstrap @@ -24,23 +26,35 @@ .PARAMETER Configuration The configuration setting for the build. The default value is 'Debug'. .PARAMETER Framework - The target framework for the build. - When not specified, the target framework is determined by the current PowerShell session: - - If the current session is PowerShell Core, then use 'netcoreapp3.1' as the default target framework. - - If the current session is Windows PowerShell, then use 'net462' as the default target framework. + The target framework when testing: + - net472: run tests with .NET Framework + - net6.0: run tests with .NET 6.0 + When not specified, the target framework is determined by the current OS platform: + - use 'net472' on Windows + - use 'net6.0' on Unix platforms #> -[CmdletBinding()] +[CmdletBinding(DefaultParameterSetName = 'default')] param( + [Parameter(ParameterSetName = 'cleanup')] [switch] $Clean, + + [Parameter(ParameterSetName = 'bootstrap')] [switch] $Bootstrap, + + [Parameter(ParameterSetName = 'test')] [switch] $Test, + + [Parameter(ParameterSetName = 'test')] [switch] $CheckHelpContent, - [ValidateSet("Debug", "Release")] - [string] $Configuration = "Debug", + [Parameter(ParameterSetName = 'test')] + [ValidateSet("net472", "net6.0")] + [string] $Framework, - [ValidateSet("net462", "net6.0")] - [string] $Framework + [Parameter(ParameterSetName = 'default')] + [Parameter(ParameterSetName = 'test')] + [ValidateSet("Debug", "Release")] + [string] $Configuration = "Debug" ) # Clean step @@ -48,10 +62,11 @@ if ($Clean) { try { Push-Location $PSScriptRoot git clean -fdX - return } finally { Pop-Location } + + return } Import-Module "$PSScriptRoot/tools/helper.psm1" @@ -78,7 +93,7 @@ if (-not (Get-Module -Name InvokeBuild -ListAvailable)) { $buildTask = if ($Test) { "RunTests" } else { "ZipRelease" } $arguments = @{ Task = $buildTask; Configuration = $Configuration } -if ($Framework) { $arguments.Add("Framework", $Framework) } +if ($Framework) { $arguments.Add("TestFramework", $Framework) } if ($CheckHelpContent) { $arguments.Add("CheckHelpContent", $true) } Invoke-Build @arguments diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index 465743ec8..34598cbe9 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -5,7 +5,7 @@ library UnitTestPSReadLine PSReadLine.Tests - net462;net6.0 + net472;net6.0 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} False @@ -14,28 +14,24 @@ 9.0 - - - - - + - + - - - + + + all runtime; build; native; contentfiles; analyzers - - + + From 9a9b4dfff4b22284d246285beda69ec5a174959b Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 28 Feb 2025 13:36:15 -0800 Subject: [PATCH 099/127] Prepare for the v2.4.1-beta1 release of PSReadLine --- PSReadLine/Changes.txt | 28 ++++++++++++++++++++++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 4 +--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 35c73813e..1ab063e69 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,31 @@ +### [2.4.1-beta1] - 2025-02-28 + +#### Code Changes + +- Avoid querying for cursor position when it's not necessary (#4448) +- Handle buffer changes made by an event handler (#4442) +- Update `SelectCommandArgument` to properly handle POSIX style options for CLI commands (#4016) + +#### Build Changes + +- Update PSReadLine build to target `netstandard2.0` (#4584) +- Update documentation about the building of PSReadLine (#4286) (Thanks @sangafabrice!) +- Update `HelpInfoUri` for 7.5 (#4284) +- Update the release pipeline to remove `AzFeed` from display name (#4204) +- Update the OneBranch pipeline to keep it compliant and remove SBOM files from module (#4201) +- Make sure the `CodeQL` result from release pipeline gets uploaded (#4082) +- Add 'ob_restore_phase' for every task before the signing task to work around the signing issue (#4046) +- Change the NuGet feed to use the governed PowerShell feed (#4044) +- Update "Code of Conduct" and "Security Policy" (#4037) +- Disable SBOM, signing, and codeQL for the publish job (#3986) +- Update triage messages to use the latest stable version (#3985) +- Fix the release stage and update the changelog for v2.3.5 servicing release (#3984) +- Add the release stage to the pipeline and exclude test folders from Component Governance (#3982) +- Change back to 'external_distribution' for nupkg signing (#3977) +- Migrate PSReadLine release build pipeline to OneBranch (#3975) + +[2.4.1-beta1]: https://github.com/PowerShell/PSReadLine/compare/v2.4.0-beta0...v2.4.1-beta1 + ### [2.4.0-beta0] - 2024-03-01 - Fix the null-reference exception when running `Debug-Job` on a thread job (#3957) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 72fc8fe88..b9c3b2378 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.0.0 - 2.4.0 - 2.4.0-beta0 + 2.4.1.0 + 2.4.1 + 2.4.1-beta1 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index d32db386a..2f72646e8 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,15 +1,13 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.0' +ModuleVersion = '2.4.1' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' Copyright = '(c) Microsoft Corporation. All rights reserved.' Description = 'Great command line editing in the PowerShell console host' PowerShellVersion = '5.1' -DotNetFrameworkVersion = '4.6.2' -CLRVersion = '4.0.0' FormatsToProcess = 'PSReadLine.format.ps1xml' AliasesToExport = @() FunctionsToExport = 'PSConsoleHostReadLine' From 4a2bf98ac4568cbd5f23ecd877b792b2de015356 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 14 Apr 2025 16:57:12 -0700 Subject: [PATCH 100/127] Use CFS for installing module and deploy box for module publish (#4700) 1. Add pipeline parameter to control whether to release 2. Handle bootstrapping explicitly 3. Use 'server' instead of 'agentless' for manual job --- .pipelines/PSReadLine-Official.yml | 54 ++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 01d18522c..3aa8748b4 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -12,6 +12,11 @@ name: PSReadLine-ModuleBuild-$(Build.BuildId) trigger: none pr: none +parameters: + - name: Release + type: boolean + default: true # Set false to skip release stage + variables: DOTNET_CLI_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1 @@ -28,9 +33,12 @@ resources: extends: template: v2/OneBranch.Official.CrossPlat.yml@templates parameters: + release: + category: NonAzure featureFlags: WindowsHostVersion: Version: 2022 + Network: Netlock globalSdl: disableLegacyManifest: true cg: # Component Governance parameters. Ignore test components. @@ -93,11 +101,19 @@ extends: # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. ob_restore_phase: true + - task: UseDotNet@2 + displayName: Bootstrap - install .NET + env: + # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + ob_restore_phase: true + inputs: + packageType: sdk + - pwsh: | Write-Host "PS Version: $($PSVersionTable.PSVersion)" - Set-Location -Path '$(repoRoot)' - .\build.ps1 -Bootstrap - displayName: Bootstrap + Register-PSResourceRepository -Name CFS -Uri "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/PowerShellGalleryMirror/nuget/v3/index.json" -Trusted + Install-PSResource -Repository CFS -Name InvokeBuild -Version 5.12.1 -Verbose + displayName: Bootstrap - install InvokeBuild env: # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. ob_restore_phase: true @@ -184,9 +200,8 @@ extends: value: $(Build.SourcesDirectory)\PSReadLine - name: ob_sdl_tsa_configFile value: $(repoRoot)\.config\tsaoptions.json - # Disable because SBOM was already built in the previous job - name: ob_sdl_sbom_enabled - value: false + value: true - name: signOutPath value: $(repoRoot)\signed\PSReadLine - name: nugetPath @@ -254,12 +269,14 @@ extends: - stage: release dependsOn: buildstage displayName: Release PSReadLine + variables: + ob_release_environment: Production jobs: - job: validation displayName: Manual validation pool: - type: agentless + type: server timeoutInMinutes: 1440 steps: @@ -272,11 +289,16 @@ extends: - job: publish dependsOn: validation displayName: Publish to PSGallery + pool: + type: release + os: windows + templateContext: + inputs: + - input: pipelineArtifact + artifactName: drop_buildstage_nupkg variables: - name: ob_outputDirectory value: '$(Build.ArtifactStagingDirectory)/ONEBRANCH_ARTIFACT' - - name: nugetPath - value: $(Pipeline.Workspace)\NuGetPackage # Disable SBOM, signing, and codeQL for this job - name: ob_sdl_sbom_enabled value: false @@ -284,24 +306,20 @@ extends: value: false - name: ob_sdl_codeql_compiled_enabled value: false - pool: - type: windows steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download nupkg artifact' + - task: PowerShell@2 inputs: - targetPath: $(nugetPath) - artifact: drop_buildstage_nupkg - - - pwsh: | - Get-ChildItem $(nugetPath) -Recurse | Out-String -Width 120 -Stream + targetType: 'inline' + script: | + Get-ChildItem $(Pipeline.Workspace) -Recurse | Out-String -Width 120 -Stream displayName: Find signed Nupkg - task: NuGetCommand@2 + condition: ${{ parameters.Release }} displayName: Push PSReadLine module to PSGallery feed inputs: command: push - packagesToPush: $(nugetPath)\PSReadLine.*.nupkg + packagesToPush: $(Pipeline.Workspace)\PSReadLine.*.nupkg nuGetFeedType: external publishFeedCredentials: PowerShellGalleryFeed From b9269598407c6de15dc0ed7693184ce899792708 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 16 Apr 2025 10:09:46 -0700 Subject: [PATCH 101/127] Add a private field to indicate if PSReadLine is initialized and ready (#4706) --- PSReadLine/ReadLine.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 02a0455b0..677025aab 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -43,6 +43,8 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods #pragma warning restore CS0649 private bool _delayedOneTimeInitCompleted; + // This is used by AIShell to check if PSReadLine is initialized and ready to render. + private bool _readLineReady; private IPSConsoleReadLineMockableMethods _mockableMethods; private IConsole _console; @@ -400,6 +402,7 @@ public static string ReadLine( _singleton.Initialize(runspace, engineIntrinsics); } + _singleton._readLineReady = true; _singleton._cancelReadCancellationToken = cancellationToken; return _singleton.InputLoop(); } @@ -472,6 +475,8 @@ public static string ReadLine( } finally { + _singleton._readLineReady = false; + try { // If we are closing, restoring the old console settings isn't needed, From 69c6f032ccc105b1bcedfec40b8af4962aa6fcee Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 16 Apr 2025 11:01:11 -0700 Subject: [PATCH 102/127] Prepare for the v2.4.2-beta2 release of PSReadLine --- PSReadLine/Changes.txt | 7 +++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 1ab063e69..f3d277752 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,10 @@ +### [2.4.2-beta2] - 2025-04-16 + +- Add a private field to indicate if PSReadLine is initialized and ready (#4706) +- Use CFS for installing module and deploy box for module publish (#4700) + +[2.4.2-beta2]: https://github.com/PowerShell/PSReadLine/compare/v2.4.1-beta1...v2.4.2-beta2 + ### [2.4.1-beta1] - 2025-02-28 #### Code Changes diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index b9c3b2378..1c8090f84 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.1.0 - 2.4.1 - 2.4.1-beta1 + 2.4.2.0 + 2.4.2 + 2.4.2-beta2 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 2f72646e8..543d3f442 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.1' +ModuleVersion = '2.4.2' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From b1f8c8979f8588dc46cbfea18e41d41352b989a3 Mon Sep 17 00:00:00 2001 From: Maxime Labelle Date: Thu, 17 Apr 2025 20:08:30 +0200 Subject: [PATCH 103/127] Improve test reliability by making sure the PSReadLine one-time initialization is done (#4686) --- PSReadLine/PSReadLine.sln | 114 ++++++++++++++++++++------------------ test/UnitTestReadLine.cs | 8 +++ 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/PSReadLine/PSReadLine.sln b/PSReadLine/PSReadLine.sln index 3ce7ce89f..866d96120 100644 --- a/PSReadLine/PSReadLine.sln +++ b/PSReadLine/PSReadLine.sln @@ -1,53 +1,61 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyfill", "..\Polyfill\Polyfill.csproj", "{DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockPSConsole", "..\MockPSConsole\MockPSConsole.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine.Tests", "..\test\PSReadLine.Tests.csproj", "{8ED51D01-158C-4B29-824A-35B9B861E45A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - CodeCoverage|Any CPU = CodeCoverage|Any CPU - Debug|Any CPU = Debug|Any CPU - Linux|Any CPU = Linux|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.Build.0 = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {EA13C704-483F-4CE4-A3FB-8F79295F1071} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyfill", "..\Polyfill\Polyfill.csproj", "{DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockPSConsole", "..\MockPSConsole\MockPSConsole.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine.Tests", "..\test\PSReadLine.Tests.csproj", "{8ED51D01-158C-4B29-824A-35B9B861E45A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + CodeCoverage|Any CPU = CodeCoverage|Any CPU + Debug|Any CPU = Debug|Any CPU + Linux|Any CPU = Linux|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.Build.0 = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Linux|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Release|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA13C704-483F-4CE4-A3FB-8F79295F1071} + EndGlobalSection +EndGlobal diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index ad248096b..a6c1572ce 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -537,6 +537,7 @@ private void TestMustDing(string expectedResult, object[] items) private string _emptyLine; private TestConsole _console; private MockedMethods _mockedMethods; + private bool _oneTimeInitCompleted; private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); @@ -626,6 +627,13 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] } var colorOptions = new SetPSReadLineOption {Colors = colors}; PSConsoleReadLine.SetOptions(colorOptions); + + if (!_oneTimeInitCompleted) + { + typeof(PSConsoleReadLine).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(instance, new object[] { /* Runspace */ null, /* EngineIntrinsics */ null, }); + _oneTimeInitCompleted = true; + } } } From 7b840fdef05425ead41e6b906773abac3651e877 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Apr 2025 12:06:56 -0700 Subject: [PATCH 104/127] Fix line ending and cache some reflection operations (#4709) --- test/UnitTestReadLine.cs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index a6c1572ce..d09e2abc9 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -538,6 +538,8 @@ private void TestMustDing(string expectedResult, object[] items) private TestConsole _console; private MockedMethods _mockedMethods; private bool _oneTimeInitCompleted; + private object _psrlInstance; + private FieldInfo _psrlConsole, _psrlMockableMethods; private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); @@ -554,14 +556,17 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] _console = console ?? new TestConsole(_); _mockedMethods = new MockedMethods(); - var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine) - .GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); - typeof(PSConsoleReadLine) - .GetField("_mockableMethods", BindingFlags.Instance | BindingFlags.NonPublic) - .SetValue(instance, _mockedMethods); - typeof(PSConsoleReadLine) - .GetField("_console", BindingFlags.Instance | BindingFlags.NonPublic) - .SetValue(instance, _console); + + if (_psrlInstance is null) + { + Type psrlType = typeof(PSConsoleReadLine); + _psrlInstance = psrlType.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + _psrlConsole = psrlType.GetField("_console", BindingFlags.Instance | BindingFlags.NonPublic); + _psrlMockableMethods = psrlType.GetField("_mockableMethods", BindingFlags.Instance | BindingFlags.NonPublic); + } + + _psrlConsole.SetValue(_psrlInstance, _console); + _psrlMockableMethods.SetValue(_psrlInstance, _mockedMethods); _emptyLine ??= new string(' ', _console.BufferWidth); @@ -628,11 +633,11 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] var colorOptions = new SetPSReadLineOption {Colors = colors}; PSConsoleReadLine.SetOptions(colorOptions); - if (!_oneTimeInitCompleted) - { - typeof(PSConsoleReadLine).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic) - .Invoke(instance, new object[] { /* Runspace */ null, /* EngineIntrinsics */ null, }); - _oneTimeInitCompleted = true; + if (!_oneTimeInitCompleted) + { + typeof(PSConsoleReadLine).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(_psrlInstance, new object[] { /* Runspace */ null, /* EngineIntrinsics */ null, }); + _oneTimeInitCompleted = true; } } } From 9b6a47d7980109b652d33e7c26c5763ccfbf8862 Mon Sep 17 00:00:00 2001 From: Mahir Cadirci <95286636+mahir-cadirci@users.noreply.github.com> Date: Tue, 13 May 2025 21:10:26 +0200 Subject: [PATCH 105/127] Fix typo in `SamplePSReadLineProfile.ps1` (#4725) --- PSReadLine/SamplePSReadLineProfile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/SamplePSReadLineProfile.ps1 b/PSReadLine/SamplePSReadLineProfile.ps1 index 0956dc2f3..6da7e8617 100644 --- a/PSReadLine/SamplePSReadLineProfile.ps1 +++ b/PSReadLine/SamplePSReadLineProfile.ps1 @@ -522,7 +522,7 @@ Set-PSReadLineKeyHandler -Key F1 ` # # Ctrl+Shift+j then type a key to mark the current directory. -# Ctrj+j then the same key will change back to that directory without +# Ctrl+j then the same key will change back to that directory without # needing to type cd and won't change the command line. # From addd1a6f1f181e6540f14727b698fca6583e51aa Mon Sep 17 00:00:00 2001 From: jftkcs <120062444+jftkcs@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:32:13 -0700 Subject: [PATCH 106/127] Add bound check for the cursor top value to `InvokePrompt` (#4791) --- PSReadLine/ReadLine.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 677025aab..d810a3287 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -1035,16 +1035,28 @@ public static void DigitArgument(ConsoleKeyInfo? key = null, object arg = null) public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) { var console = _singleton._console; - console.CursorVisible = false; if (arg is int newY) { + if (newY < 0 || newY >= console.BufferHeight) + { + throw new ArgumentOutOfRangeException(nameof(arg)); + } + + console.CursorVisible = false; console.SetCursorPosition(0, newY); } else { newY = _singleton._initialY - _singleton._options.ExtraPromptLineCount; + // Silently return if user has implicitly requested an impossible prompt invocation. + if (newY < 0) + { + return; + } + + console.CursorVisible = false; console.SetCursorPosition(0, newY); // We need to rewrite the prompt, so blank out everything from a previous prompt invocation From a1c82791e42f37f8686f404adab7857c89929424 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:08:49 -0700 Subject: [PATCH 107/127] Add build script support to VS Code tasks --- .gitignore | 3 -- .vscode/tasks.json | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 .vscode/tasks.json diff --git a/.gitignore b/.gitignore index 8a1e32649..fb0d46c89 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,6 @@ PSReadline.zip [Oo]bj/ .ionide/ -# VSCode directories that are not at the repository root -/**/.vscode/ - # mstest test results TestResults FakesAssemblies/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..09cbef734 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,90 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Bootstrap", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Bootstrap" + ], + "group": "build", + "detail": "Install build prerequisites (InvokeBuild, .NET SDK)" + }, + { + "label": "Build", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Configuration", + "${input:configuration}" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "detail": "Build with user-selected configuration" + }, + { + "label": "Run Tests", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Test", + "-Configuration", + "${input:configuration}", + "-Framework", + "${input:framework}" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "focus": true, + "panel": "dedicated", + "clear": true + }, + "detail": "Run unit tests with selected configuration and framework" + }, + { + "label": "Clean", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Clean" + ], + "group": "build", + "detail": "Clean build artifacts" + } + ], + "inputs": [ + { + "id": "configuration", + "description": "Build Configuration", + "type": "pickString", + "options": [ + "Debug", + "Release" + ], + "default": "Debug" + }, + { + "id": "framework", + "description": "Target Framework", + "type": "pickString", + "options": [ + "net472", + "net6.0" + ], + "default": "net6.0" + } + ] +} From 5863e8d81cc28a13fa6798cd77bb43d472d05f5e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:43:35 -0700 Subject: [PATCH 108/127] Add a VS Code launch target for debugging --- .vscode/launch.json | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d929fb4b..c01006234 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,24 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - + { + "name": "Launch PSReadLine", + "type": "coreclr", + "request": "launch", + "program": "pwsh", + "args": [ + "-NonInteractive", + "-NoProfile", + "-NoExit", + "-Command", + "Import-Module '${workspaceFolder}/PSReadLine/bin/Debug/netstandard2.0/PSReadLine.psd1'" + ], + "console": "integratedTerminal", + "justMyCode": false, + "suppressJITOptimizations": true, + "enableStepFiltering": false, + "preLaunchTask": "Build", + }, { "name": ".NET Core Attach", "type": "coreclr", @@ -16,4 +33,4 @@ } } ] -} \ No newline at end of file +} From a7d988fb5e8286a55ac7d06e589a31c835a6af40 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 17 Jul 2025 11:08:42 -0700 Subject: [PATCH 109/127] Allow accepting the current input automatically from within an `OnIdle` event handler (#4830) --- PSReadLine/ReadLine.cs | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index d810a3287..da890bbba 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -26,6 +26,7 @@ namespace Microsoft.PowerShell { class ExitException : Exception { } + class LineAcceptedException : Exception { } public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods { @@ -44,7 +45,10 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private bool _delayedOneTimeInitCompleted; // This is used by AIShell to check if PSReadLine is initialized and ready to render. + #pragma warning disable CS0414 private bool _readLineReady; + #pragma warning restore CS0414 + private bool _lineAcceptedExceptionThrown; private IPSConsoleReadLineMockableMethods _mockableMethods; private IConsole _console; @@ -175,9 +179,18 @@ internal static PSKeyInfo ReadKey() // By waiting for a key on a different thread, our pipeline execution thread // (the thread ReadLine is called from) avoid being blocked in code that can't // be unblocked and instead blocks on events we control. - - // First, set an event so the thread to read a key actually attempts to read a key. - _singleton._readKeyWaitHandle.Set(); + if (_singleton._lineAcceptedExceptionThrown) + { + // If we threw a 'LineAcceptedException', it means that "AcceptLine" was called within an 'OnIdle' handler the last time + // this method was called, and thus we didn't wait for '_keyReadWaitHandle' to be signalled by the 'readkey thread'. + // In this case, we don't want to signal '_readKeyWaitHandle' again as the 'readkey thread' already got a chance to run. + _singleton._lineAcceptedExceptionThrown = false; + } + else + { + // Set an event so the 'readkey thread' actually attempts to read a key. + _singleton._readKeyWaitHandle.Set(); + } int handleId; System.Management.Automation.PowerShell ps = null; @@ -277,6 +290,16 @@ internal static PSKeyInfo ReadKey() _singleton.Render(); } } + + if (_singleton._inputAccepted && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // 'AcceptLine' was called by an 'OnIdle' handler. + // In this case, we only want to break out of the loop and accept the current input on Windows, because + // accepting input without a keystroke would leave the 'readkey thread' blocked on the 'ReadKey()' call, + // and that will make all subsequent writes to console blocked on Linux and macOS until a key is pressed. + _singleton._lineAcceptedExceptionThrown = true; + throw new LineAcceptedException(); + } } } } @@ -531,8 +554,16 @@ private string InputLoop() // window resizing cannot and shouldn't happen within the processing of a given keybinding. _handlePotentialResizing = true; - var key = ReadKey(); - ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); + try + { + var key = ReadKey(); + ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); + } + catch (LineAcceptedException) + { + Debug.Assert(_inputAccepted, "LineAcceptedException should only be thrown when input was accepted within an 'OnIdle' handler."); + } + if (_inputAccepted) { _acceptedCommandLine = _buffer.ToString(); From 782afd977de0ff5133f2ad8d424e391d8610abf0 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:27:59 -0700 Subject: [PATCH 110/127] Remove configuration selection from default build task (#4855) --- .vscode/tasks.json | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 09cbef734..04326ed55 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,6 +1,6 @@ { - "version": "2.0.0", - "tasks": [ + "version": "2.0.0", + "tasks": [ { "label": "Bootstrap", "type": "shell", @@ -17,9 +17,7 @@ "type": "shell", "command": "pwsh", "args": [ - "./build.ps1", - "-Configuration", - "${input:configuration}" + "./build.ps1" ], "group": { "kind": "build", @@ -37,8 +35,6 @@ "args": [ "./build.ps1", "-Test", - "-Configuration", - "${input:configuration}", "-Framework", "${input:framework}" ], @@ -51,7 +47,7 @@ "panel": "dedicated", "clear": true }, - "detail": "Run unit tests with selected configuration and framework" + "detail": "Run unit tests with selected framework" }, { "label": "Clean", @@ -66,16 +62,6 @@ } ], "inputs": [ - { - "id": "configuration", - "description": "Build Configuration", - "type": "pickString", - "options": [ - "Debug", - "Release" - ], - "default": "Debug" - }, { "id": "framework", "description": "Target Framework", From 26c25ae56aeb1fcac9b23fa41e45692b642fe20f Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 23 Jul 2025 10:03:22 -0700 Subject: [PATCH 111/127] Prepare for the v2.4.3-beta3 release of PSReadLine --- PSReadLine/Changes.txt | 11 +++++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index f3d277752..919f92154 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,14 @@ +### [2.4.3-beta3] - 2025-07-23 + +- Allow accepting the current input automatically from within an `OnIdle` event handler (#4830) +- Add VS Code tasks and debug config (#4834, #4855) +- Add bound check for the cursor top value to `InvokePrompt` (#4791) (Thanks @jftkcs!) +- Fix typo in `SamplePSReadLineProfile.ps1` (#4725) (Thanks @mahir-cadirci!) +- Fix line ending and cache some reflection operations (#4709) +- Improve test reliability by making sure the PSReadLine one-time initialization is done (#4686) (Thanks @springcomp!) + +[2.4.3-beta3]: https://github.com/PowerShell/PSReadLine/compare/v2.4.2-beta2...v2.4.3-beta3 + ### [2.4.2-beta2] - 2025-04-16 - Add a private field to indicate if PSReadLine is initialized and ready (#4706) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 1c8090f84..01576b652 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.2.0 - 2.4.2 - 2.4.2-beta2 + 2.4.3.0 + 2.4.3 + 2.4.3-beta3 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 543d3f442..53d70cfd3 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.2' +ModuleVersion = '2.4.3' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From 49071163a16b550da71206b3c931d7eae8d9c086 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:47:37 -0700 Subject: [PATCH 112/127] Fix ellipsis / continuation character encoding issue in tests This character was always meant to be an ellipsis. I'm unsure exactly why Visual Studio interprets it so, but VS Code and GitHub do not. This commit replaces it with the actual Unicode character which ensures the tests continue to pass when these files are edited and saved in VS Code. --- test/ListPredictionTest.cs | 122 ++++++++++++++++++------------------ test/ListViewTooltipTest.cs | 8 +-- 2 files changed, 65 insertions(+), 65 deletions(-) diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 4931867a5..1b28339f7 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -1355,8 +1355,8 @@ public void List_PluginSource_Acceptance() TokenClassification.Command, "ec", NextLine, TokenClassification.ListPrediction, "<-/3>", - TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1377,9 +1377,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1395,10 +1395,10 @@ public void List_PluginSource_Acceptance() TokenClassification.None, " TEXT BEFORE ec", NextLine, TokenClassification.ListPrediction, "<1/3>", - TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. dimmedColors, '<', TokenClassification.ListPrediction, "TestPredictor(1/2) ", - dimmedColors, "LongNamePredic…(1)>", + dimmedColors, "LongNamePredic…(1)>", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", @@ -1419,9 +1419,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1434,10 +1434,10 @@ public void List_PluginSource_Acceptance() TokenClassification.Selection, "SOME TEXT BEFORE ec", NextLine, TokenClassification.ListPrediction, "<1/3>", - TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. dimmedColors, '<', TokenClassification.ListPrediction, "TestPredictor(1/2) ", - dimmedColors, "LongNamePredic…(1)>", + dimmedColors, "LongNamePredic…(1)>", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME TEXT BEFORE ", @@ -1458,9 +1458,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1473,8 +1473,8 @@ public void List_PluginSource_Acceptance() TokenClassification.Command, "j", NextLine, TokenClassification.ListPrediction, "<-/3>", - TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1495,9 +1495,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1518,9 +1518,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, " NEW TEXT", NextLine, TokenClassification.ListPrediction, "<3/3>", - TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<1/3>' plus ''. dimmedColors, "', NextLine, TokenClassification.ListPrediction, '>', @@ -1542,9 +1542,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1558,8 +1558,8 @@ public void List_PluginSource_Acceptance() TokenClassification.None, " NEW TEX", NextLine, TokenClassification.ListPrediction, "<-/3>", - TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1582,9 +1582,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1604,10 +1604,10 @@ public void List_PluginSource_Acceptance() TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, TokenClassification.ListPrediction, "<2/3>", - TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. dimmedColors, '<', TokenClassification.ListPrediction, "TestPredictor(2/2) ", - dimmedColors, "LongNamePredic…(1)>", + dimmedColors, "LongNamePredic…(1)>", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1630,9 +1630,9 @@ public void List_PluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1676,8 +1676,8 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.Command, "ec", NextLine, TokenClassification.ListPrediction, "<-/5>", - TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/5>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/5>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', @@ -1716,9 +1716,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1733,10 +1733,10 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.Selection, "eca -zoo", NextLine, TokenClassification.ListPrediction, "<1/5>", - TokenClassification.None, new string(' ', listWidth - 38), // 38 is the length of '<1/5>' plus ''. + TokenClassification.None, new string(' ', listWidth - 38), // 38 is the length of '<1/5>' plus ''. dimmedColors, '<', TokenClassification.ListPrediction, "History(1/2) ", - dimmedColors, "TestPredictor(2) …>", + dimmedColors, "TestPredictor(2) …>", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, ' ', @@ -1775,9 +1775,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1789,8 +1789,8 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.Command, "j", NextLine, TokenClassification.ListPrediction, "<-/4>", - TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', @@ -1820,9 +1820,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1842,9 +1842,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, " NEW TEXT", NextLine, TokenClassification.ListPrediction, "<4/4>", - TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '<4/4>' plus '<… TestPredictor(2) LongNamePredic…(1/1)>'. - dimmedColors, "<… TestPredictor(2) ", - TokenClassification.ListPrediction, "LongNamePredic…(1/1)", + TokenClassification.None, new string(' ', listWidth - 46), // 46 is the length of '<4/4>' plus '<… TestPredictor(2) LongNamePredic…(1/1)>'. + dimmedColors, "<… TestPredictor(2) ", + TokenClassification.ListPrediction, "LongNamePredic…(1/1)", dimmedColors, '>', NextLine, TokenClassification.ListPrediction, '>', @@ -1875,9 +1875,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.ListPredictionSelected, " SOME NEW TEXT", - TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.ListPredictionSelected, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.ListPredictionSelected, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.ListPredictionSelected, ']', // List view is done, no more list item following. NextLine, @@ -1891,8 +1891,8 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, " NEW TEX", NextLine, TokenClassification.ListPrediction, "<-/3>", - TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 42), // 42 is the length of '<-/3>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1915,9 +1915,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -1937,10 +1937,10 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, " NEW TEX SOME TEXT AFTER", NextLine, TokenClassification.ListPrediction, "<2/3>", - TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. + TokenClassification.None, new string(' ', listWidth - 44), // 44 is the length of '<2/3>' plus ''. dimmedColors, '<', TokenClassification.ListPrediction, "TestPredictor(2/2) ", - dimmedColors, "LongNamePredic…(1)>", + dimmedColors, "LongNamePredic…(1)>", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME TEXT BEFORE ", @@ -1963,9 +1963,9 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, ' ', emphasisColors, "SOME NEW TEX", TokenClassification.None, 'T', - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -2011,8 +2011,8 @@ public void List_HistoryAndPluginSource_Deduplication() TokenClassification.Command, "de-dup", NextLine, TokenClassification.ListPrediction, "<-/4>", - TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', @@ -2042,9 +2042,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, @@ -2075,8 +2075,8 @@ public void List_HistoryAndPluginSource_Deduplication() TokenClassification.Command, "de-dup", NextLine, TokenClassification.ListPrediction, "<-/4>", - TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. - dimmedColors, "", + TokenClassification.None, new string(' ', listWidth - 36), // 36 is the length of '<-/4>' plus ''. + dimmedColors, "", NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, ' ', @@ -2105,9 +2105,9 @@ public void List_HistoryAndPluginSource_Deduplication() NextLine, TokenClassification.ListPrediction, '>', TokenClassification.None, " SOME NEW TEXT", - TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' + TokenClassification.None, new string(' ', listWidth - 32), // 32 is the length of '> SOME NEW TEXT' plus '[LongNamePredic…]' TokenClassification.None, '[', - TokenClassification.ListPrediction, "LongNamePredic…", + TokenClassification.ListPrediction, "LongNamePredic…", TokenClassification.None, ']', // List view is done, no more list item following. NextLine, diff --git a/test/ListViewTooltipTest.cs b/test/ListViewTooltipTest.cs index 624ec0c99..3d93be47c 100644 --- a/test/ListViewTooltipTest.cs +++ b/test/ListViewTooltipTest.cs @@ -134,7 +134,7 @@ public void List_Item_Tooltip_4_Lines() dimmedColors, " >> Hello", NextLine, dimmedColors, " Binary", NextLine, dimmedColors, " World", NextLine, - dimmedColors, " PowerShell is a task automation an… ", + dimmedColors, " PowerShell is a task automation an… ", TokenClassification.ListPrediction, "( to view all)", NextLine, TokenClassification.ListPrediction, '>', @@ -287,7 +287,7 @@ public void List_Item_Tooltip_2_Lines() TokenClassification.ListPredictionSelected, ']', NextLine, dimmedColors, " >> Hello", NextLine, - dimmedColors, " Binary … ", + dimmedColors, " Binary … ", TokenClassification.ListPrediction, "( to view all)", NextLine, TokenClassification.ListPrediction, '>', @@ -406,7 +406,7 @@ public void List_Item_Tooltip_1_Line() TokenClassification.ListPrediction, "Tooltip", TokenClassification.ListPredictionSelected, ']', NextLine, - dimmedColors, " >> Hello … ", + dimmedColors, " >> Hello … ", TokenClassification.ListPrediction, "( to view all)", NextLine, TokenClassification.ListPrediction, '>', @@ -462,7 +462,7 @@ public void List_Item_Tooltip_1_Line() TokenClassification.ListPrediction, "Tooltip", TokenClassification.ListPredictionSelected, ']', NextLine, - dimmedColors, " >> Hello … ", + dimmedColors, " >> Hello … ", TokenClassification.ListPrediction, "( to view all)", // List view is done, no more list item following. NextLine From 5e72587cca7bd07e0d51aeb75db6967526739d5c Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:57:12 -0700 Subject: [PATCH 113/127] Improve `IsScreenReaderActive()` This supports checking for the built-in screen readers VoiceOver on macOS and Windows Narrator, as well as the popular open-source option, NVDA. The VoiceOver check spawns a quick `defaults` process since in .NET using the macOS events is difficult, but this is quick and easy. The Windows Narrator check inspects a system mutex. Notably though this screen reader handles re-rendering better than others. The check for NVDA et. al. inspects the system parameter information. While this approach is known to be buggy, the preferable and commonly used algorithm (as implemented by Electron) which checks for loaded libraries was tested and found to be unsupported for a non-windowed program like PowerShell. It's unknown if the SPI check will detect JAWS, Window-Eyes, or ZoomText, so a command-line option for the upcoming screen reader mode should also be provided. Linux is not yet supported. --- PSReadLine/Accessibility.cs | 77 +++++++++++++++++++++++++++++++++-- PSReadLine/PlatformWindows.cs | 15 +++++++ 2 files changed, 88 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index 4938da233..aae6c0f52 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -2,6 +2,7 @@ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ +using System.Diagnostics; using System.Runtime.InteropServices; namespace Microsoft.PowerShell.Internal @@ -10,14 +11,82 @@ internal class Accessibility { internal static bool IsScreenReaderActive() { - bool returnValue = false; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0); + return IsAnyWindowsScreenReaderEnabled(); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return IsVoiceOverEnabled(); + } + + // TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility + return false; + } + + private static bool IsAnyWindowsScreenReaderEnabled() + { + // The supposedly official way to check for a screen reader on + // Windows is SystemParametersInfo(SPI_GETSCREENREADER, ...) but it + // doesn't detect the in-box Windows Narrator and is otherwise known + // to be problematic. + // + // Unfortunately, the alternative method used by Electron and + // Chromium, where the relevant screen reader libraries (modules) + // are checked for does not work in the context of PowerShell + // because it relies on those applications injecting themselves into + // the app. Which they do not because PowerShell is not a windowed + // app, so we're stuck using the known-to-be-buggy way. + bool spiScreenReader = false; + PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref spiScreenReader, 0); + if (spiScreenReader) + { + return true; + } + + // At least we can correctly check for Windows Narrator using the + // NarratorRunning mutex. Windows Narrator is mostly not broken with + // PSReadLine, not in the way that NVDA and VoiceOver are. + if (PlatformWindows.IsMutexPresent("NarratorRunning")) + { + return true; + } + + return false; + } + + private static bool IsVoiceOverEnabled() + { + try + { + // Use the 'defaults' command to check if VoiceOver is enabled + // This checks the com.apple.universalaccess preference for voiceOverOnOffKey + ProcessStartInfo startInfo = new() + { + FileName = "defaults", + Arguments = "read com.apple.universalaccess voiceOverOnOffKey", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using Process process = Process.Start(startInfo); + process.WaitForExit(250); + if (process.HasExited && process.ExitCode == 0) + { + string output = process.StandardOutput.ReadToEnd().Trim(); + // VoiceOver is enabled if the value is 1 + return output == "1"; + } + } + catch + { + // If we can't determine the status, assume VoiceOver is not enabled } - return returnValue; + return false; } } } diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index c7e0313b9..32cf653fb 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -79,6 +79,21 @@ IntPtr templateFileWin32Handle [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr GetStdHandle(uint handleId); + internal const int ERROR_ALREADY_EXISTS = 0xB7; + + internal static bool IsMutexPresent(string name) + { + try + { + using var mutex = new System.Threading.Mutex(false, name); + return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS; + } + catch + { + return false; + } + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); From 7a5826bac3212f62e30a700fccb61ef977e1afa1 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 14 Aug 2025 18:52:38 -0700 Subject: [PATCH 114/127] Fix hard-coded continuation prompt in tests These tests were using a hard-coded continuation prompt (and its length) which blocks tests for the upcoming screen reader mode. This is also simply a reasonable refactor, thanks Claude (for trying). --- test/BasicEditingTest.VI.cs | 47 +++++-------- test/BasicEditingTest.cs | 38 +++++----- test/KillYankTest.cs | 26 +++---- test/MovementTest.VI.Multiline.cs | 68 +++++++----------- test/MovementTest.cs | 112 +++++++++++++++--------------- test/RenderTest.cs | 5 +- test/TextObjects.Vi.Tests.cs | 6 -- test/UnitTestReadLine.cs | 6 ++ test/YankPasteTest.VI.cs | 34 +++------ 9 files changed, 147 insertions(+), 195 deletions(-) diff --git a/test/BasicEditingTest.VI.cs b/test/BasicEditingTest.VI.cs index 1ab2bca51..33c3ad17f 100644 --- a/test/BasicEditingTest.VI.cs +++ b/test/BasicEditingTest.VI.cs @@ -494,8 +494,6 @@ public void ViDeletePreviousLines() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\n\"", Keys(  _.DQuote, _.Enter, "one", _.Enter, @@ -503,7 +501,7 @@ public void ViDeletePreviousLines() "three", _.Enter, _.DQuote, _.Escape, "kl", // go to the 'hree' portion of "three" - "2dk", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + "2dk", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -512,8 +510,6 @@ public void ViDeletePreviousLines_LastLine() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\none\ntwo\n\"", Keys(  _.DQuote, _.Enter, "one", _.Enter, @@ -521,7 +517,7 @@ public void ViDeletePreviousLines_LastLine() "three", _.Enter, _.DQuote, _.Escape, "dk", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), CheckThat(() => AssertCursorTopIs(2)), CheckThat(() => AssertLineIs("\"\none\ntwo")), // finish the buffer to close the multiline string @@ -571,8 +567,6 @@ public void ViDeleteToEnd() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\no\nthree\n\"", Keys( _.DQuote, _.Enter, "one", _.Enter, @@ -582,7 +576,7 @@ public void ViDeleteToEnd() "kkkl", // go to the 'ne' portion of "one" // delete to the end of the next line "2d$", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -591,18 +585,16 @@ public void ViBackwardDeleteLine() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nsome words\n\"", Keys( _.DQuote, _.Enter, " this is a line with some words", _.Enter, _.DQuote, _.Escape, "k6W", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 23)), + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 23)), // delete from first non blank of line "d0", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength)) )); } @@ -611,17 +603,15 @@ public void ViDeleteLineToFirstChar() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\n some spaces\n\"", Keys( _.DQuote, _.Enter, " this is a line with some spaces", _.Enter, _.DQuote, _.Escape, "k6W", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 23)), + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 23)), // delete from first non blank of line "d^", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)) )); } @@ -630,8 +620,6 @@ public void ViDeleteNextLines() { TestSetup(KeyMode.Vi); - int continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\n\"", Keys(  _.DQuote, _.Enter, "one", _.Enter, @@ -639,7 +627,7 @@ public void ViDeleteNextLines() "three", _.Enter, _.DQuote, _.Escape, "kkkl", // go to the 'ne' portion of "one" - "2dj", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + "2dj", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -648,8 +636,6 @@ public void ViDeleteRelativeLines() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nthree\n\"", Keys( _.DQuote, _.Enter, "one", _.Enter, @@ -659,7 +645,7 @@ public void ViDeleteRelativeLines() "kkl", // go to the 'wo' portion of "two" // delete from line 2 to the current line (3) "2dgg", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); Test("\"\none\nthree\n\"", Keys( @@ -671,7 +657,7 @@ public void ViDeleteRelativeLines() "kkl", // go to the 'wo' portion of "two" // delete the current line (3) "3dgg", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); Test("\"\none\n\"", Keys( @@ -683,7 +669,7 @@ public void ViDeleteRelativeLines() "kkl", // go to the 'wo' portion of "two" // delete from the current line (3) to line 4 "4dgg", - CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -977,7 +963,6 @@ public void ViDefect651() [SkippableFact] public void ViInsertLine() { - int adder = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; TestSetup(KeyMode.Vi); Test("line1\n", Keys( @@ -985,8 +970,8 @@ public void ViInsertLine() )); Test("\nline1", Keys( - _.Escape, "oline1", CheckThat(() => AssertCursorLeftIs(5 + adder)), CheckThat(() => AssertLineIs("\nline1")), - _.Escape, CheckThat(() => AssertCursorLeftIs(4 + adder)) + _.Escape, "oline1", CheckThat(() => AssertCursorLeftIs(5 + ContinuationPromptLength)), CheckThat(() => AssertLineIs("\nline1")), + _.Escape, CheckThat(() => AssertCursorLeftIs(4 + ContinuationPromptLength)) )); Test("", Keys( @@ -1010,10 +995,10 @@ public void ViInsertLine() )); Test("", Keys( - _.Escape, "oline4", CheckThat(() => AssertLineIs("\nline4")), CheckThat(() => AssertCursorLeftIs(5 + adder)), - _.Escape, "Oline2", CheckThat(() => AssertLineIs("\nline2\nline4")), CheckThat(() => AssertCursorLeftIs(5 + adder)), + _.Escape, "oline4", CheckThat(() => AssertLineIs("\nline4")), CheckThat(() => AssertCursorLeftIs(5 + ContinuationPromptLength)), + _.Escape, "Oline2", CheckThat(() => AssertLineIs("\nline2\nline4")), CheckThat(() => AssertCursorLeftIs(5 + ContinuationPromptLength)), _.Escape, "oline3", CheckThat(() => AssertLineIs("\nline2\nline3\nline4")), - _.Escape, CheckThat(() => AssertLineIs("\nline2\nline3\nline4")), CheckThat(() => AssertCursorLeftIs(4 + adder)), + _.Escape, CheckThat(() => AssertLineIs("\nline2\nline3\nline4")), CheckThat(() => AssertCursorLeftIs(4 + ContinuationPromptLength)), 'u', CheckThat(() => AssertLineIs("\nline2\nline4")), 'u', CheckThat(() => AssertLineIs("\nline4")), 'u' diff --git a/test/BasicEditingTest.cs b/test/BasicEditingTest.cs index 16d8d277d..ac5c7836f 100644 --- a/test/BasicEditingTest.cs +++ b/test/BasicEditingTest.cs @@ -340,8 +340,6 @@ public void InsertLineAbove() { TestSetup(KeyMode.Cmd); - var continutationPromptLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - // Test case - start with single line, cursor at end Test("56\n1234", Keys("1234", _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(0, 0)), "56")); @@ -356,13 +354,13 @@ public void InsertLineAbove() // Test case - start with multi-line, cursor at end of second line (end of input) Test("1234\n9ABC\n5678", Keys("1234", _.Shift_Enter, "5678", - _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at beginning of second line Test("1234\n9ABC\n5678", Keys("1234", _.Shift_Enter, "5678", - _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at end of first line @@ -380,15 +378,15 @@ public void InsertLineAbove() // Test case - insert multiple blank lines Test("1234\n9ABC\n\n5678", Keys("1234", _.Shift_Enter, "5678", - _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "9ABC")); // Test case - create leading blank line, cursor to stay on same line Test("\n\n1234", Keys("1234", _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(0,0)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)))); + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.Ctrl_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)))); } [SkippableFact] @@ -396,51 +394,49 @@ public void InsertLineBelow() { TestSetup(KeyMode.Cmd); - var continutationPromptLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - // Test case - start with single line, cursor at end Test("1234\n56", Keys("1234", - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "56")); // Test case - start with single line, cursor in home position Test("1234\n56", Keys("1234", - _.Home, _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Home, _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "56")); // Test case - start with single line, cursor in middle Test("1234\n56", Keys("1234", - _.LeftArrow, _.LeftArrow, _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.LeftArrow, _.LeftArrow, _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "56")); // Test case - start with multi-line, cursor at end of second line (end of input) Test("1234\n5678\n9ABC", Keys("1234", _.Shift_Enter, "5678", - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 2)), "9ABC")); // Test case - start with multi-line, cursor at beginning of second line Test("1234\n5678\n9ABC", Keys("1234", _.Shift_Enter, "5678", - _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), + _.LeftArrow, _.Home, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 2)), "9ABC")); // Test case - start with multi-line, cursor at end of first line Test("1234\n9ABC\n5678", Keys("1234", _.Shift_Enter, "5678", _.UpArrow, _.LeftArrow, _.End, CheckThat(() => AssertCursorLeftTopIs(4, 0)), - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "9ABC")); // Test case - start with multi-line, cursor at beginning of first line - temporarily having to press Home twice to // work around bug in home handler. Test("1234\n9ABC\n5678", Keys("1234", _.Shift_Enter, "5678", _.UpArrow, _.LeftArrow, _.Home, _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), "9ABC")); // Test case - insert multiple blank lines Test("1234\n5678\n\n9ABC", Keys("1234", _.Shift_Enter, "5678", - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), - _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 3)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 2)), + _.Ctrl_Shift_Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 3)), "9ABC")); } diff --git a/test/KillYankTest.cs b/test/KillYankTest.cs index 8705faab6..e884fcf81 100644 --- a/test/KillYankTest.cs +++ b/test/KillYankTest.cs @@ -674,6 +674,8 @@ public void SelectCommandArgument_HereStringArgs() { TestSetup(KeyMode.Cmd); + var continuationPrompt = PSConsoleReadLine.GetOptions().ContinuationPrompt; + Test("", Keys( "& Test-Sca a1 @'\nabc\n'@ -p1 \"$false\"", // Command name or command expression should be skipped. @@ -685,10 +687,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, ' ', TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", @@ -704,10 +706,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, " a1 ", TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.Selection, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", @@ -723,10 +725,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, " a1 ", TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", @@ -744,10 +746,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, ' ', TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", @@ -766,10 +768,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, ' ', TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", @@ -786,10 +788,10 @@ public void SelectCommandArgument_HereStringArgs() TokenClassification.None, " a1 ", TokenClassification.String, "@'", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "abc", NextLine, - TokenClassification.None, ">> ", + TokenClassification.None, continuationPrompt, TokenClassification.String, "'@", TokenClassification.None, ' ', TokenClassification.Parameter, "-p1", diff --git a/test/MovementTest.VI.Multiline.cs b/test/MovementTest.VI.Multiline.cs index 68e49ba62..0754efd39 100644 --- a/test/MovementTest.VI.Multiline.cs +++ b/test/MovementTest.VI.Multiline.cs @@ -12,8 +12,6 @@ public void ViMoveToLine_DesiredColumn() const string buffer = "\"\n12345\n1234\n123\n12\n1\n\""; - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test(buffer, Keys( _.DQuote, _.Enter, "12345", _.Enter, @@ -25,22 +23,22 @@ public void ViMoveToLine_DesiredColumn() _.Escape, // move to second line at column 4 - "ggj3l", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), + "ggj3l", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), // moving down on shorter lines will position the cursor at the end of each logical line - _.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), - _.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 2)), + _.j, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), + _.j, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 2)), // moving back up will position the cursor at the end of shorter lines or at the desired column number - _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), - _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), + _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), + _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), // move at end of line (column 5) - _.Dollar, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4)), + _.Dollar, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 4)), // moving down on shorter lines will position the cursor at the end of each logical line - _.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), - _.j, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 2)), + _.j, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), + _.j, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 2)), // moving back up will position the cursor at the end of each logical line - _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 3)), - _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4)) + _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 3)), + _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 4)) )); } @@ -52,19 +50,17 @@ public void ViBackwardChar() const string buffer = "\"\nline2\nline3\n\""; - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test(buffer, Keys( _.DQuote, _.Enter, "line2", _.Enter, "line3", _.Enter, _.DQuote, _.Escape, - _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), // move left - _.h, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), - _.l, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 1)), - "2h", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + _.h, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), + _.l, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 1)), + "2h", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -75,18 +71,16 @@ public void ViForwardChar() const string buffer = "\"\nline2\nline3\n\""; - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test(buffer, Keys( _.DQuote, _.Enter, "line2", _.Enter, "line3", _.Enter, _.DQuote, _.Escape, - _.k, _.k, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + _.k, _.k, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), // move right - _.l, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 1)), - "10l", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4)) + _.l, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 1)), + "10l", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 4)) )); } @@ -97,8 +91,6 @@ public void ViMoveToFirstLogicalLineThenJumpToLastLogicalLine() const string buffer = "\"Multiline buffer\n containing an empty line\n\nand text aligned on the left\n\""; - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test(buffer, Keys( _.DQuote, "Multiline buffer", _.Enter, " containing an empty line", _.Enter, @@ -107,7 +99,7 @@ public void ViMoveToFirstLogicalLineThenJumpToLastLogicalLine() _.DQuote, _.Escape, CheckThat(() => AssertCursorTopIs(4)), "gg", CheckThat(() => AssertCursorLeftTopIs(0, 0)), - 'G', CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength, 4)) + 'G', CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 4)) )); } @@ -148,17 +140,15 @@ public void ViMoveToFirstNonBlankOfLogicalLineThenJumpToEndOfLogicalLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - const string buffer = "\"\n line\""; Test(buffer, Keys( - _.DQuote, _.Enter, " line", _.DQuote, _.Escape, CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 6)), - _.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 2, 1)), - _.Dollar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 6, 1)), + _.DQuote, _.Enter, " line", _.DQuote, _.Escape, CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 6)), + _.Underbar, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 2, 1)), + _.Dollar, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 6, 1)), // also works forward - '0', CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength, 1)), - _.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 2, 1)) + '0', CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.Underbar, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 2, 1)) )); } @@ -167,14 +157,12 @@ public void ViMoveToFirstNonBlankOfLogicalLine_NoOp_OnEmptyLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - const string buffer = "\"\n\n\""; Test(buffer, Keys( _.DQuote, _.Enter, _.Enter, _.DQuote, _.Escape, _.k, - CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)), - _.Underbar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)) + CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 1)), + _.Underbar, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 1)) )); } @@ -193,14 +181,12 @@ public void ViMoveToEndOfLine_NoOp_OnEmptyLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - const string buffer = "\"\n\n\""; Test(buffer, Keys( _.DQuote, _.Enter, _.Enter, _.DQuote, _.Escape, _.k, - CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)), - _.Dollar, CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 1)) + CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 1)), + _.Dollar, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 1)) )); } diff --git a/test/MovementTest.cs b/test/MovementTest.cs index 0c687cd0a..4bf4b99a0 100644 --- a/test/MovementTest.cs +++ b/test/MovementTest.cs @@ -35,7 +35,6 @@ public void MultilineCursorMovement_WithWrappedLines() { TestSetup(KeyMode.Cmd); - int continutationPromptLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; string line_0 = "4444"; string line_1 = "33"; string line_2 = "666666"; @@ -43,8 +42,8 @@ public void MultilineCursorMovement_WithWrappedLines() int wrappedLength_1 = 9; int wrappedLength_2 = 2; - string wrappedLine_1 = new string('8', _console.BufferWidth - continutationPromptLength + wrappedLength_1); // Take 2 physical lines - string wrappedLine_2 = new string('6', _console.BufferWidth - continutationPromptLength + wrappedLength_2); // Take 2 physical lines + string wrappedLine_1 = new string('8', _console.BufferWidth - ContinuationPromptLength + wrappedLength_1); // Take 2 physical lines + string wrappedLine_2 = new string('6', _console.BufferWidth - ContinuationPromptLength + wrappedLength_2); // Take 2 physical lines Test("", Keys( "", _.Shift_Enter, // physical line 0 @@ -57,45 +56,45 @@ public void MultilineCursorMovement_WithWrappedLines() // Starting at the end of the last line. // Verify that UpArrow goes to the end of the previous logical line. - CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), + CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), // Press Up/Down/Up _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)), // Press Up/Down/Up _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)), _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)), _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)), // Press Up/Down/Up - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_2.Length, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_2.Length, 3)), _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_1, 5)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_2.Length, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_2.Length, 3)), // Press Up/Up - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length, 1)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_1.Length, 2)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length, 1)), // Move to left for 1 character, so the cursor now is not at the end of line. // Verify that DownArrow/UpArrow goes to the previous logical line at the same column. - _.LeftArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 1)), + _.LeftArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 1)), // Press Down all the way to the end - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 3)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 4)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 5)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 6)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_1.Length, 2)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 3)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 4)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 5)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 6)), _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_3.Length, 8)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_3.Length, 8)), // Press Up all the way to the physical line 1 _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(wrappedLength_2, 7)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 6)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 5)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 4)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 3)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_1.Length, 2)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength + line_0.Length - 1, 1)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 6)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 5)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 4)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_1.Length, 2)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + line_0.Length - 1, 1)), // Clear the input, we were just testing movement _.Escape @@ -107,7 +106,6 @@ public void MultilineCursorMovement() { TestSetup(KeyMode.Cmd); - var continutationPromptLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; Test("", Keys( "4444", _.Shift_Enter, "666666", _.Shift_Enter, @@ -118,39 +116,39 @@ public void MultilineCursorMovement() // Starting at the end of the next to last line (because it's not blank) // Verify that Home first goes to the start of the line, then the start of the input. _.LeftArrow, - _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 4)), + _.Home, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 4)), _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), // Now (because we're at the start), verify first End goes to end of the line // and the second End goes to the end of the input. _.End, CheckThat(() => AssertCursorLeftTopIs(4, 0)), - _.End, CheckThat(() => AssertCursorLeftTopIs(0 + continutationPromptLength, 5)), + _.End, CheckThat(() => AssertCursorLeftTopIs(0 + ContinuationPromptLength, 5)), _.Home, _.Home, - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 3)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 4)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 5)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 2)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 3)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 4)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 5)), _.LeftArrow, _.Home, - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 3)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 2)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 0)), // was (4,0), but seems wrong + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 2)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 0)), // was (4,0), but seems wrong // Make sure that movement between lines stays at the end of a line if it starts // at the end of a line _.End, _.End, - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(4 + continutationPromptLength, 4)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 3)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(8 + continutationPromptLength, 2)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 1)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(4 + ContinuationPromptLength, 4)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(8 + ContinuationPromptLength, 2)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 1)), _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(4, 0)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 1)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(8 + continutationPromptLength, 2)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 3)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(4 + continutationPromptLength, 4)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(0 + continutationPromptLength, 5)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 1)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(8 + ContinuationPromptLength, 2)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 3)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(4 + ContinuationPromptLength, 4)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(0 + ContinuationPromptLength, 5)), _.Escape, _.Shift_Enter, @@ -160,22 +158,22 @@ public void MultilineCursorMovement() "55555", _.Shift_Enter, "88888888", _.LeftArrow, _.LeftArrow, - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(5 + continutationPromptLength, 4)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(2 + continutationPromptLength, 3)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(5 + continutationPromptLength, 2)), - _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 1)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(5 + ContinuationPromptLength, 4)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(2 + ContinuationPromptLength, 3)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(5 + ContinuationPromptLength, 2)), + _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 1)), _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(0, 0)), _.UpArrow, CheckThat(() => AssertCursorLeftTopIs(0, 0)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 1)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(5 + continutationPromptLength, 2)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(2 + continutationPromptLength, 3)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(5 + continutationPromptLength, 4)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + continutationPromptLength, 5)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 1)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(5 + ContinuationPromptLength, 2)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(2 + ContinuationPromptLength, 3)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(5 + ContinuationPromptLength, 4)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(6 + ContinuationPromptLength, 5)), // Using the input previously entered, check for correct cursor movements when first line is blank _.Home, _.Home, CheckThat(() => AssertCursorLeftTopIs(0, 0)), - _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(8 + continutationPromptLength, 1)), - _.Home, CheckThat(() => AssertCursorLeftTopIs(continutationPromptLength, 1)), + _.DownArrow, CheckThat(() => AssertCursorLeftTopIs(8 + ContinuationPromptLength, 1)), + _.Home, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), _.Home, CheckThat(() => AssertCursorLeftTopIs(0,0)), // Clear the input, we were just testing movement diff --git a/test/RenderTest.cs b/test/RenderTest.cs index a1222c115..cb4943a50 100644 --- a/test/RenderTest.cs +++ b/test/RenderTest.cs @@ -182,7 +182,6 @@ public void MultiLine() // Make sure when input is incomplete actually puts a newline // wherever the cursor is. - var continationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; Test("{\n\nd\n}", Keys( '{', _.Enter, CheckThat(() => AssertCursorTopIs(1)), @@ -190,8 +189,8 @@ public void MultiLine() _.Enter, CheckThat(() => AssertCursorTopIs(2)), _.Home, _.RightArrow, CheckThat(() => AssertCursorLeftTopIs(1, 0)), - _.Enter, CheckThat(() => AssertCursorLeftTopIs(continationPrefixLength, 1)), - _.End, CheckThat(() => AssertCursorLeftTopIs(continationPrefixLength, 3)), + _.Enter, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 1)), + _.End, CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength, 3)), '}')); // Make sure when input successfully parses accepts the input regardless diff --git a/test/TextObjects.Vi.Tests.cs b/test/TextObjects.Vi.Tests.cs index f819b3879..d38e44bc6 100644 --- a/test/TextObjects.Vi.Tests.cs +++ b/test/TextObjects.Vi.Tests.cs @@ -82,8 +82,6 @@ public void ViTextObject_diw_empty_line() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nhello, world!\n\noh, bitter world!\n\"", Keys( _.DQuote, _.Enter, "hello, world!", _.Enter, @@ -105,8 +103,6 @@ public void ViTextObject_diw_end_of_buffer() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("", Keys( _.DQuote, "hello, world!", _.Enter, @@ -143,8 +139,6 @@ public void ViTextObject_diw_new_lines() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\ntwo\n\"", Keys( _.DQuote, _.Enter, "one", _.Enter, diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index d09e2abc9..71b8a9434 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -131,6 +131,8 @@ protected ReadLine(ConsoleFixture fixture, ITestOutputHelper output, string lang internal virtual bool KeyboardHasCtrlRBracket => true; internal virtual bool KeyboardHasCtrlAt => true; + internal int ContinuationPromptLength => _continuationPromptLength; + static ReadLine() { var iss = InitialSessionState.CreateDefault(); @@ -540,6 +542,7 @@ private void TestMustDing(string expectedResult, object[] items) private bool _oneTimeInitCompleted; private object _psrlInstance; private FieldInfo _psrlConsole, _psrlMockableMethods; + private int _continuationPromptLength; private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); @@ -633,6 +636,9 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] var colorOptions = new SetPSReadLineOption {Colors = colors}; PSConsoleReadLine.SetOptions(colorOptions); + // Cache the continuation prompt length for use in tests + _continuationPromptLength = PSConsoleReadLine.GetOptions().ContinuationPrompt.Length; + if (!_oneTimeInitCompleted) { typeof(PSConsoleReadLine).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic) diff --git a/test/YankPasteTest.VI.cs b/test/YankPasteTest.VI.cs index 590f04be1..56261d5b6 100644 --- a/test/YankPasteTest.VI.cs +++ b/test/YankPasteTest.VI.cs @@ -48,13 +48,11 @@ public void ViPasteAfterDelete() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("abcd", Keys( "abcd", _.Escape, "dd", CheckThat(() => AssertLineIs("")), CheckThat(() => AssertCursorLeftIs(0)), - 'p', CheckThat(() => AssertLineIs("\nabcd")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), - 'P', CheckThat(() => AssertLineIs("\nabcd\nabcd")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + 'p', CheckThat(() => AssertLineIs("\nabcd")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), + 'P', CheckThat(() => AssertLineIs("\nabcd\nabcd")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), "uuu" )); @@ -206,12 +204,10 @@ public void ViPasteAfterDeleteLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("abc def", Keys( "abc def", _.Escape, "dd", CheckThat(() => AssertLineIs("")), CheckThat(() => AssertCursorLeftIs(0)), - 'p', CheckThat(() => AssertLineIs("\nabc def")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + 'p', CheckThat(() => AssertLineIs("\nabc def")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), "dd", CheckThat(() => AssertLineIs("")), CheckThat(() => AssertCursorLeftIs(0)), 'P', CheckThat(() => AssertLineIs("abc def\n")), CheckThat(() => AssertCursorLeftIs(0)), "uuuu" @@ -223,13 +219,11 @@ public void ViPasteAfterYankLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("012 456", Keys( "012 456", _.Escape, "byyP", CheckThat(() => AssertLineIs("012 456\n012 456")), CheckThat(() => AssertCursorLeftIs(0)), "u", CheckThat(() => AssertLineIs("012 456")), CheckThat(() => AssertCursorLeftIs(4)), - "p", CheckThat(() => AssertLineIs("012 456\n012 456")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), + "p", CheckThat(() => AssertLineIs("012 456\n012 456")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), "u", CheckThat(() => AssertLineIs("012 456")), CheckThat(() => AssertCursorLeftIs(4)) )); } @@ -338,8 +332,6 @@ public void ViPasteAfterYankBeginningOfLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("012", Keys( "012", _.Escape, "y0P", CheckThat(() => AssertLineIs("01012")), CheckThat(() => AssertCursorLeftIs(1)), @@ -358,9 +350,9 @@ public void ViPasteAfterYankBeginningOfLine() " World!", _.Enter, _.DQuote, _.Escape, _.k, "5l", // move the cursor to the 'd' character of "World!" - "y0", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)), - "P", CheckThat(() => AssertLineIs("\"\nHello\n Worl World!\n\"")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 4)), - "u", CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)) + "y0", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)), + "P", CheckThat(() => AssertLineIs("\"\nHello\n Worl World!\n\"")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 4)), + "u", CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 0)) )); } @@ -369,15 +361,13 @@ public void ViPasteAfterYankEndOfLine() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nHello\nWorld!\n\"", Keys( _.DQuote, _.Enter, "Hello", _.Enter, "World!", _.Enter, _.DQuote, _.Escape, _.k, _.l, // move to the 'o' character of 'World!' - "y$P", CheckThat(() => AssertLineIs("\"\nHello\nWorld!orld!\n\"")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 5)), + "y$P", CheckThat(() => AssertLineIs("\"\nHello\nWorld!orld!\n\"")), CheckThat(() => AssertCursorLeftIs(ContinuationPromptLength + 5)), "u" )); } @@ -542,15 +532,13 @@ public void ViDeleteAndPasteLogicalLines() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nline1\nline1\nline2\nline2\n\"", Keys( _.DQuote, _.Enter, "line1", _.Enter, "line2", _.Enter, _.DQuote, _.Escape, _.k, _.k, - "2dd", 'P', 'p', CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 2)) + "2dd", 'P', 'p', CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 2)) )); } @@ -559,15 +547,13 @@ public void ViDeleteAndPasteLogicalLines_Underbar() { TestSetup(KeyMode.Vi); - var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length; - Test("\"\nline1\nline1\nline2\nline2\n\"", Keys( _.DQuote, _.Enter, "line1", _.Enter, "line2", _.Enter, _.DQuote, _.Escape, _.k, _.k, - "2d", _.Underbar, 'P', 'p', CheckThat(() => AssertCursorLeftTopIs(continuationPrefixLength + 0, 2)) + "2d", _.Underbar, 'P', 'p', CheckThat(() => AssertCursorLeftTopIs(ContinuationPromptLength + 0, 2)) )); } From 7b6f6024052c4292ec31440669affe77b8f62744 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:40:23 -0700 Subject: [PATCH 115/127] Add accessible screen reader mode This adds the `EnableScreenReaderMode` command-line switch which defaults to true if an active screen reader is detected. In screen reader mode, the existing `ForceRender()` function is replaced by `RenderForScreenReader()` which uses a differential rendering approach to minimize extraneous output to the terminal, allowing the use of screen readers better than ever before. The differential rendering relies on calculating the common prefix of the `buffer` and `previousRender` strings. Nearly all necessary changes are consolidated in the new rendering function. Features known not to be supported: * Colors: as this necessitates redrawing to insert color sequences after the input is received and the AST parsed. * Inline predictions: as this by definition changes the suffix and thus requires endless redrawing. * List view predictions: since the render implementation never calls into the prediction engine, this is not available either. * Menu completion: well, it "works" since it's not disabled and does its own rendering, but no effort has been made to improve `DrawMenu()`, so it's not accessible (and I'm not sure it could be given our current constraints). Features known to be partially supported: * Text selection: mark and select commands work as intended, but provide no visual indication. * Multiple lines: as in newlines work fine, but there is no continuation prompt. * Visually wrapped lines: editing above a wrapped line redraws all subsequent lines and hence is noisy. * Status prompt based commands: what-is-key, digit-argument, and most notably, forward/backward incremental history search all render a "status prompt" on the line below the user's input buffer. This _is_ supported; however, it can be noisy since it necessarily has to render the whole buffer when the input buffer changes, including the status prompt (and search text). But what it's reading is almost always going to be relevant. Everything else should generally work, even Vi mode, and the tests pass. That said, this isn't perfect, and moreover the approach specifically doesn't attempt to enable things from the ground up. There may be features that are available but turn out not to be accessible (like `MenuComplete`) and I believe they should be left as-is. Specifically tested with NVDA on Windows and VoiceOver on macOS within VS Code's integrated terminal, with shell integration loaded, and Code's screen reader optimizations enabled. --- PSReadLine/BasicEditing.cs | 4 +- PSReadLine/Cmdlets.cs | 17 ++- PSReadLine/Options.cs | 4 + PSReadLine/Render.cs | 239 +++++++++++++++++++++++++++++++------ 4 files changed, 222 insertions(+), 42 deletions(-) diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 33445ed36..74c76ac00 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -86,7 +86,7 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) _singleton._current = _singleton._buffer.Length; using var _ = _singleton._prediction.DisableScoped(); - _singleton.ForceRender(); + _singleton.Render(force: true); _singleton._console.Write("\x1b[91m^C\x1b[0m"); @@ -335,7 +335,7 @@ private bool AcceptLineImpl(bool validate) if (renderNeeded) { - ForceRender(); + Render(force: true); } // Only run validation if we haven't before. If we have and status line shows an error, diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 7fecf4b4e..5633a69af 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -15,6 +15,7 @@ using System.Runtime.InteropServices; using System.Threading; using Microsoft.PowerShell.PSReadLine; +using Microsoft.PowerShell.Internal; using AllowNull = System.Management.Automation.AllowNullAttribute; namespace Microsoft.PowerShell @@ -150,11 +151,6 @@ public class PSConsoleReadLineOptions public const HistorySaveStyle DefaultHistorySaveStyle = HistorySaveStyle.SaveIncrementally; - /// - /// The predictive suggestion feature is disabled by default. - /// - public const PredictionSource DefaultPredictionSource = PredictionSource.None; - public const PredictionViewStyle DefaultPredictionViewStyle = PredictionViewStyle.InlineView; /// @@ -201,6 +197,7 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) { ResetColors(); EditMode = DefaultEditMode; + ScreenReaderModeEnabled = Accessibility.IsScreenReaderActive(); ContinuationPrompt = DefaultContinuationPrompt; ContinuationPromptColor = Console.ForegroundColor; ExtraPromptLineCount = DefaultExtraPromptLineCount; @@ -533,6 +530,8 @@ public object ListPredictionTooltipColor public bool TerminateOrphanedConsoleApps { get; set; } + public bool ScreenReaderModeEnabled { get; set; } + internal string _defaultTokenColor; internal string _commentColor; internal string _keywordColor; @@ -847,6 +846,14 @@ public SwitchParameter TerminateOrphanedConsoleApps } internal SwitchParameter? _terminateOrphanedConsoleApps; + [Parameter] + public SwitchParameter EnableScreenReaderMode + { + get => _enableScreenReaderMode.GetValueOrDefault(); + set => _enableScreenReaderMode = value; + } + internal SwitchParameter? _enableScreenReaderMode; + [ExcludeFromCodeCoverage] protected override void EndProcessing() { diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 7485154b4..ffdf0241c 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -185,6 +185,10 @@ private void SetOptionsInternal(SetPSReadLineOption options) nameof(Options.TerminateOrphanedConsoleApps))); } } + if (options._enableScreenReaderMode.HasValue) + { + Options.ScreenReaderModeEnabled = options.EnableScreenReaderMode; + } } private void SetKeyHandlerInternal(string[] keys, Action handler, string briefDescription, string longDescription, ScriptBlock scriptBlock) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index fe688481a..253f7fbef 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -218,36 +218,197 @@ private void RenderWithPredictionQueryPaused() Render(); } - private void Render() + private void Render(bool force = false) { - // If there are a bunch of keys queued up, skip rendering if we've rendered very recently. - long elapsedMs = _lastRenderTime.ElapsedMilliseconds; - if (_queuedKeys.Count > 10 && elapsedMs < 50) - { - // We won't render, but most likely the tokens will be different, so make - // sure we don't use old tokens, also allow garbage to get collected. - _tokens = null; - _ast = null; - _parseErrors = null; - _waitingToRender = true; - return; + if (!force) + { + // If there are a bunch of keys queued up, skip rendering if we've rendered very recently. + long elapsedMs = _lastRenderTime.ElapsedMilliseconds; + if (_queuedKeys.Count > 10 && elapsedMs < 50) + { + // We won't render, but most likely the tokens will be different, so make + // sure we don't use old tokens, also allow garbage to get collected. + _tokens = null; + _ast = null; + _parseErrors = null; + _waitingToRender = true; + return; + } + + // If we've rendered very recently, skip the terminal window resizing check as it's unlikely + // to happen in such a short time interval. + // We try to avoid unnecessary resizing check because it requires getting the cursor position + // which would force a network round trip in an environment where front-end xtermjs talking to + // a server-side PTY via websocket. Without querying for cursor position, content written on + // the server side could be buffered, which is much more performant. + // See the following 2 GitHub issues for more context: + // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070 + // - https://github.com/PowerShell/PowerShell/issues/24696 + if (elapsedMs < 50) + { + _handlePotentialResizing = false; + } + } + + // Use simplified rendering for screen readers + if (Options.ScreenReaderModeEnabled) + { + RenderForScreenReader(); + } + else + { + ForceRender(); + } + } + + private void RenderForScreenReader() + { + int bufferWidth = _console.BufferWidth; + int bufferHeight = _console.BufferHeight; + + static int FindCommonPrefixLength(string leftStr, string rightStr) + { + if (string.IsNullOrEmpty(leftStr) || string.IsNullOrEmpty(rightStr)) + { + return 0; + } + + int i = 0; + int minLength = Math.Min(leftStr.Length, rightStr.Length); + + while (i < minLength && leftStr[i] == rightStr[i]) + { + i++; + } + + return i; + } + + // For screen readers, we are just comparing the previous and current buffer text + // (without colors) and only writing the differences. + // + // Note that we don't call QueryForSuggestion() which is the only + // entry into the prediction logic, so while it could be enabled, it + // won't do anything in this rendering implementation. + string parsedInput = ParseInput(); + StringBuilder buffer = new(parsedInput); + + // Really simple handling of a status line: append it! + if (!string.IsNullOrEmpty(_statusLinePrompt)) + { + buffer.Append("\n"); + buffer.Append(_statusLinePrompt); + buffer.Append(_statusBuffer); + } + + string currentBuffer = buffer.ToString(); + string previousBuffer = _previousRender.lines[0].Line; + + // In case the buffer was resized. + RecomputeInitialCoords(isTextBufferUnchanged: false); + + // Make cursor invisible while we're rendering. + _console.CursorVisible = false; + + if (currentBuffer == previousBuffer) + { + // No-op, such as when selecting text or otherwise re-entering. + } + else if (previousBuffer.Length == 0) + { + // Previous buffer was empty so we just render the current buffer, + // and we don't need to move the cursor. + _console.Write(currentBuffer); } + else + { + // Calculate what to render and where to start the rendering. + int commonPrefixLength = FindCommonPrefixLength(previousBuffer, currentBuffer); + + // If we're scrolling through history we always want to re-render. + // Writing only the diff in this scenario is a weird UX. + if (commonPrefixLength > 0 && _anyHistoryCommandCount == 0) + { + // We need to differentially render, possibly with a partial rewrite. + if (commonPrefixLength != previousBuffer.Length) + { + // The buffers share a common prefix but the previous buffer has additional content. + // Move cursor to where the difference starts and clear so we can rewrite. + var diffPoint = ConvertOffsetToPoint(commonPrefixLength, buffer); + _console.SetCursorPosition(diffPoint.X, diffPoint.Y); + _console.Write("\x1b[0J"); + } // Otherwise the previous buffer is a complete prefix and we just write. + + // TODO: There is a rare edge case where the common prefix can be incorrectly + // calculated because the incoming replacement text matches the text at the current + // cursor position. Unfortunately we don't have a solution yet. For example: + // + // 1. Previous line is "abcdef" and cursor is at (before) the letter "d" + // 2. Paste "defghi" so currentBuffer is "abcdefghidef" + // 3. The diff is "ghidef" and because commonPrefixLength == previousBuffer.Length + // 4. The terminal will incorrectly display "abcghidef" instead of "abcdefghidef" + + // Finally, write the diff. + var diffData = currentBuffer.Substring(commonPrefixLength); + _console.Write(diffData); + } + else + { + // The buffers are completely different so we need to rewrite from the start. + _console.SetCursorPosition(_initialX, _initialY); + _console.Write("\x1b[0J"); + _console.Write(currentBuffer); + } + } + + // If we had to wrap to render everything, update _initialY + var endPoint = ConvertOffsetToPoint(currentBuffer.Length, buffer); + if (endPoint.Y >= bufferHeight) + { + // We had to scroll to render everything, update _initialY. + int offset = 1; // Base case to handle zero-indexing. + if (endPoint.X == 0 && !currentBuffer.EndsWith("\n")) + { + // The line hasn't actually wrapped yet because we have exactly filled the line. + offset -= 1; + } + int scrolledLines = endPoint.Y - bufferHeight + offset; + _initialY -= scrolledLines; + } + + // Calculate the coord to place the cursor for the next input. + var point = ConvertOffsetToPoint(_current, buffer); - // If we've rendered very recently, skip the terminal window resizing check as it's unlikely - // to happen in such a short time interval. - // We try to avoid unnecessary resizing check because it requires getting the cursor position - // which would force a network round trip in an environment where front-end xtermjs talking to - // a server-side PTY via websocket. Without querying for cursor position, content written on - // the server side could be buffered, which is much more performant. - // See the following 2 GitHub issues for more context: - // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070 - // - https://github.com/PowerShell/PowerShell/issues/24696 - if (elapsedMs < 50) + if (point.Y == bufferHeight) { - _handlePotentialResizing = false; + // The cursor top exceeds the buffer height and it hasn't already wrapped, + // (because we have exactly filled the line) so we need to scroll up the buffer by 1 line. + if (point.X == 0) + { + _console.Write("\n"); + } + + // Adjust the initial cursor position and the to-be-set cursor position + // after scrolling up the buffer. + _initialY -= 1; + point.Y -= 1; } - ForceRender(); + _console.SetCursorPosition(point.X, point.Y); + _console.CursorVisible = true; + + // Preserve the current render data. + var renderData = new RenderData + { + lines = new RenderedLineData[] { new(currentBuffer, isFirstLogicalLine: true) }, + errorPrompt = (_parseErrors != null && _parseErrors.Length > 0) // Not yet used. + }; + _previousRender = renderData; + _previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y); + _previousRender.initialY = _initialY; + + _lastRenderTime.Restart(); + _waitingToRender = false; } private void ForceRender() @@ -261,7 +422,7 @@ private void ForceRender() // and minimize writing more than necessary on the next render.) var renderLines = new RenderedLineData[logicalLineCount]; - var renderData = new RenderData {lines = renderLines}; + var renderData = new RenderData { lines = renderLines }; for (var i = 0; i < logicalLineCount; i++) { var line = _consoleBufferLines[i].ToString(); @@ -872,9 +1033,6 @@ void UpdateColorsIfNecessary(string newColor) WriteBlankLines(lineCount); } - // Preserve the current render data. - _previousRender = renderData; - // If we counted pseudo physical lines, deduct them to get the real physical line counts // before updating '_initialY'. physicalLine -= pseudoPhysicalLineOffset; @@ -950,6 +1108,8 @@ void UpdateColorsIfNecessary(string newColor) _console.SetCursorPosition(point.X, point.Y); _console.CursorVisible = true; + // Preserve the current render data. + _previousRender = renderData; _previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y); _previousRender.initialY = _initialY; @@ -1201,17 +1361,23 @@ internal Point EndOfBufferPosition() return ConvertOffsetToPoint(_buffer.Length); } - internal Point ConvertOffsetToPoint(int offset) + internal Point ConvertOffsetToPoint(int offset, StringBuilder buffer = null) { + // This lets us re-use the logic in the screen reader rendering implementation + // where the status line is added to the buffer without modifying the local state. + buffer ??= _buffer; + int x = _initialX; int y = _initialY; int bufferWidth = _console.BufferWidth; - var continuationPromptLength = LengthInBufferCells(Options.ContinuationPrompt); + var continuationPromptLength = Options.ScreenReaderModeEnabled + ? 0 + : LengthInBufferCells(Options.ContinuationPrompt); for (int i = 0; i < offset; i++) { - char c = _buffer[i]; + char c = buffer[i]; if (c == '\n') { y += 1; @@ -1229,7 +1395,7 @@ internal Point ConvertOffsetToPoint(int offset) // If cursor is at column 0 and the next character is newline, let the next loop // iteration increment y. - if (x != 0 || !(i + 1 < offset && _buffer[i + 1] == '\n')) + if (x != 0 || !(i + 1 < offset && buffer[i + 1] == '\n')) { y += 1; } @@ -1238,9 +1404,9 @@ internal Point ConvertOffsetToPoint(int offset) } // If next character actually exists, and isn't newline, check if wider than the space left on the current line. - if (_buffer.Length > offset && _buffer[offset] != '\n') + if (buffer.Length > offset && buffer[offset] != '\n') { - int size = LengthInBufferCells(_buffer[offset]); + int size = LengthInBufferCells(buffer[offset]); if (x + size > bufferWidth) { // Character was wider than remaining space, so character, and cursor, appear on next line. @@ -1259,7 +1425,10 @@ private int ConvertLineAndColumnToOffset(Point point) int y = _initialY; int bufferWidth = _console.BufferWidth; - var continuationPromptLength = LengthInBufferCells(Options.ContinuationPrompt); + var continuationPromptLength = Options.ScreenReaderModeEnabled + ? 0 + : LengthInBufferCells(Options.ContinuationPrompt); + for (offset = 0; offset < _buffer.Length; offset++) { // If we are on the correct line, return when we find From 06e3b4bcb6058f6174115d8298617056f34c516e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:47:21 -0700 Subject: [PATCH 116/127] Add fixture to enable existing tests for screen reader mode Tests that depend on supported features such as menu completions, list view, inline predictions, continuation prompt, and colors are skipped. The "helper" module for running tests from the command-line and in AppVeyor is minimally updated to not skip the new fixture. A `BlankRestOfBuffer()` function implements the semantics of the `0J` control sequence in the mock console. --- test/CompletionTest.cs | 18 ++++++++++++++++++ test/InlinePredictionTest.cs | 22 ++++++++++++++++++++++ test/KillYankTest.cs | 2 ++ test/ListPredictionTest.cs | 22 ++++++++++++++++++++++ test/ListScrollableViewTest.cs | 6 ++++++ test/ListViewTooltipTest.cs | 6 ++++++ test/MockConsole.cs | 26 +++++++++++++++++++++++++- test/OptionsTest.cs | 16 ++++++++++++++++ test/RenderTest.cs | 10 ++++++++++ test/UnitTestReadLine.cs | 25 ++++++++++++++++++++++--- tools/helper.psm1 | 11 +++++++++-- 11 files changed, 158 insertions(+), 6 deletions(-) diff --git a/test/CompletionTest.cs b/test/CompletionTest.cs index 7a3ea5152..8287e58c9 100644 --- a/test/CompletionTest.cs +++ b/test/CompletionTest.cs @@ -177,6 +177,8 @@ public void MenuCompletions_FilterByTyping() [SkippableFact] public void MenuCompletions_Navigation1() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Test 'RightArrow' and 'LeftArrow' with the following menu: // Get-Many0 Get-Many3 Get-Many6 Get-Many9 Get-Many12 // Get-Many1 Get-Many4 Get-Many7 Get-Many10 Get-Many13 @@ -304,6 +306,8 @@ public void MenuCompletions_Navigation1() [SkippableFact] public void MenuCompletions_Navigation2() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Test 'RightArrow' with the following menu: // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 // Get-Less1 Get-Less4 Get-Less7 Get-Less10 @@ -387,6 +391,8 @@ public void MenuCompletions_Navigation2() [SkippableFact] public void MenuCompletions_Navigation3() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Test 'LeftArrow' with the following menu: // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 // Get-Less1 Get-Less4 Get-Less7 Get-Less10 @@ -462,6 +468,8 @@ public void MenuCompletions_Navigation3() [SkippableFact] public void MenuCompletions_Navigation4() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Test 'UpArrow' and 'DownArrow' with the following menu: // Get-Less0 Get-Less3 Get-Less6 Get-Less9 Get-Less12 // Get-Less1 Get-Less4 Get-Less7 Get-Less10 @@ -633,6 +641,8 @@ public void MenuCompletions_Navigation5() [SkippableFact] public void MenuCompletions_Navigation6() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Test 'UpArrow', 'DownArrow', 'LeftArrow', and 'RightArrow' with the following menu: // Get-NewDynamicParameters Get-NewStyle // Get-NewIdea @@ -736,6 +746,8 @@ public void MenuCompletions_Navigation6() [SkippableFact] public void MenuCompletions_Navigation7() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // Trigger the menu completion from the last line in the screen buffer, which will cause the screen // to scroll up. Then test 'DownArrow' and 'UpArrow' with the following menu to verify if scrolling // was handled correctly: @@ -968,6 +980,8 @@ public void MenuCompletions_ClearProperly() [SkippableFact] public void MenuCompletions_WorkWithListView() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); int listWidth = CheckWindowSize(); @@ -1032,6 +1046,8 @@ public void MenuCompletions_WorkWithListView() [SkippableFact] public void MenuCompletions_HandleScrolling1() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // This test case covers the fix to https://github.com/PowerShell/PSReadLine/issues/2928. var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 133, height: 10); TestSetup(basicScrollingConsole, KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); @@ -1127,6 +1143,8 @@ public void MenuCompletions_HandleScrolling1() [SkippableFact] public void MenuCompletions_HandleScrolling2() { + Skip.If(ScreenReaderModeEnabled, "Menu completions are not supported in screen reader mode."); + // This test case covers the fix to https://github.com/PowerShell/PSReadLine/issues/2948. var basicScrollingConsole = new BasicScrollingConsole(keyboardLayout: _, width: 133, height: 10); TestSetup(basicScrollingConsole, KeyMode.Cmd, new KeyHandler("Ctrl+Spacebar", PSConsoleReadLine.MenuComplete)); diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index d9729c675..febf0ac1f 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -14,6 +14,8 @@ public partial class ReadLine [SkippableFact] public void Inline_RenderSuggestion() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); @@ -92,6 +94,8 @@ public void Inline_RenderSuggestion() [SkippableFact] public void Inline_CustomKeyBindingsToAcceptSuggestion() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Alt+g", PSConsoleReadLine.AcceptSuggestion), new KeyHandler("Alt+f", PSConsoleReadLine.AcceptNextSuggestionWord)); @@ -156,6 +160,8 @@ public void Inline_CustomKeyBindingsToAcceptSuggestion() [SkippableFact] public void Inline_AcceptNextSuggestionWordCanAcceptMoreThanOneWords() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord), new KeyHandler("Alt+f", PSConsoleReadLine.AcceptNextSuggestionWord)); @@ -192,6 +198,8 @@ public void Inline_AcceptNextSuggestionWordCanAcceptMoreThanOneWords() [SkippableFact] public void Inline_AcceptSuggestionWithSelection() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); @@ -261,6 +269,8 @@ public void Inline_DisablePrediction() [SkippableFact] public void Inline_SetPredictionColor() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); var predictionColor = MakeCombinedColor(ConsoleColor.DarkYellow, ConsoleColor.Yellow); var predictionColorToCheck = Tuple.Create(ConsoleColor.DarkYellow, ConsoleColor.Yellow); @@ -288,6 +298,8 @@ public void Inline_SetPredictionColor() [SkippableFact] public void Inline_HistoryEditsCanUndoProperly() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); SetHistory("git checkout -b branch origin/bbbb"); @@ -320,6 +332,8 @@ public void Inline_HistoryEditsCanUndoProperly() [SkippableFact] public void Inline_AcceptSuggestionInVIMode() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Vi); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); @@ -367,6 +381,8 @@ public void Inline_AcceptSuggestionInVIMode() [SkippableFact] public void ViDefect2408() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(KeyMode.Vi); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); @@ -460,6 +476,8 @@ internal static List MockedPredictInput(Ast ast, Token[] token [SkippableFact] public void Inline_PluginSource_Acceptance() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + // Using the 'Plugin' source will make PSReadLine get prediction from the plugin only. TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); @@ -534,6 +552,8 @@ public void Inline_PluginSource_Acceptance() [SkippableFact] public void Inline_HistoryAndPluginSource_Acceptance() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + // Using the 'HistoryAndPlugin' source will make PSReadLine get prediction from the plugin and history, // and plugin takes precedence. TestSetup(KeyMode.Cmd, @@ -798,6 +818,8 @@ public void Inline_HistoryAndPluginSource_ExecutionStatus() [SkippableFact] public void Inline_TruncateVeryLongSuggestion() { + Skip.If(ScreenReaderModeEnabled, "Inline predictions are not supported in screen reader mode."); + TestSetup(new TestConsole(keyboardLayout: _, width: 10, height: 2), KeyMode.Cmd); using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); diff --git a/test/KillYankTest.cs b/test/KillYankTest.cs index e884fcf81..381e7889e 100644 --- a/test/KillYankTest.cs +++ b/test/KillYankTest.cs @@ -672,6 +672,8 @@ public void SelectCommandArgument_CLIArgs() [SkippableFact] public void SelectCommandArgument_HereStringArgs() { + Skip.If(ScreenReaderModeEnabled, "We're still investigating exactly why this test fails in screen reader mode."); + TestSetup(KeyMode.Cmd); var continuationPrompt = PSConsoleReadLine.GetOptions().ContinuationPrompt; diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 1b28339f7..a0a426962 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -96,6 +96,8 @@ public void List_RenderSuggestion_NoMatching_HistorySearchBackwardForward() [SkippableFact] public void List_RenderSuggestion_ListUpdatesWhileTyping() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); // The font effect sequences of the dimmed color used in list view metadata line @@ -184,6 +186,8 @@ public void List_RenderSuggestion_ListUpdatesWhileTyping() [SkippableFact] public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -401,6 +405,8 @@ public void List_RenderSuggestion_NavigateInList_DefaultUpArrowDownArrow() [SkippableFact] public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+p", PSConsoleReadLine.HistorySearchBackward), new KeyHandler("Ctrl+l", PSConsoleReadLine.HistorySearchForward)); @@ -620,6 +626,8 @@ public void List_RenderSuggestion_NavigateInList_HistorySearchBackwardForward() [SkippableFact] public void List_RenderSuggestion_Escape() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -783,6 +791,8 @@ public void List_RenderSuggestion_Escape() [SkippableFact] public void List_RenderSuggestion_DigitArgument() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -943,6 +953,8 @@ public void List_RenderSuggestion_DigitArgument() [SkippableFact] public void List_RenderSuggestion_CtrlZ() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -1076,6 +1088,8 @@ public void List_RenderSuggestion_CtrlZ() [SkippableFact] public void List_RenderSuggestion_Selection() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -1276,6 +1290,8 @@ public void List_RenderSuggestion_Selection() [SkippableFact] public void List_HistorySource_NoAcceptanceCallback() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -1340,6 +1356,8 @@ public void List_HistorySource_NoAcceptanceCallback() [SkippableFact] public void List_PluginSource_Acceptance() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -1661,6 +1679,8 @@ public void List_PluginSource_Acceptance() [SkippableFact] public void List_HistoryAndPluginSource_Acceptance() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); @@ -1994,6 +2014,8 @@ public void List_HistoryAndPluginSource_Acceptance() [SkippableFact] public void List_HistoryAndPluginSource_Deduplication() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); int listWidth = CheckWindowSize(); var dimmedColors = Tuple.Create(ConsoleColor.White, _console.BackgroundColor); diff --git a/test/ListScrollableViewTest.cs b/test/ListScrollableViewTest.cs index 11452bb22..3452c4625 100644 --- a/test/ListScrollableViewTest.cs +++ b/test/ListScrollableViewTest.cs @@ -9,6 +9,8 @@ public partial class ReadLine [SkippableFact] public void List_MetaLine_And_Paging_Navigation() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + int listWidth = 100; TestSetup(new TestConsole(keyboardLayout: _, width: listWidth, height: 15), KeyMode.Cmd); @@ -514,6 +516,8 @@ public void List_MetaLine_And_Paging_Navigation() [SkippableFact] public void ListView_AdapteTo_ConsoleSize() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, // and the metadata line will be reduced to only show the (index/total) info. int listWidth = 50; @@ -850,6 +854,8 @@ public void ListView_AdapteTo_ConsoleSize() [SkippableFact] public void ListView_TermSize_Warning() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + // Console size is very small (h: 6, w: 50), and thus the list view will adjust to use 3-line height, // and the metadata line will be reduced to only show the (index/total) info. int listWidth = 40; diff --git a/test/ListViewTooltipTest.cs b/test/ListViewTooltipTest.cs index 3d93be47c..79a6ba592 100644 --- a/test/ListViewTooltipTest.cs +++ b/test/ListViewTooltipTest.cs @@ -9,6 +9,8 @@ public partial class ReadLine [SkippableFact] public void List_Item_Tooltip_4_Lines() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + // Set the terminal height to 22 and width to 60, so the metadata line will be fully rendered // and maximum 4 lines can be used for tooltip for a selected list item. int listWidth = 60; @@ -206,6 +208,8 @@ public void List_Item_Tooltip_4_Lines() [SkippableFact] public void List_Item_Tooltip_2_Lines() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + // Set the terminal height to 15 and width to 60, so the metadata line will be fully rendered // and maximum 2 lines can be used for tooltip for a selected list item. int listWidth = 60; @@ -363,6 +367,8 @@ public void List_Item_Tooltip_2_Lines() [SkippableFact] public void List_Item_Tooltip_1_Line() { + Skip.If(ScreenReaderModeEnabled, "List view is not supported in screen reader mode."); + // Set the terminal height to 6 and width to 60, so the metadata line will be fully rendered // and maximum 2 lines can be used for tooltip for a selected list item. int listWidth = 60; diff --git a/test/MockConsole.cs b/test/MockConsole.cs index ee0a7c363..990698671 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -300,6 +300,17 @@ public virtual void BlankRestOfLine() buffer[writePos + i].ForegroundColor = ForegroundColor; } } + + public virtual void BlankRestOfBuffer() + { + var writePos = CursorTop * BufferWidth + CursorLeft; + for (; writePos < buffer.Length; writePos++) + { + buffer[writePos].UnicodeChar = ' '; + buffer[writePos].BackgroundColor = BackgroundColor; + buffer[writePos].ForegroundColor = ForegroundColor; + } + } public virtual void Clear() { @@ -334,6 +345,7 @@ private static void ToggleNegative(TestConsole c, bool b) c.BackgroundColor = (ConsoleColor)((int)c.BackgroundColor ^ 7); c._negative = b; } + protected static readonly Dictionary> EscapeSequenceActions = new() { {"7", c => ToggleNegative(c, true) }, @@ -376,7 +388,8 @@ private static void ToggleNegative(TestConsole c, bool b) c.ForegroundColor = DefaultForeground; c.BackgroundColor = DefaultBackground; }}, - {"2J", c => c.SetCursorPosition(0, 0) } + { "0J", c => c.BlankRestOfBuffer() }, + { "2J", c => c.SetCursorPosition(0, 0) }, }; } @@ -520,6 +533,17 @@ public override void BlankRestOfLine() } } + public override void BlankRestOfBuffer() + { + var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft; + for (; writePos < buffer.Length; writePos++) + { + buffer[writePos].UnicodeChar = ' '; + buffer[writePos].BackgroundColor = BackgroundColor; + buffer[writePos].ForegroundColor = ForegroundColor; + } + } + public override void Clear() { _offset = 0; diff --git a/test/OptionsTest.cs b/test/OptionsTest.cs index 67e36538c..5f67de71c 100644 --- a/test/OptionsTest.cs +++ b/test/OptionsTest.cs @@ -11,6 +11,8 @@ public partial class ReadLine [SkippableFact] public void ContinuationPrompt() { + Skip.If(ScreenReaderModeEnabled, "Continuation prompt is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); Test("", Keys( @@ -55,6 +57,20 @@ public void ContinuationPrompt() )); } + [SkippableFact] + public void ContinuationPromptForScreenReader() + { + Skip.IfNot(ScreenReaderModeEnabled); + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "{\n}", + CheckThat(() => AssertScreenIs(2, TokenClassification.None, '{', NextLine, '}' )), + _.Ctrl_c, + InputAcceptedNow + )); + } + [SkippableFact] public void GetKeyHandlers() { diff --git a/test/RenderTest.cs b/test/RenderTest.cs index cb4943a50..cc72f367d 100644 --- a/test/RenderTest.cs +++ b/test/RenderTest.cs @@ -205,6 +205,8 @@ public void MultiLine() [SkippableFact] public void MultiLine_ScreenCheck() { + Skip.If(ScreenReaderModeEnabled, "Continuation prompt is not supported in screen reader mode."); + TestSetup(KeyMode.Cmd); var defaultContinuationPrompt = PSConsoleReadLineOptions.DefaultContinuationPrompt; @@ -305,6 +307,14 @@ public void InvokePrompt() CheckThat(() => AssertScreenIs(1, Tuple.Create(_console.ForegroundColor, _console.BackgroundColor), "PSREADLINE> ", TokenClassification.Command, "dir")))); + } + + [SkippableFact] + public void InvokeTrickyPrompt() + { + Skip.If(ScreenReaderModeEnabled, "We can't test the colors written in this prompt in screen reader mode."); + + TestSetup(KeyMode.Cmd, new KeyHandler("Ctrl+z", PSConsoleReadLine.InvokePrompt)); // Tricky prompt - writes to console directly with colors, uses ^H trick to eliminate trailing space. using (var ps = PowerShell.Create(RunspaceMode.CurrentRunspace)) diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 71b8a9434..81cae6c6d 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -130,6 +130,7 @@ protected ReadLine(ConsoleFixture fixture, ITestOutputHelper output, string lang internal virtual bool KeyboardHasGreaterThan => true; internal virtual bool KeyboardHasCtrlRBracket => true; internal virtual bool KeyboardHasCtrlAt => true; + internal virtual bool ScreenReaderModeEnabled => false; internal int ContinuationPromptLength => _continuationPromptLength; @@ -446,8 +447,13 @@ private void AssertScreenIs(int top, int lines, params object[] items) // that shouldn't be and aren't ever set by any code in PSReadLine, so we'll // ignore those bits and just check the stuff we do set. Assert.Equal(expectedBuffer[i].UnicodeChar, consoleBuffer[i].UnicodeChar); - Assert.Equal(expectedBuffer[i].ForegroundColor, consoleBuffer[i].ForegroundColor); - Assert.Equal(expectedBuffer[i].BackgroundColor, consoleBuffer[i].BackgroundColor); + if (!ScreenReaderModeEnabled) + { + // Changing colors is not supported in screen reader mode, + // and this is the simplest way to disable checking that in all the tests. + Assert.Equal(expectedBuffer[i].ForegroundColor, consoleBuffer[i].ForegroundColor); + Assert.Equal(expectedBuffer[i].BackgroundColor, consoleBuffer[i].BackgroundColor); + } } } @@ -585,6 +591,7 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] ContinuationPrompt = PSConsoleReadLineOptions.DefaultContinuationPrompt, DingDuration = 1, // Make tests virtually silent when they ding DingTone = 37, // Make tests virtually silent when they ding + EnableScreenReaderMode = ScreenReaderModeEnabled, ExtraPromptLineCount = PSConsoleReadLineOptions.DefaultExtraPromptLineCount, HistoryNoDuplicates = PSConsoleReadLineOptions.DefaultHistoryNoDuplicates, HistorySaveStyle = HistorySaveStyle.SaveNothing, @@ -637,7 +644,9 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] PSConsoleReadLine.SetOptions(colorOptions); // Cache the continuation prompt length for use in tests - _continuationPromptLength = PSConsoleReadLine.GetOptions().ContinuationPrompt.Length; + _continuationPromptLength = ScreenReaderModeEnabled + ? 0 + : PSConsoleReadLine.GetOptions().ContinuationPrompt.Length; if (!_oneTimeInitCompleted) { @@ -674,4 +683,14 @@ public fr_FR_Windows(ConsoleFixture fixture, ITestOutputHelper output) internal override bool KeyboardHasCtrlRBracket => false; internal override bool KeyboardHasCtrlAt => false; } + + public class ScreenReader : Test.ReadLine, IClassFixture + { + public ScreenReader(ConsoleFixture fixture, ITestOutputHelper output) + : base(fixture, output, "en-US", "windows") + { + } + + internal override bool ScreenReaderModeEnabled => true; + } } diff --git a/tools/helper.psm1 b/tools/helper.psm1 index a3ee18a87..a79532a8f 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -210,7 +210,14 @@ function Start-TestRun function RunXunitTestsInNewProcess ([string] $Layout, [string] $OperatingSystem) { - $filter = "FullyQualifiedName~Test.{0}_{1}" -f ($Layout -replace '-','_'), $OperatingSystem + $filter = if ($Layout) { + Write-Log "Testing $Layout on $OperatingSystem...`n" + "FullyQualifiedName~Test.{0}_{1}" -f ($Layout -replace '-','_'), $OperatingSystem + } else { + ## Today, tests for screen-reader mode only run on Windows with the 'en-US' layout. + Write-Log "Testing screen reader mode...`n" + "FullyQualifiedName~Test.ScreenReader" + } $testResultFile = "xUnitTestResults.{0}.xml" -f $Layout $testResultFile = Join-Path $testResultFolder $testResultFile @@ -263,7 +270,6 @@ function Start-TestRun { if (Test-Path "KeyInfo-${layout}-windows.json") { - Write-Log "Testing $layout ..." $null = [KeyboardLayoutHelper]::SetKeyboardLayout($layout) # We have to use Start-Process so it creates a new window, because the keyboard @@ -283,6 +289,7 @@ function Start-TestRun $null = [KeyboardLayoutHelper]::SetKeyboardLayout($savedLayout) } } + RunXunitTestsInNewProcess } else { From 1cdbf46c9ecabe5d2cbb5197cacc01c3cd1179f2 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:21:42 -0700 Subject: [PATCH 117/127] Fix `IsMutexPresent()` to avoid incorrect use of `GetLastWin32Error()` (#4910) --- PSReadLine/PlatformWindows.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index 32cf653fb..2bf72eb61 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Microsoft.PowerShell; using Microsoft.PowerShell.Internal; using Microsoft.Win32.SafeHandles; @@ -79,19 +80,18 @@ IntPtr templateFileWin32Handle [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr GetStdHandle(uint handleId); - internal const int ERROR_ALREADY_EXISTS = 0xB7; - internal static bool IsMutexPresent(string name) { try { - using var mutex = new System.Threading.Mutex(false, name); - return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS; - } - catch - { - return false; + if (Mutex.TryOpenExisting(name, out var tempMutex)) + { + tempMutex.Dispose(); + return true; + } } + catch { } + return false; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] From b5aac30d26ce777497e85203dc59989296b73f8e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 28 Aug 2025 15:32:25 -0700 Subject: [PATCH 118/127] Prepare for the v2.4.4-beta4 release of PSReadLine --- PSReadLine/Changes.txt | 7 +++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 919f92154..9f949b617 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,10 @@ +### [2.4.4-beta4] - 2025-08-28 + +- Fix `IsMutexPresent()` to avoid incorrect use of `GetLastWin32Error()` (#4910) +- Add screen reader support to PSReadLine (#4854) + +[2.4.4-beta4]: https://github.com/PowerShell/PSReadLine/compare/v2.4.3-beta3...v2.4.4-beta4 + ### [2.4.3-beta3] - 2025-07-23 - Allow accepting the current input automatically from within an `OnIdle` event handler (#4830) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 01576b652..c52fab081 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.3.0 - 2.4.3 - 2.4.3-beta3 + 2.4.4.0 + 2.4.4 + 2.4.4-beta4 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 53d70cfd3..f0068012a 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.3' +ModuleVersion = '2.4.4' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From d642cc6026f0749190eafbf93d40d9f22a0d7f52 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:01:22 +0100 Subject: [PATCH 119/127] Replace `DOTNET_SKIP_FIRST_TIME_EXPERIENCE` with `DOTNET_NOLOGO` (#4916) --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e989afe08..0bc286805 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,8 +3,7 @@ image: Visual Studio 2022 environment: POWERSHELL_TELEMETRY_OPTOUT: 1 - # Avoid expensive initialization of dotnet cli, see: http://donovanbrown.com/post/Stop-wasting-time-during-NET-Core-builds - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 PSREADLINE_TESTRUN: 1 cache: From 4a724547a863f2915558ee58ac0bc0d017c35ecc Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 14 Oct 2025 12:24:07 -0700 Subject: [PATCH 120/127] Add the `ScreenReaderModeEnabled` property to formatting (#4970) Add the `ScreenReaderModeEnabled` property to formatting so it's displayed in the default formatting for `Get-PSReadLineOption`. --- PSReadLine/PSReadLine.format.ps1xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 24f70799a..62867e762 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -144,6 +144,9 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) ShowToolTips + + ScreenReaderModeEnabled + ViModeIndicator From b6c3e4810c681b0610932571e18004d17524fda6 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 16 Oct 2025 10:25:03 -0700 Subject: [PATCH 121/127] Fix a null reference exception when showing parameter help (#4971) --- PSReadLine/DynamicHelp.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index bbd6bf70e..a43e8f6a5 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -67,7 +67,6 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam .AddParameter("Parameter", parameterName) .Invoke() .FirstOrDefault(); - } catch (Exception) { @@ -192,11 +191,13 @@ private void WriteDynamicHelpBlock(Collection helpBlock) private void WriteParameterHelp(dynamic helpContent) { + System.Diagnostics.Debug.Assert(helpContent is not null); + Collection helpBlock; - if (helpContent?.Description is not string descriptionText) + if (helpContent.Description is not string descriptionText) { - descriptionText = helpContent?.Description?[0]?.Text; + descriptionText = helpContent.Description?[0]?.Text; } if (descriptionText is null) @@ -209,7 +210,7 @@ private void WriteParameterHelp(dynamic helpContent) } else { - string syntax = $"-{helpContent.name} <{helpContent.type.name}>"; + string syntax = $"-{helpContent.name} <{helpContent.type?.name}>"; helpBlock = new Collection { string.Empty, From 98d5610f7210924b79f553d982e10a9bd64dd52f Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 22 Oct 2025 12:12:04 -0700 Subject: [PATCH 122/127] Prepare for the v2.4.5 release of PSReadLine --- PSReadLine/Changes.txt | 8 ++++++++ PSReadLine/PSReadLine.csproj | 6 +++--- PSReadLine/PSReadLine.psd1 | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 9f949b617..0b99add82 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,11 @@ +### [2.4.5] - 2025-10-22 + +- Replace `DOTNET_SKIP_FIRST_TIME_EXPERIENCE` with `DOTNET_NOLOGO` (#4916) (Thanks @xtqqczze!) +- Add the `ScreenReaderModeEnabled` property to formatting (#4970) +- Fix a null reference exception when showing parameter help (#4971) + +[2.4.5]: https://github.com/PowerShell/PSReadLine/compare/v2.4.4-beta4...v2.4.5 + ### [2.4.4-beta4] - 2025-08-28 - Fix `IsMutexPresent()` to avoid incorrect use of `GetLastWin32Error()` (#4910) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index c52fab081..f67e29e4c 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.4.0 - 2.4.4 - 2.4.4-beta4 + 2.4.5.0 + 2.4.5 + 2.4.5 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index f0068012a..0690e85cc 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.4' +ModuleVersion = '2.4.5' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' From e563a2fc158eb732d787ea70ee45da4507f57791 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:05:10 -0600 Subject: [PATCH 123/127] Migrate PSReadLine to target .NET 8.0 (#5055) --- .vscode/launch.json | 2 +- MockPSConsole/MockPSConsole.csproj | 10 +- PSReadLine.build.ps1 | 57 +++---- PSReadLine/Cmdlets.cs | 2 +- PSReadLine/OnImportAndRemove.cs | 37 ----- PSReadLine/PSReadLine.csproj | 17 ++- PSReadLine/PSReadLine.psd1 | 4 +- Polyfill/CommandPrediction.cs | 232 ----------------------------- Polyfill/Polyfill.csproj | 22 --- appveyor.yml | 2 +- build.ps1 | 20 +-- global.json | 5 + test/InlinePredictionTest.cs | 2 +- test/PSReadLine.Tests.csproj | 11 +- tools/helper.psm1 | 17 ++- 15 files changed, 62 insertions(+), 378 deletions(-) delete mode 100644 PSReadLine/OnImportAndRemove.cs delete mode 100644 Polyfill/CommandPrediction.cs delete mode 100644 Polyfill/Polyfill.csproj create mode 100644 global.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c01006234..f15ddc52f 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/net8.0/PSReadLine.psd1'" ], "console": "integratedTerminal", "justMyCode": false, diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index cebd3941d..d0f91fa71 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -4,18 +4,14 @@ Exe MockPSConsole MockPSConsole - net472;net6.0 + net8.0 512 Program.manifest true - - - - - - + + diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index e245dfae3..4d54be206 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -19,46 +19,37 @@ param( [ValidateSet("Debug", "Release")] [string]$Configuration = (property Configuration Release), - [ValidateSet("net472", "net6.0")] - [string]$TestFramework, - [switch]$CheckHelpContent ) Import-Module "$PSScriptRoot/tools/helper.psm1" -# Final bits to release go here -$targetDir = "bin/$Configuration/PSReadLine" +# Dynamically read target framework from project file +$csprojPath = "$PSScriptRoot/PSReadLine/PSReadLine.csproj" +[xml]$csproj = Get-Content $csprojPath +$targetFramework = $csproj.Project.PropertyGroup.TargetFramework | Where-Object { $_ } | Select-Object -First 1 -if (-not $TestFramework) { - $TestFramework = $IsWindows ? "net472" : "net6.0" +if (-not $targetFramework) { + throw "Could not determine TargetFramework from $csprojPath" } +Write-Verbose "Target framework: $targetFramework" + +# Final bits to release go here +$targetDir = "bin/$Configuration/PSReadLine" + function ConvertTo-CRLF([string] $text) { $text.Replace("`r`n","`n").Replace("`n","`r`n") } -$polyFillerParams = @{ - Inputs = { Get-ChildItem Polyfill/*.cs, Polyfill/Polyfill.csproj } - Outputs = "Polyfill/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" -} - $binaryModuleParams = @{ - Inputs = { Get-ChildItem PSReadLine/*.cs, PSReadLine/PSReadLine.csproj, PSReadLine/PSReadLineResources.resx, Polyfill/*.cs, Polyfill/Polyfill.csproj } - Outputs = "PSReadLine/bin/$Configuration/netstandard2.0/Microsoft.PowerShell.PSReadLine.dll" + Inputs = { Get-ChildItem PSReadLine/*.cs, PSReadLine/PSReadLine.csproj, PSReadLine/PSReadLineResources.resx } + Outputs = "PSReadLine/bin/$Configuration/$targetFramework/Microsoft.PowerShell.PSReadLine.dll" } $xUnitTestParams = @{ Inputs = { Get-ChildItem test/*.cs, test/*.json, test/PSReadLine.Tests.csproj } - Outputs = "test/bin/$Configuration/$TestFramework/PSReadLine.Tests.dll" -} - -<# -Synopsis: Build the Polyfiller assembly -#> -task BuildPolyfiller @polyFillerParams { - exec { dotnet publish -c $Configuration -f 'netstandard2.0' Polyfill } - exec { dotnet publish -c $Configuration -f 'net6.0' Polyfill } + Outputs = "test/bin/$Configuration/$targetFramework/PSReadLine.Tests.dll" } <# @@ -72,15 +63,15 @@ task BuildMainModule @binaryModuleParams { Synopsis: Build xUnit tests #> task BuildXUnitTests @xUnitTestParams { - exec { dotnet publish -f $TestFramework -c $Configuration test } + exec { dotnet publish -f $targetFramework -c $Configuration test } } <# Synopsis: Run the unit tests #> task RunTests BuildMainModule, BuildXUnitTests, { - Write-Verbose "Run tests targeting '$TestFramework' ..." - Start-TestRun -Configuration $Configuration -Framework $TestFramework + Write-Verbose "Run tests targeting $targetFramework ..." + Start-TestRun -Configuration $Configuration -Framework $targetFramework } <# @@ -97,7 +88,7 @@ task CheckHelpContent -If $CheckHelpContent { <# Synopsis: Copy all of the files that belong in the module to one place in the layout for installation #> -task LayoutModule BuildPolyfiller, BuildMainModule, { +task LayoutModule BuildMainModule, { if (-not (Test-Path $targetDir -PathType Container)) { New-Item $targetDir -ItemType Directory -Force > $null } @@ -115,17 +106,7 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { Set-Content -Path (Join-Path $targetDir (Split-Path $file -Leaf)) -Value (ConvertTo-CRLF $content) -Force } - if (-not (Test-Path "$targetDir/netstd")) { - New-Item "$targetDir/netstd" -ItemType Directory -Force > $null - } - if (-not (Test-Path "$targetDir/net6plus")) { - New-Item "$targetDir/net6plus" -ItemType Directory -Force > $null - } - - 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/$targetFramework/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 5633a69af..596b1548e 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -178,7 +178,7 @@ static PSConsoleReadLineOptions() // Our tests expect that the default inline-view color is set to the new color, so we configure // the color based on system environment only if we are not in test runs. - if (AppDomain.CurrentDomain.FriendlyName is not "PSReadLine.Tests") + if (AppDomain.CurrentDomain.FriendlyName is not "testhost") { DefaultInlinePredictionColor = Environment.OSVersion.Version.Build >= 22621 // on Windows 11 22H2 or newer versions diff --git a/PSReadLine/OnImportAndRemove.cs b/PSReadLine/OnImportAndRemove.cs deleted file mode 100644 index 70208420a..000000000 --- a/PSReadLine/OnImportAndRemove.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.IO; -using System.Management.Automation; -using System.Reflection; - -namespace Microsoft.PowerShell.PSReadLine -{ - public class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup - { - public void OnImport() - { - AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; - } - - public void OnRemove(PSModuleInfo module) - { - AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly; - } - - /// - /// Load the correct 'Polyfiller' assembly based on the runtime. - /// - private static Assembly ResolveAssembly(object sender, ResolveEventArgs args) - { - if (args.Name != "Microsoft.PowerShell.PSReadLine.Polyfiller, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") - { - return null; - } - - string root = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location); - string subd = (Environment.Version.Major >= 6) ? "net6plus" : "netstd"; - string path = Path.Combine(root, subd, "Microsoft.PowerShell.PSReadLine.Polyfiller.dll"); - - return Assembly.LoadFrom(path); - } - } -} diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index f67e29e4c..edb39c91b 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,21 +5,22 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.5.0 - 2.4.5 - 2.4.5 + 3.0.0.0 + 3.0.0 + 3.0.0 true - netstandard2.0 + net8.0 true false 9.0 - - - - + + contentFiles + All + + diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 0690e85cc..4ce7eff73 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,13 +1,13 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.5' +ModuleVersion = '3.0.0' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' Copyright = '(c) Microsoft Corporation. All rights reserved.' Description = 'Great command line editing in the PowerShell console host' -PowerShellVersion = '5.1' +PowerShellVersion = '7.4' FormatsToProcess = 'PSReadLine.format.ps1xml' AliasesToExport = @() FunctionsToExport = 'PSConsoleHostReadLine' diff --git a/Polyfill/CommandPrediction.cs b/Polyfill/CommandPrediction.cs deleted file mode 100644 index e7b331a8b..000000000 --- a/Polyfill/CommandPrediction.cs +++ /dev/null @@ -1,232 +0,0 @@ -#if LEGACY - -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Management.Automation.Language; - -namespace System.Management.Automation.Subsystem.Prediction -{ - /// - /// Kinds of prediction clients. - /// - public enum PredictionClientKind - { - /// - /// A terminal client, representing the command-line experience. - /// - Terminal, - - /// - /// An editor client, representing the editor experience. - /// - Editor, - } - - /// - /// The class represents a client that interacts with predictors. - /// - public sealed class PredictionClient - { - /// - /// Gets the client name. - /// - [HiddenAttribute] - public string Name { get; } - - /// - /// Gets the client kind. - /// - [HiddenAttribute] - public PredictionClientKind Kind { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Name of the interactive client. - /// Kind of the interactive client. - [HiddenAttribute] - public PredictionClient(string name, PredictionClientKind kind) - { - Name = name; - Kind = kind; - } - } - - /// - /// The class represents the prediction result from a predictor. - /// - public sealed class PredictionResult - { - /// - /// Gets the Id of the predictor. - /// - [HiddenAttribute] - public Guid Id { get; } - - /// - /// Gets the name of the predictor. - /// - [HiddenAttribute] - public string Name { get; } - - /// - /// Gets the mini-session id that represents a specific invocation that returns this result. - /// When it's not specified, it's considered by a client that the predictor doesn't expect feedback. - /// - [HiddenAttribute] - public uint? Session { get; } - - /// - /// Gets the suggestions. - /// - [HiddenAttribute] - public IReadOnlyList Suggestions { get; } - - internal PredictionResult(Guid id, string name, uint? session, List suggestions) - { - Id = id; - Name = name; - Session = session; - Suggestions = suggestions; - } - } - - /// - /// The class represents a predictive suggestion generated by a predictor. - /// - public sealed class PredictiveSuggestion - { - /// - /// Gets the suggestion. - /// - [HiddenAttribute] - public string SuggestionText { get; } - - /// - /// Gets the tooltip of the suggestion. - /// - [HiddenAttribute] - public string ToolTip { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The predictive suggestion text. - [HiddenAttribute] - public PredictiveSuggestion(string suggestion) - : this(suggestion, toolTip: null) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The predictive suggestion text. - /// The tooltip of the suggestion. - [HiddenAttribute] - public PredictiveSuggestion(string suggestion, string toolTip) - { - if (string.IsNullOrEmpty(suggestion)) - { - throw new ArgumentNullException(nameof(suggestion)); - } - - SuggestionText = suggestion; - ToolTip = toolTip; - } - } - - /// - /// Provides a set of possible predictions for given input. - /// - public static class CommandPrediction - { - /// - /// Collect the predictive suggestions from registered predictors using the default timeout. - /// - /// Represents the client that initiates the call. - /// The object from parsing the current command line input. - /// The objects from parsing the current command line input. - /// A list of objects. - [HiddenAttribute] - public static Task> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens) - { - return null; - } - - /// - /// Collect the predictive suggestions from registered predictors using the specified timeout. - /// - /// Represents the client that initiates the call. - /// The object from parsing the current command line input. - /// The objects from parsing the current command line input. - /// The milliseconds to timeout. - /// A list of objects. - [HiddenAttribute] - public static Task> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) - { - return null; - } - - /// - /// Allow registered predictors to do early processing when a command line is accepted. - /// - /// Represents the client that initiates the call. - /// History command lines provided as references for prediction. - [HiddenAttribute] - public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) - { - } - - /// - /// Allow registered predictors to know the execution result (success/failure) of the last accepted command line. - /// - /// Represents the client that initiates the call. - /// The last accepted command line. - /// Whether the execution of the last command line was successful. - [HiddenAttribute] - public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) - { - } - - /// - /// Send feedback to a predictor when one or more suggestions from it were displayed to the user. - /// - /// Represents the client that initiates the call. - /// The identifier of the predictor whose prediction result was accepted. - /// The mini-session where the displayed suggestions came from. - /// - /// When the value is > 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. - /// When the value is <= 0, it means a single suggestion from the list got displayed, and the index is the absolute value. - /// - [HiddenAttribute] - public static void OnSuggestionDisplayed(PredictionClient client, Guid predictorId, uint session, int countOrIndex) - { - } - - /// - /// Send feedback to predictors about their last suggestions. - /// - /// Represents the client that initiates the call. - /// The identifier of the predictor whose prediction result was accepted. - /// The mini-session where the accepted suggestion came from. - /// The accepted suggestion text. - [HiddenAttribute] - public static void OnSuggestionAccepted(PredictionClient client, Guid predictorId, uint session, string suggestionText) - { - } - } -} - -#else - -using System.Management.Automation.Subsystem.Prediction; -using System.Runtime.CompilerServices; - -[assembly: TypeForwardedTo(typeof(PredictionClientKind))] -[assembly: TypeForwardedTo(typeof(PredictionClient))] -[assembly: TypeForwardedTo(typeof(PredictiveSuggestion))] -[assembly: TypeForwardedTo(typeof(PredictionResult))] -[assembly: TypeForwardedTo(typeof(CommandPrediction))] - -#endif diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj deleted file mode 100644 index 2cdccfbba..000000000 --- a/Polyfill/Polyfill.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Microsoft.PowerShell.PSReadLine.Polyfiller - 1.0.0.0 - netstandard2.0;net6.0 - true - - - - - - - - - - - - $(DefineConstants);LEGACY - - - diff --git a/appveyor.yml b/appveyor.yml index 0bc286805..84ed4dce3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ build_script: ./build.ps1 -Configuration Release test_script: - - pwsh: ./build.ps1 -Test -Configuration Release -Framework net472 + - pwsh: ./build.ps1 -Test -Configuration Release artifacts: - path: .\bin\Release\PSReadLine.zip diff --git a/build.ps1 b/build.ps1 index 481f2a271..8871b2179 100644 --- a/build.ps1 +++ b/build.ps1 @@ -5,18 +5,20 @@ A script that provides simple entry points for bootstrapping, building and testing. .DESCRIPTION A script to make it easy to bootstrap, build and run tests. + This build targets .NET 8.0, which is the runtime for PowerShell 7.4 LTS. + PowerShell 7.4 LTS is supported until November 2026. .EXAMPLE PS > .\build.ps1 -Bootstrap Check and install prerequisites for the build. .EXAMPLE PS > .\build.ps1 -Configuration Release - Build the main module with 'Release' configuration targeting 'netstandard2.0'. + Build the project in Release configuration. .EXAMPLE PS > .\build.ps1 - Build the main module with the default configuration (Debug) targeting 'netstandard2.0'. + Build the main module with the default configuration (Debug) configuration. .EXAMPLE PS > .\build.ps1 -Test - Run xUnit tests with the default configuration (Debug) and the default target framework (net472 on Windows or net6.0 otherwise). + Run xUnit tests with the default configuration. .PARAMETER Clean Clean the local repo, but keep untracked files. .PARAMETER Bootstrap @@ -25,13 +27,6 @@ Run tests. .PARAMETER Configuration The configuration setting for the build. The default value is 'Debug'. -.PARAMETER Framework - The target framework when testing: - - net472: run tests with .NET Framework - - net6.0: run tests with .NET 6.0 - When not specified, the target framework is determined by the current OS platform: - - use 'net472' on Windows - - use 'net6.0' on Unix platforms #> [CmdletBinding(DefaultParameterSetName = 'default')] param( @@ -47,10 +42,6 @@ param( [Parameter(ParameterSetName = 'test')] [switch] $CheckHelpContent, - [Parameter(ParameterSetName = 'test')] - [ValidateSet("net472", "net6.0")] - [string] $Framework, - [Parameter(ParameterSetName = 'default')] [Parameter(ParameterSetName = 'test')] [ValidateSet("Debug", "Release")] @@ -93,7 +84,6 @@ if (-not (Get-Module -Name InvokeBuild -ListAvailable)) { $buildTask = if ($Test) { "RunTests" } else { "ZipRelease" } $arguments = @{ Task = $buildTask; Configuration = $Configuration } -if ($Framework) { $arguments.Add("TestFramework", $Framework) } if ($CheckHelpContent) { $arguments.Add("CheckHelpContent", $true) } Invoke-Build @arguments diff --git a/global.json b/global.json new file mode 100644 index 000000000..d23a6248a --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "8.0.415" + } +} diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index febf0ac1f..db6145164 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -638,7 +638,7 @@ public void Inline_HistoryAndPluginSource_Acceptance() Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId); Assert.Null(_mockedMethods.acceptedSuggestion); Assert.NotNull(_mockedMethods.commandHistory); - Assert.Equal(1, _mockedMethods.commandHistory.Count); + Assert.Single(_mockedMethods.commandHistory); Assert.Equal("netsh show me", _mockedMethods.commandHistory[0]); _mockedMethods.ClearPredictionFields(); diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index 34598cbe9..3cf7aee80 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -5,7 +5,7 @@ library UnitTestPSReadLine PSReadLine.Tests - net472;net6.0 + net8.0 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} False @@ -14,13 +14,8 @@ 9.0 - - - - - - - + + diff --git a/tools/helper.psm1 b/tools/helper.psm1 index a79532a8f..56776ea9a 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -1,9 +1,13 @@ -$MinimalSDKVersion = '6.0.425' $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" } +# Read the required SDK version from global.json +$globalJsonPath = Join-Path $RepoRoot "global.json" +$globalJson = Get-Content -Path $globalJsonPath -Raw | ConvertFrom-Json +$MinimalSDKVersion = $globalJson.sdk.version + <# .SYNOPSIS Get the path of the currently running powershell executable. @@ -54,7 +58,7 @@ function Test-DotnetSDK if (Test-Path $dotnetExePath) { $installedVersion = & $dotnetExePath --version - return $installedVersion -ge $MinimalSDKVersion + return [Version]::new($installedVersion) -ge [Version]::new($MinimalSDKVersion) } return $false } @@ -211,13 +215,15 @@ function Start-TestRun function RunXunitTestsInNewProcess ([string] $Layout, [string] $OperatingSystem) { $filter = if ($Layout) { - Write-Log "Testing $Layout on $OperatingSystem...`n" + Write-Log "`nTesting $Layout on $OperatingSystem..." "FullyQualifiedName~Test.{0}_{1}" -f ($Layout -replace '-','_'), $OperatingSystem } else { ## Today, tests for screen-reader mode only run on Windows with the 'en-US' layout. - Write-Log "Testing screen reader mode...`n" + $Layout = "screen-reader" + Write-Log "`nTesting $Layout mode..." "FullyQualifiedName~Test.ScreenReader" } + $testResultFile = "xUnitTestResults.{0}.xml" -f $Layout $testResultFile = Join-Path $testResultFolder $testResultFile @@ -279,7 +285,7 @@ function Start-TestRun } else { - Write-Log "Testing not supported for the keyboard layout '$layout'." + Write-Log "`nTesting not supported for the keyboard layout '$layout'." } } } @@ -289,6 +295,7 @@ function Start-TestRun $null = [KeyboardLayoutHelper]::SetKeyboardLayout($savedLayout) } } + RunXunitTestsInNewProcess } else From 554035e04f8908e33a41a80150dea28f35ff7d2f Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 10 Feb 2026 15:24:48 -0500 Subject: [PATCH 124/127] Ensure CodeQL scan is published to database (#5085) --- .pipelines/PSReadLine-Official.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pipelines/PSReadLine-Official.yml b/.pipelines/PSReadLine-Official.yml index 3aa8748b4..db0f855cb 100644 --- a/.pipelines/PSReadLine-Official.yml +++ b/.pipelines/PSReadLine-Official.yml @@ -125,6 +125,7 @@ extends: ob_restore_phase: true inputs: Enabled: true + AnalyzeInPipeline: false Language: csharp - pwsh: | From e266ca6f67ab081476a0990e36e67ab4f2554e2b Mon Sep 17 00:00:00 2001 From: Snowy Date: Thu, 12 Mar 2026 04:23:22 +0800 Subject: [PATCH 125/127] Fix BackwardKillLine length calculation for multiline input (#5102) --- PSReadLine/KillYank.cs | 2 +- test/KillYankTest.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/PSReadLine/KillYank.cs b/PSReadLine/KillYank.cs index 77fe33f1b..c282b1489 100644 --- a/PSReadLine/KillYank.cs +++ b/PSReadLine/KillYank.cs @@ -135,7 +135,7 @@ public static void BackwardKillInput(ConsoleKeyInfo? key = null, object arg = nu public static void BackwardKillLine(ConsoleKeyInfo? key = null, object arg = null) { var start = GetBeginningOfLinePos(_singleton._current); - _singleton.Kill(start, _singleton._current, true); + _singleton.Kill(start, _singleton._current - start, true); } /// diff --git a/test/KillYankTest.cs b/test/KillYankTest.cs index 381e7889e..5c631d5e2 100644 --- a/test/KillYankTest.cs +++ b/test/KillYankTest.cs @@ -119,6 +119,8 @@ public void BackwardKillLine() PSConsoleReadLine.SetKeyHandler(new[] { "Shift+Tab" }, PSConsoleReadLine.BackwardKillLine, "", ""); Test("", Keys("dir", _.Shift_Tab)); + + Test("abc\n123", Keys("abc", _.Shift_Enter, "123", _.Shift_Tab, _.Ctrl_y)); } [SkippableFact] From caa53f8469e35f6ba6e1a434d4fc8efae2dde20c Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:40:01 -0700 Subject: [PATCH 126/127] Update VSCode tasks for rewritten build script (#5118) --- .vscode/tasks.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 04326ed55..4393c8736 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -34,9 +34,7 @@ "command": "pwsh", "args": [ "./build.ps1", - "-Test", - "-Framework", - "${input:framework}" + "-Test" ], "group": { "kind": "test", @@ -47,7 +45,7 @@ "panel": "dedicated", "clear": true }, - "detail": "Run unit tests with selected framework" + "detail": "Run unit tests" }, { "label": "Clean", @@ -60,17 +58,5 @@ "group": "build", "detail": "Clean build artifacts" } - ], - "inputs": [ - { - "id": "framework", - "description": "Target Framework", - "type": "pickString", - "options": [ - "net472", - "net6.0" - ], - "default": "net6.0" - } ] } From 2984546f62da9f63c31aba965ec3008fcb107024 Mon Sep 17 00:00:00 2001 From: sharpchen <77432836+sharpchen@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:38:08 +0000 Subject: [PATCH 127/127] Fix `ViFindBrace` to search for brace the same as `vim` when current char is not a brace (#4862) --- PSReadLine/Movement.vi.cs | 27 +++++++++++++++++- test/MovementTest.VI.cs | 59 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/PSReadLine/Movement.vi.cs b/PSReadLine/Movement.vi.cs index 011c146d9..542a37b7d 100644 --- a/PSReadLine/Movement.vi.cs +++ b/PSReadLine/Movement.vi.cs @@ -254,7 +254,32 @@ private int ViFindBrace(int i) case ')': return ViFindBackward(i, '(', withoutPassing: ')'); default: - return i; + ReadOnlySpan parentheses = stackalloc char[] { '{', '}', '(', ')', '[', ']' }; + int nextParen = i; + // find next of any kind of paren + for (; nextParen < _buffer.Length; nextParen++) + for (int idx = 0; idx < parentheses.Length; idx++) + if (parentheses[idx] == _buffer[nextParen]) goto Outer; + + // if not found, nextParen could exceed the range + if (nextParen >= _buffer.Length) + return i; + + Outer: + int match = _buffer[nextParen] switch + { + // if next is opening, find forward + '{' => ViFindForward(nextParen, '}', withoutPassing: '{'), + '[' => ViFindForward(nextParen, ']', withoutPassing: '['), + '(' => ViFindForward(nextParen, ')', withoutPassing: '('), + // if next is closing, find backward + '}' => ViFindBackward(nextParen, '{', withoutPassing: '}'), + ']' => ViFindBackward(nextParen, '[', withoutPassing: ']'), + ')' => ViFindBackward(nextParen, '(', withoutPassing: ')'), + _ => nextParen + }; + + return match == nextParen ? i : match; } } diff --git a/test/MovementTest.VI.cs b/test/MovementTest.VI.cs index 3c4573ebe..27dfe7956 100644 --- a/test/MovementTest.VI.cs +++ b/test/MovementTest.VI.cs @@ -370,7 +370,7 @@ public void ViGlobMovement_EmptyBuffer_Defect1195() TestSetup(KeyMode.Vi); TestMustDing("", Keys( - _.Escape, "W" + _.Escape, "W" )); } @@ -423,6 +423,11 @@ public void ViCursorMovement() [SkippableFact] public void ViGotoBrace() { + // NOTE: When the input has unmatched braces, in order to avoid an + // exception caused by AcceptLineImpl waiting for incomplete input, + // the test needs to end with the Vi command "ddi" and assert that + // the result is an empty string. + TestSetup(KeyMode.Vi); Test("0[2(4{6]8)a}c", Keys( @@ -450,10 +455,60 @@ public void ViGotoBrace() CheckThat(() => AssertCursorLeftIs(4)), _.Percent, CheckThat(() => AssertCursorLeftIs(4)), - "ddi" + "ddi" // Unmatched brace )); } + // Tests when the cursor is not on any paren + foreach (var (opening, closing) in new[] { ('(', ')'), ('{', '}'), ('[', ']') }) + { + // Closing paren with backward match + string input1 = $"0{opening}2{opening}4foo{closing}"; + Test("", Keys( + input1, + CheckThat(() => AssertCursorLeftIs(9)), + _.Escape, CheckThat(() => AssertCursorLeftIs(8)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(3)), + _.Percent, CheckThat(() => AssertCursorLeftIs(8)), + "ddi" // Unmatched closing brace + )); + + // Closing paren without backward match + string input2 = $"0]2)4foo{closing}"; + TestMustDing(input2, Keys( + input2, + CheckThat(() => AssertCursorLeftIs(9)), + _.Escape, CheckThat(() => AssertCursorLeftIs(8)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), // stay still + _.Percent, CheckThat(() => AssertCursorLeftIs(5)) + )); + + // Opening paren with forward match + string input3 = $"0{opening}2foo6{closing}"; + Test(input3, Keys( + input3, + CheckThat(() => AssertCursorLeftIs(8)), + _.Escape, CheckThat(() => AssertCursorLeftIs(7)), + "0ff", CheckThat(() => AssertCursorLeftIs(3)), + _.Percent, CheckThat(() => AssertCursorLeftIs(1)), + _.Percent, CheckThat(() => AssertCursorLeftIs(7)) + )); + + // Opening paren without forward match + string input4 = $"0)2]4foo{opening}("; + TestMustDing("", Keys( + input4, + CheckThat(() => AssertCursorLeftIs(10)), + _.Escape, CheckThat(() => AssertCursorLeftIs(9)), + "0ff", CheckThat(() => AssertCursorLeftIs(5)), + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), // stay still + _.Percent, CheckThat(() => AssertCursorLeftIs(5)), + "ddi" // Unmatched brace + )); + } + // <%> with empty text buffer should work fine. Test("", Keys( _.Escape, _.Percent,