// Copyright (c) 2010 Joe Moorhouse
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
using Microsoft.Scripting.Hosting.Shell;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Remoting;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Style = Microsoft.Scripting.Hosting.Shell.Style;
namespace PythonConsoleControl
{
public delegate void ConsoleInitializedEventHandler(object sender, EventArgs e);
///
/// Custom IronPython console. The command dispatcher runs on a separate UI thread from the REPL
/// and also from the WPF control.
///
public class PythonConsole : IConsole, IDisposable
{
private bool allowFullAutocompletion = true;
public bool AllowFullAutocompletion
{
get { return allowFullAutocompletion; }
set { allowFullAutocompletion = value; }
}
private bool disableAutocompletionForCallables = true;
public bool DisableAutocompletionForCallables
{
get { return disableAutocompletionForCallables; }
set
{
if (textEditor.CompletionProvider != null) textEditor.CompletionProvider.ExcludeCallables = value;
disableAutocompletionForCallables = value;
}
}
private bool allowCtrlSpaceAutocompletion = false;
public bool AllowCtrlSpaceAutocompletion
{
get { return allowCtrlSpaceAutocompletion; }
set { allowCtrlSpaceAutocompletion = value; }
}
private PythonTextEditor textEditor;
private int lineReceivedEventIndex = 0; // The index into the waitHandles array where the lineReceivedEvent is stored.
private ManualResetEvent lineReceivedEvent = new ManualResetEvent(false);
private ManualResetEvent disposedEvent = new ManualResetEvent(false);
private AutoResetEvent statementsExecutionRequestedEvent = new AutoResetEvent(false);
private WaitHandle[] waitHandles;
private int promptLength = 4;
private List previousLines = new List();
private CommandLine commandLine;
private CommandLineHistory commandLineHistory = new CommandLineHistory();
private volatile bool executing = false;
// This is the thread upon which all commands execute unless the dipatcher is overridden.
private Thread dispatcherThread;
public Dispatcher dispatcher;
private string scriptText = String.Empty;
private bool consoleInitialized = false;
private string prompt;
public event ConsoleInitializedEventHandler ConsoleInitialized;
public ScriptScope ScriptScope
{
get { return commandLine.ScriptScope; }
}
public PythonConsole(PythonTextEditor textEditor, CommandLine commandLine)
{
waitHandles = new WaitHandle[] { lineReceivedEvent, disposedEvent };
this.commandLine = commandLine;
this.textEditor = textEditor;
textEditor.CompletionProvider = new PythonConsoleCompletionDataProvider(commandLine) { ExcludeCallables = disableAutocompletionForCallables };
textEditor.PreviewKeyDown += textEditor_PreviewKeyDown;
textEditor.TextEntering += textEditor_TextEntering;
dispatcherThread = new Thread(new ThreadStart(DispatcherThreadStartingPoint));
dispatcherThread.SetApartmentState(ApartmentState.STA);
dispatcherThread.IsBackground = true;
dispatcherThread.Start();
// Only required when running outside REP loop.
prompt = ">>> ";
// Set commands:
this.textEditor.textArea.Dispatcher.Invoke(new Action(delegate ()
{
CommandBinding pasteBinding = null;
CommandBinding copyBinding = null;
CommandBinding cutBinding = null;
CommandBinding undoBinding = null;
CommandBinding deleteBinding = null;
foreach (CommandBinding commandBinding in (this.textEditor.textArea.CommandBindings))
{
if (commandBinding.Command == ApplicationCommands.Paste) pasteBinding = commandBinding;
if (commandBinding.Command == ApplicationCommands.Copy) copyBinding = commandBinding;
if (commandBinding.Command == ApplicationCommands.Cut) cutBinding = commandBinding;
if (commandBinding.Command == ApplicationCommands.Undo) undoBinding = commandBinding;
if (commandBinding.Command == ApplicationCommands.Delete) deleteBinding = commandBinding;
}
// Remove current bindings completely from control. These are static so modifying them will cause other
// controls' behaviour to change too.
if (pasteBinding != null) this.textEditor.textArea.CommandBindings.Remove(pasteBinding);
if (copyBinding != null) this.textEditor.textArea.CommandBindings.Remove(copyBinding);
if (cutBinding != null) this.textEditor.textArea.CommandBindings.Remove(cutBinding);
if (undoBinding != null) this.textEditor.textArea.CommandBindings.Remove(undoBinding);
if (deleteBinding != null) this.textEditor.textArea.CommandBindings.Remove(deleteBinding);
this.textEditor.textArea.CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, OnPaste, CanPaste));
this.textEditor.textArea.CommandBindings.Add(new CommandBinding(ApplicationCommands.Copy, OnCopy, PythonEditingCommandHandler.CanCutOrCopy));
this.textEditor.textArea.CommandBindings.Add(new CommandBinding(ApplicationCommands.Cut, PythonEditingCommandHandler.OnCut, CanCut));
this.textEditor.textArea.CommandBindings.Add(new CommandBinding(ApplicationCommands.Undo, OnUndo, CanUndo));
this.textEditor.textArea.CommandBindings.Add(new CommandBinding(ApplicationCommands.Delete, PythonEditingCommandHandler.OnDelete(ApplicationCommands.NotACommand), CanDeleteCommand));
}));
// Set dispatcher to run on a UI thread independent of both the Control UI thread and thread running the REPL.
WhenConsoleInitialized(delegate
{
SetCommandDispatcher(DispatchCommand);
});
}
public Action GetCommandDispatcher()
{
var languageContext = Microsoft.Scripting.Hosting.Providers.HostingHelpers.GetLanguageContext(commandLine.ScriptScope.Engine);
var pythonContext = (IronPython.Runtime.PythonContext)languageContext;
var result = pythonContext.GetSetCommandDispatcher(null);
pythonContext.GetSetCommandDispatcher(result);
return result;
}
public void SetCommandDispatcher(Action newDispatcher)
{
var languageContext = Microsoft.Scripting.Hosting.Providers.HostingHelpers.GetLanguageContext(commandLine.ScriptScope.Engine);
var pythonContext = (IronPython.Runtime.PythonContext)languageContext;
pythonContext.GetSetCommandDispatcher(newDispatcher);
}
protected void DispatchCommand(Delegate command)
{
if (command != null)
{
// Slightly involved form to enable keyboard interrupt to work.
executing = true;
var operation = dispatcher.BeginInvoke(DispatcherPriority.Normal, command);
while (executing)
{
if (operation.Status != DispatcherOperationStatus.Completed)
operation.Wait(TimeSpan.FromSeconds(1));
if (operation.Status == DispatcherOperationStatus.Completed)
executing = false;
}
}
}
///
/// Perform action only after the console was initialized.
///
public void WhenConsoleInitialized(Action action)
{
if (consoleInitialized)
{
action();
}
else
{
ConsoleInitialized += (sender, args) => action();
}
}
private void DispatcherThreadStartingPoint()
{
dispatcher = new Window().Dispatcher;
while (true)
{
try
{
System.Windows.Threading.Dispatcher.Run();
}
catch (ThreadAbortException tae)
{
if (tae.ExceptionState is Microsoft.Scripting.KeyboardInterruptException)
{
Thread.ResetAbort();
executing = false;
}
}
}
}
public void SetDispatcher(Dispatcher dispatcher)
{
this.dispatcher = dispatcher;
}
public void Dispose()
{
disposedEvent.Set();
textEditor.PreviewKeyDown -= textEditor_PreviewKeyDown;
textEditor.TextEntering -= textEditor_TextEntering;
}
public TextWriter Output
{
get { return null; }
set { }
}
public TextWriter ErrorOutput
{
get { return null; }
set { }
}
#region CommandHandling
protected void CanPaste(object target, CanExecuteRoutedEventArgs args)
{
if (IsInReadOnlyRegion)
{
args.CanExecute = false;
}
else
args.CanExecute = true;
}
protected void CanCut(object target, CanExecuteRoutedEventArgs args)
{
if (!CanDelete)
{
args.CanExecute = false;
}
else
PythonEditingCommandHandler.CanCutOrCopy(target, args);
}
protected void CanDeleteCommand(object target, CanExecuteRoutedEventArgs args)
{
if (!CanDelete)
{
args.CanExecute = false;
}
else
PythonEditingCommandHandler.CanDelete(target, args);
}
protected void CanUndo(object target, CanExecuteRoutedEventArgs args)
{
args.CanExecute = false;
}
protected void OnPaste(object target, ExecutedRoutedEventArgs args)
{
if (target != textEditor.textArea) return;
TextArea textArea = textEditor.textArea;
if (textArea != null && textArea.Document != null)
{
Debug.WriteLine(Clipboard.GetText(TextDataFormat.Html));
// convert text back to correct newlines for this document
string newLine = TextUtilities.GetNewLineFromDocument(textArea.Document, textArea.Caret.Line);
string text = TextUtilities.NormalizeNewLines(Clipboard.GetText(), newLine);
string[] commands = text.Split(new String[] { newLine }, StringSplitOptions.None);
string scriptText = "";
if (commands.Length > 1)
{
text = newLine;
foreach (string command in commands)
{
text += "... " + command + newLine;
scriptText += command.Replace("\t", " ") + newLine;
}
}
if (!string.IsNullOrEmpty(text))
{
bool fullLine = textArea.Options.CutCopyWholeLine && Clipboard.ContainsData(LineSelectedType);
bool rectangular = Clipboard.ContainsData(RectangleSelection.RectangularSelectionDataType);
if (fullLine)
{
DocumentLine currentLine = textArea.Document.GetLineByNumber(textArea.Caret.Line);
if (textArea.ReadOnlySectionProvider.CanInsert(currentLine.Offset))
{
textArea.Document.Insert(currentLine.Offset, text);
}
}
else if (rectangular && textArea.Selection.IsEmpty)
{
if (!RectangleSelection.PerformRectangularPaste(textArea, textArea.Caret.Position, text, false))
textEditor.Write(text, false);
}
else
{
textEditor.Write(text, false);
}
}
textArea.Caret.BringCaretToView();
args.Handled = true;
if (commands.Length > 1)
{
lock (this.scriptText)
{
this.scriptText = scriptText;
}
dispatcher.BeginInvoke(new Action(delegate () { ExecuteStatements(); }));
}
}
}
protected void OnCopy(object target, ExecutedRoutedEventArgs args)
{
if (target != textEditor.textArea) return;
if (textEditor.SelectionLength == 0 && executing)
{
// Send the 'Ctrl-C' abort
//if (!IsInReadOnlyRegion)
//{
MoveToHomePosition();
//textEditor.Column = GetLastTextEditorLine().Length + 1;
//textEditor.Write(Environment.NewLine);
//}
dispatcherThread.Abort(new Microsoft.Scripting.KeyboardInterruptException(""));
args.Handled = true;
}
else PythonEditingCommandHandler.OnCopy(target, args);
}
private const string LineSelectedType = "MSDEVLineSelect"; // This is the type VS 2003 and 2005 use for flagging a whole line copy
protected void OnUndo(object target, ExecutedRoutedEventArgs args)
{
}
#endregion CommandHandling
///
/// Run externally provided statements in the Console Engine.
///
///
public void RunStatements(string statements)
{
MoveToHomePosition();
lock (this.scriptText)
{
this.scriptText = statements;
}
dispatcher.BeginInvoke(new Action(delegate () { ExecuteStatements(); }));
}
///
/// Run on the statement execution thread.
///
private void ExecuteStatements()
{
lock (scriptText)
{
textEditor.Write("\r\n");
ScriptSource scriptSource = commandLine.ScriptScope.Engine.CreateScriptSourceFromString(scriptText, SourceCodeKind.Statements);
string error = "";
try
{
executing = true;
var errors = new ErrorReporter();
var command = scriptSource.Compile(errors);
if (command == null)
{
// compilation failed
error = "Syntax Error: " + string.Join("\nSyntax Error: ", errors.Errors) + "\n";
}
else
{
ObjectHandle wrapexception = null;
GetCommandDispatcher()(() => scriptSource.ExecuteAndWrap(commandLine.ScriptScope, out wrapexception));
if (wrapexception != null)
{
error = "Exception : " + wrapexception.Unwrap().ToString() + "\n";
}
}
}
catch (ThreadAbortException tae)
{
if (tae.ExceptionState is Microsoft.Scripting.KeyboardInterruptException) Thread.ResetAbort();
error = "KeyboardInterrupt" + System.Environment.NewLine;
}
catch (Microsoft.Scripting.SyntaxErrorException exception)
{
ExceptionOperations eo;
eo = commandLine.ScriptScope.Engine.GetService();
error = eo.FormatException(exception);
}
catch (Exception exception)
{
ExceptionOperations eo;
eo = commandLine.ScriptScope.Engine.GetService();
error = eo.FormatException(exception) + System.Environment.NewLine;
}
executing = false;
if (error != "") textEditor.Write(error);
textEditor.Write(prompt);
}
}
///
/// Returns the next line typed in by the console user. If no line is available this method
/// will block.
///
public string ReadLine(int autoIndentSize)
{
string indent = String.Empty;
if (autoIndentSize > 0)
{
indent = String.Empty.PadLeft(autoIndentSize);
Write(indent, Style.Prompt);
}
string line = ReadLineFromTextEditor();
if (line != null)
{
return indent + line;
}
return null;
}
///
/// Writes text to the console.
///
public void Write(string text, Style style)
{
textEditor.Write(text);
if (style == Style.Prompt)
{
promptLength = text.Length;
if (!consoleInitialized)
{
consoleInitialized = true;
if (ConsoleInitialized != null) ConsoleInitialized(this, EventArgs.Empty);
}
}
}
///
/// Writes text followed by a newline to the console.
///
public void WriteLine(string text, Style style)
{
Write(text + Environment.NewLine, style);
}
///
/// Writes an empty line to the console.
///
public void WriteLine()
{
Write(Environment.NewLine, Style.Out);
}
///
/// Indicates whether there is a line already read by the console and waiting to be processed.
///
public bool IsLineAvailable
{
get
{
lock (previousLines)
{
return previousLines.Count > 0;
}
}
}
///
/// Gets the text that is yet to be processed from the console. This is the text that is being
/// typed in by the user who has not yet pressed the enter key.
///
public string GetCurrentLine()
{
string fullLine = GetLastTextEditorLine();
return fullLine.Substring(promptLength);
}
///
/// Gets the lines that have not been returned by the ReadLine method. This does not
/// include the current line.
///
public string[] GetUnreadLines()
{
return previousLines.ToArray();
}
private string GetLastTextEditorLine()
{
return textEditor.GetLine(textEditor.TotalLines - 1);
}
private string ReadLineFromTextEditor()
{
int result = WaitHandle.WaitAny(waitHandles);
if (result == lineReceivedEventIndex)
{
lock (previousLines)
{
string line = previousLines[0];
previousLines.RemoveAt(0);
if (previousLines.Count == 0)
{
lineReceivedEvent.Reset();
}
return line;
}
}
return null;
}
///
/// Processes characters entered into the text editor by the user.
///
private void textEditor_PreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Delete:
if (!CanDelete) e.Handled = true;
return;
case Key.Tab:
if (IsInReadOnlyRegion) e.Handled = true;
return;
case Key.Back:
if (!CanBackspace) e.Handled = true;
return;
case Key.Home:
MoveToHomePosition();
e.Handled = true;
return;
case Key.Down:
if (!IsInReadOnlyRegion) MoveToNextCommandLine();
e.Handled = true;
return;
case Key.Up:
if (!IsInReadOnlyRegion) MoveToPreviousCommandLine();
e.Handled = true;
return;
}
}
///
/// Processes characters entering into the text editor by the user.
///
private void textEditor_TextEntering(object sender, TextCompositionEventArgs e)
{
if (e.Text.Length > 0)
{
if (!char.IsLetterOrDigit(e.Text[0]))
{
// Whenever a non-letter is typed while the completion window is open,
// insert the currently selected element.
textEditor.RequestCompletioninsertion(e);
}
}
if (IsInReadOnlyRegion)
{
e.Handled = true;
}
else
{
if (e.Text[0] == '\n')
{
OnEnterKeyPressed();
}
if (e.Text[0] == '.' && allowFullAutocompletion)
{
textEditor.ShowCompletionWindow();
}
if ((e.Text[0] == ' ') && (Keyboard.Modifiers == ModifierKeys.Control))
{
e.Handled = true;
if (allowCtrlSpaceAutocompletion) textEditor.ShowCompletionWindow();
}
}
}
///
/// Move cursor to the end of the line before retrieving the line.
///
private void OnEnterKeyPressed()
{
textEditor.StopCompletion();
if (textEditor.WriteInProgress) return;
lock (previousLines)
{
// Move cursor to the end of the line.
textEditor.Column = GetLastTextEditorLine().Length + 1;
// Append line.
string currentLine = GetCurrentLine();
previousLines.Add(currentLine);
commandLineHistory.Add(currentLine);
lineReceivedEvent.Set();
}
}
///
/// Returns true if the cursor is in a readonly text editor region.
///
private bool IsInReadOnlyRegion
{
get { return IsCurrentLineReadOnly || IsInPrompt; }
}
///
/// Only the last line in the text editor is not read only.
///
private bool IsCurrentLineReadOnly
{
get { return textEditor.Line < textEditor.TotalLines; }
}
///
/// Determines whether the current cursor position is in a prompt.
///
private bool IsInPrompt
{
get { return textEditor.Column - promptLength - 1 < 0; }
}
///
/// Returns true if the user can delete at the current cursor position.
///
private bool CanDelete
{
get
{
if (textEditor.SelectionLength > 0) return SelectionIsDeletable;
else return !IsInReadOnlyRegion;
}
}
///
/// Returns true if the user can backspace at the current cursor position.
///
private bool CanBackspace
{
get
{
if (textEditor.SelectionLength > 0) return SelectionIsDeletable;
else
{
int cursorIndex = textEditor.Column - promptLength - 1;
return !IsCurrentLineReadOnly && (cursorIndex > 0 || (cursorIndex == 0 && textEditor.SelectionLength > 0));
}
}
}
private bool SelectionIsDeletable
{
get
{
return (!textEditor.SelectionIsMultiline
&& !IsCurrentLineReadOnly
&& (textEditor.SelectionStartColumn - promptLength - 1 >= 0)
&& (textEditor.SelectionEndColumn - promptLength - 1 >= 0));
}
}
///
/// The home position is at the start of the line after the prompt.
///
private void MoveToHomePosition()
{
textEditor.Line = textEditor.TotalLines;
textEditor.Column = promptLength + 1;
}
///
/// Shows the previous command line in the command line history.
///
private void MoveToPreviousCommandLine()
{
if (commandLineHistory.MovePrevious())
{
ReplaceCurrentLineTextAfterPrompt(commandLineHistory.Current);
}
}
///
/// Shows the next command line in the command line history.
///
private void MoveToNextCommandLine()
{
textEditor.Line = textEditor.TotalLines;
if (commandLineHistory.MoveNext())
{
ReplaceCurrentLineTextAfterPrompt(commandLineHistory.Current);
}
}
///
/// Replaces the current line text after the prompt with the specified text.
///
private void ReplaceCurrentLineTextAfterPrompt(string text)
{
string currentLine = GetCurrentLine();
textEditor.Replace(promptLength, currentLine.Length, text);
// Put cursor at end.
textEditor.Column = promptLength + text.Length + 1;
}
}
public class ErrorReporter : ErrorListener
{
public List Errors = new List();
public override void ErrorReported(ScriptSource source, string message, SourceSpan span, int errorCode, Severity severity)
{
Errors.Add(string.Format("{0} (line {1})", message, span.Start.Line));
}
public int Count
{
get { return Errors.Count; }
}
}
}