Commit feb30b4a authored by Jesse McGrew's avatar Jesse McGrew

* working on blazor REPL and project editor

parent 0d60d2f8f165
......@@ -66,7 +66,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Package", "Package", "{3E28
Package\zip\Zip.Packaging.targets = Package\zip\Zip.Packaging.targets
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Zilf.Common.Tests", "test\Zilf.Common.Tests\Zilf.Common.Tests.csproj", "{337F098C-4DD4-420D-825C-4A40DF99AD89}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zilf.Common.Tests", "test\Zilf.Common.Tests\Zilf.Common.Tests.csproj", "{337F098C-4DD4-420D-825C-4A40DF99AD89}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Zilf.Playground", "src\Zilf.Playground\Zilf.Playground.csproj", "{D38CAE84-836D-416D-83F6-DE2F2502658C}"
EndProject
......
......@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="JunitXml.TestLogger" Version="2.1.78" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="16.9.1025">
<PrivateAssets>all</PrivateAssets>
......
......@@ -24,7 +24,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.7.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" PrivateAssets="all" />
<!--<PackageReference Update="NETStandard.Library" PrivateAssets="all" />-->
</ItemGroup>
......
#nullable enable
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Zilf.Playground
{
internal sealed class JSInterop
{
private readonly IJSRuntime js;
public JSInterop(IJSRuntime js)
{
this.js = js;
}
public void DownloadBytesAsFile(byte[] bytes, string filename, string contentType)
{
var jsu = (IJSUnmarshalledRuntime)js;
jsu.InvokeUnmarshalled<string, string, byte[], bool>("ZilfJsInterop.BlazorDownloadFileFast", filename, contentType, bytes);
}
public async Task ScrollToBottomAsync(ElementReference element)
{
await js.InvokeVoidAsync("ZilfJsInterop.scrollToBottom", element);
}
public async Task<string> PromptAsync(string prompt, string value)
{
return await js.InvokeAsync<string>("prompt", prompt, value);
}
public async Task AddEventListenerAsync(ElementReference element, string eventName, string jsHandler, params object[] extraHandlerArgs)
{
await js.InvokeVoidAsync("ZilfJsInterop.addEventListener", element, eventName, jsHandler, extraHandlerArgs);
}
}
}
@page "/doc"
<h3>Documentation</h3>
<ul>
<li><a href="http://faq.zilf.io/">Getting Started</a></li>
<li><a href="http://g.zilf.io/">ZIL Group on Facebook</a></li>
</ul>
@page "/"
@using Zapf
@using Zilf.Common
@using Zilf.Compiler
@using Zilf.Diagnostics
<h1>ZIL Playground</h1>
<div>
<div style="margin:10px 0;">
Theme:
<select @onchange="ChangeTheme">
<option value="vs">Visual Studio</option>
<option value="vs-dark">Visual Studio Dark</option>
<option value="hc-black">High Contrast Black</option>
</select>
</div>
@*<div style="margin:10px 0;">
New Value: <input type="text" @bind="ValueToSet" style="width: 400px;" /> <button @onclick="SetValue">Set Value</button>
</div>
<div style="margin:10px 0;">
<button @onclick="GetValue">Get Value</button>
</div>
<div style="margin:10px 0;">
<button @onclick="AddCommand">Add Command (Ctrl+Enter)</button>
</div>
<div style="margin:10px 0;">
<button @onclick="AddAction">Add Action (Ctrl+D)</button>
</div>
<div style="margin:10px 0;">
See the console for results.
</div>*@
<div style="margin:10px 0;">
<button @onclick="Compile">Compile</button>
@layout ContainerlessLayout
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">ZIL Forever</h1>
<p class="lead">
It&rsquo;s scary, ugly, and hard to understand&mdash;and until recently, it was dead.
But it&rsquo;s back on its feet again, and you&rsquo;re gonna love it.
</p>
<p class="text-muted">
Before video games, there was interactive fiction.
The genre was defined by classics like <a href="https://en.wikipedia.org/wiki/Zork">Zork</a>,
<a href="https://en.wikipedia.org/wiki/Planetfall">Planetfall</a>,
and <a href="https://en.wikipedia.org/wiki/Enchanter_(video_game)">Enchanter</a>,
which were written in Infocom&rsquo;s custom programming language:
the Zork Implementation Language, or ZIL.
</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">Learn more &raquo;</a></p>
</div>
</div>
<MonacoEditor @ref="_editor"
Id="code-editor"
ConstructionOptions="EditorConstructionOptions"
OnDidInit="EditorOnDidInit"
OnContextMenu="EditorOnContextMenu"
/>
<CompilerOutput CompileResult="@CompileResult"
AssembleResult="@AssembleResult"
FileSystem="@FileSystem" />
@code {
private MonacoEditor _editor { get; set; }
private string ValueToSet { get; set; }
private FrontEndResult CompileResult { get; set; }
private AssemblyResult? AssembleResult { get; set; }
private InMemoryFileSystem FileSystem { get; set; }
private StandaloneEditorConstructionOptions EditorConstructionOptions(MonacoEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "zil",
GlyphMargin = true,
Value = "<VERSION ZIP>\n" +
"\n" +
"<ROUTINE GO ()\n" +
" <TELL \"Hello world!\" CR>\n" +
" <QUIT>>",
};
}
private async Task EditorOnDidInit(MonacoEditorBase editor)
{
await _editor.AddCommand((int)KeyMode.CtrlCmd | (int)KeyCode.KEY_H, (editor, keyCode) =>
{
Console.WriteLine("Ctrl+H : Initial editor command is triggered.");
System.Diagnostics.Debugger.Break();
});
//var newDecorations = new ModelDeltaDecoration[]
//{
// new ModelDeltaDecoration
// {
// Range = new BlazorMonaco.Bridge.Range(3,1,3,1),
// Options = new ModelDecorationOptions
// {
// IsWholeLine = true,
// ClassName = "decorationContentClass",
// GlyphMarginClassName = "decorationGlyphMarginClass"
// }
// }
//};
//decorationIds = await _editor.DeltaDecorations(null, newDecorations);
// You can now use 'decorationIds' to change or remove the decorations
}
//private string[] decorationIds;
private void EditorOnContextMenu(EditorMouseEvent eventArg)
{
Console.WriteLine("OnContextMenu : " + System.Text.Json.JsonSerializer.Serialize(eventArg));
}
private async Task ChangeTheme(ChangeEventArgs e)
{
Console.WriteLine($"setting theme to: {e.Value.ToString()}");
await MonacoEditor.SetTheme(e.Value.ToString());
}
private async Task SetValue()
{
Console.WriteLine($"setting value to: {ValueToSet}");
await _editor.SetValue(ValueToSet);
}
private async Task GetValue()
{
var val = await _editor.GetValue();
Console.WriteLine($"value is: {val}");
}
private async Task AddCommand()
{
await _editor.AddCommand((int)KeyMode.CtrlCmd | (int)KeyCode.Enter, (editor, keyCode) =>
{
Console.WriteLine("Ctrl+Enter : Editor command is triggered.");
});
}
private async Task AddAction()
{
await _editor.AddAction("testAction", "Test Action", new int[] { (int)KeyMode.CtrlCmd | (int)KeyCode.KEY_D, (int)KeyMode.CtrlCmd | (int)KeyCode.KEY_B }, null, null, "navigation", 1.5, (editor, keyCodes) =>
{
Console.WriteLine("Ctrl+D : Editor action is triggered.");
});
}
private async Task Compile()
{
var fileSystem = new InMemoryFileSystem
{
["main.zil"] = await _editor.GetValue(),
};
var compiler = new FrontEnd { FileSystem = fileSystem, Logger = NullDiagnosticLogger.Instance };
var compResult = compiler.Compile("main.zil", "main.zap", false);
<div class="container">
<div class="row">
<div class="col-md-4">
<h2>Portable</h2>
<p>
ZILF games run anywhere there&rsquo;s a <a href="https://en.wikipedia.org/wiki/Z-machine">Z-machine</a>
interpreter, including most computers, phones, and web browsers. ZILF itself is built with
<a href="https://dotnet.microsoft.com/">.NET</a> and runs on most computers too&mdash;Windows,
Linux, and macOS. You can even try it out right here, on the web.
</p>
<p><a class="btn btn-secondary" href="project/new" role="button">Try it now &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Retro</h2>
<p>
ZIL is based on <a href="https://mdl-language.readthedocs.io/en/latest/">MDL</a>, a Lisp-like
language developed for AI research in the 1970s. And it feels like it. All caps? Check.
Command line interface? Oh, yeah. Cryptic names? We got &rsquo;em.
</p>
<p><a class="btn btn-secondary" href="repl" role="button">Nerd it up &raquo;</a></p>
</div>
<div class="col-md-4">
<h2>Open</h2>
<p>
ZILF is free, open source software distributed under the GNU General Public License. You
can read the code, report issues, and submit patches at the Heptapod project site.
</p>
<p><a class="btn btn-secondary" href="https://foss.heptapod.net/zilf/zilf/" role="button">View project &raquo;</a></p>
</div>
</div>
CompileResult = compResult;
AssembleResult = null;
FileSystem = fileSystem;
<hr>
if (compResult.Success)
{
var assembler = new Zapf.ZapfAssembler { FileSystem = fileSystem };
AssembleResult = assembler.Assemble("main.zap", "main.z#");
}
}
}
\ No newline at end of file
</div> <!-- /container -->
@page "/oldindex"
@using Zapf
@using Zilf.Compiler
@using Zilf.Diagnostics
@inject TemplateManager Templates
<h1>ZIL Playground</h1>
<div>
<div style="margin:10px 0;">
<label for="theme">Theme:</label>
<select id="theme" @onchange="ChangeTheme">
<option value="vs">Visual Studio</option>
<option value="vs-dark">Visual Studio Dark</option>
<option value="hc-black">High Contrast Black</option>
</select>
</div>
<div style="margin:10px 0;">
<label for="project">Project:</label>
<select id="project" @onchange="ChangeProject">
@foreach (var name in AvailableTemplates)
{
<option value="@name">@name</option>
}
</select>
</div>
@*<FileSystemBrowser FileSystem="FileSystem"
OnChange="FileSystemBrowserOnChange"
/>*@
<div style="margin:10px 0;">
<button @onclick="Compile">Compile</button>
</div>
</div>
<MonacoEditor @ref="_editor"
Id="code-editor"
ConstructionOptions="EditorConstructionOptions"
OnDidInit="EditorOnDidInit"
OnContextMenu="EditorOnContextMenu"
/>
<CompilerOutput CompileResult="@CompileResult"
AssembleResult="@AssembleResult"
FileSystem="@FileSystem" />
@code {
private MonacoEditor _editor { get; set; }
private string ValueToSet { get; set; }
private FrontEndResult CompileResult { get; set; }
private AssemblyResult? AssembleResult { get; set; }
private InMemoryFileSystem FileSystem { get; } = new InMemoryFileSystem();
private IEnumerable<string> AvailableTemplates { get; set; } = Enumerable.Empty<string>();
protected override async Task OnInitializedAsync()
{
AvailableTemplates = await Templates.GetTemplateNamesAsync();
}
private StandaloneEditorConstructionOptions EditorConstructionOptions(MonacoEditor editor)
{
return new StandaloneEditorConstructionOptions
{
Language = "zil",
GlyphMargin = true,
Value = "<VERSION ZIP>\n" +
"\n" +
"<ROUTINE GO ()\n" +
" <TELL \"Hello world!\" CR>\n" +
" <QUIT>>",
};
}
private async Task EditorOnDidInit(MonacoEditorBase editor)
{
await _editor.AddCommand((int)KeyMode.CtrlCmd | (int)KeyCode.KEY_H, (editor, keyCode) =>
{
Console.WriteLine("Ctrl+H : Initial editor command is triggered.");
System.Diagnostics.Debugger.Break();
});
//var newDecorations = new ModelDeltaDecoration[]
//{
// new ModelDeltaDecoration
// {
// Range = new BlazorMonaco.Bridge.Range(3,1,3,1),
// Options = new ModelDecorationOptions
// {
// IsWholeLine = true,
// ClassName = "decorationContentClass",
// GlyphMarginClassName = "decorationGlyphMarginClass"
// }
// }
//};
//decorationIds = await _editor.DeltaDecorations(null, newDecorations);
// You can now use 'decorationIds' to change or remove the decorations
}
//private string[] decorationIds;
private void EditorOnContextMenu(EditorMouseEvent eventArg)
{
Console.WriteLine("OnContextMenu : " + System.Text.Json.JsonSerializer.Serialize(eventArg));
}
private async Task ChangeTheme(ChangeEventArgs e)
{
Console.WriteLine($"setting theme to: {e.Value.ToString()}");
await MonacoEditor.SetTheme(e.Value.ToString());
}
private async Task SetValue()
{
Console.WriteLine($"setting value to: {ValueToSet}");
await _editor.SetValue(ValueToSet);
}
private async Task GetValue()
{
var val = await _editor.GetValue();
Console.WriteLine($"value is: {val}");
}
private async Task AddCommand()
{
await _editor.AddCommand((int)KeyMode.CtrlCmd | (int)KeyCode.Enter, (editor, keyCode) =>
{
Console.WriteLine("Ctrl+Enter : Editor command is triggered.");
});
}
private async Task AddAction()
{
await _editor.AddAction("testAction", "Test Action", new int[] { (int)KeyMode.CtrlCmd | (int)KeyCode.KEY_D, (int)KeyMode.CtrlCmd | (int)KeyCode.KEY_B }, null, null, "navigation", 1.5, (editor, keyCodes) =>
{
Console.WriteLine("Ctrl+D : Editor action is triggered.");
});
}
private async Task Compile()
{
FileSystem.SetText("main.zil", await _editor.GetValue());
var compiler = new FrontEnd { FileSystem = FileSystem, Logger = NullDiagnosticLogger.Instance };
var compResult = compiler.Compile("main.zil", "main.zap", false);
CompileResult = compResult;
AssembleResult = null;
if (compResult.Success)
{
var assembler = new Zapf.ZapfAssembler { FileSystem = FileSystem };
AssembleResult = assembler.Assemble("main.zap", "main.z#");
}
}
private Task ChangeProject(ChangeEventArgs e)
{
return Task.CompletedTask;
//if (e.Value is string name)
//{
// var (template, fileContents) = await Templates.LoadTemplateAsync(name);
// FileSystem.Clear();
// foreach (var (path, content) in fileContents)
// FileSystem.SetText(path, content);
// await _editor.SetValue(fileContents[0].content);
//}
}
}
@page "/project/{ProjectId:guid}/edit"
@inject ProjectEditorState EditState
@inject NavigationManager Nav
@layout ProjectLayout
<h3>Editing @Path</h3>
<textarea @bind="FileContent" class="w-100 h-75" ></textarea>
@code {
public static string GetLink(Project project, string path) => GetLink(project.Guid.ToString(), path);
public static string GetLink(string projectId, string path) =>
$"/project/{projectId}/edit?file={path}";
[Parameter]
public Guid ProjectId { get; set; }
[Parameter]
public string Path { get; set; }
[System.Diagnostics.CodeAnalysis.MaybeNull]
private ProjectFile File => EditState.Project.Files.SingleOrDefault(f => f.Path == Path);
public string FileContent
{
get => File?.Content;
set
{
var f = File;
if (f != null)
f.Content = value;
}
}
[CascadingParameter]
public ProjectLayout ProjectLayout { get; set; }
protected override void OnParametersSet()
{
var uri = new Uri(Nav.Uri);
var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
Path = query.GetValueOrDefault("file");
if (Path == null && EditState.Project.Files.Count > 0)
{
Nav.NavigateTo(GetLink(EditState.Project, EditState.Project.MainFile.Path));
return;
}
ProjectLayout.ActivePath = Path;
}
}
@page "/project/new"
@inject TemplateManager Templates
@inject NavigationManager Nav
@inject ProjectEditorState EditState
@layout ContainerlessLayout
<div class="px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Select template</h1>
<p class="lead">Start a new project without having to write out all the boilerplate, or learn ZIL by hacking on one of the example games.</p>
</div>
<div class="container">
@if (Showcase.Length == 0)
{
<div class="d-flex justify-content-center">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
}
else
{
<div class="card-deck mb-3 text-center">
@foreach (var (name, template) in Showcase)
{
<ProjectTemplateCard IsDefault="@(name == "$default")" Template="@template" OnClick="@(e => LoadTemplate(name))" />
}
</div>
}
</div>
@code {
private KeyValuePair<string, Template>[] Showcase { get; set; } = { };
protected override async Task OnInitializedAsync()
{
Showcase = await Templates.GetShowcaseAsync();
}
private async Task LoadTemplate(string name)
{
EditState.Project = await Templates.CopyToNewProjectAsync(name);
Nav.NavigateTo($"/project/{EditState.Project.Guid}/edit?file={EditState.Project.MainFile.Path}");
}
}
@page "/repl"
@implements IDisposable
@inject JSInterop Interop
@inject ReplState ReplState
<div class="d-flex">
<h3>REPL</h3>
<button type="button"
class="btn btn-sm btn-outline-primary ml-3 @RestartSessionVisibility"
@onclick="() => ReplState.RestartSession()">Restart Session</button>
</div>
<div id="replLog" @ref="ReplLog" style="max-height: 60vh; overflow: auto">
<p class="font-italic">Enter a ZIL or MDL expression below to evaluate it. The evaluation result will be shown here.</p>
@foreach (var exchange in ReplState.Exchanges)
{
var responseColor = exchange.IsError ? "text-danger" : "text-secondary";
<p class="text-monospace text-primary pr-2 mb-0">@exchange.Input</p>
<p class="text-monospace @responseColor pl-2">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-arrow-return-right" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z" />
</svg>
@exchange.Output
</p>
}
</div>
@*<input type="text" class="w-100" @bind="Command" @onkeyup="InputOnKeyUp" placeholder="Expression" />*@
<textarea @ref="InputBox" class="w-100" rows="@InputRows" @bind-value="Command" @bind-value:event="oninput" placeholder="Expression" />
@code {
private ElementReference ReplLog { get; set; }
private ElementReference InputBox { get; set; }
private DotNetObjectReference<Repl> SelfReference { get; set; }
private string Command { get; set; }
private string RestartSessionVisibility => ReplState.Exchanges.Count == 0 ? "invisible" : "visible";
private bool shouldScrollToBottom = false;
private int InputRows
{
get
{
if (Command == null)
return 1;
int crs = 0, nls = 0;
foreach (char c in Command)
{
switch (c)
{
case '\r': crs++; break;
case '\n': nls++; break;
}
}
return Math.Max(crs, nls) + 1;
}
}
[JSInvokable]
public void EvaluateInput()
{
var input = Command;
Command = "";
ReplState.Execute(input);
}