This recent Abbot Blog Post covers abbot-cli a new open source command-line tool. abbot-cli makes it possible to work on Abbot skills in your local editor. In that post, I mentioned that when you retrieve a C# skill to edit locally, the tool writes a few aditional files on your machine.

For C# skills, these other files make it possible for us to provide Intellisense for the skill editing experience. More on that in another post for those interested.

In this post, I want to follow-up on that and cover how that works and why it’s interesting.

C# Scripts

First, a bit of background. If you go to Abbot today and edit a C# skill, you get a nice in-browser editor with IntelliSense.

Screenshot of C# IntelliSense within Abbot

In truth, these aren’t exactly C# skills. They’re a dialect of C# known as CSharpScript. Roslyn supports this class in the Microsoft.CodeAnalysis.CSharp.Scripting namespace.

The benefit of using CSharpScript over proper C# is it removes a lot of the ceremony that comes with writing C# skills. For example, CSharpScript supports top-level statements, which are only now being introduced in the latest version of C# proper. Also, since Abbot hosts the C# scripting runtime, it can inject a global variable (Bot) into the script. This lets script authors focus on writing code to accomplish their task and not worry about declaring a class and method (unless they want to).

MirrorSharp for the Web

To provide IntelliSense in our web editor, we use the excellent MirrorSharp project by Andrey Shchekin who is also the creator of the more well-known SharpLab.

MirrorSharp sets up Roslyn on a websocket to provide IntelliSense. More importantly, it supports CSharpScript.

endpoints.MapMirrorSharp("/mirrorsharp",
    new MirrorSharpOptions
    {
        SelfDebugEnabled = true,
        IncludeExceptionDetails = true
    }
    .SetupCSharp(o =>
    {
        // Other stuff omitted
        o.SetScriptMode(hostObjectType: typeof(IScriptGlobals));
    })
);

Note that the Bot instance we inject into Abbot skills is the Bot property of IScriptGlobals, which is specified as the hostObjectType in the SetupCSharp call. With this in place, we can provide really great IntelliSense when editing a C# Abbot skill in our web editor.

OmniSharp for the desktop

But what happens when you want edit a skill on your local machine? Can we provide a good IntelliSense experience? Unfortunately, we can’t use MirrorSharp for that as it’s focused on the web. But there is a solution for desktop editors, omnisharp-roslyn.

OmniSharp is a .NET development platform based on Roslyn workspaces. It provides project dependencies and C# language services to various IDEs and plugins.

Several editors support OmniSharp including Visual Studio Code via the omnisharp-vscode extension.

But OmniSharp doesn’t understand the Abbot runtime. It’s not going to know about the Bot instance we inject into Abbot skills. Also, what about the namespaces and dependencies we inject? And finally, how do we tell OmniSharp we’re using CSharpScript and not C#?

OmniSharp Configuration

The first step we did was to write a .csx file instead of .cs. .csx is a known extension for C# Script files and is recognized by OmniSharp.

The next step is to configure OmniSharp by writing an omnisharp.json file into the same directory as our .csx file.

{
    "script": {
        "enabled": true,
        "defaultTargetFramework": "net5.0",
        "enableScriptNuGetReferences": true,
        "RspFilePath": "../.abbot/references.rsp"
    }
}

This lets OmniSharp know that we’re using C# Script, and that we want to use references.rsp to specify the references. What is an RSP file? It provides command-line options to the C# compiler. For example, this is the one used by csc.exe.

Here’s the one we write for Abbot, references.rsp.

/u:System
/u:System.Collections
/u:System.Collections.Concurrent;
/u:System.Collections.Generic
/u:System.Data
/u:System.Dynamic
/u:System.Globalization
/u:System.Linq
/u:System.Linq.Expressions
/u:System.Net.Http
/u:System.Text
/u:System.Text.RegularExpressions
/u:System.Threading
/u:System.Threading.Tasks
/u:Serious.Abbot.Scripting
/u:NodaTime

With this configuration in place, OmniSharp includes all these namespaces in every .csx file in the project. That’s an improvement, but we still have some work to do. For one thing, we can’t assume Serious.Abbot.Scripting, nor NodaTime is on your machine. Also, we still haven’t injected the Bot instance into the script.

Ideally, we could use the .rsp file to inject our Script Globals type into the script, but that’s not possible at this time. There’s an open issue in the Roslyn repository to add this feature. There’s also an issue in the omnisharp-roslyn repo to provide a configuration option for the script host, but it’s not feasible at this time.

Engaging in some dark arts

This is where I had to engage in some dark arts. One thing that OmniSharp supports for .csx file is a #load directive. This lets you load another script into the script. When the abbot-cli tool downloads an Abbot skill, it writes the following directive at the top of the file:

#load ".meta/globals.csx" // This is required for Intellisense in VS Code, etc. DO NOT TOUCH THIS LINE!
// The rest of your skill code...

When abbot-cli runs the skill or deploys it, it strips that directive.

This directive injects .meta/globals.csx into the beginning of the script. Let’s take a look at it.

#r "nuget:NodaTime,3.0.5"
#r "nuget:HtmlAgilityPack,1.11.34"
#r "nuget:Abbot.Scripting.Stubs,0.9.0"

var Bot = new Serious.Abbot.Scripting.Bot();

The first three lines use the #r directive which is used to reference an Assembly. However, OmniSharp supports the nuget: prefix that lets you reference NuGet packages. And then we just instantiate a local variable named Bot. It’s not exactly the same thing as the Bot property of IScriptGlobals, but it’s close enough.

With this in place, if you open an Abbot .csx file in your editor, you’ll see something like this:

Screen shot of VS Code showing Intellisense for Bot

Magic!

Next Steps

I’m not above engaging in some dark arts here and there, but it’s not a great permanent solution. I opened an omnisharp-roslyn issue to allow specifying includes within omnisharp.json. It’s not a perfect solution, but it would allow us to stop injecting our #load directive. I’m happy to work on that feature, but I’m waiting on the project maintainers to provide feedback to make sure the feature as described makes sense.

If you’re writing C# skills for Abbot, I hope you give abbot-cli a try and give us some feedback!