GUI REPL for Roslyn
If you recall from REPL for the Rosyln CTP 10/2011, I’ve been playing around building a little C# REPL app using Roslyn. That version was built as a Console application, but I’ve refactored and rebuilt it as a WPF application:
You can download the source code for both the Console and the WPF versions here:
The benefit of a real GUI app is that output selection makes a lot more sense and that you could imagine real data visualization into data controls instead of just into strings. However, implementing a REPL shell in a GUI environment requires doing things considerably differently than in a Console app. Besides the stupid things I did, like doing a lot of Console.Write, and things that don’t make sense, like #exit or #prompt, there are a few interesting things that I did with this code, including handling partial submissions, rethinking history and rewiring Console.Write (just ’cuz it’s stupid when I do it doesn’t mean that it shouldn’t work).
Partial Submissions
In this REPL, I decided that Enter means “execute” or “newline” depending on whether the submission is complete enough, according to Roslyn, to execute or not. If it is, I execute it, produce the output and move focus to either the next or a new submission TextBox. If the submission isn’t yet complete, e.g. “void SayHi() {”, then I just put in a newline. Further, I do some work to work properly with selections, i.e. if you press Enter when there’s a selection, the selection will be replaced with the Enter key.
So far I like this model a lot, since I don’t have to something like separate “execute” and “newline” into Enter and Alt+Enter or some such.
Rethinking History
In a GUI shell with partial submissions and multi-line editing, the arrows are important editing keys, so can’t be used for access to previous lines in history. Further, a GUI apps makes it very easy to simply scroll to the command that you want via the mouse or Shift+Tab, so there’s not a lot of use for Alt+Arrow keys. Pressing Enter again replaces the old output (or error) with new output (or error):
Currently when you re-execute a command from history, the command stays where it is in the history sequence, but it could as easily move to the end. I haven’t yet decided which I like better.
Redirecting Console.Write
Since this is a REPL environment works and acts like a shell, I expect that Console.Write (and it’s cousins like Console.WriteLine) to work. However, to make that work, I need to redirect standard output:
Console.SetOut(new ReplHostTextWriter(host));
The ReplTextWriterClass simply forwards the text onto the host:
class ReplHostTextWriter : TextWriter { readonly IReplHost host;
public ReplHostTextWriter(IReplHost host) { this.host = host; } public override void Write(char value) { host.Write(value.ToString()); } public override Encoding Encoding { get { return Encoding.Default; } } }
The hosts implementation of IReplHost.Write simply forwards it onto the currently executing submission (the ReplSubmissionControl represents both a submission’s input and output bundled together). You’ll notice that the TextWriter takes each character one at a time. It would be nice to do some buffering for efficiency, but you’d also like the output to appear as its produced, so I opted out of buffering.
However, one thing I don’t like is the extra newline at the end of most string output. I want the main window to decide how things are output, setting margins and the newline looks like a wacky margin, so the trailing CR/LF had to go. That’s an interesting algorithm to implement, however, since the characters come in one at a time and not line-by-line. I want separating newlines to appear, just not trailing newlines. I implement this policy with the TrimmedStringBuilder class:
// Output a stream of strings with \r\n pairs potentially spread across strings, // trimming the trailing \r and \r\n to avoid the output containing the extra spacing. class TrimmedStringBuilder { readonly StringBuilder sb; public TrimmedStringBuilder(string s = "") { sb = new StringBuilder(s); } public void Clear() { sb.Clear(); } public void Append(string s) { sb.Append(s); } public override string ToString() { int len = sb.Length; if (len >= 1 && sb[len - 1] == '\r') { len -= 1; } else if (len >= 2 && sb[len - 2] == '\r' && sb[len - 1] == '\n') { len -= 2; } return sb.ToString(0, len); } }
Usage inside the ReplSubmissionControl.Write method is like so:
public partial class ReplSubmissionControl : UserControl {
...TrimmedStringBuilder trimmedOutput = new TrimmedStringBuilder(); public void Write(string s) { if (s == null) { trimmedOutput.Clear(); } else { trimmedOutput.Append(s); } consoleContainer.Content = GetTextControl(trimmedOutput.ToString()); } }
Now, as the input comes in one character at a time, the trailing newlines are removed but separating newlines are kept. Also, you may be interested to know that the GetTextControl function builds a new read-only TextBox control on the fly to host the string content. This is so that the text can be selected, which isn’t possible when you set the content directly.
Right now, there’s no support for Console.Read, since I don’t really know how I want that to happen yet. Pop-up a dialog box? Something else?
Completions, Syntax Highlighting and Auto-indent
I was a few hundred lines into implementing completions using Roslyn with the help of the Roslyn team when I realized two things:
- Implementing completions to mimic the VS editor is hard.
- Completions aren’t enough — I really want an entire C# editor with completions, syntax highlighting and auto-indentation.
Maybe a future release of Roslyn will fix one or both of these issues, but for now, both are out of scope for my little REPL project.