diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
new file mode 100644
index 0000000..e1f5b58
--- /dev/null
+++ b/.github/workflows/cd.yaml
@@ -0,0 +1,25 @@
+name: CD
+
+on:
+ push:
+ paths-ignore:
+ - "docs/**"
+ - "*.md"
+
+jobs:
+ package:
+ env:
+ DisableGitVersionTask: true
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: 5.0.x
+ - name: Package
+ run: dotnet run --project build/Build.csproj -- --target Package
+ - uses: actions/upload-artifact@v2
+ with:
+ name: Textrude.zip
+ path: publish/Textrude.zip
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6827e78..db3967b 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -1,6 +1,14 @@
name: CI
-on: [push, pull_request]
+on:
+ push:
+ paths-ignore:
+ - "docs/**"
+ - "*.md"
+ pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "*.md"
jobs:
test:
@@ -20,11 +28,21 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- - name: Clean
- run: dotnet clean -p:Configuration=${{ matrix.build_config }} && dotnet nuget locals all --clear
- - name: Restore dependencies
- run: dotnet restore -p:Configuration=${{ matrix.build_config }}
- - name: Build
- run: dotnet build -p:Configuration=${{ matrix.build_config }} --no-restore
- name: Test
- run: dotnet test -p:Configuration=${{ matrix.build_config }} --no-build
+ run: dotnet run --project build/Build.csproj -- --target Test --configuration ${{ matrix.build_config }} --clean
+ - name: Coveralls
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ flag-name: run-on-${{ matrix.os }}
+ path-to-lcov: TestResults/lcov.info
+ parallel: true
+ coveralls-finish:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Coveralls Finished
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.github_token }}
+ parallel-finished: true
diff --git a/.gitignore b/.gitignore
index fc02ad8..afa0dcc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
/act
+/tools
+/TestReports
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
diff --git a/Engine/Application/ModelPath.cs b/Engine/Application/ModelPath.cs
index 5ab8cc1..85c91a2 100644
--- a/Engine/Application/ModelPath.cs
+++ b/Engine/Application/ModelPath.cs
@@ -12,6 +12,14 @@ namespace Engine.Application
///
public class ModelPath
{
+ public enum PathType
+ {
+ Method,
+ Property,
+ Unknown,
+ Keyword
+ }
+
public const string Separator = ".";
///
@@ -37,6 +45,8 @@ public class ModelPath
///
public int Length => _tokens.Length;
+ public PathType ModelType { get; private set; } = PathType.Unknown;
+
public static ModelPath FromTokens(IEnumerable tokens)
{
var tokenArray = tokens.ToImmutableArray();
@@ -50,6 +60,14 @@ public static ModelPath FromTokens(IEnumerable tokens)
///
public ModelPath WithChild(string child) => FromTokens(_tokens.Append(child));
+ public ModelPath WithType(PathType type)
+ {
+ var m = FromTokens(_tokens);
+ m.ModelType = type;
+ return m;
+ }
+
+
///
/// Returns a "dotted" string representation
///
@@ -57,5 +75,7 @@ public static ModelPath FromTokens(IEnumerable tokens)
public static ModelPath FromString(string path) =>
FromTokens((path.Split(Separator)));
+
+ public string Terminal() => IsEmpty ? string.Empty : _tokens.Last();
}
-}
\ No newline at end of file
+}
diff --git a/Engine/Application/TemplateManager.cs b/Engine/Application/TemplateManager.cs
index aa750ea..e92c0e8 100644
--- a/Engine/Application/TemplateManager.cs
+++ b/Engine/Application/TemplateManager.cs
@@ -136,7 +136,15 @@ public static ImmutableArray PathsForObjectTree(IDictionary child)
ret.AddRange(PathsForObjectTree(child, p));
else ret.Add(p);
@@ -148,7 +156,23 @@ public static ImmutableArray PathsForObjectTree(IDictionary GetBuiltIns() => PathsForObjectTree(_context.BuiltinObject, ModelPath.Empty);
public ImmutableArray GetObjectTree() => PathsForObjectTree(_top, ModelPath.Empty);
- public ImmutableArray ModelPaths() => GetBuiltIns().Concat(GetObjectTree())
- .ToImmutableArray();
+ public ImmutableArray GetKeywords()
+ {
+ var keywords =
+ @"func end if else for break continue
+ in while capture readonly import
+ with wrap include ret case when this
+ empty tablerow";
+ return keywords.Split(" \r\n".ToCharArray(),
+ StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(s => ModelPath.FromString(s).WithType(ModelPath.PathType.Keyword))
+ .ToImmutableArray();
+ }
+
+ public ImmutableArray ModelPaths() =>
+ GetBuiltIns()
+ .Concat(GetObjectTree())
+ .Concat(GetKeywords())
+ .ToImmutableArray();
}
-}
\ No newline at end of file
+}
diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj
index 472fb05..2bd3abb 100644
--- a/Engine/Engine.csproj
+++ b/Engine/Engine.csproj
@@ -5,10 +5,10 @@
-
+
-
+
diff --git a/README.md b/README.md
index c45d08b..4a87d04 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# Textrude
+# ![Textude](img/textrude_logo.png)
+![GitHub release (latest by SemVer including pre-releases)](https://img.shields.io/github/downloads-pre/NeilMacmullen/Textrude/total)
+[![Coverage Status](https://coveralls.io/repos/github/NeilMacMullen/Textrude/badge.svg?branch=main&kill_cache=1)](https://coveralls.io/github/NeilMacMullen/Textrude?branch=main) [![Join the chat at https://gitter.im/Textrude/community](https://badges.gitter.im/Textrude/community.svg)](https://gitter.im/Textrude/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Give a Star! :star:
@@ -26,7 +28,7 @@ Let's face it, there are any number of code-generation technologies you might co
## Download/build
-The current release is **v1.3.0**.
+The current release is **v1.4.0**.
**Textrude requires [.Net 5.0](https://dotnet.microsoft.com/download/dotnet/5.0). If it's not already on your machine you will be prompted to install it.**
@@ -40,29 +42,20 @@ The current release is **v1.3.0**.
## What's new
-### vNext (source only)
-- Update to Monaco 22.3 which supports an extra couple of languages
-- Rewire font-size control, line numbers, wordwarp to model/output panes
-- remove minimap from model/output panes
-- added spinner to monaco panes and made white flash a bit briefer
-- whitespace can be made visible in input/output panes
-- reuse single Monaco edit pane for multiple models/outputs for cleaner switching & lower resource use
-- definitions and includes are now move to main input section and used Monaco editor
-- the template is now edited using Monaco and (rudimentary) syntax hightlight is applied
-- models and outputs can now be renamed in TextrudeInteractive
-- Textrude CLI now supports named models/outputs via "mymodel=d:/model.csv" syntax
-
-### v1.3.0 (source/binary)
-- Models and outputs can be added/removed on a per-project basis
-- Syntax highlighting for output panes
-- Input/ouput panes can be "linked" to files and load/save are supported
-- fontsize, wordwrap and line-number settings are now persisted
-- warning dialog is now shown if the current project has unsaved changes
-- default rendering throttle reduced to 50ms for better responsiveness
-- Taskbar icon now shows jumplist, TextrudeInteractive can be started with name of project as parameter
-- TextrudeInteractive now opens last used project when reopened
-- TextrudeInteractive now uses the Monaco editor (from VS Code) hugely improving the syntax highlighting abilities. Massive thanks to [Martin Hochstrasser](https://github.com/highstreeto) for this!
-- upgrade to latest Scriban which supports [captured variables in anonymous functions](https://github.com/scriban/scriban/issues/322)
+### v1.4.0 (source/binary)
+- The Monaco text editor is now used for all edit panes including the template editor, definitions and include paths.
+- A single Monaco edit pane is now used for multiple models/outputs for cleaner switching & improved responsiveness
+- The view menu allows visible-whitespace to be toggled on and off
+- Rudimentary syntax highlighting and intellisense are provided for the template editor
+- Models and outputs can now be assigned names
+- Help menu now includes a link to gitter-chat
+- Model, template and output panes now support linking to files.
+- Export/Build... menu now brings up a dialog to help build CLI options.
+- Updated to latest Scriban for [multi-line pipes](https://github.com/scriban/scriban/pull/327)
+- Special thanks to [Martin Hochstrasser](https://github.com/highstreeto) for
+ - CAKE build support
+ - The fancy new logo!
+
[Full change history](doc/changeHistory.md)
@@ -86,7 +79,6 @@ Textrude makes heavy use of the following components:
- [Json.Net](https://www.newtonsoft.com/json) for Json deserialisation
- [Humanizr](https://github.com/Humanizr/Humanizer) for useful text-processing
- [MaterialDesignToolkit](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit), [MaterialDesignExtensions](https://spiegelp.github.io/MaterialDesignExtensions) and [Ookii Dialogs](https://github.com/augustoproiete/ookii-dialogs-wpf)to make the UI a bit less clunky
-- [AvalonEdit](http://avalonedit.net/) for text-editing goodness
Huge thanks to the contributors:
- [Martin Hochstrasser](https://github.com/highstreeto) - Docker support, general build enhancements and integration of the Monaco editor
diff --git a/SharedApplication/CommandLineBuilder.cs b/SharedApplication/CommandLineBuilder.cs
index 7f97bd8..e66e9db 100644
--- a/SharedApplication/CommandLineBuilder.cs
+++ b/SharedApplication/CommandLineBuilder.cs
@@ -1,17 +1,21 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
using System.Text.Json;
using YamlDotNet.Serialization;
namespace SharedApplication
{
+ ///
+ /// Builds command lines for the CLI version of textrude
+ ///
public class CommandLineBuilder
{
- private readonly StringBuilder builder = new();
- private readonly RenderOptions options;
+ private readonly StringBuilder _builder = new();
+ private readonly RenderOptions _options;
private string _exe = string.Empty;
- public CommandLineBuilder(RenderOptions theoptions) => options = theoptions;
+ public CommandLineBuilder(RenderOptions op) => _options = op;
public CommandLineBuilder WithExe(string exe)
{
@@ -19,71 +23,59 @@ public CommandLineBuilder WithExe(string exe)
return this;
}
- private void AppendFlag(string flag) => builder.Append($" --{flag.ToLowerInvariant()}");
- private void AppendItem(string item) => builder.Append($" \"{item}\"");
- private void AppendFileName(string file) => AppendItem(file);
+ private string QuoteItem(string item)
+ => item.Contains(" ") ? $"\"{item}\"" : item;
- public string BuildRenderInvocation()
- {
- StartInvocation("render");
- if (options.Lazy)
- AppendFlag(nameof(RenderOptions.Lazy));
+ private void AppendFlag(string flag) => _builder.Append($" --{flag.ToLowerInvariant()}");
+ private void AppendItem(string item) => _builder.Append($" {QuoteItem(item)}");
- if (options.Models.Any())
+ private void AppendIfPresent(string option, IEnumerable items)
+ {
+ var clean = items.Clean();
+ if (clean.Any())
{
- AppendFlag(nameof(RenderOptions.Models));
- foreach (var model in options.Models)
+ AppendFlag(option);
+ foreach (var i in clean)
{
- AppendFileName(model);
+ AppendItem(i);
}
}
+ }
- AppendFlag(nameof(options.Template));
- AppendFileName("template.sbn");
-
-
- if (options.Definitions.Any())
- {
- AppendFlag(nameof(RenderOptions.Definitions));
- foreach (var d in options.Definitions)
- AppendItem(d);
- }
-
- if (options.Include.Any())
- {
- AppendFlag(nameof(RenderOptions.Include));
- foreach (var inc in options.Include)
- AppendFileName(inc);
- }
+ public string BuildRenderInvocation()
+ {
+ StartInvocation("render");
+ if (_options.Lazy)
+ AppendFlag(nameof(RenderOptions.Lazy));
+ AppendIfPresent(nameof(RenderOptions.Models), _options.Models);
- if (options.Output.Any())
- {
- AppendFlag(nameof(RenderOptions.Output));
- foreach (var outfile in options.Output)
- AppendFileName(outfile);
- }
+ AppendFlag(nameof(_options.Template));
+ AppendItem(_options.Template);
- return builder.ToString();
+ AppendIfPresent(nameof(RenderOptions.Definitions), _options.Definitions);
+ AppendIfPresent(nameof(RenderOptions.Include), _options.Include);
+ AppendIfPresent(nameof(RenderOptions.Output), _options.Output);
+ return _builder.ToString();
}
private void StartInvocation(string cmd)
{
- builder.Clear();
+ _builder.Clear();
if (_exe.Length != 0)
- AppendFileName(_exe);
- builder.Append($" {cmd}");
+ AppendItem(_exe);
+ _builder.Append($" {cmd}");
}
public (string argsContent, string invocation) BuildJson(string argsFile)
{
StartInvocation("renderFromFile ");
AppendFlag(nameof(RenderFromFileOptions.Arguments));
- AppendFileName(argsFile);
- var invocation = builder.ToString();
+ AppendItem(argsFile);
+ var invocation = _builder.ToString();
return (
- JsonSerializer.Serialize(options, new JsonSerializerOptions {WriteIndented = true}),
+ JsonSerializer.Serialize(_options, new JsonSerializerOptions {WriteIndented = true}),
invocation
);
}
@@ -92,10 +84,10 @@ private void StartInvocation(string cmd)
{
StartInvocation("renderFromFile ");
AppendFlag(nameof(RenderFromFileOptions.Arguments));
- AppendFileName(argsFile);
- var invocation = builder.ToString();
+ AppendItem(argsFile);
+ var invocation = _builder.ToString();
- return (new Serializer().Serialize(options),
+ return (new Serializer().Serialize(_options),
invocation);
}
}
diff --git a/SharedApplication/EnumerableExtensions.cs b/SharedApplication/EnumerableExtensions.cs
new file mode 100644
index 0000000..56e5f46
--- /dev/null
+++ b/SharedApplication/EnumerableExtensions.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SharedApplication
+{
+ ///
+ /// Simple extensions for IEnumerable
+ ///
+ public static class EnumerableExtensions
+ {
+ ///
+ /// return only the non-empty strings and trim them as we go
+ ///
+ public static IEnumerable Clean(this IEnumerable items)
+ {
+ return items.Select(i => i.Trim()).Where(i => i.Length != 0);
+ }
+ }
+}
diff --git a/Textrude/NamedFile.cs b/SharedApplication/NamedFile.cs
similarity index 86%
rename from Textrude/NamedFile.cs
rename to SharedApplication/NamedFile.cs
index b44e46f..a1927ce 100644
--- a/Textrude/NamedFile.cs
+++ b/SharedApplication/NamedFile.cs
@@ -1,4 +1,4 @@
-namespace Textrude
+namespace SharedApplication
{
///
/// Simple record to hold both a path to a file and the associated name of the model/output
diff --git a/Textrude/NamedFileFactory.cs b/SharedApplication/NamedFileFactory.cs
similarity index 81%
rename from Textrude/NamedFileFactory.cs
rename to SharedApplication/NamedFileFactory.cs
index 812d1da..585d0cd 100644
--- a/Textrude/NamedFileFactory.cs
+++ b/SharedApplication/NamedFileFactory.cs
@@ -4,7 +4,7 @@
using System.Linq;
using System.Text.RegularExpressions;
-namespace Textrude
+namespace SharedApplication
{
///
/// Creates NamedFiles from raw text input
@@ -29,5 +29,10 @@ Func defaultNamer
)
.ToImmutableArray();
}
+
+ public static ImmutableArray Squash(IEnumerable files)
+ {
+ return files.Select(f => $"{f.Name}={f.Path}").ToImmutableArray();
+ }
}
}
diff --git a/Tests/CommandLineBuilderTests.cs b/Tests/CommandLineBuilderTests.cs
new file mode 100644
index 0000000..370bc57
--- /dev/null
+++ b/Tests/CommandLineBuilderTests.cs
@@ -0,0 +1,71 @@
+using FluentAssertions;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using SharedApplication;
+
+namespace Tests
+{
+ [TestClass]
+ public class CommandLineBuilderTests
+ {
+ [TestMethod]
+ public void EmptyOptionsIncludesTemplate()
+ {
+ var options = new RenderOptions();
+ var builder = new CommandLineBuilder(options);
+ var cmd = builder.BuildRenderInvocation();
+ cmd.Should().Contain("render");
+ cmd.Should().Contain("--template");
+ }
+
+
+ [TestMethod]
+ public void DefinitionsNotShownIfNonePresent()
+ {
+ var options = new RenderOptions
+ {
+ Definitions = new[] {string.Empty}
+ };
+ var builder = new CommandLineBuilder(options);
+ var cmd = builder.BuildRenderInvocation();
+ cmd.Should().NotContain("definitions");
+ }
+
+ [TestMethod]
+ public void IncludesNotShownIfNonePresent()
+ {
+ var options = new RenderOptions
+ {
+ Include = new[] {string.Empty}
+ };
+ var builder = new CommandLineBuilder(options);
+ var cmd = builder.BuildRenderInvocation();
+ cmd.Should().NotContain("include");
+ }
+
+ [TestMethod]
+ public void UsesModelNames()
+ {
+ var options = new RenderOptions
+ {
+ Models = new[] {"test=filename"}
+ };
+
+ var builder = new CommandLineBuilder(options);
+ var cmd = builder.BuildRenderInvocation();
+ cmd.Should().Contain("--models test=filename");
+ }
+
+ [TestMethod]
+ public void QuotesFileNamesWithSpaces()
+ {
+ var options = new RenderOptions
+ {
+ Models = new[] {"test=file name"}
+ };
+
+ var builder = new CommandLineBuilder(options);
+ var cmd = builder.BuildRenderInvocation();
+ cmd.Should().Contain("--models \"test=file name\"");
+ }
+ }
+}
diff --git a/Tests/TemplateManagerTests.cs b/Tests/TemplateManagerTests.cs
index b926e7d..c711a59 100644
--- a/Tests/TemplateManagerTests.cs
+++ b/Tests/TemplateManagerTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Engine.Application;
@@ -40,5 +41,15 @@ public void CodeCompletionCanFetchObjects()
f.Should().Contain("test.element");
}
+
+
+ [TestMethod]
+ public void CodeCompletionDoesNotThrowIfNullInjected()
+ {
+ var mgr = new TemplateManager(_files);
+ mgr.AddVariable("test", new Dictionary {["element"] = null});
+ Action getModelPaths = () => mgr.ModelPaths();
+ getModelPaths.Should().NotThrow();
+ }
}
-}
\ No newline at end of file
+}
diff --git a/Textrude.sln b/Textrude.sln
index 4d887ef..0bb3b9f 100644
--- a/Textrude.sln
+++ b/Textrude.sln
@@ -28,6 +28,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedApplication", "Shared
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptLibrary", "ScriptLibrary\ScriptLibrary.csproj", "{B82D3016-E1F3-42E1-87B8-2BF1310FBE48}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9FBD6430-C7F0-4D07-974C-EBEB5EB962C2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Build", "build\Build.csproj", "{A93C9654-6AE7-4A4E-9988-6B0B89158CE7}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub workflows", "GitHub workflows", "{02A73D75-89E1-4C65-8799-A3A9DBE32A40}"
+ ProjectSection(SolutionItems) = preProject
+ .github\workflows\ci.yaml = .github\workflows\ci.yaml
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -70,10 +79,17 @@ Global
{B82D3016-E1F3-42E1-87B8-2BF1310FBE48}.Linux-Debug|Any CPU.Build.0 = Debug|Any CPU
{B82D3016-E1F3-42E1-87B8-2BF1310FBE48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B82D3016-E1F3-42E1-87B8-2BF1310FBE48}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A93C9654-6AE7-4A4E-9988-6B0B89158CE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A93C9654-6AE7-4A4E-9988-6B0B89158CE7}.Linux-Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A93C9654-6AE7-4A4E-9988-6B0B89158CE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A93C9654-6AE7-4A4E-9988-6B0B89158CE7} = {9FBD6430-C7F0-4D07-974C-EBEB5EB962C2}
+ {02A73D75-89E1-4C65-8799-A3A9DBE32A40} = {9FBD6430-C7F0-4D07-974C-EBEB5EB962C2}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {57A6CC74-8B87-40FD-8982-02692EF23818}
EndGlobalSection
diff --git a/Textrude/CmdInfo.cs b/Textrude/CmdInfo.cs
index 3982f35..0621322 100644
--- a/Textrude/CmdInfo.cs
+++ b/Textrude/CmdInfo.cs
@@ -20,6 +20,7 @@ public static async Task Run(Options o, RunTimeEnvironment rte)
Useful links:
- Textrude homepage: https://github.com/NeilMacMullen/Textrude
- Scriban language docs: https://github.com/scriban/scriban/blob/master/doc/language.md
+ - Chat and questions: https://gitter.im/Textrude/community
");
var latestVersion = await UpgradeManager.GetLatestVersion();
@@ -37,4 +38,4 @@ public class Options
{
}
}
-}
\ No newline at end of file
+}
diff --git a/Textrude/Textrude.csproj b/Textrude/Textrude.csproj
index 337bae2..ab09aee 100644
--- a/Textrude/Textrude.csproj
+++ b/Textrude/Textrude.csproj
@@ -7,6 +7,7 @@
HASGITVERSION
+ app.ico
diff --git a/Textrude/app.ico b/Textrude/app.ico
new file mode 100644
index 0000000..736fd3e
Binary files /dev/null and b/Textrude/app.ico differ
diff --git a/TextrudeInteractive/App.xaml b/TextrudeInteractive/App.xaml
index c3df1f7..d19ba40 100644
--- a/TextrudeInteractive/App.xaml
+++ b/TextrudeInteractive/App.xaml
@@ -6,6 +6,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
+
diff --git a/TextrudeInteractive/AutoCompletion/AvalonEditCompletionHelper.cs b/TextrudeInteractive/AutoCompletion/AvalonEditCompletionHelper.cs
deleted file mode 100644
index 47e73cf..0000000
--- a/TextrudeInteractive/AutoCompletion/AvalonEditCompletionHelper.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using System.Windows.Input;
-using Engine.Application;
-using ICSharpCode.AvalonEdit;
-using ICSharpCode.AvalonEdit.CodeCompletion;
-using ICSharpCode.AvalonEdit.Editing;
-
-namespace TextrudeInteractive.AutoCompletion
-{
- ///
- /// Helps to add the code-completion behavior to the AvalonEdit control
- ///
- public class AvalonEditCompletionHelper
- {
- private readonly TextEditor _textEditor;
-
- private ImmutableArray _completionNodes = ImmutableArray.Empty;
- private CompletionWindow _completionWindow;
-
- public AvalonEditCompletionHelper(TextEditor editor) => _textEditor = editor;
-
- public void Register()
- {
- _textEditor.TextArea.TextEntering += TextEntering;
- _textEditor.TextArea.TextEntered += TextEntered;
- }
-
-
- private void TextEntered(object sender, TextCompositionEventArgs e)
- {
- var area = sender as TextArea;
- if (e.Text != ".") return;
- var prevText = LeadingString(area);
-
-
- var matches = _completionNodes
- .Select(n => n.Find(prevText))
- .SelectMany(m => m.Children)
- .ToArray();
-
- //only show the popup if there are matches
- if (!matches.Any()) return;
-
- _completionWindow = new CompletionWindow(area);
- var data = _completionWindow.CompletionList.CompletionData;
- foreach (var child in matches)
- {
- data.Add(new CompletionData(child.Stem));
- }
-
- _completionWindow.Show();
- _completionWindow.Closed += delegate { _completionWindow = null; };
- }
-
- private void TextEntering(object sender, TextCompositionEventArgs e)
- {
- if (e.Text.Length <= 0 || _completionWindow == null) return;
-
- if (!char.IsLetterOrDigit(e.Text[0]))
- {
- // Whenever a non-letter is typed while the completion window is open,
- // insert the currently selected element.
- _completionWindow.CompletionList.RequestInsertion(e);
- }
-
- // Do not set e.Handled=true.
- // We still want to insert the character that was typed.
- }
-
- ///
- /// Get the bit of valid path just before the dot
- ///
- private static string LeadingString(TextArea area)
- {
- // Open code completion after the user has pressed dot:
- var offset = area.Caret.Offset;
- var currentLine = area.Document.GetLineByOffset(offset);
-
- var leadingText = area.Document.GetText(currentLine.Offset, offset - currentLine.Offset - 1);
-
- static bool IsValidChar(char c)
- => char.IsLetterOrDigit(c) || "._".Contains(c);
-
- var badChars = leadingText.Select((c, i) => !IsValidChar(c) ? i : -1).ToArray();
- if (badChars.Any())
- leadingText = leadingText.Substring(badChars.Max() + 1);
- return leadingText;
- }
-
- public void SetCompletion(IEnumerable paths)
- {
- _completionNodes = CompletionTreeNode.Build(paths);
- }
- }
-}
\ No newline at end of file
diff --git a/TextrudeInteractive/AutoCompletion/CompletionData.cs b/TextrudeInteractive/AutoCompletion/CompletionData.cs
deleted file mode 100644
index d409cf6..0000000
--- a/TextrudeInteractive/AutoCompletion/CompletionData.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using System.Windows.Media;
-using ICSharpCode.AvalonEdit.CodeCompletion;
-using ICSharpCode.AvalonEdit.Document;
-using ICSharpCode.AvalonEdit.Editing;
-
-namespace TextrudeInteractive.AutoCompletion
-{
- /// Implements AvalonEdit ICompletionData interface to provide the entries in the
- /// completion drop down.
- public class CompletionData : ICompletionData
- {
- public CompletionData(string text) => Text = text;
-
- public ImageSource Image => null;
-
- public string Text { get; }
-
- // Use this property if you want to show a fancy UIElement in the list.
- public object Content => Text;
-
- public object Description => Text;
- public double Priority { get; }
-
- public void Complete(TextArea textArea, ISegment completionSegment,
- EventArgs insertionRequestEventArgs)
- {
- textArea.Document.Replace(completionSegment, Text);
- }
- }
-}
\ No newline at end of file
diff --git a/TextrudeInteractive/AutoCompletion/CompletionTreeNode.cs b/TextrudeInteractive/AutoCompletion/CompletionTreeNode.cs
deleted file mode 100644
index ed5b00a..0000000
--- a/TextrudeInteractive/AutoCompletion/CompletionTreeNode.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Linq;
-using Engine.Application;
-
-namespace TextrudeInteractive.AutoCompletion
-{
- ///
- /// Simple tree structure to allow us to suggest auto completions
- ///
- ///
- /// We don't current support back-tracking/partial matching and we certainly don't make use
- /// of any parser data so won't always manage to make the most relevant suggestions. The
- /// main missing case here is that we don't recognise arrays or variable substitutions so
- /// can't provide very good matches for parts of models.
- ///
- public class CompletionTreeNode
- {
- public static readonly CompletionTreeNode Empty =
- new(string.Empty, Array.Empty());
-
- public readonly ImmutableArray Children;
- public readonly string Stem;
-
-
- public CompletionTreeNode(string stem, IEnumerable children)
- {
- Stem = stem;
- Children = children.ToImmutableArray();
- }
-
- public CompletionTreeNode Find(ModelPath tokens)
- {
- if (tokens.IsEmpty) return Empty;
- if (tokens.RootToken != Stem) return Empty;
-
- if (!tokens.IsParent) return this;
-
- foreach (var child in Children)
- {
- var c = child.Find(tokens.Child());
- if (c != Empty)
- return c;
- }
-
- return Empty;
- }
-
- public CompletionTreeNode Find(string path) => Find(ModelPath.FromString(path));
-
- public static ImmutableArray Build(IEnumerable paths)
- {
- var nodes = paths
- .Where(p => !p.IsEmpty)
- .GroupBy(p => p.RootToken)
- .ToArray();
- return nodes.Select(n =>
- new CompletionTreeNode(n.Key, Build(n.Select(p => p.Child())))
- ).ToImmutableArray();
- }
- }
-}
\ No newline at end of file
diff --git a/TextrudeInteractive/ClipboardHelper.cs b/TextrudeInteractive/ClipboardHelper.cs
new file mode 100644
index 0000000..731ae12
--- /dev/null
+++ b/TextrudeInteractive/ClipboardHelper.cs
@@ -0,0 +1,26 @@
+using System.Windows;
+
+namespace TextrudeInteractive
+{
+ ///
+ /// Simple class to wrap Windows Clipboard flakiness
+ ///
+ public static class ClipboardHelper
+ {
+ public static void CopyToClipboard(string text)
+ {
+ var maxAttempts = 3;
+ for (var i = 0; i < maxAttempts; i++)
+ {
+ try
+ {
+ Clipboard.SetText(text);
+ return;
+ }
+ catch
+ {
+ }
+ }
+ }
+ }
+}
diff --git a/TextrudeInteractive/ExportDialog.xaml b/TextrudeInteractive/ExportDialog.xaml
new file mode 100644
index 0000000..a7a53ff
--- /dev/null
+++ b/TextrudeInteractive/ExportDialog.xaml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ Fully-qualified exe
+ Only run if input newer than output
+ Ignore unlinked files
+
+
+
+
+
+
+
+
+ Absolute
+ Relative to...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TextrudeInteractive/ExportDialog.xaml.cs b/TextrudeInteractive/ExportDialog.xaml.cs
new file mode 100644
index 0000000..5563c0c
--- /dev/null
+++ b/TextrudeInteractive/ExportDialog.xaml.cs
@@ -0,0 +1,135 @@
+using System.IO;
+using System.Linq;
+using System.Windows;
+using Engine.Application;
+using MaterialDesignExtensions.Controls;
+using Ookii.Dialogs.Wpf;
+using SharedApplication;
+
+namespace TextrudeInteractive
+{
+ ///
+ /// Interaction logic for ExportDialog.xaml
+ ///
+ public partial class ExportDialog : MaterialWindow
+ {
+ private readonly PathManipulator exePath = PathManipulator.FromExe();
+
+ private readonly TextrudeProject proj;
+ private string _homeFolder;
+
+ public ExportDialog(TextrudeProject project)
+ {
+ proj = project;
+ _homeFolder = new RunTimeEnvironment(new FileSystemOperations()).ApplicationFolder();
+ InitializeComponent();
+ RenderCli.IsChecked = true;
+ UseAbsolutePaths.IsChecked = true;
+ RootFolder.Text = _homeFolder;
+ IgnoreUnlinked.IsChecked = true;
+ UpdateUi();
+ }
+
+ public void UpdateHomePath()
+ {
+ var dlg = new VistaFolderBrowserDialog();
+ if (dlg.ShowDialog(this) == false)
+ return;
+ _homeFolder = dlg.SelectedPath;
+ RootFolder.Text = _homeFolder;
+ UpdateUi();
+ }
+
+ public void UpdateUi()
+ {
+ var engine = proj.EngineInput;
+
+ WorkingDirectorySection.IsEnabled = UseAbsolutePaths.IsChecked == false;
+
+ string RelAbsPath(string path)
+ {
+ path = exePath.ToAbsolute(path);
+
+ var rootManipulator = new PathManipulator(_homeFolder);
+ return UseAbsolutePaths.IsChecked == true
+ ? rootManipulator.ToAbsolute(path)
+ : rootManipulator.ToRelative(path);
+ }
+
+ NamedFile GetFile(string name, string path) => new NamedFile(name, RelAbsPath(path));
+
+ var options = new RenderOptions
+ {
+ Definitions = engine.Definitions.Clean().ToArray(),
+ Include = engine.IncludePaths.Clean()
+ .ToArray(),
+ Models = NamedFileFactory.Squash(
+ engine.Models
+ .Select(m => GetFile(m.Name, m.Path))
+ .Where(CheckLink)
+ )
+ .ToArray(),
+ Output = NamedFileFactory.Squash(
+ proj.OutputControl.Outputs.Select(m => GetFile(m.Name, m.Path))
+ .Where(CheckLink))
+ .ToArray(),
+ Lazy = IsLazy.IsChecked == true,
+ Template = RelAbsPath(engine.TemplatePath)
+ };
+
+ const string exeName = "textrude.exe";
+ var exe = UseFullyQualifiedExe.IsChecked == true
+ ? Path.Combine(exePath.Root,
+ exeName)
+ : exeName;
+
+ var builder = new CommandLineBuilder(options).WithExe(exe);
+ ArgsFileSection.Visibility = RenderCli.IsChecked == true
+ ? Visibility.Collapsed
+ : Visibility.Visible;
+ if (RenderCli.IsChecked == true)
+ {
+ CommandText.Text = builder.BuildRenderInvocation();
+ JsonYaml.Text = string.Empty;
+ }
+
+ if (RenderJson.IsChecked == true)
+ {
+ var (json, jsoncmd) = builder.BuildJson("args.json");
+ CommandText.Text = jsoncmd;
+ JsonYaml.Text = json;
+ }
+
+ if (RenderYaml.IsChecked == true)
+ {
+ var (yaml, yamlcmd) = builder.BuildYaml("args.yaml");
+ CommandText.Text = yamlcmd;
+ JsonYaml.Text = yaml;
+ }
+ }
+
+ private bool CheckLink(NamedFile f) =>
+ IgnoreUnlinked.IsChecked != true ||
+ f.Path.Length != 0;
+
+ private void OnUpdateUI(object sender, RoutedEventArgs e)
+ {
+ UpdateUi();
+ }
+
+ private void OnUpdateHomeFolder(object sender, RoutedEventArgs e)
+ {
+ UpdateHomePath();
+ }
+
+ private void CopyCmdToClipboard(object sender, RoutedEventArgs e)
+ {
+ ClipboardHelper.CopyToClipboard(CommandText.Text);
+ }
+
+ private void CopyArgsToClipboard(object sender, RoutedEventArgs e)
+ {
+ ClipboardHelper.CopyToClipboard(JsonYaml.Text);
+ }
+ }
+}
diff --git a/TextrudeInteractive/FileBar.xaml b/TextrudeInteractive/FileBar.xaml
index bd4a4a8..98ffb9f 100644
--- a/TextrudeInteractive/FileBar.xaml
+++ b/TextrudeInteractive/FileBar.xaml
@@ -11,7 +11,9 @@
-
+
+
+