Terminal/SRxMonacoEditor.psm1
|
<#
SRxMonacoEditor.psm1 (Terminal subsystem - Iteration 10.1: Monaco Editor + read-only mode) ------------------------------------------------------------------------ UI-FREE logic layer for the Monaco editor tabs hosted by SRxTerminalBoxControl. Contains NO WinForms and NO WebView2 type literals, so it imports cleanly anywhere and is fully testable headless. - Read-SRxEditorFile / Write-SRxEditorFile encoding-preserving (UTF-8 +/- BOM, UTF-16 LE/BE, UTF-32, ANSI) and line-ending-preserving (CRLF / LF) file I/O; atomic write (temp file + File.Replace) so a failed save never truncates the original file. - Get-SRxMonacoLanguage file extension -> Monaco language id - ConvertTo-/ConvertFrom-SRxJsonMessage JavaScriptSerializer with MaxJsonLength = Int32.MaxValue. Windows PowerShell 5.1 ConvertFrom-Json is unreliable for large payloads (whole file contents travel through these messages). - class SRxMonacoEditor one open file (one Monaco model) - class SRxMonacoEditorCollection Open / Get / Select / Save / Close OWNER CONTRACT (dynamic dispatch - SRxTerminalBoxControl implements it, tests implement a mock). Every call is wrapped in try/catch here. editorOpened($editor, [string]$text) create tab + Monaco model editorSelected($editor, [bool]$show) $editor may be $null (= no editors left) editorClosed($editor) -> [bool] remove tab + model; return $true when the next editor should be SHOWN editorStateChanged($editor) dirty/saving changed (tab text, save button) promptUnsaved($editor) show Save / Discard / Cancel prompt requestContent($editor, [string]$req) ask Monaco for the text; the answer comes back via CompleteContentRequest() markSaved($editor, [int]$version) tell Monaco which version is now clean editorReadOnlyChanged($editor) (Iteration 10.1) read-only flag changed on an OPEN editor - lock/unlock Monaco, hide Save status([string]$text) READ-ONLY MODE (Iteration 10.1) Open($path, $true) -> read-only editor (Monaco readOnly + domReadOnly) Open($path, $false) -> read/write editor Open($path) -> new file: read/write; already open: mode unchanged $editor.SetReadOnly($bool) switches an open editor at any time. Switching to read-only is REFUSED (returns $false, LastError set) while the editor has unsaved changes or a save in flight - edits are never silently lost. Guards for a read-only editor: Save() returns $false without requesting content, SetDirty($true) is ignored, a late content reply is never written, Close() never prompts - so the Save button can never appear. SAVE IS ASYNCHRONOUS: the text lives in Monaco (JavaScript). Save() posts a content request; the reply (UI thread, WebMessageReceived) calls CompleteContentRequest(), which writes the file. CommitText() is the synchronous core and can be called directly. ------------------------------------------------------------------------ #> #region ---- JSON message helpers --------------------------------------------- function Get-SRxJsonSerializer { [CmdletBinding()] param() Add-Type -AssemblyName System.Web.Extensions -ErrorAction Stop $ser = New-Object -TypeName 'System.Web.Script.Serialization.JavaScriptSerializer' $ser.MaxJsonLength = [int]::MaxValue $ser.RecursionLimit = 64 return $ser } function ConvertTo-SRxJsonMessage { <# .SYNOPSIS Serializes a hashtable to compact JSON (no size limit). #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][System.Collections.IDictionary]$Message) $ser = Get-SRxJsonSerializer return $ser.Serialize($Message) } function ConvertFrom-SRxJsonMessage { <# .SYNOPSIS Parses a JSON object string into a Dictionary[string,object] (index with $msg['key']). Returns $null for empty/invalid input. #> [CmdletBinding()] param([string]$Json) if ([string]::IsNullOrEmpty($Json)) { return $null } try { $ser = Get-SRxJsonSerializer $obj = $ser.DeserializeObject($Json) if ($obj -is [System.Collections.IDictionary]) { return ,$obj } return $null } catch { return $null } } #endregion #region ---- language map ----------------------------------------------------- function Get-SRxMonacoLanguage { <# .SYNOPSIS Maps a file path to a Monaco language id ('plaintext' if unknown). #> [CmdletBinding()] [OutputType([string])] param([Parameter(Mandatory)][string]$Path) $ext = ([System.IO.Path]::GetExtension($Path)).ToLowerInvariant() switch ($ext) { { $_ -in '.ps1', '.psm1', '.psd1', '.ps1xml' } { return 'powershell' } { $_ -in '.xml', '.xsd', '.xsl', '.xslt', '.config', '.csproj', '.resx', '.pnp' } { return 'xml' } { $_ -in '.json', '.jsonc' } { return 'json' } { $_ -in '.js', '.mjs', '.cjs' } { return 'javascript' } '.ts' { return 'typescript' } { $_ -in '.html', '.htm' } { return 'html' } '.css' { return 'css' } { $_ -in '.md', '.markdown' } { return 'markdown' } '.cs' { return 'csharp' } { $_ -in '.yml', '.yaml' } { return 'yaml' } '.sql' { return 'sql' } { $_ -in '.bat', '.cmd' } { return 'bat' } { $_ -in '.ini', '.inf', '.properties' } { return 'ini' } default { return 'plaintext' } } return 'plaintext' } #endregion #region ---- encoding-preserving file I/O -------------------------------------- function Get-SRxEditorEncoding { <# .SYNOPSIS Returns the System.Text.Encoding for an EncodingName produced by Read-SRxEditorFile ('utf-8','utf-16','utf-16BE','utf-32','ansi'). #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$EncodingName, [bool]$HasBom = $false ) switch ($EncodingName) { 'utf-8' { return (New-Object System.Text.UTF8Encoding -ArgumentList $HasBom) } 'utf-16' { return (New-Object System.Text.UnicodeEncoding -ArgumentList $false, $HasBom) } 'utf-16BE' { return (New-Object System.Text.UnicodeEncoding -ArgumentList $true, $HasBom) } 'utf-32' { return (New-Object System.Text.UTF32Encoding -ArgumentList $false, $HasBom) } 'ansi' { return [System.Text.Encoding]::Default } default { return (New-Object System.Text.UTF8Encoding -ArgumentList $HasBom) } } return (New-Object System.Text.UTF8Encoding -ArgumentList $HasBom) } function Read-SRxEditorFile { <# .SYNOPSIS Reads a text file, detecting encoding (BOM / strict UTF-8 / ANSI) and the dominant line ending. Throws on I/O errors. .OUTPUTS [pscustomobject] Path, Text, EncodingName, HasBom, NewLine #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Path) $bytes = [System.IO.File]::ReadAllBytes($Path) $n = $bytes.Length $encName = 'utf-8'; $hasBom = $false; $skip = 0 if ($n -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $encName = 'utf-8'; $hasBom = $true; $skip = 3 } elseif ($n -ge 4 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE -and $bytes[2] -eq 0 -and $bytes[3] -eq 0) { $encName = 'utf-32'; $hasBom = $true; $skip = 4 } elseif ($n -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { $encName = 'utf-16'; $hasBom = $true; $skip = 2 } elseif ($n -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { $encName = 'utf-16BE'; $hasBom = $true; $skip = 2 } $text = '' if ($hasBom) { $enc = Get-SRxEditorEncoding -EncodingName $encName -HasBom $true $text = $enc.GetString($bytes, $skip, $n - $skip) } else { # No BOM: strict UTF-8 first (throws on invalid sequences), else ANSI. $strict = New-Object System.Text.UTF8Encoding -ArgumentList $false, $true try { $text = $strict.GetString($bytes) $encName = 'utf-8' } catch { $encName = 'ansi' $text = [System.Text.Encoding]::Default.GetString($bytes) } } $newLine = "`r`n" if ($text.IndexOf("`r`n") -lt 0 -and $text.IndexOf("`n") -ge 0) { $newLine = "`n" } return [pscustomobject]@{ Path = $Path Text = $text EncodingName = $encName HasBom = $hasBom NewLine = $newLine } } function Write-SRxEditorFile { <# .SYNOPSIS Writes text back preserving encoding/BOM and normalizing line endings to -NewLine. Atomic: writes a temp file in the same folder, then File.Replace (falls back to copy-over). Never throws. .OUTPUTS [pscustomobject] Success, Error, Bytes #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Path, [AllowEmptyString()][string]$Text = '', [string]$EncodingName = 'utf-8', [bool]$HasBom = $false, [string]$NewLine = "`r`n" ) $tmp = $null try { if ($null -eq $Text) { $Text = '' } if ([System.IO.File]::Exists($Path)) { $attr = [System.IO.File]::GetAttributes($Path) if (($attr -band [System.IO.FileAttributes]::ReadOnly) -ne 0) { return [pscustomobject]@{ Success = $false; Error = "File is read-only: $Path"; Bytes = 0 } } } # normalize line endings to the file's original convention $normalized = $Text.Replace("`r`n", "`n").Replace("`r", "`n") if ($NewLine -eq "`r`n") { $normalized = $normalized.Replace("`n", "`r`n") } $enc = Get-SRxEditorEncoding -EncodingName $EncodingName -HasBom $HasBom $dir = [System.IO.Path]::GetDirectoryName($Path) $tmp = [System.IO.Path]::Combine($dir, ('.~' + [System.IO.Path]::GetFileName($Path) + '.' + [guid]::NewGuid().ToString('N') + '.tmp')) # WriteAllText emits the preamble (BOM) only if the encoding has one [System.IO.File]::WriteAllText($tmp, $normalized, $enc) $len = (New-Object System.IO.FileInfo -ArgumentList $tmp).Length if ([System.IO.File]::Exists($Path)) { try { [System.IO.File]::Replace($tmp, $Path, [NullString]::Value) } catch { # e.g. some network shares do not support ReplaceFile [System.IO.File]::Copy($tmp, $Path, $true) } } else { [System.IO.File]::Move($tmp, $Path) } return [pscustomobject]@{ Success = $true; Error = ''; Bytes = [long]$len } } catch { return [pscustomobject]@{ Success = $false; Error = $_.Exception.Message; Bytes = 0 } } finally { try { if ($tmp -and [System.IO.File]::Exists($tmp)) { [System.IO.File]::Delete($tmp) } } catch { } } } function Test-SRxAbsolutePath { <# .SYNOPSIS $true for 'C:\...' / 'C:/...' or UNC '\\server\share\...'. Rejects relative and drive-relative ('\foo', 'C:foo') paths. #> [CmdletBinding()] [OutputType([bool])] param([string]$Path) if ([string]::IsNullOrWhiteSpace($Path)) { return $false } return ($Path -match '^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+)') } #endregion #region ---- SRxMonacoEditor (one open file) ----------------------------------- class SRxMonacoEditor { [string] $Id = '' [string] $Path = '' [string] $FileName = '' [string] $Language = 'plaintext' [string] $EncodingName = 'utf-8' [bool] $HasBom = $false [string] $NewLine = "`r`n" [bool] $IsDirty = $false [bool] $IsSaving = $false [bool] $IsClosed = $false [bool] $IsReadOnly = $false # Iteration 10.1 - change via SetReadOnly() [string] $LastError = '' [object] $LastSaved = $null hidden [object] $_collection = $null SRxMonacoEditor([object]$collection, [string]$fullPath, [object]$doc) { $this._collection = $collection $this.Id = [guid]::NewGuid().ToString('N') $this.Path = $fullPath $this.FileName = [System.IO.Path]::GetFileName($fullPath) $this.Language = Get-SRxMonacoLanguage -Path $fullPath if ($null -ne $doc) { $this.EncodingName = [string]$doc.EncodingName $this.HasBom = [bool]$doc.HasBom $this.NewLine = [string]$doc.NewLine } } # Asynchronous save (content is pulled from Monaco). $true = started / nothing to do. [bool] Save() { if ($null -eq $this._collection) { return $false } return $this._collection.Save($this) } # Close with "unsaved changes" prompt. $true = closed now, $false = prompt shown. [bool] Close() { if ($null -eq $this._collection) { return $true } return $this._collection.Close($this, $false) } # Close; -force $true discards unsaved changes without prompting. [bool] Close([bool]$force) { if ($null -eq $this._collection) { return $true } return $this._collection.Close($this, $force) } [void] Select() { if ($null -ne $this._collection) { $this._collection.Select($this) } } # Iteration 10.1 - $true = mode applied; $false = refused (see LastError) [bool] SetReadOnly([bool]$readOnly) { if ($null -eq $this._collection) { $this.IsReadOnly = $readOnly; return $true } return $this._collection.SetReadOnly($this, $readOnly) } # Synchronous core of Save(): writes $text to disk with the original # encoding / BOM / line endings. Returns Write-SRxEditorFile's result. [object] CommitText([string]$text) { $r = Write-SRxEditorFile -Path $this.Path -Text $text -EncodingName $this.EncodingName -HasBom $this.HasBom -NewLine $this.NewLine if ($r.Success) { $this.LastError = '' $this.LastSaved = [datetime]::Now } else { $this.LastError = [string]$r.Error } return $r } [string] DisplayTitle() { if ($this.IsDirty) { return ($this.FileName + ' ' + [string][char]0x25CF) } return $this.FileName } [string] ToString() { return $this.Path } } #endregion #region ---- SRxMonacoEditorCollection ----------------------------------------- class SRxMonacoEditorCollection { [object] $Owner = $null [object] $Selected = $null [string] $LastError = '' hidden [System.Collections.Generic.List[object]] $_items = $null hidden [hashtable] $_byPath = $null # case-insensitive full path -> editor hidden [hashtable] $_byId = $null hidden [hashtable] $_pending = $null # requestId -> @{ Editor; After } SRxMonacoEditorCollection([object]$owner) { $this.Owner = $owner $this._items = New-Object 'System.Collections.Generic.List[object]' $this._byPath = @{} $this._byId = @{} $this._pending = @{} } # ---------------------------------------------------------------- queries [int] Count() { return $this._items.Count } [object[]] Items() { return $this._items.ToArray() } [object] Get([string]$path) { if (-not (Test-SRxAbsolutePath -Path $path)) { return $null } try { $full = [System.IO.Path]::GetFullPath($path) } catch { return $null } if ($this._byPath.ContainsKey($full)) { return $this._byPath[$full] } return $null } [object] GetById([string]$id) { if ([string]::IsNullOrEmpty($id)) { return $null } if ($this._byId.ContainsKey($id)) { return $this._byId[$id] } return $null } [int] IndexOf([object]$editor) { return $this._items.IndexOf($editor) } [bool] HasUnsavedChanges() { foreach ($ed in $this._items) { if ($ed.IsDirty) { return $true } } return $false } [int] PendingRequestCount() { return $this._pending.Count } # ---------------------------------------------------------------- Open # Opens (or re-selects) the file at an ABSOLUTE path. Returns the editor, # or $null with .LastError set. # 1-arg form: a NEW editor is read/write; an already-open one keeps its mode. [object] Open([string]$path) { return $this.openInternal($path, $null) } # Iteration 10.1 - $readOnly: $true = read-only, $false = read/write. # Applied to an already-open editor too (refused while it has unsaved # changes: the editor is still returned/selected, LastError is set). [object] Open([string]$path, [bool]$readOnly) { return $this.openInternal($path, $readOnly) } hidden [object] openInternal([string]$path, [object]$readOnly) { $this.LastError = '' if (-not (Test-SRxAbsolutePath -Path $path)) { $this.LastError = "Open: path must be absolute: '$path'." $this.notifyStatus($this.LastError) return $null } $full = $null try { $full = [System.IO.Path]::GetFullPath($path) } catch { $this.LastError = "Open: invalid path '$path': $($_.Exception.Message)" $this.notifyStatus($this.LastError) return $null } if ($this._byPath.ContainsKey($full)) { $existing = $this._byPath[$full] $this.Select($existing) if ($null -ne $readOnly) { [void]$this.SetReadOnly($existing, [bool]$readOnly) } return $existing } if (-not [System.IO.File]::Exists($full)) { $this.LastError = "Open: file not found: '$full'." $this.notifyStatus($this.LastError) return $null } $doc = $null try { $doc = Read-SRxEditorFile -Path $full } catch { $this.LastError = "Open: cannot read '$full': $($_.Exception.Message)" $this.notifyStatus($this.LastError) return $null } $ed = [SRxMonacoEditor]::new($this, $full, $doc) if ($null -ne $readOnly) { $ed.IsReadOnly = [bool]$readOnly } $this._items.Add($ed) $this._byPath[$full] = $ed $this._byId[$ed.Id] = $ed try { if ($this.Owner) { $this.Owner.editorOpened($ed, [string]$doc.Text) } } catch { Write-Host "SRxMonacoEditorCollection.Open/editorOpened - $($_.Exception.Message)" } $this.Select($ed) return $ed } # ---------------------------------------------------------------- Select [void] Select([object]$editor) { $this.selectInternal($editor, $true) } hidden [void] selectInternal([object]$editor, [bool]$show) { if ($null -ne $editor -and ($editor.IsClosed -or -not $this._byId.ContainsKey($editor.Id))) { return } $this.Selected = $editor try { if ($this.Owner) { $this.Owner.editorSelected($editor, $show) } } catch { Write-Host "SRxMonacoEditorCollection.Select - $($_.Exception.Message)" } } # ---------------------------------------------------------------- Save [bool] Save([object]$editor) { return $this.beginSave($editor, '') } hidden [bool] beginSave([object]$editor, [string]$after) { if ($null -eq $editor -or $editor.IsClosed) { return $false } # Iteration 10.1 guard: a read-only editor is never saved if ($editor.IsReadOnly) { $editor.LastError = "Save: '$($editor.FileName)' is open read-only." $this.LastError = $editor.LastError if ($after -eq 'close') { $this.removeEditor($editor) } return $false } if (-not $editor.IsDirty) { if ($after -eq 'close') { $this.removeEditor($editor) } return $true } # already waiting for this editor's content -> just upgrade the follow-up action foreach ($k in @($this._pending.Keys)) { $p = $this._pending[$k] if ($p.Editor -eq $editor) { if ($after -eq 'close') { $p.After = 'close' } return $true } } $req = [guid]::NewGuid().ToString('N') $this._pending[$req] = @{ Editor = $editor; After = $after } $editor.IsSaving = $true $this.notifyState($editor) try { if ($this.Owner) { $this.Owner.requestContent($editor, $req) } } catch { $this._pending.Remove($req) $editor.IsSaving = $false $editor.LastError = "Save: content request failed: $($_.Exception.Message)" $this.LastError = $editor.LastError $this.notifyState($editor) $this.notifyStatus($editor.LastError) return $false } return $true } # Called with Monaco's reply to requestContent(). Unknown/stale ids are ignored. [void] CompleteContentRequest([string]$requestId, [string]$text, [int]$version) { if ([string]::IsNullOrEmpty($requestId) -or -not $this._pending.ContainsKey($requestId)) { return } $p = $this._pending[$requestId] $this._pending.Remove($requestId) $ed = $p.Editor $ed.IsSaving = $false if ($ed.IsClosed) { return } if ($ed.IsReadOnly) { $this.notifyState($ed); return } # Iteration 10.1 guard $r = $ed.CommitText($text) if (-not $r.Success) { $this.LastError = "Save failed for '$($ed.FileName)': $($r.Error)" $this.notifyState($ed) $this.notifyStatus($this.LastError) return # a pending 'close' is cancelled - unsaved edits stay open } $ed.IsDirty = $false # provisional; Monaco confirms via SetDirty() try { if ($this.Owner) { $this.Owner.markSaved($ed, $version) } } catch { Write-Host "SRxMonacoEditorCollection.markSaved - $($_.Exception.Message)" } $this.notifyState($ed) $this.notifyStatus("Saved '$($ed.Path)'.") if ([string]$p.After -eq 'close') { $this.removeEditor($ed) } } [void] FailContentRequest([string]$requestId, [string]$message) { if ([string]::IsNullOrEmpty($requestId) -or -not $this._pending.ContainsKey($requestId)) { return } $p = $this._pending[$requestId] $this._pending.Remove($requestId) $ed = $p.Editor $ed.IsSaving = $false $ed.LastError = "Save failed: $message" $this.LastError = $ed.LastError $this.notifyState($ed) $this.notifyStatus($ed.LastError) } # Monaco -> host dirty notifications [void] SetDirty([string]$id, [bool]$dirty) { $ed = $this.GetById($id) if ($null -eq $ed) { return } if ($dirty -and $ed.IsReadOnly) { return } # Iteration 10.1 guard if ($ed.IsDirty -eq $dirty) { return } $ed.IsDirty = $dirty $this.notifyState($ed) } # ---------------------------------------------------------------- Read-only (Iteration 10.1) [bool] SetReadOnly([object]$editor, [bool]$readOnly) { if ($null -eq $editor -or $editor.IsClosed) { return $false } if ($editor.IsReadOnly -eq $readOnly) { return $true } if ($readOnly -and ($editor.IsDirty -or $editor.IsSaving)) { $editor.LastError = "SetReadOnly: '$($editor.FileName)' has unsaved changes - save or discard them first." $this.LastError = $editor.LastError $this.notifyStatus($editor.LastError) return $false } $editor.IsReadOnly = $readOnly try { if ($this.Owner) { $this.Owner.editorReadOnlyChanged($editor) } } catch { Write-Host "SRxMonacoEditorCollection.editorReadOnlyChanged - $($_.Exception.Message)" } $this.notifyState($editor) return $true } # ---------------------------------------------------------------- Close # $true = closed now; $false = unsaved changes -> prompt shown (answer via ResolvePrompt) [bool] Close([object]$editor, [bool]$force) { if ($null -eq $editor -or $editor.IsClosed) { return $true } if (-not $force -and $editor.IsDirty -and -not $editor.IsReadOnly) { $this.Select($editor) try { if ($this.Owner) { $this.Owner.promptUnsaved($editor) } } catch { Write-Host "SRxMonacoEditorCollection.promptUnsaved - $($_.Exception.Message)" } return $false } $this.removeEditor($editor) return $true } # Answer to promptUnsaved(): 'save' | 'discard' | 'cancel' [void] ResolvePrompt([object]$editor, [string]$choice) { if ($null -eq $editor -or $editor.IsClosed) { return } switch ($choice) { 'save' { [void]$this.beginSave($editor, 'close') } 'discard' { $this.removeEditor($editor) } default { } } } # Closes everything WITHOUT prompting (host shutdown). [void] Clear() { foreach ($ed in @($this._items.ToArray())) { $ed.IsClosed = $true } $this._items.Clear() $this._byPath.Clear() $this._byId.Clear() $this._pending.Clear() $this.Selected = $null } hidden [void] removeEditor([object]$editor) { $idx = $this._items.IndexOf($editor) if ($idx -lt 0) { return } $wasSelected = ($this.Selected -eq $editor) $this._items.RemoveAt($idx) $this._byPath.Remove($editor.Path) $this._byId.Remove($editor.Id) foreach ($k in @($this._pending.Keys)) { if ($this._pending[$k].Editor -eq $editor) { $this._pending.Remove($k) } } $editor.IsClosed = $true $editor.IsSaving = $false $showNext = $true try { if ($this.Owner) { $r = $this.Owner.editorClosed($editor) if ($r -is [bool]) { $showNext = $r } } } catch { Write-Host "SRxMonacoEditorCollection.editorClosed - $($_.Exception.Message)" } $this.notifyStatus("Closed '$($editor.Path)'.") if ($wasSelected) { if ($this._items.Count -gt 0) { $next = $this._items[[Math]::Min($idx, $this._items.Count - 1)] $this.selectInternal($next, $showNext) } else { $this.selectInternal($null, $showNext) } } } # ---------------------------------------------------------------- owner helpers hidden [void] notifyState([object]$editor) { try { if ($this.Owner) { $this.Owner.editorStateChanged($editor) } } catch { Write-Host "SRxMonacoEditorCollection.editorStateChanged - $($_.Exception.Message)" } } hidden [void] notifyStatus([string]$text) { try { if ($this.Owner) { $this.Owner.status($text) } } catch { } } } #endregion function New-SRxMonacoEditorCollection { <# .SYNOPSIS Creates an editor collection bound to an owner implementing the OWNER CONTRACT (see module header). #> [CmdletBinding()] [OutputType([object])] param([Parameter(Mandatory)][object]$Owner) return [SRxMonacoEditorCollection]::new($Owner) } Export-ModuleMember -Function New-SRxMonacoEditorCollection, Read-SRxEditorFile, Write-SRxEditorFile, Get-SRxEditorEncoding, Get-SRxMonacoLanguage, ConvertTo-SRxJsonMessage, ConvertFrom-SRxJsonMessage, Test-SRxAbsolutePath |