vim.psm1

using namespace System.Management.Automation;
using namespace System.Collections.Generic;
$NewInstanceName = 'gPsVim'

class GvimErrorFile {
  hidden [List[string]] $ErrorLines = [List[string]]::new(100)

  hidden [List[ErrorRecord]] $Errors

  [bool] HasLines() {    
    return $this.ErrorLines && $this.ErrorLines.Count -gt 0
  }

  [string] WriteErrorLinesToTemporaryFile() {
    $outfile = [io.path]::GetTempFileName() + '.psvimerror'
    $tmp = [io.path]::GetTempPath()
    Remove-Item -Path $tmp\*.psvimerror -ErrorAction SilentlyContinue
    [System.IO.File]::WriteAllLines($outfile, $this.ErrorLines, [System.Text.Encoding]::UTF8)
    return $outFile
  }

  [System.Management.Automation.ErrorRecord[]] AddData([psobject[]] $psobject) {
    foreach ($o in $psobject) {
      if ($this.TryAddMatchInfoLine($o)) { continue }
      if ($this.TryAddFileLine($o)) { continue }
      if ($this.IsDirectory($o)) { continue }
      if ($this.TryAddObjectWithPSPath($o)) { continue }
      if ($this.TryAddString($o)) { continue }
      
      $this.AddError($o)
    }

    return $this.GetErrors()
  }

  [ErrorRecord[]] GetErrors() {
    $retVal = [Array]::Empty[ErrorRecord]()

    if ($this.Errors) {
      $retVal = $this.Errors.ToArray()
      $this.Errors.Clear()
    }

    return $retVal
  }

  [void] AddError([psobject] $obj) {
    if (!$this.Errors) {
      $this.Errors = [List[ErrorRecord]]::new(10)
    }
    $typeName = $obj.psobject.TypeNames[0]
    $er = [ErrorRecord]::new([InvalidOperationException]::new("The type '$typeName' is not valid as input. MatchInfo, FileInfo, a full path as a string,`nand any object with a PSPath property will work."), 'InvalidType', [ErrorCategory]::InvalidArgument, $obj)
    $this.Errors.Add($er)
  }

  hidden [bool] IsDirectory([psobject] $obj) {
    return $obj -is [System.IO.DirectoryInfo]
  }

  hidden [bool] TryAddString([psobject] $obj) {
    if ($obj -is [string]) {
      $this.ErrorLines.Add($obj)
      return $true
    }
    return $false
  }

  hidden [bool] TryAddObjectWithPSPath([PSObject] $obj) {
    if (!$obj.psobject.Properties["PSPath"]) { return $false }
    
    $path = (Resolve-Path $obj.PSPath).ProviderPath
    $lines = $path.Foreach{ '{0}:{1}' -f $_, [System.IO.Path]::GetFileName($_) }
    $this.ErrorLines.Add($lines)
    return $true
  }

  hidden [bool] TryAddMatchInfoLine([PSObject] $psobject) {
    $matchInfo = $psobject -as [Microsoft.PowerShell.Commands.MatchInfo]
    if (!$matchInfo) { return $false }

    $line = $this.ToErrorLine($matchInfo)
    $this.ErrorLines.Add($line)
    return $true
  }

  hidden [bool] TryAddFileLine([PSObject] $obj) {
    $file = $obj -as [System.IO.FileInfo]
    if (!$file) { return $false }

    $line = $this.ToErrorLine($file)
    $this.ErrorLines.Add($line)
    return $true
  }

  hidden [string] ToErrorLine([Microsoft.PowerShell.Commands.MatchInfo] $matchInfo) {
    $groups = $matchInfo.Matches[0].Groups
    if ($groups.Count -gt 1) {
      # use the first match if we have capture groups
      $column = $groups[1].Index + 1
    }
    else {
      $column = $groups[0].Index + 1
    }

    $msg = $matchInfo.ToString()
    $line = '{0}:{1}:{2}:{3}' -f $matchInfo.path, $matchInfo.LineNumber, $column, $msg
    return $line
  }

  hidden [string] ToErrorLine([System.IO.FileInfo] $fileInfo) {
    return '{0}:{1}' -f $fileInfo.FullName, $fileInfo.Name
  }
}


if (!(Test-Path alias:gvim)) {
  $gvimPath = (Get-ItemProperty HKLM:\SOFTWARE\Vim\Gvim -Name Path).Path
  Set-Alias gvim $gvimPath
}


function LaunchVim {
  param
  (
    [string]
    $errorFile,
    [string]
    $ErrorFormat,
    [Switch]
    $NewInstance
  )
  $vimArgs = @()
  if ($NewInstance) {
    if ($ErrorFormat) {
      $vimArgs = '-c', ":set errorformat=$ErrorFormat"
    }

    $vimArgs += '-c', ":cf $errorFile"
  }
  else {
    $vimArgs = '--servername', $NewInstanceName, '--remote-silent'
    if ($ErrorFormat) {
      $vimArgs += "+<C-\><C-N>:set errorformat=$ErrorFormat<CR>:cf $errorFile<CR>", $errorFile
    }
    else {
      $vimArgs += "+<C-\><C-N>:cf $errorFile<CR>", $errorFile
    }
  }

  Write-Verbose "gvim $vimArgs"
  gvim $vimArgs
}



function Invoke-Gvim {
  <#
.Synopsis
    Starts the Gvim editor.
.Description
    This command creates an errorfile from the pipeline input and invokes
    gvim with that file as an argument.
    This enables easy navigations in search results from multiple files
 
.Example
    PS> Get-ChildItem *.txt | Invoke-GVim
 
    Opens all text files in the current directory
 
.Example
    PS> Select-String 'Foo' *.txt | Invoke-GVim
 
    Opens all text files containing 'Foo' and enables navigation between the instances
    with :cn and :cp
 
.Example
    Get-ChildItem -Recurse -Filter *.txt | Select-String 'aaa(bbb)ccc' | Invoke-GVim
 
    The example above open gvim with all textfiles containg 'aaabbbccc' with the selection
    on the first regex group.
 
    some text aaabbbccc
                 ^ cursor
 
 
.Link
 
.INPUTS
System.IO.FileInfo
Microsoft.PowerShell.Commands.MatchInfo
System.String
 
Object with PSPath property
 
 
.Notes
NAME: Invoke-GVim
#Requires -Version 2.0
#>

  [CmdletBinding()]
  param(
    [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]
    [ValidateNotNull()]
    [PSObject[]] $data,
    # Open results in new Gvim window
    [switch] $NewInstance
  )

  begin {
    $gvimErrorFile = [GvimErrorFile]::new()
  }
  process {
    $errors = $gvimErrorFile.AddData($data)
    foreach ($err in $errors) {
      Write-Error -ErrorRecord $err
    }
  }
  end {
    if ($gvimErrorFile.HasLines()) {
      $errorsFile = $gvimErrorFile.WriteErrorLinesToTemporaryFile()
      $PSCmdlet.WriteDebug([System.IO.File]::ReadAllText($errorsFile))
      LaunchVim $errorsFile -NewInstance:$NewInstance -ErrorFormat '%f:%l:%c:%m,%f:%m'
    }
  }
}

function Set-VimNewInstanceName {
  param
  (
    [string] $name = 'PsVim'
  )

  $script:NewInstanceName = $name
}



Set-Alias igv Invoke-GVim