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... + + + + + +