460 lines
16 KiB
C#
460 lines
16 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.Design;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Consolus;
|
|
using Scriban;
|
|
using Scriban.Functions;
|
|
using Scriban.Parsing;
|
|
using Scriban.Runtime;
|
|
using Scriban.Syntax;
|
|
|
|
namespace Kalk.Core
|
|
{
|
|
public partial class KalkEngine
|
|
{
|
|
private Stopwatch _clockReplInput;
|
|
|
|
public ConsoleRepl Repl { get; private set; }
|
|
|
|
public int OnErrorToNextLineMaxDelayInMilliseconds { get; set; }
|
|
|
|
private void InitializeRepl()
|
|
{
|
|
_clockReplInput = Stopwatch.StartNew();
|
|
OnErrorToNextLineMaxDelayInMilliseconds = 300;
|
|
|
|
OnClearScreen = ClearScreen;
|
|
|
|
Repl.GetClipboardTextImpl = GetClipboardText;
|
|
Repl.SetClipboardTextImpl = SetClipboardText;
|
|
|
|
Repl.BeforeRender = OnBeforeRendering;
|
|
Repl.GetCancellationTokenSource = () => _cancellationTokenSource;
|
|
Repl.TryPreProcessKey = TryPreProcessKey;
|
|
Repl.OnTextValidatingEnter = OnTextValidatingEnter;
|
|
|
|
Repl.Prompt.Clear();
|
|
Repl.Prompt.Begin(ConsoleStyle.BrightBlack).Append(">>> ").Append(ConsoleStyle.BrightBlack, false);
|
|
}
|
|
|
|
private bool OnTextValidatingEnter(string text, bool hasControl)
|
|
{
|
|
try
|
|
{
|
|
return OnTextValidatingEnterInternal(text, hasControl);
|
|
}
|
|
finally
|
|
{
|
|
_clockReplInput.Restart();
|
|
}
|
|
}
|
|
|
|
private bool OnTextValidatingEnterInternal(string text, bool hasControl)
|
|
{
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
CancellationToken = _cancellationTokenSource.Token;
|
|
|
|
var elapsed = _clockReplInput.ElapsedMilliseconds;
|
|
Template script = null;
|
|
|
|
object result = null;
|
|
string error = null;
|
|
int column = -1;
|
|
bool isCancelled = false;
|
|
try
|
|
{
|
|
script = Parse(text);
|
|
|
|
if (script.HasErrors)
|
|
{
|
|
var errorBuilder = new StringBuilder();
|
|
foreach (var message in script.Messages)
|
|
{
|
|
if (errorBuilder.Length > 0) errorBuilder.AppendLine();
|
|
if (column <= 0 && message.Type == ParserMessageType.Error)
|
|
{
|
|
column = message.Span.Start.Column;
|
|
}
|
|
errorBuilder.Append(message.Message);
|
|
}
|
|
|
|
error = errorBuilder.ToString();
|
|
}
|
|
else
|
|
{
|
|
Repl.AfterEditLine.Clear();
|
|
HighlightOutput.Clear();
|
|
|
|
result = EvaluatePage(script.Page);
|
|
|
|
if (Repl.ExitOnNextEval)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex is ScriptRuntimeException scriptEx)
|
|
{
|
|
column = scriptEx.Span.Start.Column;
|
|
error = scriptEx.OriginalMessage;
|
|
isCancelled = ex is ScriptAbortException;
|
|
}
|
|
else
|
|
{
|
|
error = ex.Message;
|
|
}
|
|
}
|
|
|
|
if (error != null)
|
|
{
|
|
Repl.AfterEditLine.Clear();
|
|
Repl.AfterEditLine.Append('\n');
|
|
Repl.AfterEditLine.Begin(ConsoleStyle.Red);
|
|
Repl.AfterEditLine.Append(error);
|
|
if (column >= 0 && column <= Repl.EditLine.Count)
|
|
{
|
|
Repl.EnableCursorChanged = false;
|
|
Repl.CursorIndex = column;
|
|
Repl.EnableCursorChanged = true;
|
|
}
|
|
|
|
bool emitReturnOnError = hasControl || elapsed < OnErrorToNextLineMaxDelayInMilliseconds || isCancelled;
|
|
if (emitReturnOnError)
|
|
{
|
|
Repl.AfterEditLine.Append('\n');
|
|
}
|
|
|
|
return emitReturnOnError;
|
|
}
|
|
else
|
|
{
|
|
if (result != null)
|
|
{
|
|
Write(script.Page.Span, result);
|
|
}
|
|
var resultStr = Output.ToString();
|
|
var output = Output as StringBuilderOutput;
|
|
if (output != null)
|
|
{
|
|
output.Builder.Length = 0;
|
|
}
|
|
|
|
Repl.AfterEditLine.Clear();
|
|
bool hasOutput = resultStr != string.Empty || HighlightOutput.Count > 0;
|
|
if (!Repl.IsClean || hasOutput)
|
|
{
|
|
Repl.AfterEditLine.Append('\n');
|
|
|
|
bool hasNextOutput = HighlightOutput.Count > 0;
|
|
if (hasNextOutput)
|
|
{
|
|
Repl.AfterEditLine.AddRange(HighlightOutput);
|
|
HighlightOutput.Clear();
|
|
}
|
|
|
|
if (resultStr != string.Empty)
|
|
{
|
|
if (hasNextOutput)
|
|
{
|
|
Repl.AfterEditLine.AppendLine();
|
|
}
|
|
Repl.AfterEditLine.Begin(ConsoleStyle.Bold);
|
|
Repl.AfterEditLine.Append(resultStr);
|
|
}
|
|
|
|
if (hasOutput)
|
|
{
|
|
Repl.AfterEditLine.AppendLine();
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
private void OnEnterNextText(string textToEnter)
|
|
{
|
|
Repl?.EnqueuePendingTextToEnter(textToEnter);
|
|
}
|
|
|
|
private bool TryPreProcessKey(ConsoleKeyInfo arg, ref int cursorIndex)
|
|
{
|
|
return OnKey(arg, Repl.EditLine, ref cursorIndex);
|
|
}
|
|
|
|
private void OnBeforeRendering()
|
|
{
|
|
UpdateSyntaxHighlighting();
|
|
}
|
|
|
|
private void UpdateSyntaxHighlighting()
|
|
{
|
|
if (Repl != null)
|
|
{
|
|
Repl.EditLine.ClearStyles();
|
|
Highlight(Repl.EditLine, Repl.CursorIndex);
|
|
}
|
|
}
|
|
|
|
public void ClearScreen()
|
|
{
|
|
Repl?.Clear();
|
|
HighlightOutput.Clear();
|
|
}
|
|
|
|
public void ReplExit()
|
|
{
|
|
if (Repl == null) return;
|
|
Repl.ExitOnNextEval = true;
|
|
}
|
|
|
|
internal bool OnKey(ConsoleKeyInfo arg, ConsoleText line, ref int cursorIndex)
|
|
{
|
|
bool resetMap = true;
|
|
bool resetCompletion = true;
|
|
try
|
|
{
|
|
bool hasShift = (arg.Modifiers & ConsoleModifiers.Shift) != 0;
|
|
|
|
// For now, we discard SHIFT entirely, as it is handled separately for range selection
|
|
// TODO: we should handle them not differently from ctrl/alt, but it's complicated
|
|
arg = new ConsoleKeyInfo(arg.KeyChar, arg.Key, false, (arg.Modifiers & ConsoleModifiers.Alt) != 0, (arg.Modifiers & ConsoleModifiers.Control) != 0);
|
|
|
|
KalkConsoleKey kalkKey = arg;
|
|
if (cursorIndex >= 0 && cursorIndex <= line.Count)
|
|
{
|
|
if (_currentShortcutKeyMap.TryGetValue(kalkKey, out var value))
|
|
{
|
|
if (value is KalkShortcutKeyMap map)
|
|
{
|
|
_currentShortcutKeyMap = map;
|
|
resetMap = false; // we don't reset if
|
|
}
|
|
else
|
|
{
|
|
var expression = (ScriptExpression) value;
|
|
var result = EvaluateExpression(expression);
|
|
if (result is KalkActionObject command)
|
|
{
|
|
// Particular case the completion action, we handle it here
|
|
if (command.Action == "completion")
|
|
{
|
|
// In case of shift we go backward
|
|
if (OnCompletionRequested(hasShift, line, ref cursorIndex))
|
|
{
|
|
resetCompletion = false;
|
|
return true;
|
|
}
|
|
}
|
|
else if (OnAction != null)
|
|
{
|
|
command.Call(OnAction);
|
|
}
|
|
}
|
|
else if (result != null)
|
|
{
|
|
var resultStr = ObjectToString(result);
|
|
line.Insert(cursorIndex, resultStr);
|
|
cursorIndex += resultStr.Length;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (resetMap)
|
|
{
|
|
// Restore the root key map in case of an error.
|
|
_currentShortcutKeyMap = Shortcuts.ShortcutKeyMap;
|
|
}
|
|
|
|
if (resetCompletion)
|
|
{
|
|
ResetCompletion();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool OnCompletionRequested(bool backward, ConsoleText line, ref int cursorIndex)
|
|
{
|
|
// Nothing to complete
|
|
if (cursorIndex == 0) return false;
|
|
|
|
// We expect to have at least:
|
|
// - one letter identifier before the before the cursor
|
|
// - no letter identifier on the cursor (e.g middle of an existing identifier)
|
|
if (cursorIndex < line.Count && IsIdentifierLetter(line[cursorIndex].Value) || !IsIdentifierLetter(line[cursorIndex - 1].Value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// _currentIndexInCompletionMatchingList
|
|
if (_currentIndexInCompletionMatchingList < 0)
|
|
{
|
|
if (!CollectCompletionList(line, cursorIndex))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Go to next word
|
|
_currentIndexInCompletionMatchingList = (_currentIndexInCompletionMatchingList + (backward ? -1 : 1));
|
|
|
|
// Wrap the result
|
|
if (_currentIndexInCompletionMatchingList >= _completionMatchingList.Count) _currentIndexInCompletionMatchingList = 0;
|
|
if (_currentIndexInCompletionMatchingList < 0) _currentIndexInCompletionMatchingList = _completionMatchingList.Count - 1;
|
|
|
|
if (_currentIndexInCompletionMatchingList < 0 || _currentIndexInCompletionMatchingList >= _completionMatchingList.Count) return false;
|
|
|
|
var index = _startIndexForCompletion;
|
|
var newText = _completionMatchingList[_currentIndexInCompletionMatchingList];
|
|
|
|
line.RemoveRangeAt(index, cursorIndex - index);
|
|
line.Insert(index, newText);
|
|
cursorIndex = index + newText.Length;
|
|
return true;
|
|
}
|
|
|
|
private void ResetCompletion()
|
|
{
|
|
_currentIndexInCompletionMatchingList = -1;
|
|
_completionMatchingList.Clear();
|
|
}
|
|
|
|
private bool CollectCompletionList(ConsoleText line, int cursorIndex)
|
|
{
|
|
var text = line.ToString();
|
|
var lexer = new Lexer(text, options: _lexerOptions);
|
|
var tokens = lexer.ToList();
|
|
|
|
// Find that we are in a correct place
|
|
var index = FindTokenIndexFromColumnIndex(cursorIndex, text.Length, tokens);
|
|
if (index >= 0)
|
|
{
|
|
// If we are in the middle of a comment/integer/float/string
|
|
// we don't expect to make any completion
|
|
var token = tokens[index];
|
|
switch (token.Type)
|
|
{
|
|
case TokenType.Comment:
|
|
case TokenType.CommentMulti:
|
|
case TokenType.Identifier:
|
|
case TokenType.IdentifierSpecial:
|
|
case TokenType.Integer:
|
|
case TokenType.HexaInteger:
|
|
case TokenType.BinaryInteger:
|
|
case TokenType.Float:
|
|
case TokenType.String:
|
|
case TokenType.ImplicitString:
|
|
case TokenType.VerbatimString:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Look for the start of the work to complete
|
|
_startIndexForCompletion = cursorIndex - 1;
|
|
while (_startIndexForCompletion >= 0)
|
|
{
|
|
var c = text[_startIndexForCompletion];
|
|
if (!IsIdentifierLetter(c))
|
|
{
|
|
break;
|
|
}
|
|
|
|
_startIndexForCompletion--;
|
|
}
|
|
_startIndexForCompletion++;
|
|
|
|
if (!IsFirstIdentifierLetter(text[_startIndexForCompletion]))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var startTextToFind = text.Substring(_startIndexForCompletion, cursorIndex - _startIndexForCompletion);
|
|
|
|
Collect(startTextToFind, ScriptKeywords, _completionMatchingList);
|
|
Collect(startTextToFind, ValueKeywords, _completionMatchingList);
|
|
Collect(startTextToFind, Variables.Keys, _completionMatchingList);
|
|
Collect(startTextToFind, Builtins.Keys, _completionMatchingList);
|
|
|
|
// If we are not able to match anything from builtin and user variables/functions
|
|
// continue on units
|
|
if (_completionMatchingList.Count == 0)
|
|
{
|
|
Collect(startTextToFind, Units.Keys, _completionMatchingList);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
private static void Collect(string startText, IEnumerable<string> keys, List<string> matchingList)
|
|
{
|
|
foreach (var key in keys.OrderBy(x => x))
|
|
{
|
|
if (key.StartsWith(startText))
|
|
{
|
|
matchingList.Add(key);
|
|
}
|
|
}
|
|
}
|
|
private static bool IsIdentifierCharacter(char c)
|
|
{
|
|
var newCategory = GetCharCategory(c);
|
|
|
|
switch (newCategory)
|
|
{
|
|
case UnicodeCategory.UppercaseLetter:
|
|
case UnicodeCategory.LowercaseLetter:
|
|
case UnicodeCategory.TitlecaseLetter:
|
|
case UnicodeCategory.ModifierLetter:
|
|
case UnicodeCategory.OtherLetter:
|
|
case UnicodeCategory.NonSpacingMark:
|
|
case UnicodeCategory.DecimalDigitNumber:
|
|
case UnicodeCategory.ModifierSymbol:
|
|
case UnicodeCategory.ConnectorPunctuation:
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsFirstIdentifierLetter(char c)
|
|
{
|
|
return c == '_' || char.IsLetter(c);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsIdentifierLetter(char c)
|
|
{
|
|
return IsFirstIdentifierLetter(c) || char.IsDigit(c);
|
|
}
|
|
|
|
private static UnicodeCategory GetCharCategory(char c)
|
|
{
|
|
if (c == '_' || (c >= '0' && c <= '9')) return UnicodeCategory.LowercaseLetter;
|
|
c = char.ToLowerInvariant(c);
|
|
return char.GetUnicodeCategory(c);
|
|
}
|
|
}
|
|
} |