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 }

ここをもう少し賢くすれば、もっと便利になりそうです。
(アクセスキーを使うとか)