From ac7ef405eeb319d9d962f7e41e19c0cf6648bc1e Mon Sep 17 00:00:00 2001 From: Marco Russo Date: Tue, 8 Sep 2026 22:24:24 +0200 Subject: [PATCH 1/5] Separate language selection from detection, and answer F6 with a vote The first step of docs/additional-language-plan.md. TextLanguageIds.All was serving as both the language inventory and the paste detection order, so the eleven new languages could not be added to one without joining the other. All is now the fifteen languages a text container can be set to, in selector order; DetectionOrder is the four that can read a snippet, unchanged. A manual-only identifier is skipped by NormalizeOrder rather than read as plain text, which would move plain text up an order it was never part of. Services carry CanDetect beside CanFormat, and detection is filtered before TryAccept. The eleven languages are selectable and persist, but nothing colors them yet. F6 opens their voting issue instead, through one shared check both format entry points pass before their CanFormat return, so invalid DAX is still a format that did nothing rather than a prompt. Co-Authored-By: Claude Opus 5 --- docs/additional-language-plan.md | 268 ++++++++++++++++++ docs/decisions.md | 9 + .../Model/BoardObjects.cs | 93 ++++-- .../Settings/AppSettings.cs | 4 +- .../FormattingRequestWindow.xaml | 59 ++++ .../FormattingRequestWindow.xaml.cs | 62 ++++ src/SQLBI.Whiteboard/MainWindow.xaml | 2 +- src/SQLBI.Whiteboard/MainWindow.xaml.cs | 54 +++- src/SQLBI.Whiteboard/SettingsCatalog.cs | 2 +- src/SQLBI.Whiteboard/TextLanguages.cs | 77 ++++- .../Program.cs | 115 +++++++- 11 files changed, 721 insertions(+), 24 deletions(-) create mode 100644 docs/additional-language-plan.md create mode 100644 src/SQLBI.Whiteboard/FormattingRequestWindow.xaml create mode 100644 src/SQLBI.Whiteboard/FormattingRequestWindow.xaml.cs diff --git a/docs/additional-language-plan.md b/docs/additional-language-plan.md new file mode 100644 index 0000000..44f3cfc --- /dev/null +++ b/docs/additional-language-plan.md @@ -0,0 +1,268 @@ +# Additional languages: highlighting and manual selection + +Status: step 1 of the delivery sequence is implemented — the capability split, its +persistence tests, the highlighting spike recorded below, and the F6 voting prompt. +No language is colored yet. The eleven formatting/detection voting issues below were +created on 8 September 2026. + +## Outcome and scope + +Add Python, C, C++, Java, C#, JavaScript, TypeScript, Visual Basic .NET, R, Rust, +and PHP to both text-container language selectors. Selecting a language colors the +existing text in edit mode, on the board, and in exports. It does not rewrite the text. +The chosen language survives saving and reopening the board. + +These languages initially provide highlighting only. F6 shows a language-specific +message inviting the user to vote for formatting and automatic detection on GitHub. +This plan interprets the request's "SQLBI Formatter" as Whiteboard's existing F6 +command, both while editing and on a selected text container. + +Visual Basic means VB.NET. VBA and VB6 are separate future scopes. JavaScript and +TypeScript initially cover ordinary source snippets; JSX and TSX are excluded. PHP +initially covers PHP code with or without opening tags, not HTML syntax highlighting +in mixed templates. Coloring is lexical: keywords, comments, literals, operators, +punctuation, and recognizable language constructs, without project-aware symbol +resolution, compilation, diagnostics, or completion. + +No new content detection, extension inference, or `.wimport` fence aliases are added +in this phase. Existing import and paste behavior remains available; the user chooses +one of the new languages after creating a text container. An explicitly saved language +in a `.wboard` is restored. Existing Plain text, DAX, SQL Server, and KQL behavior stays +unchanged. + +## Language inventory and voting destinations + +Use stable language IDs, independent of menu captions. Each issue collects votes for +both formatting and detection for that language. + +| Caption | ID | Highlighting cases to cover | Voting issue | +| --- | --- | --- | --- | +| Python | `python` | Decorators, comments, numeric literals, triple-quoted/raw strings, f-strings | [#108](https://github.com/sql-bi/SQLBI-Whiteboard/issues/108) | +| C | `c` | Comments, character/string literals, escapes, preprocessor directives, numeric suffixes | [#109](https://github.com/sql-bi/SQLBI-Whiteboard/issues/109) | +| C++ | `cpp` | C constructs plus templates, modern keywords, raw strings and their delimiters | [#110](https://github.com/sql-bi/SQLBI-Whiteboard/issues/110) | +| Java | `java` | Annotations, comments, strings, text blocks, modern keywords | [#111](https://github.com/sql-bi/SQLBI-Whiteboard/issues/111) | +| C# | `csharp` | Attributes, directives, verbatim/interpolated/raw strings, contextual keywords | [#112](https://github.com/sql-bi/SQLBI-Whiteboard/issues/112) | +| JavaScript | `javascript` | Comments, strings, template interpolation, regex literals versus division | [#113](https://github.com/sql-bi/SQLBI-Whiteboard/issues/113) | +| TypeScript | `typescript` | JavaScript constructs plus type keywords, annotations and generic syntax | [#114](https://github.com/sql-bi/SQLBI-Whiteboard/issues/114) | +| Visual Basic .NET | `vbnet` | Case-insensitive keywords, apostrophe/REM comments, escaped/interpolated strings, continuations, XML literals | [#115](https://github.com/sql-bi/SQLBI-Whiteboard/issues/115) | +| R | `r` | Comments, backtick names, strings/raw strings, formulas, pipes and custom infix operators | [#116](https://github.com/sql-bi/SQLBI-Whiteboard/issues/116) | +| Rust | `rust` | Nested comments, raw strings, lifetimes versus character literals, macro invocations | [#117](https://github.com/sql-bi/SQLBI-Whiteboard/issues/117) | +| PHP | `php` | Variables, comments, strings, heredoc/nowdoc and optional PHP tags | [#118](https://github.com/sql-bi/SQLBI-Whiteboard/issues/118) | + +Do not promise exact IDE coloring for ambiguous contextual constructs. Recognize string +and comment boundaries correctly and favor conservative coloring for ambiguous names. + +## 1. Separate selection from detection + +The current `TextLanguageIds.All` serves both language inventory and default detection +order. Simply appending these eleven IDs would put them in Snippet format order and +settings migration. Split those responsibilities before adding services. + +- In `src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs`, register all fifteen IDs for + persistence/selection, and introduce a distinct detection-capable list containing + only DAX, SQL Server, KQL, and Plain text in their current default order. +- Make `Normalize` recognize the eleven new IDs. Keep the existing unknown-language + fallback for genuinely unknown IDs. +- Make `NormalizeOrder` validate against the detection-capable list, ignoring + manual-only IDs rather than converting them into Plain text. Preserve the existing + treatment of unknown values unless a separate migration is needed. +- Update defaults and normalization in `Core/Settings/AppSettings.cs`; preserve chosen + detection order, Plain-text-first behavior, and historical default migration. +- Keep `PreferencesWindow.xaml.cs` limited to detection-capable languages. Do not show + unavailable features as reorderable detection options. +- `TextLanguageRegistry.All` exposes all fifteen services to `LanguageChipCombo` and + `TextEditorLanguageCombo`. Retain the existing four entries in their current order; + append the eleven entries in the inventory order above. Ensure the menu scrolls. +- Add an explicit detection capability to language metadata, separate from + `CanFormat`. New services have detection disabled, `CanFormat = false`, and + `TryAccept` always returns false. Their `TryFormat` returns false and unchanged text. + Filter detection before calling `TryAccept`, so no manual-only analyzer runs on paste. + +Decision 28's automatic insertion rule continues to apply to detection-capable +languages only. Record that qualification in `docs/decisions.md` when implemented. + +## 2. Implement one reusable highlighting adapter + +Use the existing AvalonEdit dependency as the first implementation candidate: inspect +the pinned package's highlighting definitions and highlighter API, then run a small +spike converting line highlighting into Whiteboard `StyledTextSpan` values. Do not +assume the bundled grammars cover all eleven languages or their modern syntax. + +The spike must show identical spans in edit and display paths, multiline state across +lines, safe handling of incomplete code, and reuse by export. Use full-document +analysis rather than viewport-only coloring. Evaluate C# raw strings, JavaScript +template/regex syntax, and Rust nested comments early, because they expose grammar +limitations. Approve the engine approach against these cases before authoring all +eleven definitions. If XSHD cannot represent a construct reliably, use a small stateful +scanner for that language behind the same adapter rather than a growing single regex. + +Implement the adapter and definitions inside `src/SQLBI.Whiteboard`; avoid introducing +eleven parser projects or external runtimes for highlighting. Reuse compatible bundled +definitions where they pass the corpus, and ship missing definitions as embedded +resources under a dedicated `Highlighting/` folder. Record source/version/license for +any imported definitions and include required notices. No runtime downloads. + +The adapter must: + +- Return ordered, nonoverlapping, in-bounds spans using UTF-16 offsets, matching the + existing `TextClassificationColorizer` and export consumers. Resolve nested grammar + styles into flat runs before returning them. +- Map token categories to a shared readable palette consistent with the existing + languages, and use Consolas. Retain current wrapping, scaling and line-number policy. +- Preserve every source character, including whitespace and line endings. Highlight + incomplete snippets without rejecting them; recover after unterminated constructs + according to the language's lexical rules. +- Use generic titles such as "Python Code"; semantic definition-name discovery is + outside this phase. Follow existing title-override behavior for imported containers. +- Reuse `TextLanguageAnalysis` for `TextContainerVisual`, the editor colorizer and + `Export/EditableSlide.cs`, rather than adding an editor-only coloring path. Verify + PDF/vector and preview rendering through their existing consumers too. +- Keep mutable document/highlighter state local to an analysis operation. Freeze WPF + brushes before sharing across threads and bound caches. Enable background edit + analysis where useful, respecting cancellation/stale-result handling already present. +- Check long lines and incomplete strings for pathological runtime. A failed analyzer + must fall back to uncolored text without losing source or crashing display/export. + +### Spike result: adopt the engine, author the definitions + +A throwaway spike ran the pinned AvalonEdit 6.3.1.120 over fixtures for the constructs +named above, converting `DocumentHighlighter` line output into flat spans by painting each +line's sections in order and grouping the result. The engine is approved; the bundled +grammars are not. + +What the adapter shape delivers, measured rather than assumed: + +- Spans came back ordered, nonoverlapping and in bounds on every fixture, including + unterminated strings and comments. +- Offsets are document UTF-16 offsets and stayed correct across a surrogate pair, which is + what `TextClassificationColorizer` and the export consumers need. +- Span state carries across lines, so a block comment, a heredoc and a triple-quoted string + color to their real end. CRLF and LF both work; no span ever covered a line delimiter. +- Empty input produces no spans. An unterminated construct colors to the end and the next + construct recovers, which is the language's own lexical rule. +- 200 concurrent analyses sharing one definition returned identical spans, for a bundled + definition and a hand-written one, so a definition can be shared and the document and + highlighter kept local to the operation. +- Runtime is linear in the ordinary cases — 380 KB over 20,000 lines with an unterminated + block comment took 79 ms — but one 429 KB line of 80,000 tokens took 837 ms. The adapter + needs a size guard rather than trust. + +What the bundled definitions do not cover. Seven of the eleven languages have one (Python, +C++, Java, C#, JavaScript, VB, PHP); C has to borrow C++, and R, Rust and TypeScript have +none at all. The seven that exist predate the syntax people write now: C# raw strings and +Java text blocks are read as an empty string followed by loose code, JavaScript template +literals are not recognized at all, Python decorators and Java annotations are uncolored, +and `1_000` colors as `1`. They are not a shortcut worth taking. + +What our own XSHD can and cannot express, tested by writing three definitions: + +- A span whose rule set contains itself gives true nested comments: `/* outer /* inner */ + still comment */` came back as one comment run, 2,000 levels deep took 59 ms, and an + unterminated nested comment swallows the rest as Rust says it should. +- Ordering rules buys the ambiguous cases: a lifetime rule with a negative lookahead before + the character rule separates `&'static` from `'a'`. Nested rule sets buy string escapes, + and a span that re-enters the code rule set buys interpolation — `` `sum ${a + b} end` `` + and a template nested inside its own hole both came out right. +- XSHD cannot count. Bounded rules cover C# raw strings at three, four and five quotes and + Rust `r#"` and `r##"`, which is most real code, but a longer delimiter is beyond it. +- A rule regex cannot see far enough left to settle regex versus division: `/ab+c/gi` and + `foo(/lit/, 1 / 2)` are right, while `let x = a / b, re2 = /x\/y/` misses the second + literal, because the lookbehind window starts where the previous rule stopped. + +So: reuse the AvalonEdit engine and the flattening adapter, ship our own definitions under +`Highlighting/` rather than the bundled ones, and keep a small stateful scanner behind the +same adapter for the three constructs XSHD cannot reach — C# raw string delimiters, +JavaScript regex versus division, and Rust raw strings past a bounded hash count. + +## 3. Add the F6 voting prompt + +Associate an optional immutable `FormattingRequestUri` with each service (or its +descriptor), using the exact issue links above. Plain text has no voting URI. Keep +`CanFormat` false: showing a request dialog is not formatting support. + +In `MainWindow.xaml.cs`, route both `FormatTextEdit` and `FormatSelectedText` through +one shared unsupported-format check before their current `!language.CanFormat` early +returns. A manual-only language opens the dialog; a supported formatter follows its +existing path. Invalid DAX/SQL/KQL input must not trigger a voting prompt. + +Proposed copy, substituting the selected language: + +> **Python formatting is not available yet** +> +> Vote for Python formatting and automatic detection on GitHub. Add a thumbs-up +> reaction to the issue's opening post to help us prioritize this language. + +Show a clickable **Vote on GitHub** link and a **Close** button. The link opens the +language's fixed HTTPS issue URL in the default browser only after the user activates +it, following the application's existing shell-link pattern. It does not submit a vote; +GitHub handles sign-in and reactions. Never append the snippet or local paths to the URL. + +The dialog should be owned by the main window, keyboard accessible, dismissible with +Escape, and avoid stacking on repeated F6. Closing it returns focus to the editor or +board without changing source, language, caret, selection, dimensions, dirty state, or +undo history. Handle browser-launch failures with a selectable/copyable issue URL. +Show no network request or dialog merely from selecting a language. Plain text and +F6 with no selected text remain quiet. Reuse this check for any additional format +entry points found during implementation. + +## 4. Tests and release verification + +Extend the framework-neutral smoke tests for language-ID persistence and detection-order +normalization. The existing harness targets `net10.0` and cannot directly host AvalonEdit +WPF highlighting; add focused Windows-targeted highlighting tests if the adapter remains +in WPF. If a new first-party test assembly is published, follow the signing requirements +in CONTRIBUTING.md; do not package test-only artifacts with the application. + +Automated acceptance coverage: + +- All eleven IDs round-trip through `.wboard`; older boards and unknown IDs keep their + existing behavior. Document that older app versions may display the new IDs as plain + text; this feature does not require changing the archive schema. +- Default and custom detection orders still contain only the original four languages. + Manual-only IDs injected into settings are ignored. Existing paste fixtures produce + the same result; none of the eleven new services accepts source automatically. +- Every language has fixtures for ordinary tokens, multiline constructs from the table, + incomplete input, CRLF/LF, Unicode before tokens, and empty input. Assert meaningful + token categories and valid spans, not just successful execution. +- Check string/comment containment, absence of text mutation, stale analysis after a + language switch, and bounded behavior on representative large snippets. +- Each manual-only service resolves to its exact issue URI; Plain/DAX/SQL/KQL have no + unsupported-feature prompt. Verify both format entry points use the shared behavior. + +Manual acceptance matrix, for each language: + +1. Paste text, choose the language in display mode, then edit and switch language again. + Confirm the selector works with all fifteen entries and editing remains responsive. +2. Check multiline coloring, resizing, wrapping and zoom; save and reopen the board. +3. Export representative content to editable PowerPoint and vector PDF, and check the + saved preview for matching text and highlighting. +4. Press F6 while editing and again in display mode. Confirm the correct language in + the prompt and the exact GitHub destination; check keyboard dismissal, unchanged + caret/selection/source/history, and browser failure fallback. +5. Confirm DAX, SQL and KQL still format, and Plain text remains quiet. + +Run `dotnet build Whiteboard.sln -c Release`, the existing Core smoke-test executable, +and the focused Windows tests. Review new resource inclusion in a published build. +Update README, relevant guide/shortcut pages and release notes when the behavior ships; +do not describe the plan as an already available feature. Follow the repository's +branch, PR and version/release-note workflow. + +## Delivery sequence and prioritization + +1. Shared capability split, persistence tests, adapter spike and voting dialog. +2. C and C# definitions to establish the first end-to-end examples; C++, Java and VB.NET. +3. JavaScript and TypeScript together; Python and R; Rust and PHP. +4. Complete the eleven-language regression/export matrix and ship the complete set. + +Use reviewable implementation PRs, keeping incomplete services out of the public +registry until their highlighting and voting links are ready. All eleven languages +are in the committed scope; voting does not gate their highlighting work. + +For subsequent formatting/detection work, compare the number of thumbs-up reactions on +the opening post of these eleven issues when planning the next feature. Higher-voted +languages receive priority, with implementation dependencies and shared engines noted +when they affect delivery order. Comments can distinguish demand for formatting versus +detection; reaction totals deliberately measure the combined per-language request. +No automatic vote polling, telemetry, scheduling, or delivery dates are part of this phase. +When a capability ships, update its issue and service metadata; if only formatting +ships, remove the F6 prompt while retaining the issue for remaining detection work. diff --git a/docs/decisions.md b/docs/decisions.md index 9a55e3f..0b27ca6 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -682,6 +682,15 @@ Two things were changed together, and the second is what makes the first safe: KQL a pipe, query operator, keyword, command, or function. The parser still has the last word after the signal. +Both rules are about the languages that can read a snippet. Choosing a language by hand +and recognizing one automatically are now two separate lists: a text container can be set +to any of fifteen languages, while only DAX, T-SQL, KQL, and plain text take part in the +snippet format order. A language that only colors never claims a paste, never joins a +saved order, and is ignored if one is written into settings by hand — reading it as plain +text instead would move plain text up an order it was never part of. The archive format is +unchanged by this: a board saved with one of the new languages opens in an older release +as plain text, with its source intact. + --- ## 29. A text container's width in columns is the line width of its snippet diff --git a/src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs b/src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs index bbd3437..1711aab 100644 --- a/src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs +++ b/src/SQLBI.Whiteboard.Core/Model/BoardObjects.cs @@ -246,28 +246,52 @@ public static class TextLanguageIds public const string Dax = "dax"; public const string SqlServer = "sqlserver"; public const string Kql = "kql"; + public const string Python = "python"; + public const string C = "c"; + public const string Cpp = "cpp"; + public const string Java = "java"; + public const string CSharp = "csharp"; + public const string JavaScript = "javascript"; + public const string TypeScript = "typescript"; + public const string VbNet = "vbnet"; + public const string R = "r"; + public const string Rust = "rust"; + public const string Php = "php"; - public static string Normalize(string? languageId) => - languageId?.Trim().ToLowerInvariant() switch - { - Dax => Dax, - SqlServer => SqlServer, - Kql => Kql, - _ => Plain, - }; + /// + /// Every language a text container can be set to, in the order the selector + /// offers them. Choosing a language is not the same as recognizing one: all + /// of these are saved and restored, and only + /// ever claims a paste of its own. + /// + public static IReadOnlyList All { get; } = + [ + Plain, Dax, SqlServer, Kql, + Python, C, Cpp, Java, CSharp, JavaScript, TypeScript, VbNet, R, Rust, Php, + ]; /// - /// The default snippet format order, which is also the order a language - /// missing from a saved order joins it in. Plain text accepts everything, - /// so it comes last: a paste is code if any language says it is, and text - /// otherwise, with no setting to change. + /// The languages that can recognize a snippet, in the default snippet + /// format order, which is also the order a language missing from a saved + /// order joins it in. Plain text accepts everything, so it comes last: a + /// paste is code if any language says it is, and text otherwise, with no + /// setting to change. /// - public static IReadOnlyList All { get; } = [Dax, SqlServer, Kql, Plain]; + public static IReadOnlyList DetectionOrder { get; } = [Dax, SqlServer, Kql, Plain]; + + /// + /// Whether the language can claim a paste. The rest are chosen by hand, so + /// they take no part in the snippet format order. + /// + public static bool CanDetect(string? languageId) => + Known(languageId) is { } id && DetectionOrder.Contains(id, StringComparer.Ordinal); + + public static string Normalize(string? languageId) => Known(languageId) ?? Plain; /// /// The default orders earlier releases wrote into settings, with plain text /// first. A saved order equal to one of these was never chosen by anyone, so - /// an upgrade may replace it with . + /// an upgrade may replace it with . /// public static IReadOnlyList> LegacyDefaultOrders { get; } = [ @@ -291,7 +315,9 @@ public static bool IsLegacyDefaultOrder(IEnumerable? languageIds) /// order does not know. Those go in front of plain text wherever it sits, /// because an order that puts plain text after some languages means "try /// the languages first"; only an order that starts with plain text means - /// "keep my pastes plain", and there they go last. + /// "keep my pastes plain", and there they go last. A language that is only + /// ever chosen by hand is skipped rather than read as plain text, which + /// would move plain text up an order it was never part of. /// public static IReadOnlyList NormalizeOrder(IEnumerable? languageIds) { @@ -300,6 +326,12 @@ public static IReadOnlyList NormalizeOrder(IEnumerable? language { foreach (var languageId in languageIds) { + if (Known(languageId) is { } known && + !DetectionOrder.Contains(known, StringComparer.Ordinal)) + { + continue; + } + var normalized = Normalize(languageId); if (!ordered.Contains(normalized, StringComparer.Ordinal)) { @@ -308,7 +340,7 @@ public static IReadOnlyList NormalizeOrder(IEnumerable? language } } - var missing = All.Where(languageId => !ordered.Contains(languageId, StringComparer.Ordinal)).ToArray(); + var missing = DetectionOrder.Where(languageId => !ordered.Contains(languageId, StringComparer.Ordinal)).ToArray(); var plainIndex = ordered.IndexOf(Plain); if (plainIndex > 0) { @@ -321,6 +353,35 @@ public static IReadOnlyList NormalizeOrder(IEnumerable? language return ordered; } + + /// + /// The issue collecting votes for formatting and automatic detection of a + /// language Whiteboard only colors. The languages that already format have + /// none, and neither has plain text: F6 answers those itself. + /// + public static string? FormattingRequestUrl(string? languageId) => Normalize(languageId) switch + { + Python => Issue(108), + C => Issue(109), + Cpp => Issue(110), + Java => Issue(111), + CSharp => Issue(112), + JavaScript => Issue(113), + TypeScript => Issue(114), + VbNet => Issue(115), + R => Issue(116), + Rust => Issue(117), + Php => Issue(118), + _ => null, + }; + + private static string Issue(int number) => + $"https://github.com/sql-bi/SQLBI-Whiteboard/issues/{number}"; + + private static string? Known(string? languageId) => + languageId?.Trim().ToLowerInvariant() is { } id && All.Contains(id, StringComparer.Ordinal) + ? id + : null; } public record TextBoardObject( diff --git a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs index 5907d5e..9770115 100644 --- a/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs +++ b/src/SQLBI.Whiteboard.Core/Settings/AppSettings.cs @@ -134,7 +134,7 @@ public sealed class AppSettings /// public bool ShowEraserButton { get; set; } - public List SnippetFormatOrder { get; set; } = [.. TextLanguageIds.All]; + public List SnippetFormatOrder { get; set; } = [.. TextLanguageIds.DetectionOrder]; public InkToolSettings Pen { get; set; } = InkToolSettings.From(InkPalettes.DefaultPen); @@ -288,7 +288,7 @@ private static AppSettings Normalize(AppSettings? settings) if (settings.Version < VersionWithPlainTextLast && TextLanguageIds.IsLegacyDefaultOrder(settings.SnippetFormatOrder)) { - settings.SnippetFormatOrder = [.. TextLanguageIds.All]; + settings.SnippetFormatOrder = [.. TextLanguageIds.DetectionOrder]; } settings.SnippetFormatOrder = [.. TextLanguageIds.NormalizeOrder(settings.SnippetFormatOrder)]; diff --git a/src/SQLBI.Whiteboard/FormattingRequestWindow.xaml b/src/SQLBI.Whiteboard/FormattingRequestWindow.xaml new file mode 100644 index 0000000..0a6a8aa --- /dev/null +++ b/src/SQLBI.Whiteboard/FormattingRequestWindow.xaml @@ -0,0 +1,59 @@ + + + + + + + Vote on GitHub + + + + + +