diff --git a/Autocomplete/PowershellAutocomplete.cs b/Autocomplete/PowershellAutocomplete.cs new file mode 100644 index 0000000..a4cd51b --- /dev/null +++ b/Autocomplete/PowershellAutocomplete.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using Terminal.Gui; + +namespace psedit +{ + public class PowershellAutocomplete : Autocomplete + { + private readonly Runspace _runspace; + + public PowershellAutocomplete(Runspace runspace) + { + _runspace = runspace; + } + + private IEnumerable _suggestions; + + public void Force() + { + var host = (EditorTextView)HostControl; + var offset = 0; + + for (var lineOffset = 0; lineOffset <= host.CurrentRow; lineOffset++) + { + if (lineOffset == host.CurrentRow) + { + offset += host.CurrentColumn; + } + else + { + offset += host.Runes[lineOffset].Count + Environment.NewLine.Length; + } + } + + using (var powerShell = PowerShell.Create()) + { + powerShell.Runspace = _runspace; + var results = CommandCompletion.CompleteInput(host.Text.ToString(), offset, new Hashtable(), powerShell); + Suggestions = results.CompletionMatches.Select(m => m.CompletionText).ToList().AsReadOnly(); + _suggestions = Suggestions; + } + } + + private void TryGenerateSuggestions() + { + var host = (EditorTextView)HostControl; + var offset = 0; + + for (var lineOffset = 0; lineOffset <= host.CurrentRow; lineOffset++) + { + if (lineOffset == host.CurrentRow) + { + offset += host.CurrentColumn; + } + else + { + offset += host.Runes[lineOffset].Count + Environment.NewLine.Length; + } + } + var text = host.Text.ToString(); + + if (text.Length == 0 || offset == 0 || host.CurrentColumn == 0) + { + ClearSuggestions(); + return; + } + + var currentChar = text[offset - 1]; + + if (currentChar != '-' && + currentChar != ':' && + currentChar != '.' && + currentChar != '.' && + currentChar != '\\' && + currentChar != '$') + { + if (_suggestions != null) + { + var word = GetCurrentWord(); + if (!System.String.IsNullOrEmpty(word)) + { + Suggestions = _suggestions.Where(m => m.StartsWith(word, StringComparison.OrdinalIgnoreCase)).ToList().AsReadOnly(); + } + else + { + ClearSuggestions(); + } + } + + return; + } + + using (var powerShell = PowerShell.Create()) + { + powerShell.Runspace = _runspace; + var results = CommandCompletion.CompleteInput(host.Text.ToString(), offset, new Hashtable(), powerShell); + Suggestions = results.CompletionMatches.Select(m => m.CompletionText).ToList().AsReadOnly(); + _suggestions = Suggestions; + } + } + + public override void GenerateSuggestions() + { + try + { + TryGenerateSuggestions(); + } + catch { } + } + + public override bool IsWordChar(Rune rune) + { + var c = (char)rune; + return Char.IsLetterOrDigit(c) || c == '$' || c == '-'; + } + + /// + protected override string GetCurrentWord() + { + var host = (TextView)HostControl; + var currentLine = host.GetCurrentLine(); + var cursorPosition = Math.Min(host.CurrentColumn, currentLine.Count); + return IdxToWord(currentLine, cursorPosition); + } + + /// + protected override void DeleteTextBackwards() + { + ((TextView)HostControl).DeleteCharLeft(); + } + + /// + protected override void InsertText(string accepted) + { + ((TextView)HostControl).InsertText(accepted); + } + } +} \ No newline at end of file diff --git a/Editor/SyntaxErrorDialog.cs b/Editor/SyntaxErrorDialog.cs index ff8464e..c414681 100644 --- a/Editor/SyntaxErrorDialog.cs +++ b/Editor/SyntaxErrorDialog.cs @@ -1,27 +1,31 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; -using System.Text; using Terminal.Gui; namespace psedit { internal class SyntaxErrorDialog { - public static void Show(ParseError[] errors) + public static void Show(ConcurrentDictionary errors) { var dataTable = new DataTable(); dataTable.Columns.Add("Line"); dataTable.Columns.Add("Column"); dataTable.Columns.Add("Message"); + + // sort errors by line / column + List sortedList = new List(errors.Keys); + sortedList.Sort((x,y) => { + var ret = x.Y.CompareTo(y.Y); + if (ret == 0) ret = x.X.CompareTo(y.X); + return ret; + }); - foreach (var error in errors) + foreach (var error in sortedList) { - dataTable.Rows.Add(error.Extent.StartLineNumber, error.Extent.StartColumnNumber, error.Message); - + dataTable.Rows.Add(error.Y, error.X, errors[error]); } var tableView = new TableView(dataTable); diff --git a/EditorContext/EditorContext.cs b/EditorContext/EditorContext.cs new file mode 100644 index 0000000..7d50e7d --- /dev/null +++ b/EditorContext/EditorContext.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Terminal.Gui; + +namespace psedit +{ + public abstract class EditorContext + { + public string _originalText; + public int _lastParseTopRow; + public int _lastParseRightColumn; + public int _tabWidth; + public bool CanFormat = false; + public bool CanAutocomplete = false; + public bool CanRun = false; + public bool CanSyntaxHighlight = false; + public ConcurrentDictionary Errors = new ConcurrentDictionary(); + public ConcurrentDictionary ColumnErrors = new ConcurrentDictionary(); + public Dictionary pointColorDict = new Dictionary(); + public abstract void ParseText(int height, int topRow, int left, int right, string text, List> Runes); + public virtual string Format(string text) + { + throw new NotImplementedException(); + } + public virtual string Run(string path) + { + throw new NotImplementedException(); + } + public virtual string RunText(string text) + { + throw new NotImplementedException(); + } + public virtual void RunCurrentRunspace(string path) + { + throw new NotImplementedException(); + } + public virtual void RunTextCurrentRunspace(string text) + { + throw new NotImplementedException(); + } + + public Color GetColorByPoint(Point point) + { + Color returnColor = Color.Green; + if (pointColorDict.ContainsKey(point)) + { + returnColor = pointColorDict[point]; + } + return returnColor; + } + } +} \ No newline at end of file diff --git a/EditorContext/JSONEditorContext.cs b/EditorContext/JSONEditorContext.cs new file mode 100644 index 0000000..ef0a2db --- /dev/null +++ b/EditorContext/JSONEditorContext.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Terminal.Gui; +using System.Diagnostics; +using System.IO; +using Newtonsoft.Json; + +namespace psedit +{ + public class JSONEditorContext : EditorContext + { + private Dictionary tokens; + private List parsedErrors = new List(); + public JSONEditorContext(int TabWidth) + { + _tabWidth = TabWidth; + CanFormat = true; + CanSyntaxHighlight = true; + } + public Terminal.Gui.Color GetColor(JsonToken token) + { + Color textColor; + textColor = Color.White; + + switch (token) + { + case JsonToken.StartObject: + textColor = Color.BrightYellow; + break; + case JsonToken.EndObject: + textColor = Color.BrightYellow; + break; + case JsonToken.StartArray: + textColor = Color.BrightYellow; + break; + case JsonToken.EndArray: + textColor = Color.BrightYellow; + break; + case JsonToken.PropertyName: + textColor = Color.Gray; + break; + case JsonToken.Comment: + textColor = Color.Green; + break; + case JsonToken.String: + textColor = Color.Brown; + break; + case JsonToken.Integer: + textColor = Color.Cyan; + break; + case JsonToken.Float: + textColor = Color.Cyan; + break; + case JsonToken.Boolean: + textColor = Color.Cyan; + break; + default: + textColor = Color.White; + break; + } + + return textColor; + } + public Dictionary ParseJsonToken(string text, List> Runes) + { + List resultList = new List(); + Dictionary returnList = new Dictionary(); + + JsonTextReader reader = new JsonTextReader(new StringReader(text)); + int oldPos = 1; + int oldLine = 0; + bool reading = true; + while (reading) + { + try + { + var read = reader.Read(); + if (read == false) + { + reading = false; + } + var lineNumber = reader.LineNumber; + if (oldLine != lineNumber) + { + oldLine = lineNumber; + oldPos = 1; + } + var startIndex = oldPos; + var endIndex = reader.LinePosition; + var color = GetColor(reader.TokenType); + var result = new ParseResult { StartIndex = startIndex, EndIndex = endIndex, Color = color, LineNumber = lineNumber }; + resultList.Add(result); + oldPos = endIndex; + } + catch (JsonReaderException ex) + { + // this is to catch the scenario where newtonsoft json reader will get stuck in a loop in certain error scenarios + if (Errors.ContainsKey(new Point((int)ex.LinePosition, (int)ex.LineNumber))) + { + reading = false; + break; + } + if (oldLine != (int)ex.LineNumber) + { + oldLine = (int)ex.LineNumber; + oldPos = 1; + } + var startIndex = oldPos; + var endIndex = ex.LinePosition; + var errorMessage = StringExtensions.ParseNewtonsoftErrorMessage(ex.Message); + Errors.TryAdd(new Point((int)ex.LinePosition, (int)ex.LineNumber), errorMessage); + + parsedErrors.Add(new ErrorParseResult + { + StartIndex = startIndex, + EndIndex = endIndex, + LineNumber = ex.LineNumber, + ErrorMessage = errorMessage + }); + oldPos = endIndex; + } + } + + for (int idxRow = 0; idxRow < Runes.Count; idxRow++) + { + var line = EditorExtensions.GetLine(Runes, idxRow); + var tokenCol = 1; + int lineRuneCount = line.Count; + var rowTokens = resultList.Where(p => (p.LineNumber == idxRow + 1)); + for (int idxCol = 0; idxCol < lineRuneCount; idxCol++) + { + + var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; + var cols = Rune.ColumnWidth(rune); + + var match = rowTokens.Where(p => (tokenCol >= p.StartIndex && tokenCol <= p.EndIndex)).FirstOrDefault(); + var color = Color.Green; + if (match != null) + { + color = match.Color; + } + + tokenCol++; + var point = new Point(idxCol + 1, idxRow + 1); + returnList.Add(point, color); + + } + } + + return returnList; + } + public override void ParseText(int height, int topRow, int left, int right, string text, List> Runes) + { + // quick exit when text is the same and the top row / right col has not changed + if (_originalText == text && topRow == _lastParseTopRow && right == _lastParseRightColumn) + { + return; + } + + + if (_originalText != text) + { + parsedErrors.Clear(); + Errors.Clear(); + ColumnErrors.Clear(); + tokens = ParseJsonToken(text, Runes); + } + + Dictionary returnDict = new Dictionary(); + int bottom = topRow + height; + _originalText = text; + _lastParseTopRow = topRow; + _lastParseRightColumn = right; + long count = 0; + var row = 0; + for (int idxRow = topRow; idxRow < Runes.Count; idxRow++) + { + if (row > bottom) + { + break; + } + var line = EditorExtensions.GetLine(Runes, idxRow); + int lineRuneCount = line.Count; + var col = left; + var tokenCol = 1 + left; + + var rowErrors = parsedErrors.Where(e => e.LineNumber == idxRow + 1); + + for (int idxCol = left; idxCol < lineRuneCount; idxCol++) + { + var colError = rowErrors.FirstOrDefault(e => + (e.StartIndex == null && e.EndIndex == null) || + (e.StartIndex == null && tokenCol <= e.EndIndex) || + (tokenCol >= e.StartIndex && e.EndIndex == null) || + (tokenCol >= e.StartIndex && tokenCol <= e.EndIndex)); + + if (colError != null) + { + ColumnErrors.TryAdd(new Point(idxCol, idxRow), colError.ErrorMessage); + } + + var jsonParseMatch = tokens[new Point(tokenCol, idxRow + 1)]; + count++; + + var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; + var cols = Rune.ColumnWidth(rune); + var point = new Point(idxCol, row); + returnDict.Add(point, jsonParseMatch); + tokenCol++; + + if (!EditorExtensions.SetCol(ref col, right, cols)) + { + break; + } + if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth(line[idxCol + 1]) > right) + { + break; + } + } + row++; + } + pointColorDict = returnDict; + } + public override string Format(string text) + { + var returnValue = String.Empty; + try + { + var parsedJson = JsonConvert.DeserializeObject(text); + returnValue = JsonConvert.SerializeObject(parsedJson, Formatting.Indented); + } + catch + { + returnValue = text; + } + + return returnValue; + } + } +} \ No newline at end of file diff --git a/EditorContext/LanguageEnum.cs b/EditorContext/LanguageEnum.cs new file mode 100644 index 0000000..92b7b38 --- /dev/null +++ b/EditorContext/LanguageEnum.cs @@ -0,0 +1,9 @@ +namespace psedit +{ + public enum LanguageEnum + { + Text, + Powershell, + JSON + } +} \ No newline at end of file diff --git a/EditorContext/ParseResult.cs b/EditorContext/ParseResult.cs new file mode 100644 index 0000000..ba374f0 --- /dev/null +++ b/EditorContext/ParseResult.cs @@ -0,0 +1,20 @@ + +using Terminal.Gui; + +namespace psedit +{ + public class ParseResult + { + public int? StartIndex = null; + public int? EndIndex = null; + public int LineNumber; + public Color Color; + } + public class ErrorParseResult + { + public int? StartIndex = null; + public int? EndIndex = null; + public int LineNumber; + public string ErrorMessage; + } +} \ No newline at end of file diff --git a/EditorContext/PowerShellEditorContext.cs b/EditorContext/PowerShellEditorContext.cs new file mode 100644 index 0000000..6496536 --- /dev/null +++ b/EditorContext/PowerShellEditorContext.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; +using Terminal.Gui; +using System.Text; + +namespace psedit +{ + public class PowerShellEditorContext : EditorContext + { + private List parsedTokens; + private List parsedErrors; + private Runspace _runspace; + public PowerShellEditorContext(int TabWidth, Runspace Runspace) + { + _tabWidth = TabWidth; + _runspace = Runspace; + + CanAutocomplete = true; + CanRun = true; + CanSyntaxHighlight = true; + + // verify that psscriptanalyzer is available for formatting + using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddCommand("Get-Module"); + powerShell.AddParameter("Name", "PSScriptAnalyzer"); + powerShell.AddParameter("ListAvailable", true); + var result = powerShell.Invoke(); + if (result.Any()) + { + CanFormat = true; + } + } + } + public Terminal.Gui.Color GetColor(Token token) + { + Color textColor; + textColor = Color.White; + + if (token != null) + { + switch (token.Kind) + { + case TokenKind.If: + textColor = Color.White; + break; + case TokenKind.Else: + textColor = Color.White; + break; + case TokenKind.LCurly: + textColor = Color.BrightYellow; + break; + case TokenKind.RCurly: + textColor = Color.BrightYellow; + break; + case TokenKind.LParen: + textColor = Color.White; + break; + case TokenKind.RParen: + textColor = Color.White; + break; + case TokenKind.Parameter: + textColor = Color.White; + break; + case TokenKind.Identifier: + textColor = Color.BrightYellow; + break; + case TokenKind.Equals: + textColor = Color.White; + break; + case TokenKind.Param: + textColor = Color.White; + break; + case TokenKind.Function: + textColor = Color.BrightBlue; + break; + case TokenKind.StringExpandable: + case TokenKind.StringLiteral: + case TokenKind.HereStringExpandable: + case TokenKind.HereStringLiteral: + textColor = Color.Brown; + break; + case TokenKind.Variable: + textColor = Color.Cyan; + break; + case TokenKind.Comment: + textColor = Color.Green; + break; + case TokenKind.Command: + case TokenKind.Generic: + if (token.TokenFlags == TokenFlags.CommandName) + textColor = Color.BrightYellow; + else + textColor = Color.Gray; + + break; + default: + textColor = Color.White; + break; + } + } + + return textColor; + } + public List GetErrors(ParseError[] errors) + { + List returnValue = new List(); + + foreach (var error in errors) + { + // Verify if error has already been reported + Errors.TryAdd(new Point(error.Extent.StartColumnNumber, error.Extent.StartLineNumber), error.Message); + + if (error.Extent.StartLineNumber == error.Extent.EndLineNumber) + { + returnValue.Add(new ErrorParseResult + { + StartIndex = error.Extent.StartColumnNumber, + EndIndex = error.Extent.EndColumnNumber, + LineNumber = error.Extent.StartLineNumber, + ErrorMessage = error.Message + }); + } + else + { + for (int line = error.Extent.StartLineNumber; line <= error.Extent.EndLineNumber; line++) + { + if (line == error.Extent.StartLineNumber) + { + returnValue.Add(new ErrorParseResult + { + StartIndex = error.Extent.StartColumnNumber, + LineNumber = error.Extent.StartLineNumber, + ErrorMessage = error.Message + }); + } + else if (line == error.Extent.EndLineNumber) + { + returnValue.Add(new ErrorParseResult + { + EndIndex = error.Extent.EndColumnNumber, + LineNumber = error.Extent.EndLineNumber, + ErrorMessage = error.Message + }); + } + else + { + returnValue.Add(new ErrorParseResult + { + LineNumber = line, + ErrorMessage = error.Message + }); + } + } + } + } + return returnValue; + } + public List ParsePowershellToken(Token[] tokens, string text, List> Runes) + { + List returnValue = new List(); + foreach (var token in tokens) + { + if (token.Kind == TokenKind.NewLine) + { + continue; + } + + var color = GetColor(token); + + if (token.Extent.StartLineNumber == token.Extent.EndLineNumber) + { + returnValue.Add(new ParseResult + { + StartIndex = token.Extent.StartColumnNumber, + EndIndex = token.Extent.EndColumnNumber, + LineNumber = token.Extent.StartLineNumber, + Color = color + }); + } + else + { + for (int line = token.Extent.StartLineNumber; line <= token.Extent.EndLineNumber; line++) + { + if (line == token.Extent.StartLineNumber) + { + returnValue.Add(new ParseResult + { + StartIndex = token.Extent.StartColumnNumber, + LineNumber = token.Extent.StartLineNumber, + Color = color + }); + } + else if (line == token.Extent.EndLineNumber) + { + returnValue.Add(new ParseResult + { + EndIndex = token.Extent.EndColumnNumber, + LineNumber = token.Extent.EndLineNumber, + Color = color + }); + } + else + { + returnValue.Add(new ParseResult + { + LineNumber = line, + Color = color + }); + } + } + } + } + return returnValue; + } + public override void ParseText(int height, int topRow, int left, int right, string text, List> Runes) + { + // quick exit when text is the same and the top row / right col has not changed + if (_originalText == text && topRow == _lastParseTopRow && right == _lastParseRightColumn) + { + return; + } + + if (_originalText != text) + { + // clear errors before parsing new ones + Errors.Clear(); + ColumnErrors.Clear(); + + Parser.ParseInput(text, out Token[] tokens, out ParseError[] errors); + parsedTokens = ParsePowershellToken(tokens, text, Runes); + parsedErrors = GetErrors(errors); + } + + Dictionary returnDict = new Dictionary(); + int bottom = topRow + height; + _originalText = text; + _lastParseTopRow = topRow; + _lastParseRightColumn = right; + var row = 0; + for (int idxRow = topRow; idxRow < Runes.Count; idxRow++) + { + if (row > bottom) + { + break; + } + var line = EditorExtensions.GetLine(Runes, idxRow); + int lineRuneCount = line.Count; + var col = 0; + var tokenCol = 1 + left; + var rowTokens = parsedTokens.Where(m => m.LineNumber == idxRow + 1); + + var rowErrors = parsedErrors.Where(e => e.LineNumber == idxRow + 1); + + for (int idxCol = left; idxCol < lineRuneCount; idxCol++) + { + var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; + var cols = Rune.ColumnWidth(rune); + + // get token, note that we must provide +1 for the end column, as Start will be 1 and End will be 2 for the example: A + var colToken = rowTokens.FirstOrDefault(e => + (e.StartIndex == null && e.EndIndex == null) || + (e.StartIndex == null && tokenCol + 1 <= e.EndIndex) || + (tokenCol >= e.StartIndex && e.EndIndex == null) || + (tokenCol >= e.StartIndex && tokenCol + 1 <= e.EndIndex)); + + var colError = rowErrors.FirstOrDefault(e => + (e.StartIndex == null && e.EndIndex == null) || + (e.StartIndex == null && tokenCol + 1 <= e.EndIndex) || + (tokenCol >= e.StartIndex && e.EndIndex == null) || + (tokenCol >= e.StartIndex && tokenCol + 1 <= e.EndIndex)); + + if (colError != null) + { + ColumnErrors.TryAdd(new Point(idxCol, idxRow), colError.ErrorMessage); + } + + if (rune == '\t') + { + cols += _tabWidth + 1; + if (col + cols > right) + { + cols = right - col; + } + for (int i = 1; i < cols; i++) + { + if (col + i < right) + { + //var color = colToken.Color; + //var point = new Point(col + 1, row); + } + } + tokenCol++; + } + else + { + var color = Color.White; + if (colToken != null) + { + color = colToken.Color; + + } + var point = new Point(idxCol, row); + returnDict.Add(point, color); + tokenCol++; + } + if (!EditorExtensions.SetCol(ref col, right, cols)) + { + break; + } + if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth(line[idxCol + 1]) > right) + { + break; + } + } + row++; + } + + pointColorDict = returnDict; + } + public override string Run(string path) + { + StringBuilder output = new StringBuilder(); + using (var ps = PowerShell.Create()) + { + ps.Runspace = _runspace; + ps.AddScript($". '{path}' | Out-String"); + try + { + var result = ps.Invoke(); + + if (ps.HadErrors) + { + foreach (var error in ps.Streams.Error) + { + output.AppendLine(error.ToString()); + } + } + + foreach (var r in result) + { + output.AppendLine(r.ToPlainText()); + } + } + catch (Exception ex) + { + output.AppendLine(ex.ToString()); + } + } + return output.ToString(); + } + public override string RunText(string text) + { + StringBuilder output = new StringBuilder(); + using (var ps = PowerShell.Create()) + { + ps.Runspace = _runspace; + ps.AddScript(text); + ps.AddCommand("Out-String"); + try + { + var result = ps.Invoke(); + + if (ps.HadErrors) + { + foreach (var error in ps.Streams.Error) + { + output.AppendLine(error.ToString()); + } + } + + foreach (var r in result) + { + output.AppendLine(r.ToPlainText()); + } + } + catch (Exception ex) + { + output.AppendLine(ex.ToString()); + } + } + return output.ToString(); + } + public override void RunCurrentRunspace(string path) + { + using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddScript($". {path}"); + powerShell.AddCommand("Out-Default"); + powerShell.Invoke(); + } + } + public override void RunTextCurrentRunspace(string text) + { + using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddScript(text); + powerShell.AddCommand("Out-Default"); + powerShell.Invoke(); + } + } + public override string Format(string text) + { + var returnValue = String.Empty; + using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddCommand("Invoke-Formatter"); + powerShell.AddParameter("ScriptDefinition", text); + var result = powerShell.Invoke(); + var formatted = result.FirstOrDefault(); + if (formatted != null) + { + returnValue = formatted.BaseObject as string; + } + } + return returnValue; + } + } +} \ No newline at end of file diff --git a/EditorExtensions.cs b/EditorExtensions.cs new file mode 100644 index 0000000..f6d8253 --- /dev/null +++ b/EditorExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using NStack; + +namespace psedit +{ + public static class EditorExtensions + { + public static List GetLine(List> lines, int line) + { + if (lines.Count > 0) + { + if (line < lines.Count) + { + return lines[line]; + } + else + { + return lines[lines.Count - 1]; + } + } + else + { + lines.Add(new List()); + return lines[0]; + } + } + + public static bool SetCol(ref int col, int width, int cols) + { + if (col + cols <= width) + { + col += cols; + return true; + } + + return false; + } + + public static List> StringToRunes(ustring content) + { + var lines = new List>(); + int start = 0, i = 0; + var hasCR = false; + // ASCII code 13 = Carriage Return. + // ASCII code 10 = Line Feed. + for (; i < content.Length; i++) + { + if (content[i] == 13) + { + hasCR = true; + continue; + } + if (content[i] == 10) + { + if (i - start > 0) + lines.Add(ToRunes(content[start, hasCR ? i - 1 : i])); + else + lines.Add(ToRunes(ustring.Empty)); + start = i + 1; + hasCR = false; + } + } + if (i - start >= 0) + lines.Add(ToRunes(content[start, null])); + return lines; + } + + public static List ToRunes(ustring str) + { + List runes = new List(); + foreach (var x in str.ToRunes()) + { + runes.Add(x); + } + return runes; + } + + } +} \ No newline at end of file diff --git a/EditorTextView.cs b/EditorTextView.cs new file mode 100644 index 0000000..0cfaace --- /dev/null +++ b/EditorTextView.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation.Runspaces; +using Terminal.Gui; +using System.Collections.Concurrent; + +namespace psedit +{ + + public class EditorTextView : TextView + { + public ConcurrentDictionary Errors { get; set; } = new ConcurrentDictionary(); + public ConcurrentDictionary ColumnErrors { get; set; } = new ConcurrentDictionary(); + public bool modified = false; + public Runspace _runspace; + public List> Runes { get; private set; } + private EditorContext editorContext; + public bool CanFormat = false; + public bool CanRun = false; + public bool CanSyntaxHighlight = false; + public LanguageEnum _language = LanguageEnum.Powershell; + public EditorTextView(Runspace runspace) + { + _runspace = runspace; + SetLanguage(LanguageEnum.Powershell); + } + public void SetLanguage(LanguageEnum language) + { + _language = language; + + // initialize autocomplete for selected language + if (language == LanguageEnum.Powershell) + { + editorContext = new PowerShellEditorContext(TabWidth, _runspace); + Autocomplete = new PowershellAutocomplete(_runspace); + Autocomplete.MaxWidth = 30; + Autocomplete.MaxHeight = 10; + Autocomplete.HostControl = this; + Autocomplete.SelectionKey = Key.Tab; + } + else if (language == LanguageEnum.JSON) + { + editorContext = new JSONEditorContext(TabWidth); + } + else + { + editorContext = null; + } + + // reset formatting + if (editorContext != null) + { + CanFormat = editorContext.CanFormat; + CanRun = editorContext.CanRun; + CanSyntaxHighlight = editorContext.CanSyntaxHighlight; + } + else + { + CanFormat = false; + CanRun = false; + CanSyntaxHighlight = false; + } + } + public void Format() + { + Text = editorContext.Format(Text.ToString()); + } + public string Run(string path, bool exit = false) + { + var output = String.Empty; + + if (exit == true) + { + editorContext.RunCurrentRunspace(path); + } + else + { + output = editorContext.Run(path); + } + + return output; + } + public string RunText(string text, bool exit = false) + { + var output = String.Empty; + + if (editorContext.CanRun) + { + if (exit == true) + { + editorContext.RunTextCurrentRunspace(text); + } + else + { + output = editorContext.RunText(text); + } + } + return output; + } + private void ColorNormal() + { + // this is default color / background when there is no content + Driver.SetAttribute(Terminal.Gui.Attribute.Make(Color.Green, Color.Black)); + } + + private void ColorSelected() + { + // this is default color / background when content is selected + Driver.SetAttribute(Terminal.Gui.Attribute.Make(Color.Green, Color.Blue)); + } + public override void Redraw(Rect bounds) + { + if (IsDirty) + { + modified = true; + } + + var text = Text.ToString(); + Runes = EditorExtensions.StringToRunes(text); + ColorNormal(); + + var offB = OffSetBackground(); + int right = Frame.Width + offB.width + RightOffset; + int bottom = Frame.Height + offB.height + BottomOffset; + var row = 0; + + if (editorContext != null) + { + editorContext.ParseText(bounds.Height, TopRow, LeftColumn, LeftColumn + right, Text.ToString(), Runes); + ColumnErrors = editorContext.ColumnErrors; + Errors = editorContext.Errors; + } + + for (int idxRow = TopRow; idxRow < Runes.Count; idxRow++) + { + if (row > bottom) + { + break; + } + var line = EditorExtensions.GetLine(Runes, idxRow); + int lineRuneCount = line.Count; + var col = 0; + + // identify token for specific row + for (int idxCol = LeftColumn; idxCol < lineRuneCount; idxCol++) + { + var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; + var cols = Rune.ColumnWidth(rune); + if (editorContext != null) + { + var point = new Point(idxCol, row); + var errorPoint = new Point(idxCol, idxRow); + var color = editorContext.GetColorByPoint(point); + + if (Selecting && PointInSelection(idxCol, idxRow)) + { + Driver.SetAttribute(Terminal.Gui.Attribute.Make(color, Color.Blue)); + } + else if (ColumnErrors.ContainsKey(errorPoint)) + { + Driver.SetAttribute(Terminal.Gui.Attribute.Make(color, Color.Red)); + } + else + { + Driver.SetAttribute(Terminal.Gui.Attribute.Make(color, Color.Black)); + } + } + else if (Selecting && PointInSelection(idxCol, idxRow)) + { + ColorSelected(); + } + else + { + ColorNormal(); + } + // add rune with previously set color + if (rune == '\t') + { + cols += TabWidth + 1; + if (col + cols > right) + { + cols = right - col; + } + for (int i = 0; i < cols; i++) + { + if (col + i < right) + { + AddRune(col + i, row, ' '); + } + } + } + else + { + AddRune(col, row, rune); + } + + if (!EditorExtensions.SetCol(ref col, bounds.Right, cols)) + { + break; + } + if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth(line[idxCol + 1]) > right) + { + break; + } + } + Move(0, row); + + if (col < right) + { + ColorNormal(); + ClearRegion(col, row, right, row + 1); + } + row++; + } + if (row < bottom) + { + ColorNormal(); + ClearRegion(bounds.Left, row, right, bottom); + } + + PositionCursor(); + + if (SelectedLength > 0) + return; + if (editorContext != null) + { + if (editorContext.CanAutocomplete) + { + // draw autocomplete + Autocomplete.GenerateSuggestions(); + + var renderAt = new Point( + CursorPosition.X - LeftColumn, + Autocomplete.PopupInsideContainer + ? (CursorPosition.Y + 1) - TopRow + : 0); + + Autocomplete.RenderOverlay(renderAt); + } + } + + } + void ClearRegion(int left, int top, int right, int bottom) + { + for (int row = top; row < bottom; row++) + { + Move(left, row); + for (int col = left; col < right; col++) + AddRune(col, row, ' '); + } + } + + bool PointInSelection(int col, int row) + { + long start, end; + GetEncodedRegionBounds(out start, out end); + var q = ((long)(uint)row << 32) | (uint)col; + return q >= start && q <= end - 1; + } + + void GetEncodedRegionBounds(out long start, out long end) + { + long selection = ((long)(uint)SelectionStartRow << 32) | (uint)SelectionStartColumn; + long point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; + if (selection > point) + { + start = point; + end = selection; + } + else + { + start = selection; + end = point; + } + } + + (int width, int height) OffSetBackground() + { + int w = 0; + int h = 0; + if (SuperView?.Frame.Right - Frame.Right < 0) + { + w = SuperView.Frame.Right - Frame.Right - 1; + } + if (SuperView?.Frame.Bottom - Frame.Bottom < 0) + { + h = SuperView.Frame.Bottom - Frame.Bottom - 1; + } + return (w, h); + } + } +} \ No newline at end of file diff --git a/PowerShellEditorTextView.cs b/PowerShellEditorTextView.cs deleted file mode 100644 index 2b99f65..0000000 --- a/PowerShellEditorTextView.cs +++ /dev/null @@ -1,529 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; -using NStack; -using Terminal.Gui; -using System.Collections.Concurrent; - -namespace psedit -{ - public class PowerShellEditorTextView : TextView - { - public ConcurrentDictionary Errors { get; set; } = new ConcurrentDictionary(); - public ConcurrentDictionary ColumnErrors { get; set; } = new ConcurrentDictionary(); - private ParseError[] _errors; - - public bool modified = false; - - public PowerShellEditorTextView(Runspace runspace) - { - Autocomplete = new PowerShellAutocomplete(runspace); - Autocomplete.MaxWidth = 30; - Autocomplete.MaxHeight = 10; - } - - public Terminal.Gui.Attribute GetColor(Token token, Color background) - { - Color textColor; - textColor = Color.White; - - if (token != null) - { - switch (token.Kind) - { - case TokenKind.If: - textColor = Color.White; - break; - case TokenKind.Else: - textColor = Color.White; - break; - case TokenKind.LCurly: - textColor = Color.BrightYellow; - break; - case TokenKind.RCurly: - textColor = Color.BrightYellow; - break; - case TokenKind.LParen: - textColor = Color.White; - break; - case TokenKind.RParen: - textColor = Color.White; - break; - case TokenKind.Parameter: - textColor = Color.White; - break; - case TokenKind.Identifier: - textColor = Color.BrightYellow; - break; - case TokenKind.Equals: - textColor = Color.White; - break; - case TokenKind.Param: - textColor = Color.White; - break; - case TokenKind.Function: - textColor = Color.BrightBlue; - break; - case TokenKind.StringExpandable: - case TokenKind.StringLiteral: - case TokenKind.HereStringExpandable: - case TokenKind.HereStringLiteral: - textColor = Color.Brown; - break; - case TokenKind.Variable: - textColor = Color.Cyan; - break; - case TokenKind.Comment: - textColor = Color.Green; - break; - case TokenKind.Command: - case TokenKind.Generic: - if (token.TokenFlags == TokenFlags.CommandName) - textColor = Color.BrightYellow; - else - textColor = Color.Gray; - - break; - default: - textColor = Color.White; - break; - } - } - - return Terminal.Gui.Attribute.Make(textColor, background); - } - - public void ColorToken(Token token, ParseError error, int line, int column, bool selection) - { - var hasError = error != null; - - var background = Color.Black; - if (selection) - { - background = Color.Blue; - } - else if (hasError) - { - background = Color.Red; - } - - var attributeColors = GetColor(token, background); - Driver.SetAttribute(attributeColors); - - } - - public List> Runes { get; private set; } - - private void ColorNormal() - { - // this is default color / background when there is no content - Driver.SetAttribute(Terminal.Gui.Attribute.Make(Color.Green, Color.Black)); - } - public override void Redraw(Rect bounds) - { - if (IsDirty) - { - modified = true; - } - var text = Text.ToString(); - Parser.ParseInput(text, out Token[] tokens, out ParseError[] errors); - Errors.Clear(); - ColumnErrors.Clear(); - _errors = errors; - Runes = StringToRunes(text); - foreach (var error in _errors) - { - // Verify if error has already been reported - var foundError = Errors.Where( err => - error.Extent.StartColumnNumber == err.Value.Extent.StartColumnNumber && - error.Extent.EndColumnNumber == err.Value.Extent.EndColumnNumber && - error.Extent.StartLineNumber == err.Value.Extent.StartLineNumber && - error.Extent.EndLineNumber == err.Value.Extent.EndLineNumber); - - if (!foundError.Any()) - { - Errors.TryAdd(new Point(error.Extent.StartColumnNumber, error.Extent.StartLineNumber), error); - } - } - ColorNormal(); - - var offB = OffSetBackground(); - int right = Frame.Width + offB.width + RightOffset; - int bottom = Frame.Height + offB.height + BottomOffset; - var row = 0; - for (int idxRow = TopRow; idxRow < Runes.Count; idxRow++) - { - if (row > bottom) - { - break; - } - var line = GetLine(Runes, idxRow); - int lineRuneCount = line.Count; - var col = 0; - - // identify token for specific row - var rowTokens = tokens.Where(m => - // single line token - m.Extent.StartLineNumber == (idxRow + 1) || - // multiline token - (m.Extent.EndLineNumber >= (idxRow + 1) && - m.Extent.StartLineNumber <= (idxRow + 1) - )); - - var rowErrors = _errors?.Where(m => - m.Extent.StartLineNumber == (idxRow + 1)); - - var tokenCol = 1 + LeftColumn; - - Move(0, row); - for (int idxCol = LeftColumn; idxCol < lineRuneCount; idxCol++) - { - // identify rows with runes - var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; - var cols = Rune.ColumnWidth(rune); - - // get token, note that we must provide +1 for the end column, as Start will be 1 and End will be 2 for the example: A - var colToken = rowTokens.FirstOrDefault(m => - // single line token - (((m.Extent.StartColumnNumber <= (tokenCol) && - m.Extent.EndColumnNumber >= (tokenCol + 1) && - m.Extent.StartLineNumber == (idxRow + 1))) || - // multiline token - (m.Extent.EndLineNumber >= (idxRow + 1) && - m.Extent.StartLineNumber <= (idxRow + 1) && - m.Extent.StartLineNumber != (idxRow + 1))) && - m.Kind != TokenKind.NewLine - ); - - - // get any errors - var colError = rowErrors?.FirstOrDefault(m => - m.Extent.StartColumnNumber <= (tokenCol) && - m.Extent.EndColumnNumber <= (tokenCol) - ); - - if (colError != null) - { - ColumnErrors.TryAdd(new Point(idxCol, idxRow), colError.Message); - } - - if (rune == '\t') - { - cols += TabWidth + 1; - if (col + cols > right) - { - cols = right - col; - } - for (int i = 0; i < cols; i++) - { - if (col + i < right) - { - ColorToken(colToken, colError, row, col, Selecting && PointInSelection(idxCol, idxRow)); - AddRune(col + i, row, ' '); - } - } - tokenCol++; - } - else - { - ColorToken(colToken, colError, row, col, Selecting && PointInSelection(idxCol, idxRow)); - AddRune(col, row, rune); - tokenCol++; - } - if (!SetCol(ref col, bounds.Right, cols)) - { - break; - } - if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth(line[idxCol + 1]) > right) - { - break; - } - } - if (col < right) - { - ColorNormal(); - ClearRegion(col, row, right, row + 1); - } - row++; - } - if (row < bottom) - { - ColorNormal(); - ClearRegion(bounds.Left, row, right, bottom); - } - - PositionCursor(); - - if (SelectedLength > 0) - return; - - // draw autocomplete - Autocomplete.GenerateSuggestions(); - - var renderAt = new Point( - CursorPosition.X - LeftColumn, - Autocomplete.PopupInsideContainer - ? (CursorPosition.Y + 1) - TopRow - : 0); - - Autocomplete.RenderOverlay(renderAt); - } - - public List GetLine(List> lines, int line) - { - if (lines.Count > 0) - { - if (line < lines.Count) - { - return lines[line]; - } - else - { - return lines[lines.Count - 1]; - } - } - else - { - lines.Add(new List()); - return lines[0]; - } - } - - void ClearRegion(int left, int top, int right, int bottom) - { - for (int row = top; row < bottom; row++) - { - Move(left, row); - for (int col = left; col < right; col++) - AddRune(col, row, ' '); - } - } - - bool PointInSelection(int col, int row) - { - long start, end; - GetEncodedRegionBounds(out start, out end); - var q = ((long)(uint)row << 32) | (uint)col; - return q >= start && q <= end - 1; - } - - void GetEncodedRegionBounds(out long start, out long end) - { - long selection = ((long)(uint)SelectionStartRow << 32) | (uint)SelectionStartColumn; - long point = ((long)(uint)CurrentRow << 32) | (uint)CurrentColumn; - if (selection > point) - { - start = point; - end = selection; - } - else - { - start = selection; - end = point; - } - } - - (int width, int height) OffSetBackground() - { - int w = 0; - int h = 0; - if (SuperView?.Frame.Right - Frame.Right < 0) - { - w = SuperView.Frame.Right - Frame.Right - 1; - } - if (SuperView?.Frame.Bottom - Frame.Bottom < 0) - { - h = SuperView.Frame.Bottom - Frame.Bottom - 1; - } - return (w, h); - } - - internal static bool SetCol(ref int col, int width, int cols) - { - if (col + cols <= width) - { - col += cols; - return true; - } - - return false; - } - - public static List> StringToRunes(ustring content) - { - var lines = new List>(); - int start = 0, i = 0; - var hasCR = false; - // ASCII code 13 = Carriage Return. - // ASCII code 10 = Line Feed. - for (; i < content.Length; i++) - { - if (content[i] == 13) - { - hasCR = true; - continue; - } - if (content[i] == 10) - { - if (i - start > 0) - lines.Add(ToRunes(content[start, hasCR ? i - 1 : i])); - else - lines.Add(ToRunes(ustring.Empty)); - start = i + 1; - hasCR = false; - } - } - if (i - start >= 0) - lines.Add(ToRunes(content[start, null])); - return lines; - } - - internal static List ToRunes(ustring str) - { - List runes = new List(); - foreach (var x in str.ToRunes()) - { - runes.Add(x); - } - return runes; - } - - } - - - public class PowerShellAutocomplete : Autocomplete - { - private readonly Runspace _runspace; - - public PowerShellAutocomplete(Runspace runspace) - { - _runspace = runspace; - } - - private IEnumerable _suggestions; - - public void Force() - { - var host = (PowerShellEditorTextView)HostControl; - var offset = 0; - - for (var lineOffset = 0; lineOffset <= host.CurrentRow; lineOffset++) - { - if (lineOffset == host.CurrentRow) - { - offset += host.CurrentColumn; - } - else - { - offset += host.Runes[lineOffset].Count + Environment.NewLine.Length; - } - } - - using (var powerShell = PowerShell.Create()) - { - powerShell.Runspace = _runspace; - var results = CommandCompletion.CompleteInput(host.Text.ToString(), offset, new Hashtable(), powerShell); - Suggestions = results.CompletionMatches.Select(m => m.CompletionText).ToList().AsReadOnly(); - _suggestions = Suggestions; - } - } - - private void TryGenerateSuggestions() - { - var host = (PowerShellEditorTextView)HostControl; - var offset = 0; - - for (var lineOffset = 0; lineOffset <= host.CurrentRow; lineOffset++) - { - if (lineOffset == host.CurrentRow) - { - offset += host.CurrentColumn; - } - else - { - offset += host.Runes[lineOffset].Count + Environment.NewLine.Length; - } - } - var text = host.Text.ToString(); - - if (text.Length == 0 || offset == 0 || host.CurrentColumn == 0) - { - ClearSuggestions(); - return; - } - - var currentChar = text[offset - 1]; - - if (currentChar != '-' && - currentChar != ':' && - currentChar != '.' && - currentChar != '.' && - currentChar != '\\' && - currentChar != '$') - { - if (_suggestions != null) - { - var word = GetCurrentWord(); - if (!System.String.IsNullOrEmpty(word)) - { - Suggestions = _suggestions.Where(m => m.StartsWith(word, StringComparison.OrdinalIgnoreCase)).ToList().AsReadOnly(); - } - else - { - ClearSuggestions(); - } - } - - return; - } - - using (var powerShell = PowerShell.Create()) - { - powerShell.Runspace = _runspace; - var results = CommandCompletion.CompleteInput(host.Text.ToString(), offset, new Hashtable(), powerShell); - Suggestions = results.CompletionMatches.Select(m => m.CompletionText).ToList().AsReadOnly(); - _suggestions = Suggestions; - } - } - - public override void GenerateSuggestions() - { - try - { - TryGenerateSuggestions(); - } - catch { } - } - - public override bool IsWordChar(Rune rune) - { - var c = (char)rune; - return Char.IsLetterOrDigit(c) || c == '$' || c == '-'; - } - - /// - protected override string GetCurrentWord() - { - var host = (TextView)HostControl; - var currentLine = host.GetCurrentLine(); - var cursorPosition = Math.Min(host.CurrentColumn, currentLine.Count); - return IdxToWord(currentLine, cursorPosition); - } - - /// - protected override void DeleteTextBackwards() - { - ((TextView)HostControl).DeleteCharLeft(); - } - - /// - protected override void InsertText(string accepted) - { - ((TextView)HostControl).InsertText(accepted); - } - } -} \ No newline at end of file diff --git a/ShowEditorCommand.cs b/ShowEditorCommand.cs index 3f2b863..d4e5e8d 100644 --- a/ShowEditorCommand.cs +++ b/ShowEditorCommand.cs @@ -1,10 +1,8 @@ using System; using System.Diagnostics; using System.IO; -using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; -using System.Text; using Terminal.Gui; using System.Collections.Generic; @@ -14,15 +12,16 @@ namespace psedit [Alias("psedit")] public class ShowEditorCommand : PSCmdlet { - private PowerShellEditorTextView textEditor; + private EditorTextView textEditor; private StatusBar statusBar; private StatusItem fileNameStatus; private StatusItem position; private StatusItem cursorStatus; + private StatusItem languageStatus; private Toplevel top; private Runspace _runspace; private string currentDirectory; - + private string _fileName; #region Find and replace variables private Window _winDialog; private TabView _tabView; @@ -48,7 +47,7 @@ protected override void BeginProcessing() protected override void ProcessRecord() { - textEditor = new PowerShellEditorTextView(_runspace); + textEditor = new EditorTextView(_runspace); textEditor.UnwrappedCursorPosition += (k) => { UpdatePosition(); @@ -69,12 +68,11 @@ protected override void ProcessRecord() Application.Init(); top = Application.Top; - + languageStatus = new StatusItem(Key.Unknown, "Text", () => { }); var versionStatus = new StatusItem(Key.Unknown, base.MyInvocation.MyCommand.Module.Version.ToString(), () => { }); position = new StatusItem(Key.Unknown, "", () => { }); cursorStatus = new StatusItem(Key.Unknown, "", () => { }); - - statusBar = new StatusBar(new StatusItem[] { fileNameStatus, versionStatus, position, cursorStatus }); + statusBar = new StatusBar(new StatusItem[] { fileNameStatus, versionStatus, position, cursorStatus, languageStatus }); top.Add(new MenuBar(new MenuBarItem[] { new MenuBarItem ("_File", new MenuItem [] { @@ -95,13 +93,13 @@ protected override void ProcessRecord() null, new MenuItem ("_Select All", "", () => SelectAll(), shortcut: Key.CtrlMask | Key.T), null, - new MenuItem("Format", "", Format, CanFormat, shortcut: Key.CtrlMask | Key.ShiftMask | Key.R), + new MenuItem("Format", "", Format, shortcut: Key.CtrlMask | Key.ShiftMask | Key.R), //new MenuItem("Autocomplete", "", Autocomplete, shortcut: Key.CtrlMask | Key.Space), }), new MenuBarItem("_View", new [] { - new MenuItem("Errors", "", () => ErrorDialog.Show(_runspace)), - new MenuItem("Syntax Errors", "", () => SyntaxErrorDialog.Show(textEditor.Errors.Values.ToArray())), + new MenuItem("Errors", "", () => { if (textEditor._language == LanguageEnum.Powershell) ErrorDialog.Show(_runspace); }), + new MenuItem("Syntax Errors", "", () => { if (textEditor.CanSyntaxHighlight) SyntaxErrorDialog.Show(textEditor.Errors); }), //new MenuItem("History", "", () => HistoryDialog.Show(_runspace)) }), new MenuBarItem("_Debug", new [] @@ -118,7 +116,7 @@ protected override void ProcessRecord() FileName = "https://docs.poshtools.com/powershell-pro-tools-documentation/powershell-module/show-pseditor", UseShellExecute = true }; - Process.Start (psi); + Process.Start(psi); }) }) })); @@ -149,8 +147,6 @@ protected override void ProcessRecord() } }; - textEditor.Autocomplete.SelectionKey = Key.Tab; - top.Add(textEditor); top.Add(statusBar); @@ -165,45 +161,61 @@ protected override void ProcessRecord() _runspace.Dispose(); } } - - private bool? _canFormat; - - private bool CanFormat() + private void SetLanguage(string path) { - if (!_canFormat.HasValue) - { - _canFormat = InvokeCommand.InvokeScript("Get-Module PSScriptAnalyzer -ListAvailable").Any(); + var extension = System.IO.Path.GetExtension(path); + + switch (extension) + { + case ".ps1": case ".psm1": case ".psd1": + textEditor.SetLanguage(LanguageEnum.Powershell); + break; + case ".json": + textEditor.SetLanguage(LanguageEnum.JSON); + break; + case ".txt": + textEditor.SetLanguage(LanguageEnum.Text); + break; + default: + textEditor.SetLanguage(LanguageEnum.Text); + break; } - return _canFormat.Value; } private void Format() { - try + if (textEditor.CanFormat) { - var formatValue = textEditor.Text.ToString(); - if (!System.String.IsNullOrEmpty(formatValue)) + try { - var previousCursorPosition = textEditor.CursorPosition; - var previousTopRow = textEditor.TopRow; - using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + var formatValue = textEditor.Text.ToString(); + if (!System.String.IsNullOrEmpty(formatValue)) { - powerShell.AddCommand("Invoke-Formatter"); - powerShell.AddParameter("ScriptDefinition", formatValue); - var result = powerShell.Invoke(); - var formatted = result.FirstOrDefault(); - if (formatted != null) + var previousCursorPosition = textEditor.CursorPosition; + var previousTopRow = textEditor.TopRow; + // format text in editor + textEditor.Format(); + if (textEditor.Text != _originalText) { - textEditor.Text = formatted.BaseObject as string; - textEditor.CursorPosition = previousCursorPosition; - textEditor.TopRow = previousTopRow; + if (!fileNameStatus.Title.EndsWith("*")) + { + fileNameStatus.Title += "*"; + textEditor.modified = true; + } } + else + { + textEditor.modified = false; + fileNameStatus.Title = fileNameStatus.Title.TrimEnd("*"); + } + textEditor.TopRow = previousTopRow; + textEditor.CursorPosition = previousCursorPosition; } } - } - catch (Exception ex) - { - MessageBox.ErrorQuery("Formatting Failed", ex.Message); + catch (Exception ex) + { + MessageBox.ErrorQuery("Formatting Failed", ex.Message); + } } } private bool CanCloseFile() @@ -212,9 +224,9 @@ private bool CanCloseFile() { return true; } - + var fileName = _fileName != null ? _fileName : fileNameStatus.Title; var r = MessageBox.ErrorQuery("Save File", - $"Do you want save changes in {fileNameStatus.Title}?", "Yes", "No", "Cancel"); + $"Do you want save changes in {fileName}?", "Yes", "No", "Cancel"); if (r == 0) { return Save(false); @@ -235,6 +247,10 @@ private void Open() } List allowedFileTypes = new List(); allowedFileTypes.Add(".ps1"); + allowedFileTypes.Add(".psm1"); + allowedFileTypes.Add(".psd1"); + allowedFileTypes.Add(".json"); + allowedFileTypes.Add(".txt"); var dialog = new OpenDialog("Open file", "", allowedFileTypes); dialog.CanChooseDirectories = false; dialog.CanChooseFiles = true; @@ -248,7 +264,7 @@ private void Open() return; } - if (!System.IO.Path.HasExtension(dialog.FilePath.ToString()) || !System.IO.Path.GetExtension(dialog.FilePath.ToString()).Equals(".ps1", StringComparison.CurrentCultureIgnoreCase)) + if (!System.IO.Path.HasExtension(dialog.FilePath.ToString())) { return; } @@ -282,8 +298,16 @@ private void LoadFile() if (Path != null) { textEditor.LoadFile(Path); + textEditor.modified = false; _originalText = textEditor.Text.ToByteArray(); - fileNameStatus.Title = System.IO.Path.GetFileName(Path); + _fileName = System.IO.Path.GetFileName(Path); + currentDirectory = System.IO.Path.GetDirectoryName(Path); + fileNameStatus.Title = _fileName; + SetLanguage(Path); + if (statusBar != null) + { + UpdatePosition(); + } } } @@ -291,156 +315,76 @@ private void ExitAndRun() { var text = textEditor.Text.ToString(); - try + if (textEditor.CanRun) { - Application.RequestStop(); - } - catch { } - - if (Path != null) - { - Save(false); - using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + try { - powerShell.AddScript($". {Path}"); - powerShell.AddCommand("Out-Default"); - powerShell.Invoke(); + Application.RequestStop(); } - } - else - { - using (var powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace)) + catch { } + + if (Path != null) + { + Save(false); + textEditor.Run(Path, true); + } + else { - powerShell.AddScript(text); - powerShell.AddCommand("Out-Default"); - powerShell.Invoke(); + textEditor.RunText(text, true); } } } private void Run() { - StringBuilder output = new StringBuilder(); - if (Path != null) + if (textEditor.CanRun) { - Save(false); - - using (var ps = PowerShell.Create()) + string output = String.Empty; + if (Path != null) { - ps.Runspace = _runspace; - ps.AddScript($". '{Path}' | Out-String"); - try - { - var result = ps.Invoke(); - - if (ps.HadErrors) - { - foreach (var error in ps.Streams.Error) - { - output.AppendLine(error.ToString()); - } - } - - foreach (var r in result) - { - output.AppendLine(r.ToPlainText()); - } - } - catch (Exception ex) - { - output.AppendLine(ex.ToString()); - } + Save(false); + output = textEditor.Run(Path); } - } - else - { - using (var ps = PowerShell.Create()) + else { - ps.Runspace = _runspace; - ps.AddScript(textEditor.Text.ToString()); - ps.AddCommand("Out-String"); - try - { - var result = ps.Invoke(Path); - - if (ps.HadErrors) - { - foreach (var error in ps.Streams.Error) - { - output.AppendLine(error.ToString()); - } - } - - foreach (var r in result) - { - output.AppendLine(r.ToPlainText()); - } - } - catch (Exception ex) - { - output.AppendLine(ex.ToString()); - } + output = textEditor.RunText(textEditor.Text.ToString()); } - } - var dialog = new Dialog(); - var button = new Button("Ok"); - dialog.AddButton(button); + var dialog = new Dialog(); + var button = new Button("Ok"); + dialog.AddButton(button); - dialog.Add(new TextView - { - Text = output.ToString(), - Height = Dim.Fill(), - Width = Dim.Fill() - }); + dialog.Add(new TextView + { + Text = output, + Height = Dim.Fill(), + Width = Dim.Fill() + }); - Application.Run(dialog); + Application.Run(dialog); + } } private void ExecuteSelection() { - StringBuilder output = new StringBuilder(); - - using (var ps = PowerShell.Create()) + if (textEditor.CanRun) { - ps.Runspace = _runspace; - ps.AddScript(textEditor.SelectedText.ToString()); - ps.AddCommand("Out-String"); - try - { - var result = ps.Invoke(); + string output = String.Empty; + output = textEditor.RunText(textEditor.SelectedText.ToString()); + + var dialog = new Dialog(); + var button = new Button("Ok"); + dialog.AddButton(button); - if (ps.HadErrors) - { - foreach (var error in ps.Streams.Error) - { - output.AppendLine(error.ToString()); - } - } - - foreach (var r in result) - { - output.AppendLine(r.ToPlainText()); - } - } - catch (Exception ex) + dialog.Add(new TextView { - output.Append(ex.ToString()); - } - } + Text = output, + Height = Dim.Fill(), + Width = Dim.Fill() + }); - var dialog = new Dialog(); - var button = new Button("Ok"); - dialog.AddButton(button); - - dialog.Add(new TextView - { - Text = output.ToString(), - Height = Dim.Fill(), - Width = Dim.Fill() - }); - - Application.Run(dialog); + Application.Run(dialog); + } } private void New() @@ -450,9 +394,11 @@ private void New() return; } fileNameStatus.Title = "Unsaved"; + _fileName = "Unsaved"; Path = null; _originalText = new System.IO.MemoryStream().ToArray(); textEditor.Text = _originalText; + textEditor.SetLanguage(LanguageEnum.Powershell); } private bool Save(bool saveAs) @@ -461,6 +407,10 @@ private bool Save(bool saveAs) { List allowedFileTypes = new List(); allowedFileTypes.Add(".ps1"); + allowedFileTypes.Add(".psm1"); + allowedFileTypes.Add(".psd1"); + allowedFileTypes.Add(".json"); + allowedFileTypes.Add(".txt"); var dialog = new SaveDialog(saveAs ? "Save file as" : "Save file", "", allowedFileTypes); dialog.DirectoryPath = currentDirectory; Application.Run(dialog); @@ -470,20 +420,21 @@ private bool Save(bool saveAs) return false; } Path = dialog.FilePath.ToString(); + _fileName = dialog.FileName.ToString(); fileNameStatus.Title = dialog.FileName; } - fileNameStatus.Title = fileNameStatus.Title.TrimEnd("*"); - textEditor.modified = false; statusBar.SetNeedsDisplay(); try { - if (!DisableFormatOnSave && CanFormat()) + if (!DisableFormatOnSave && textEditor.CanFormat) { Format(); } File.WriteAllText(Path, textEditor.Text.ToString()); _originalText = textEditor.Text.ToByteArray(); + fileNameStatus.Title = fileNameStatus.Title.TrimEnd("*"); + textEditor.modified = false; return true; } catch (Exception ex) @@ -862,29 +813,30 @@ private void DisposeWinDialog() #endregion private void UpdatePosition() { - if (textEditor.ColumnErrors.ContainsKey(textEditor.CursorPosition)) - { - cursorStatus.Title = textEditor.ColumnErrors[textEditor.CursorPosition]; - } - else + if (statusBar != null) { - cursorStatus.Title = string.Empty; - } - if ((textEditor.IsDirty == true || textEditor.modified == true) && !fileNameStatus.Title.EndsWith("*") && fileNameStatus.Title != "Unsaved") - { - fileNameStatus.Title += "*"; - } - position.Title = $"Ln {textEditor.CursorPosition.Y + 1}, Col {textEditor.CursorPosition.X + 1}"; - statusBar.SetNeedsDisplay(); - } + if (textEditor.ColumnErrors.ContainsKey(textEditor.CursorPosition)) + { + cursorStatus.Title = textEditor.ColumnErrors[textEditor.CursorPosition]; + } + else + { + cursorStatus.Title = string.Empty; + } + if ((textEditor.IsDirty == true || textEditor.modified == true) && _originalText != textEditor.Text && !fileNameStatus.Title.EndsWith("*") && fileNameStatus.Title != "Unsaved") + { + fileNameStatus.Title += "*"; + } + else if (_originalText == textEditor.Text && fileNameStatus.Title.EndsWith("*")) + { + fileNameStatus.Title = fileNameStatus.Title.TrimEnd("*"); + } - private void Autocomplete() - { - var autocomplete = textEditor.Autocomplete as PowerShellAutocomplete; + position.Title = $"Ln {textEditor.CursorPosition.Y + 1}, Col {textEditor.CursorPosition.X + 1}"; + languageStatus.Title = textEditor._language.ToString(); + statusBar.SetNeedsDisplay(); + } - autocomplete.Force(); - autocomplete.RenderOverlay(textEditor.CursorPosition); - textEditor.Redraw(textEditor.Bounds); } } } \ No newline at end of file diff --git a/StringExtensions.cs b/StringExtensions.cs index 86904f0..85a2886 100644 --- a/StringExtensions.cs +++ b/StringExtensions.cs @@ -17,5 +17,15 @@ public static string ToPlainText(this string output) { return AnsiRegex.Replace(output, string.Empty); } + public static string ParseNewtonsoftErrorMessage(string message) + { + var pattern = $"(.*)Path(.*)"; + var matches = Regex.Matches(message, pattern); + if (matches[0].Groups[1].Value != null) + { + return matches[0].Groups[1].Value; + } + return message; + } } } \ No newline at end of file diff --git a/psedit.csproj b/psedit.csproj index 0f64cd8..88881b5 100644 --- a/psedit.csproj +++ b/psedit.csproj @@ -7,6 +7,7 @@ +