Public/Add-PinvokeMethod.ps1
using namespace Microsoft.PowerShell.EditorServices using namespace Microsoft.PowerShell.EditorServices.Extensions using namespace System.Management.Automation.Host using namespace System.Management.Automation.Language function Add-PinvokeMethod { <# .EXTERNALHELP EditorServicesCommandSuite-help.xml #> [EditorCommand(DisplayName='Insert Pinvoke Method Definition')] [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string] $Function, [ValidateNotNullOrEmpty()] [string] $Module ) begin { if (-not $script:PinvokeWebService) { # Get the web service async so there isn't a hang before prompting for function name. $script:PinvokeWebService = async { $newWebServiceProxySplat = @{ Namespace = 'PinvokeWebService' Class = 'Main' Uri = 'http://pinvoke.net/pinvokeservice.asmx?wsdl' } New-WebServiceProxy @newWebServiceProxySplat } } # Return parameters if they exist, otherwise handle user input. function GetFunctionInfo([string] $functionName, [string] $moduleName) { if ($functionName -and $moduleName) { return [PSCustomObject]@{ Function = $functionName Module = $moduleName } } if (-not $functionName) { $functionName = ReadInputPrompt $Strings.PInvokeFunctionNamePrompt if (-not $functionName) { return } } $pinvoke = await $script:PinvokeWebService $searchResults = $pinvoke.SearchFunction($functionName, $null) if (-not $searchResults) { ThrowError -Exception ([ArgumentException]::new($Strings.CannotFindPInvokeFunction -f $functionName)) ` -Id CannotFindPInvokeFunction ` -Category InvalidArgument ` -Target $functionName ` -Show } $choice = $null if ($searchResults.Count -gt 1) { $choices = $searchResults.ForEach{ [ChoiceDescription]::new( $PSItem.Function, ('Module: {0} Function: {1}' -f $PSItem.Module, $PSItem.Function)) } $choice = ReadChoicePrompt $Strings.PInvokeFunctionChoice -Choices $choices if ($null -eq $choice) { return } } $searchResults[[int]$choice] } # Some modules don't always return correctly, commonly structs. This is a last ditch catch # all that parses the HTML content directly. # TODO: Replace calls to IE COM object with HtmlAgilityPack or similar. function GetUnsupportedSignature { $url = 'http://pinvoke.net/default.aspx/{0}/{1}.html' -f $functionInfo.Module, $functionInfo.Function try { $request = Invoke-WebRequest $url } catch { return } if ($request.Content -match 'The module <b>([^<]+)</b> does not exist') { $PSCmdlet.WriteDebug('Module {0} not found.' -f $matches[1]) return } if ($request.Content -match 'You are about to create a new page called <b>([^<]+)</b>') { $PSCmdlet.WriteDebug('Function {0} not found' -f $matches[1]) return } $nodes = $request.ParsedHtml.body.getElementsByClassName('TopicBody')[0].childNodes for ($i = 0; $i -lt $nodes.length; $i++) { $node = $nodes[$i] if ($node.tagName -ne 'H4') { continue } if ($node.innerText -notmatch 'C# Definition') { continue } $sig = $nodes[$i + 1] if ($sig.tagName -ne 'P' -or $sig.className -ne 'pre') { continue } return [PSCustomObject]@{ Signature = $sig.innerText -replace '\r?\n', '|' Url = $url } } } # Get template and insertion extent. If cursor is in a Add-Type command AST that has a member # definiton parameter, it will insert the signature into the existing command. Otherwise it # will create a new Add-Type command expression at the current cursor position. function GetTemplateInfo { $defaultAction = { [PSCustomObject]@{ Template = "# Source: <SourceUri><\n>" + "Add-Type -Namespace <Namespace> -Name <Class> -MemberDefinition '<\n><Signature>'" Position = [FullScriptExtent]::new( $context.CurrentFile, [BufferRange]::new( $context.CursorPosition.Line, $context.CursorPosition.Column, $context.CursorPosition.Line, $context.CursorPosition.Column)) } } $context = $psEditor.GetEditorContext() $commandAst = Find-Ast -AtCursor | Find-Ast -Ancestor -First { $PSItem -is [CommandAst] } if (-not $commandAst -or $commandAst.GetCommandName() -ne 'Add-Type') { return & $defaultAction } $binding = [StaticParameterBinder]::BindCommand($commandAst, $true) $memberDefinition = $binding.BoundParameters.MemberDefinition if (-not $memberDefinition) { return & $defaultAction } $targetOffset = $memberDefinition.Value.Extent.EndOffset - 1 return [PSCustomObject]@{ Template = '<\n><\n>// Source: <SourceUri><\n><Signature>' Position = [FullScriptExtent]::new($context.CurrentFile, $targetOffset, $targetOffset) } } # Get first non-whitespace character location if the line has text, otherwise get the current # cursor column. function GetIndentLevel { try { $context = $psEditor.GetEditorContext() $lineStart = $context.CursorPosition.GetLineStart() $lineEnd = $context.CursorPosition.GetLineEnd() $lineText = $context.CurrentFile.GetText( [BufferRange]::new( $lineStart.Line, $lineStart.Column, $lineEnd.Line, $lineEnd.Column)) if ($lineText -match '\S') { return $lineStart.Column - 1 } } catch { $PSCmdlet.WriteDebug('Exception occurred while getting indent level') } return $context.CursorPosition.Column - 1 } } end { $functionInfo = GetFunctionInfo $Function $Module if (-not $functionInfo) { return } $pinvoke = await $script:PinvokeWebService # Get signatures from pinvoke.net and filter by C# $signatureInfo = $null try { $signatureInfo = $pinvoke. GetResultsForFunction( $functionInfo.Function, $functionInfo.Module). Where{ $PSItem.Language -eq 'C#' } } catch [System.Web.Services.Protocols.SoapException] { if ($PSItem.Exception.Message -match 'but no signatures could be extracted') { $signatureInfo = GetUnsupportedSignature } } if (-not $signatureInfo) { ThrowError -Exception ([InvalidOperationException]::new($Strings.MissingPInvokeSignature)) ` -Id MissingPInvokeSignature ` -Category InvalidOperation ` -Target $functionInfo ` -Show } # - Replace pipes with new lines # - Add public modifier # - Trim white trailing whitespace # - Escape single quotes $signature = $signatureInfo.Signature ` -split '\|' ` -join [Environment]::NewLine ` -replace '(?<!public )(?<!\[)(?:private |internal )?(static|struct)', 'public $1' ` -replace '\s+$' ` -replace "'", "''" # Strip module name of numbers and make PascalCase. $formattedModuleName = [regex]::Replace( ($functionInfo.Module -replace '\d'), '^\w', { $args[0].Value.ToUpper() }) $templateInfo = GetTemplateInfo $expression = Invoke-StringTemplate -Definition $templateInfo.Template -Parameters @{ Namespace = 'PinvokeMethods' Class = $formattedModuleName Signature = $signature SourceUri = $signatureInfo.Url.Where({ $PSItem }, 'First')[0] } $indentLevel = GetIndentLevel $indent = ' ' * ($indentLevel - 1) $expression = $expression -split '\r?\n' -join ([Environment]::NewLine + $indent) Set-ScriptExtent -Extent $templateInfo.Position -Text $expression } } |