PowerShellのタブ補完をPopupMenuに拡張してみる
最近事情により手元のWin7機のPowerShellを3.0に更新しました。
3.0ではクラス名を補完できるのですね。
Tab補完を先日作ったShow-PopupMenuを使って、PopupMenuにします。
スクリプト
Show-PopupMenuに依存しています。
PowerShellでポップアップメニューを表示する - じゅんじゅんのきまぐれ
このため、System.Windows.Formsがロードされます。
また、必須ではありませんが[June.Win32Api]も追加されます。
この二点が不可逆です。
PowerShell 3.0からは、タブ補完はTabExpansion関数ではなく、TabExpansion2になっているようです。
これを容赦なく更新してしまいます。
元のTabExpansion2が気になる方は、「$function:TabExpansion2」を見ておいてください。
function TabExpansion2() { [CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')] Param( [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)] [string] $inputScript, [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)] [int] $cursorColumn, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)] [System.Management.Automation.Language.Ast] $ast, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)] [System.Management.Automation.Language.Token[]] $tokens, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)] [System.Management.Automation.Language.IScriptPosition] $positionOfCursor, [Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)] [Parameter(ParameterSetName = 'AstInputSet', Position = 3)] [Hashtable] $options = $null ) Begin { if(!('June.Win32Api' -as [type])) { Add-Type -TypeDefinition @" using System; using System.Diagnostics; using System.Runtime.InteropServices; namespace June { public class Win32Api { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; public int X { get { return Left; } set { Right += value - Left; Left = value; } } public int Y { get { return Top; } set { Bottom += value - Top; Top = value; } } public int Width { get { return Right - Left; } set { Right = Left + value; } } public int Height { get { return Bottom - Top; } set { Bottom = Top + value; } } } [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; public POINT(int x, int y) { this.X = x; this.Y = y; } } [DllImport("user32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [DllImport("user32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); [DllImport("user32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ScreenToClient(IntPtr hwnd, out POINT lpPoint); [DllImport("user32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ClientToScreen(IntPtr hwnd, out POINT lpPoint); public static RECT? GetWindowRect(IntPtr hwnd, out int lastError) { RECT? ret = null; RECT res = new RECT(); if(GetWindowRect(hwnd, out res)) { lastError = 0; ret = res; } else { lastError = Marshal.GetLastWin32Error(); } return ret; } public static RECT? GetWindowRect(IntPtr hwnd) { int lastError; return GetWindowRect(hwnd, out lastError); } public static RECT? GetMainWindowRect(int pid, out int lastError) { return GetWindowRect(Process.GetProcessById(pid).MainWindowHandle, out lastError); } public static RECT? GetMainWindowRect(int pid) { int lastError; return GetMainWindowRect(pid, out lastError); } public static RECT? GetClientRect(IntPtr hwnd, out int lastError) { RECT? ret = null; RECT res = new RECT(); POINT pos = new POINT(); if(GetClientRect(hwnd, out res) && ClientToScreen(hwnd, out pos)) { res.X = pos.X; res.Y = pos.Y; lastError = 0; ret = res; } else { lastError = Marshal.GetLastWin32Error(); } return ret; } public static RECT? GetClientRect(IntPtr hwnd) { int lastError; return GetClientRect(hwnd, out lastError); } public static RECT? GetMainClientRect(int pid, out int lastError) { return GetClientRect(Process.GetProcessById(pid).MainWindowHandle, out lastError); } public static RECT? GetMainClientRect(int pid) { int lastError; return GetMainClientRect(pid, out lastError); } } } "@} } End { $ret = $null if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet') { $ret = [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $cursorColumn, $options) } else { $ret = [System.Management.Automation.CommandCompletion]::CompleteInput($ast, $tokens, $positionOfCursor, $options) } # 補完候補が複数なら編集する if($ret -ne $null -and $ret.CompletionMatches.Count -gt 1) { # 補完候補を取り出す $ss = @() $ret.CompletionMatches | %{ $ss += $_.CompletionText } # PopupMenuの表示位置を算出する $x = [int]::MinValue $y = [int]::MinValue if('June.Win32Api' -as [type]) { $rect = [June.Win32Api]::GetMainClientRect($PID) if($rect -ne $null) { $ru = $Host.UI.RawUI $x = $rect.X + ($ru.CursorPosition.X - $ru.WindowPosition.X - $ret.ReplacementLength) * ($rect.Width / $ru.WindowSize.Width) $y = $rect.Y + ($ru.CursorPosition.Y - $ru.WindowPosition.Y + 1) * ($rect.Height / $ru.WindowSize.Height) } } # ポップアップ! $s = Show-PopupMenu $ss -x $x -y $y if($s.Length -gt 0) { # 結果が選択されたなら、それだけにする $ret.CompletionMatches | %{ if($s -eq $_.CompletionText) { $s = $_ } } $ret.CompletionMatches.Clear() $ret.CompletionMatches.Add($s) } elseif($psCmdlet.ParameterSetName -eq 'ScriptInputSet' -and $ret.ReplacementLength -gt 0) { # 結果が選択されてない場合は、可能なら補完なしを候補に入れておく $s = New-Object System.Management.Automation.CompletionResult $inputScript.Substring($ret.ReplacementIndex,$ret.ReplacementLength) $ret.CompletionMatches.Insert(0, $s) } } return $ret } }
使い方
関数を更新したら、いつも通りタブ補完するだけです。
あ、Show-PopupMenu関数も追加しておいてくださいね。
大量だと、PopupMenuでは見にくい、、、ですね。
PopupMenu、手元の環境だと出ると同時にフォーカスがあたってくれますが、上手くいかない環境もある気がします。
ってか、たまにうまく行きません。その時は、マウス使ってください、、、。
PopupMenuの初期アクセスキーは先頭文字なので、先頭文字を必要なだけ押せば、ホームポジションのまま補完可能です。
解説
もともとのTabExpansion2関数では、End節しかなく、End節の最初のif文がある程度でした。
Begin節には、Win32Apiを呼ぶ用のクラス定義があります。
これは、ポップアップメニューの表示位置を決定するために使っているもので、もし表示位置がマウスポインターの場所で良かったりするなら、不要なクラスです。
Begin節毎削除してしまって構いません。(問題ない作りです)
表示位置は、$Host.UI.RawUIを使って算出しています。
ISEの場合は、、、算出できないので、マウス位置になりますね。
ってか、ISEならインテリセンスがあるので、タブ補完はどうなんでしょうね。
ポップアップメニューをEsc等でキャンセルされた場合、極力補完なしを追加しようとしています。
が、長さゼロの補完候補を作成できないため、長さゼロケースではうまくいきません。
やめたケースは、補完しないと判断して、補完候補をClearしてしまうのも手かもしれません。
補完候補の取り出しは、単純にやってしまっています。
# 補完候補を取り出す $ss = @() $ret.CompletionMatches | %{ $ss += $_.CompletionText }
ここをもう少し賢くすれば、もっと便利になりそうです。
(アクセスキーを使うとか)