Sicherung
This commit is contained in:
460
Kalk/Kalk.Core/KalkEngine.Repl.cs
Normal file
460
Kalk/Kalk.Core/KalkEngine.Repl.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user