diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index bf9f1af1fdd..ef4f666f47d 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,77 +1,40 @@ aci -admins allcolors -Apc -apc -backpressure breadcrumb breadcrumbs -bsd -calt ccmp ccon -changelog clickable -clig CMMI +colorbrewer consvc copyable -Counterintuitively -CtrlDToClose -CVS -CUI -cybersecurity dalet -Dcs dcs deselection dialytika diffing dje -downside downsides dze dzhe -DTo -EDDB -EDDC Emacspeak -Enum'd Fitt -formattings FTCS -ftp -fvar gantt -gcc -geeksforgeeks ghe -github gje godbolt -hostname -hostnames -https -hyperlink hyperlinking hyperlinks -iconify -ID -img -inlined -issuetitle -It'd kje libfuzzer -libuv liga lje Llast -llvm Lmid locl lol -lorem Lorigin maxed megathread @@ -80,28 +43,24 @@ mkmk mnt mru nje -noreply notwrapped ogonek -ok'd overlined perlw -pipeline postmodern Powerline -powerline ptys +pwn pwshw -quickfix qof qps -Remappings -Retargets +quickfix rclt reimplementation +Remappings reserialization -reserialize reserializes +Retargets rlig rubyw runtimes @@ -109,34 +68,20 @@ servicebus shcha similaritytolerance slnt -Sos -ssh -sustainability stakeholders +sustainability sxn -timeline -timelines -timestamped TLDR -tokenizes tonos toolset -truthiness tshe -ubuntu UEFI uiatextrange -UIs und -unregister -versioned vsdevcmd -walkthrough -walkthroughs -We'd westus -wildcards workarounds +wtconfig XBox YBox yeru diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index a2f9f7be00e..3a6a2a6b7c1 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -515,6 +515,7 @@ dsound DSSCL DSwap DTest +DTo DTTERM dup'ed dvi @@ -737,6 +738,7 @@ HABCDEF Hackathon HALTCOND HANGEUL +hardlinks hashalg HASSTRINGS hbitmap @@ -1079,6 +1081,7 @@ MOUSEFIRST MOUSEHWHEEL MOVESTART msb +msbuildcache msctf msctls msdata @@ -1488,6 +1491,7 @@ reparented reparenting replatformed Replymessage +reportfileaccesses repositorypath Requiresx rerasterize @@ -2004,6 +2008,7 @@ wincontypes WINCORE windbg WINDEF +windir windll WINDOWALPHA windowdpiapi diff --git a/.gitignore b/.gitignore index 3db8546ef4e..9e8281ee5dc 100644 --- a/.gitignore +++ b/.gitignore @@ -283,3 +283,6 @@ MSG*.bin profiles.json *.metaproj *.swp + +# MSBuildCache +/MSBuildCacheLogs/ \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000000..312ae503ed4 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,60 @@ + + + + + + + false + + + false + + + Microsoft.MSBuildCache.AzurePipelines + Microsoft.MSBuildCache.Local + + + + + 202310210737 + + + + $(MSBuildCacheAllowFileAccessAfterProjectFinishFilePatterns); + \**\ApplicationInsights.config; + $(LocalAppData)\Microsoft\VSApplicationInsights\**; + $(LocalAppData)\Microsoft\Windows\INetCache\**; + A:\; + E:\; + $(windir)\**; + + + + $(MSBuildCacheIdenticalDuplicateOutputPatterns);bin\** + + + $(MSBuildThisFileDirectory)\dep\nuget\packages.config + $(MSBuildCacheIgnoredInputPatterns);$(PackagesConfigFile) + + + + $([System.IO.File]::ReadAllText("$(PackagesConfigFile)")) + $([System.Text.RegularExpressions.Regex]::Match($(PackagesConfigContents), 'Microsoft.MSBuildCache.*?version="(.*?)"').Groups[1].Value) + $(MSBuildThisFileDirectory)packages\$(MSBuildCachePackageName).$(MSBuildCachePackageVersion) + $(MSBuildThisFileDirectory)packages\Microsoft.MSBuildCache.SharedCompilation.$(MSBuildCachePackageVersion) + + + + + + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000000..df6e6b7ebb1 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md index fb48315d254..091060db2cd 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -325,6 +325,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` +## ColorBrewer +**Source**: [https://colorbrewer2.org/](https://colorbrewer2.org/) + +### License + +``` +Apache-Style Software License for ColorBrewer software and ColorBrewer Color Schemes + +Copyright (c) 2002 Cynthia Brewer, Mark Harrower, and The Pennsylvania State University. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +``` + # Microsoft Open Source This product also incorporates source code from other Microsoft open source projects, all licensed under the MIT license. diff --git a/build/pipelines/ci-caching.yml b/build/pipelines/ci-caching.yml index 96b79c46999..e8379b556e9 100644 --- a/build/pipelines/ci-caching.yml +++ b/build/pipelines/ci-caching.yml @@ -1,30 +1,32 @@ trigger: batch: true -# branches: -# include: -# - main -# - feature/* -# - gh-readonly-queue/* -# paths: -# exclude: -# - doc/* -# - samples/* -# - tools/* + branches: + include: + - main + - feature/* + - gh-readonly-queue/* + paths: + exclude: + - doc/* + - samples/* + - tools/* -#pr: -# branches: -# include: -# - main -# - feature/* -# paths: -# exclude: -# - doc/* -# - samples/* -# - tools/* +pr: + branches: + include: + - main + - feature/* + paths: + exclude: + - doc/* + - samples/* + - tools/* variables: - name: runCodesignValidationInjectionBG value: false + - name: EnablePipelineCache + value: true # 0.0.yyMM.dd## # 0.0.1904.0900 @@ -81,6 +83,8 @@ stages: buildConfigurations: [Release] buildEverything: true keepAllExpensiveBuildOutputs: false + ${{ if eq(variables['System.PullRequest.IsFork'], 'False') }}: + enableCaching: true - ${{ if eq(parameters.runTests, true) }}: - stage: Test_${{ platform }} diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index ae5d3a16ac6..ac350da72fd 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -68,6 +68,9 @@ parameters: - name: signingIdentity type: object default: {} + - name: enableCaching + type: boolean + default: false jobs: - job: ${{ parameters.jobName }} @@ -95,6 +98,7 @@ jobs: # Yup. BuildTargetParameter: ' ' SelectedSigningFragments: ' ' + MSBuildCacheParameters: ' ' # When building the unpackaged distribution, build it in portable mode if it's Canary-branded ${{ if eq(parameters.branding, 'Canary') }}: UnpackagedBuildArguments: -PortableMode @@ -111,6 +115,7 @@ jobs: clean: true submodules: true persistCredentials: True + # This generates either nothing for BuildTargetParameter, or /t:X;Y;Z, to control targets later. - pwsh: |- If (-Not [bool]::Parse("${{ parameters.buildEverything }}")) { @@ -139,6 +144,17 @@ jobs: } displayName: Prepare Build and Sign Targets + - ${{ if eq(parameters.enableCaching, true) }}: + - pwsh: |- + $MSBuildCacheParameters = "" + $MSBuildCacheParameters += " -graph" + $MSBuildCacheParameters += " -reportfileaccesses" + $MSBuildCacheParameters += " -p:MSBuildCacheEnabled=true" + $MSBuildCacheParameters += " -p:MSBuildCacheLogDirectory=$(Build.SourcesDirectory)\MSBuildCacheLogs" + Write-Host "MSBuildCacheParameters: $MSBuildCacheParameters" + Write-Host "##vso[task.setvariable variable=MSBuildCacheParameters]$MSBuildCacheParameters" + displayName: Prepare MSBuildCache variables + - pwsh: |- .\build\scripts\Generate-ThirdPartyNotices.ps1 -MarkdownNoticePath .\NOTICE.md -OutputPath .\src\cascadia\CascadiaPackage\NOTICE.html displayName: Generate NOTICE.html from NOTICE.md @@ -160,21 +176,37 @@ jobs: ${{ parameters.additionalBuildOptions }} /bl:$(Build.SourcesDirectory)\msbuild.binlog $(BuildTargetParameter) + $(MSBuildCacheParameters) platform: $(BuildPlatform) configuration: $(BuildConfiguration) + msbuildArchitecture: x64 maximumCpuCount: true + ${{ if eq(parameters.enableCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) - ${{ if eq(parameters.publishArtifacts, true) }}: - publish: $(Build.SourcesDirectory)/msbuild.binlog artifact: logs-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} condition: always() displayName: Publish Build Log + - ${{ if eq(parameters.enableCaching, true) }}: + - publish: $(Build.SourcesDirectory)\MSBuildCacheLogs + artifact: logs-msbuildcache-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + condition: always() + displayName: Publish MSBuildCache Logs - ${{ else }}: - task: CopyFiles@2 displayName: Copy Build Log inputs: contents: $(Build.SourcesDirectory)/msbuild.binlog TargetFolder: $(Terminal.BinDir) + - ${{ if eq(parameters.enableCaching, true) }}: + - task: CopyFiles@2 + displayName: Copy MSBuildCache Logs + inputs: + contents: $(Build.SourcesDirectory)/MSBuildCacheLogs/** + TargetFolder: $(Terminal.BinDir)/MSBuildCacheLogs # This saves ~2GiB per architecture. We won't need these later. # Removes: diff --git a/dep/nuget/packages.config b/dep/nuget/packages.config index e6fd7ec3a68..a69adb22942 100644 --- a/dep/nuget/packages.config +++ b/dep/nuget/packages.config @@ -17,4 +17,9 @@ + + + + + diff --git a/doc/specs/#1595 - Suggestions UI/Snippets.md b/doc/specs/#1595 - Suggestions UI/Snippets.md new file mode 100644 index 00000000000..4fd12702ff2 --- /dev/null +++ b/doc/specs/#1595 - Suggestions UI/Snippets.md @@ -0,0 +1,648 @@ +--- +author: Mike Griese +created on: 2022-08-22 +last updated: 2024-06-13 +issue id: 1595 +--- + +# Windows Terminal - Snippets + +## Abstract + +The command line is a highly powerful tool. However, its power is dependent on +the user's knowledge of the specific commands, flags and parameters needed to +perform tasks from the command-line. For simple everyday commands, this might +not be so hard. For longer commands, or ones used less frequently, there's quite +a bit of mental overhead trying to recall the exact syntax. For teams, it might +be helpful to share these tasks with everyone on the project. The Terminal can +be an avenue by which complicated tasks can be remembered, shared, discovered, +and recalled by the user simply thinking **"what do I want to do"**, rather than +"how do I do it". + +## Background + +> **Note**: +> +> This largely builds off of work in the [Suggestions UI], for displaying these +> tasks to the user. Make sure to read that spec first. + +### Inspiration + +The primordial version of this idea was probably [#keep] - a command-line tool I +wrote for stashing long command-lines and directories, and recalling them with +just a number. We've had many variations on this idea over the years - [#1595] +was probably the first such request on the Terminal repo. ITerm2 also had [a +similar feature](https://iterm2.com/images/CommandHistory.png). Theirs was more +directly tied to shell integration (that menu is populated from commands that +they know were run in the shell). In the absence of shell integration though, it +should be able to save these commands to a menu manually. + +Consider [VsCode Tasks]. These are files which can be placed in the root of +a workspace, and share common tasks between users of that workspace. They've got +support for starting processes, with a set of args. These args can also be +picked at runtime, and custom sets of arguments can be specified for individual +arguments. + +It is hard to say that the ultimate vision here isn't partially inspired by the +"[workflows]" of [Warp], or by [Fig]. These are modern tools that seek to +augment the command-line experience, by making the command-line more +approachable. Warp quite clearly has the same concept in "workflows" - scripts +which the user can build and Warp (a Terminal emulator) can insert quickly. Fig, +on the other hand, is more focused on just simplifying the command-line +experience. Fig is more about providing additional metadata to the user as +they're typing. They are [also working on workflows], so there's clearly quite a +bit of ecosystem-wide demand for more discoverable command-line tooling. + +We've had verbatim feedback that developers already attempt to record useful +commandlines in various different ways - in OneNotes, in shell scripts, in +aliases. Furthermore, developers often share these commands with the rest of +their teams. Providing a unified way to easily store, browse, and use these +command lines should be valuable to developers already doing this. A static +file in their project containing commands for the whole team seems like a simple +solution to this problem. + +### User Stories + +Story | Size | Description +--|-----------|-- +A | โœ… Done | Users can bring up a menu of command line tasks and quickly execute them +B | โœ… Done | Fragment apps can provide tasks to a users settings +C | ๐Ÿšถ Walk | The user can save commands straight to their settings with a `wt` command +D | ๐Ÿšถ Walk | Users can have different tasks enabled for different profiles(/shells?) +E | ๐Ÿšถ Walk | The Terminal displays a Snippets Pane for easy browsing of relevant snippets +F | ๐Ÿƒโ€โ™‚๏ธ Run | The terminal can automatically look for command fragments in the tree of the CWD +G | ๐Ÿƒโ€โ™‚๏ธ Run | Snippets with multiple lines can be sent only conditionally on the success of the previous command (with shell integration) +H | โœ… Done | Snippets can be filtered by text the user has already typed +I | ๐Ÿš€ Sprint | Snippets can have prompt-able sections of input +J | ๐Ÿš€ Sprint | Community tasks are hosted in a public GH repo +K | ๐Ÿš€ Sprint | A simple UX (either web or in Terminal) is exposed for interacting with public GH repo of tasks + +### Elevator Pitch + +The Terminal can remember long command-lines and display them with user-friendly +descriptions of _what they actually do_. These tasks can be searched by intent, +rather than the particular combination of flags. They can be shared with members +of your team, so everyone has easy access to common tasks for a project. + +### Why not just aliases / native script files? + +Why not just take these tasks and put them into a shell alias? For longer tasks, +why not just stick them in a `.ps1`/`.bat`/`.sh`/etc file? This is an option +that's existed since the time immemorial. However, this still requires the user +to remember that they've created these aliases/scripts, remember where they're +stored, and remember how they work. + +By providing a dedicated UI for these command-lines, they can always be at your +fingertips. No need to remember what the alias for a particular command-line is - +just look up what you want to do. Aliases and scripts are no longer scattered +across `.bashrc`, `.bash_profile`, `.profile`, etc, they can all be stashed in +the Terminal config, or in the project they're relevant to. By stashing them +alongside the code, then anyone else coming to work on the code can have +immediate access to useful sets of tasks. + +Aliases have a tendency towards more experienced shell users. This proposal +instead brings the power of these aliases and scripts right to the foreground, +with a cross-shell mechanism of exposing them to even beginners. With fragment +extensions, tools can bundle common workflows together with their application so +the Terminal can automatically load them for the user. + +## Business Justification + +It will delight developers. + +## Scenario Details + +### Implementation Details + +For the most part, this is already implemented as the `sendInput` action. These +actions send text to the terminal already, and work quite well as snippets. + +#### Basics + +We'll want to also augment `sendInput` to add support for `input` as an array of +strings, not only a single string value. When the input is a list of strings, +then the terminal can send each string, separated by the enter key. +We can also add a `waitForSuccess` parameter to `sendInput` (with a default +value of `false`). If that's set to `true`, **and shell integration is enabled**, +then the Terminal will wait to send each command until the previous command +exits. + +As another minor improvement, we'll add a `description` property to Commands. +This will allow users to add additional information to snippets which we can +surface. Additionally, extension authors could provide more details as well. + +As a matter of renaming, we'll also update `"source": "tasks"` for the +`SuggestionsSource` enum to instead be `snippets` (and gracefully update that +where we find it). "tasks" was an older name for this feature, and "snippets" +will better align with our partners in VsCode. + +##### Multi-line snippets example + +Consider the [following script](https://gist.github.com/zadjii-msft/b598eebd6c5601328498e3e7acc581a7): + +```pwsh +$s=Invoke-GitHubGraphQlApi "query{organization(login:`"Microsoft`"){projectV2(number: 159) { id } } }" + +$tasks = get-GitHubIssue -Labels "Issue-Task" -state open +$bugs = get-GitHubIssue -Labels "Issue-Bug" -state open +$issues = $tasks + $bugs + +$issues | ? {$_.labels.Name -NotContains "Needs-Triage" } | ? { $_.milestone.title -Ne "Icebox โ„" } | ? type -Ne "PullRequest" | select -expand node_id | % { + $resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ; +} +``` + +As just a raw sendInput action with a single `input`, this would look like the following: + +```jsonc +{ + "command": + { + "action": "sendInput", + "input": "$s=Invoke-GitHubGraphQlApi \"query{organization(login:`\"Microsoft`\"){projectV2(number: 159) { id } } }\"\r\n$tasks = get-GitHubIssue -Labels \"Issue-Task\" -state open\r\n$bugs = get-GitHubIssue -Labels \"Issue-Bug\" -state open\r\n$issues = $tasks + $bugs\r\n$issues | ? {$_.labels.Name -NotContains \"Needs-Triage\" } | ? { $_.milestone.title -Ne \"Icebox โ„\" } | ? type -Ne \"PullRequest\" | select -expand node_id | % {\r\n $resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ;\r\n}" + }, + "name": "Upload to project board", + "description": "Sync all our issues and bugs that have been triaged and are actually on the backlog to the big-ol project", +}, +``` + +This JSON is basically entirely unusable. Since JSON doesn't support multiline +strings, then every line has to be joined to a single line, separated by `\r\n`. + +Instead, the following version of this command uses an array for the `input` +parameter. This then implies that each string should be sent in sequence, with +enter between them. + +```jsonc +{ + "command": + { + "action": "sendInput", + "input": + [ + "$s=Invoke-GitHubGraphQlApi \"query{organization(login:`\"Microsoft`\"){projectV2(number: 159) { id } } }\"", + "$tasks = get-GitHubIssue -Labels \"Issue-Task\" -state open", + "$bugs = get-GitHubIssue -Labels \"Issue-Bug\" -state open", + "$issues = $tasks + $bugs", + "$issues | ? {$_.labels.Name -NotContains \"Needs-Triage\" } | ? { $_.milestone.title -Ne \"Icebox โ„\" } | ? type -Ne \"PullRequest\" | select -expand node_id | % {", + " $resp = Add-GitHubBetaProjectItem -ProjectNodeId $s.organization.projectV2.id -ContentNodeId $_ ;", + "}", + "" + ] + }, + "name": "Upload to project board", + "description": "Sync all our issues and bugs that have been triaged and are actually on the backlog to the big-ol project", +}, +``` + +This is slightly more maintainable. Assuming the user also has shell integration +enabled, they could also set `"waitForSuccess": true`, and if any part of the +script fails, then the rest of it won't be sent to the shell[[1](#footnote-1)]. + +#### Fragment actions + +This was already added in [#16185]. These will allow third-party developers to +create apps which add additional snippets to the Terminal. These will require +app developers to add `id`s to each action they add in this way. Users can then +bind that action `id` to a keybinding, if they so choose. + +Case in point: +https://github.com/abduvik/just-enough-series/tree/master/courses/docker+docker-compose. +Something like that should be able to be easily added directly to the Terminal. + +### Snippets pane + +With non-terminal content landing in 1.21 Preview, it's now simple to add +additional types of panes to add to the Terminal. We'll support a new pane +`"type": "snippets"`, to support opening the Snippets pane. + +This will be a pane with a `TreeView` in it and a text box to filter results +(ala the Command Palette). + +Each item in the TreeView will be a kind of `FilteredCommand`, with a play +button to support quickly running the command. + +This pane could also support all the different suggestion sources that the +Suggestions UI supports - `recentCommands` could be plumbed into it from the +currently active This pane could also support checkboxes to filter different +suggestion sources. + +### Per-Project Snippets (`.wt.json`) + +Users may also want to leave snippets in the root of their repo, for others to +use as well. To support this, the Terminal will automatically look for a +`.wt.json` file in any directories that are parents of the CWD of the shell, and +load actions from that file as well. The syntax for this file will be a modified +version of the standard settings schema. As an example: + +```json +{ + "$version": "1.0.0", + "snippets": + [ + { + "input": "bx", + "name": "Build project", + "description": "Build the project in the CWD" + }, + { + "input": "bcz", + "name": "Clean & build solution", + "icon": "\uE8e6", + "description": "Start over. Go get your coffee. " + }, + { + "input": "nuget push -ApiKey az -source TerminalDependencies %userprofile%\\Downloads" , + "name": "Upload package to nuget feed", + "icon": "\uE898", + "description": "Go download a .nupkg, put it in ~/Downloads, and use this to push to our private feed." + }, + ] +} +``` + +Instead of `actions`, the top-level list is `snippets`. These snippet objects +are a simplification of the `Command` object. They have a `name`, `description`, +and `icon` properties, just like a `Command`. However, instead of an arbitrary +`action`, we will just have the `SendInput` action's args as properties directly +in the object. + +Additionally, we'll also support a `$version` field, in case we ever want to +make breaking changes to the schema. When this is missing, we'll just assume the +version to be `1.0.0`, which is this originally proposed schema. + +By default, a `TermControl` is always initialized with the CWD set to the +`startingDirectory` of a profile. So, even for users that don't have shell +integration enabled, the Terminal will still be able to load snippets from the +`.wt.json` in the profile's `startingDirectory`. If the user has shell +integration configured to tell the Terminal about the CWD, then we'll refresh +that list as the user changes directories. + +* In `Terminal.Settings.Model`, we will store a cached map of path->actions. + * that if multiple panes are all in the same CWD, they don't need to + individually re-read the file from disk and regenerate that part of the map. +* I believe it should be impossible for a keybinding to be bound to a local + action. Even if it has an ID, the file won't be loaded when we build the + keymap, and we don't really want the keymap changing based on CWD. Also, with + the actions living in an entirely separate map of CWD->actions, the + keybindings in the main map won't be able to easily get to them. See also + [Security considerations](#security) for more. +* If the Snippets pane or Suggestions UI is opened with `snippets` as a source, + then we'll just append the appropriate list of suggestions for the active + control's CWD. + * We don't need to have the control raise an event when the CWD changes - we + can lazy-load these actions when a UI element that requires it is first + invoked. +* These _local_ snippets will not be included in the Command Palette. + (`sendInput` actions in the settings file and in fragments still will be, + however) + * The Command Palette is quite tightly bound to our own ActionMap settings + model, accumulated through complicated layering of defaults, fragments, and + user settings. It's not trivially mutable at runtime in the way that + context-sensitive snippets would require. + * The Suggestions UI and Snippets pane are both surfaces that are better + equipped to handle context-relevant actions, especially where the context is + a `TermControl`. + * The Suggestions UI and snippets pane will give us more flexibility in + customizing the experience specifically for snippets. Case in point - we'll + want to filter the suggestions UI based on both the `Name` _and_ the `Input` + of the send input command. Contrast that with the Command Palette which is + currently only capable of filtering based on names. +* If we find multiple `.wt.json` files in the ancestors of the CWD (e.g. for + `c:\a\b\c\d\`, there's a `c:\a\.wt.json` and a `c:\a\b\c\.wt.json`), then + we'll add each one separately to the map of paths->directories. When + requesting the actual actions for `c:\a\b\c\d\`, we'll layer the ones from + `c:\a\` before the ones from `c:\a\b\c`, so that deeper descendants take + precedence. + * For example, `c:\a\.wt.json` has a snippet with `input: "foo", name: + "Build"`, and `c:\a\b\c\.wt.json` has a snippet with `input: "bar", name: + "Build"`. When the user is under `c:\a\b\c`, the Terminal will show `bar` + when the user selects the `Build` suggestion. Otherwise, if the user is + under `c:\a`, then the Terminal will show `foo`. +* If we fail to parse the `.wt.json` file, then we'll ignore it. For parsing + errors, we'll want to display warnings to the user: + * If the user had opened the suggestions UI, we can display a Toast the first + time we fail to load it to show the error. + * In the snippets pane, we can have static text along the lines of "failed to + parse snippets found in `path/to/file`" at the top of the pane. + +### Saving snippets from the commandline + +_This has already been prototyped in [#16513]_ + +Users should be able to save commands as snippets directly from the commandline. +Consider: you've just run the command that worked the way you need it to. You +shouldn't have to open the settings to then separately copy-paste the command in +to save it. It should be as easy as Up, Home, `wt x-save `, +Enter. + +This will be powered by a `saveSnippet` action behind the scenes. However, we +won't actually parse these actions from a user's settings file. They don't +really make sense to have the action to save a snippet to the settings file, in +the settings file already. + +The exact syntax of `x-save` is as follows: + +#### `x-save` subcommand + +`x-save [--name,-n name][--description,-d description][-- commandline]` + +Saves a given commandline as a sendInput action to the Terminal settings file. +This will immediately write the Terminal settings file. + +**Parameters**: +* `--name,-n name`: The name to assign to the `name` parameter of the saved + command. If omitted, then the parameter will be left blank, and the command + will use the auto-generated "Send input:..." name in menus. +* `--description,-d`: The description to optionally assign to the command. +* `commandline`: The commandline to save as the `input` of the `sendInput` action. + +If the `save` subcommand is ran without any other subcommands, the Terminal will +imply the `-w 0` arguments, to attempt to send this action to the current +Terminal window (unless of course, `-w` was manually provided on the +commandline). This is done to avoid a new terminal window popping up, just to +inform the user a command was saved. When run with other subcommands, then the +action will just be ran in the same window as all the other subcommands. + +> [!NOTE] +> In team discussions, we've decided to accept this for now as +> experimental. We have some concerns about how effective we'll be at dealing +> with all the weird edge cases of string escaping. For now, we'll keep this +> subcommand as `x-save`, where `x-` implies "experimental". +> +> Perhaps once we add a dialog for saving these snippets, we can promote it out of experimental. + +### UI/UX Design + +For the most part, we'll be using the [Suggestions UI] to display tasks to the +user. This is a text cursor-relative UI surface that can quickly display actions +to the user, in the context of what they're working on. + +The following are some examples from VsCode and Warp. These are meant to be +illustrative of what these menus already look like in the wild: + +![VS Code demo of tasks](img/vscode-tasks-000.gif) + +![Warp demo of workflows](img/warp-workflows-000.gif) + +A prototype of saving a command directly to the user's settings, then invoking +it via the suggestions UI + +![Example of using `wt save foo bar --baz` to save the command, then invoke the saved command via the suggestions UI](img/save-command.gif) + +A prototype of reading tasks from the CWD + +![navigate into a folder with a .wt.json file, view that it's populated, then invoke a snippet from that file using the suggestions UI](img/tasks-from-cwd.gif) + +The initial version of the snippets pane: + +![snippets-pane](https://github.com/microsoft/terminal/assets/18356694/f4aee6f8-dbaa-4a70-a2ac-98244eb0e4f1) + +## Tenets + + + + + + + + + + + + + +
Compatibility + +I considered supporting YAML for local snippets (`.wt.json`), instead of JSON. +JSON is not super friendly to command-lines - since everything's gotta be +encapsulated as a string. Embedding tabs `\t`, newlines `\r`, escape characters, +is fairly straightforward. However, quotes can get complicated fast in JSON, +since they've got to be escaped too, and with many CLI utilities also having +separate quote-parsing rules, JSON can get unwieldy quickly. + +However, supporting YAML directly would require us to spec out a YAML syntax for +these files, and also find an OSS YAML parser and implement support for it. That +would be quite a bit more expensive than JSON. + +
Accessibility + +Nothing particular to call out here. The Snippets pane will need to be a11y +tested, like most of our other UI surfaces. + +
Sustainability + +No substantial climate impacts expected here. We're not using expensive compute +resources for this feature, so the impact should be comparable to any other +Terminal feature. + +
Localization + +I'm mildly worried here about the potential for community-driven tasks to have +non-localized descriptions. We may need to accept a `description:{ en-us:"", +pt-br:"", ...}`-style map of language->string descriptions. That may just need +to be a future consideration for now. + +
Security + +Another reason we shouldn't support keys being able to be lazy-bound to local +snippets: It's entirely too easy for `malicious.exe` to create a file in +`%HomePath%` that creates a snippet for `\u003pwn-your-machine.exe\r` (or +similar). Any app can read your settings file, and it is again too easy for that +malicious app to set it's own action `id` to the same as some other well-meaning +local snippet's ID which you DO have bound to a key. + +When we first load the snippets from the `.wt.json` file, we'll want to also ask +them if they trust that folder. This is similar to the way that VsCode et. al. +If they accept, then we'll add that folder to a list of trusted folders (and +store permanently in `state.json`). If they don't, then we'll just ignore that +file. To make things easier for the user, we can also add a checkbox to "trust +the parent folder" in the dialog (again, similar to VsCode). + +We'll also want to engage our security partners to see if there's anything extra +we'll need to do to ensure we're securely parsing random JSON files that we +don't own. + +
+ +### Other potential issues + +Something like `wt save ping 8.8.8.8 > foo.txt` isn't going to work the way +users want. The shell is gonna get the first crack at parsing that commandline, +and is going to try and redirect the _output of `wt`_ to `foo.txt`. + +Predictably, local snippets won't work over ssh or other remote connections. +Terminal is only able to read files off the local filesystem. We'll at best be +able to read snippets from the directory they `ssh`'d _from_ locally. + +## Implementation Plan + +### ๐Ÿฃ Crawl +* [ ] The Command Palette and Suggestions UI need to be able to display both the + command name and a tooltip for the comment + - This will need to be reconciled with [#7039], which tracks displaying + non-localized names in the command palette + - It won't be a TeachingTip, since those are an unmitigated disaster. But we + can just fake it with another text box. + - A prototype can be found in [#17376] +* [X] [#1595] Add the Suggestions UI, with support for `tasks` +* [x] Fragments can add **actions** to a user's settings + +### ๐Ÿšถ Walk +* [ ] The terminal can look for a settings file of tasks in a profile's + `startingDirectory` (regardless of shell integration being enabled) +* [ ] [#5790] - profile specific actions +* [ ] [#12857] Ability to save selected text as a `sendInput` action +* [x] [#12861] Re-evaluate showing some sort of "ghost text" or other preview for snippets + +### ๐Ÿƒโ€โ™‚๏ธ Run +* [ ] When the user `cd`s to a directory (with shell integration enabled), the + terminal can load the tasks from that directory tree +* [ ] [#10436] Users can manage all their fragments extensions directly in the Settings UI +* [ ] The suggestions UI & snippets pane can filter not only on name of a + command, but for snippets, the input as well. +* [ ] [#12927] Enlighten the suggestions UI to support (_a yet undeclared syntax + for_) snippets with prompt-able sections in them + + + +## Conclusion + +Snippets are something that developers immediately understand the value of. +After talking with users, everyone we talked with immediately understood the +concept, and you could see the gears turning on ways to integrate this into +their own workflows. + +### Future Considerations + +* We may want to add additional params to the `save` subcommand in the future, + to configure where the snippet is saved: + * `--local`: Save to the `.wt.json` in the CWD, if there is one (or create + one) + * `--parent`: Save to the `.wt.json` in the first ancestor of the CWD, if + there is one. Otherwise create one here. + * `--settings`: Manually save to the settings file? + * `--profile`: save to this profile???? Not sure if this is actually possible. + Maybe with the `WT_SESSION_ID` env var to figure out which profile is in use + for the pane with that ID + * This would probably require per-profile actions, which are woefully under specified + * `--local`/`--parent`/`--settings` was well received in team discussion - + maybe we should just do them now. +* Longer workflows might be better exposed as notebooks. We've already got a + mind to support [markdown in a notebook-like + experience](https://github.com/microsoft/terminal/issues/16495) in the + Terminal. For longer scripts that may need rich markup between commands, that + will likely be a better UX. +* For what it is worth, [Warp] uses .yaml files for their "workflows". As an + example, see + [`clone_all_repos_in_org.yaml`](https://github.com/warpdotdev/workflows/blob/main/specs/git/clone_all_repos_in_org.yaml). + We may want to straight up just seamlessly support that syntax as well. + * Converting them to WT-compatible json is fairly trivial [[2](#footnote-2)]. + * We may want to consider supporting YAML like this for `wt import`, ala [#10083] + * Similarly, we could import the YAML in the settings UI in a fashion similar + to how we import color schemes: + * Furthermore, the commands are all licensed under Apache 2.0, which means they + can be easily consumed by other OSS projects and shared with other developers. + * This leads us to the next future consideration: +* Discoverability will be important. Perhaps the actions page could have a + toggle to immediately filter to "snippets"? Which then also displays some text + like "Tip: save snippets directly from the commandline with + `wt save `". +* We should easily be able to put "Save command as snippet" into the quick fix + menu next to an individual prompt, when shell integration is enabled. +* We should most definitely add a dialog for saving snippets directly in the Terminal. + * We'd have inputs for the commandline, name, description. + * Obviously, it'd be easy to have a "Add new" button (to open that dialog) on + the snippets pane. + * We could have `wt save` open that dialog pre-populated, rather than just + saving the command directly. + * We could even also find a way to pre-populate that dialog with the recent + commands (from shell integration)! +* As a potential v2.0 of the snippets file schema, we may look to the + `.vscode/tasks.json` schema for inspiration. That file supports much more + complex task definitions. Notably, with the ability to prompt the user for + different inputs, for different parameter values. This is something that would + play well off of [#12927] +* We may want to consider a future property of snippets like `shell`, which + specifies which shells a snippet can be used with. We could then only filter + to the snippets that will work with the current shell. This is left for the + future because we don't have a reliable way of knowing what shell application + the user is currently running. +* We may want to consider promoting `sendInput` actions to a top-level + `snippets` array in `settings.json` in the future. That might make sharing + them from one user's settings, to a `.wt.json`, and back, a little easier. + +#### Community Snippets + +_The big stretch version of this feature._ + +It would be supremely cool to have a community curated list of Snippets, for +various tools. Stored publicly on a GitHub repo (a la the winget-pkgs repo). +Users can submit Snippets with descriptions of what the Snippet does. The +Terminal can plug into that repo automatically and fetch the latest community +commands, immediately giving the user access to a wide berth of common +Snippets. That could easily be done as another suggestion source. + +#### Profiles in `.wt.json` + +If we've got a `.wt.json` in a given directory, should we be dynamically +adding/removing other settings too? Wouldn't profiles also make sense? Take for +example, the Terminal repo. We've got a PowerShell build environment and a CMD +one. What if we could drop two profiles in the `.wt.json` file, with the +`commandline`'s set up to call those scripts as needed? + +However, what does that even mean? We wouldn't know that file exists till we see +it the first time. Maybe there's room to integrate that with Dev Home ala +[microsoft/DevHome/3005]. Though, that probably makes the most sense as a winget +DSC to create a fragment profile instead. + +## Resources + +### Footnotes + +[1]: Shell integration would be a strict requirement +for that parameter to work as intended. Without also enabling shell integration, +then the Terminal would only send the first line of the script, then wait +forever for a `FTCS_COMMAND_FINISHED`. + +[2]: For your consideration, I made a python script +that will take the Warp workflow YAML and convert it into json that the Terminal +can load. Go checkout [`dump_workflows.py`](./dump-workflows.py) to see it. It's +super straightforward. + + +[Fig]: https://github.com/withfig/autocomplete +[Warp]: https://www.warp.dev/ +[workflows]: https://docs.warp.dev/features/workflows +[also working on workflows]: https://fig.io/user-manual/workflows +[winget script]: https://github.com/microsoft/PowerToys/blob/main/.github/workflows/package-submissions.yml +[#1595]: https://github.com/microsoft/terminal/issues/1595 +[#7039]: https://github.com/microsoft/terminal/issues/7039 +[#3121]: https://github.com/microsoft/terminal/issues/3121 +[#10436]: https://github.com/microsoft/terminal/issues/10436 +[#12927]: https://github.com/microsoft/terminal/issues/12927 +[#12857]: https://github.com/microsoft/terminal/issues/12857 +[#5790]: https://github.com/microsoft/terminal/issues/5790 +[Notebooks]: ./Markdown%20Notebooks.md +[Suggestions UI]: ./Suggestions-UI.md +[#keep]: https://github.com/zadjii/keep +[VsCode Tasks]: https://github.com/microsoft/terminal/blob/main/.vscode/tasks.json + +[#16185]: https://github.com/microsoft/terminal/pull/16185 +[#16513]: https://github.com/microsoft/terminal/pull/16513 +[#12861]: https://github.com/microsoft/terminal/issues/12861 +[#16495]: https://github.com/microsoft/terminal/issues/16495 +[#17376]: https://github.com/microsoft/terminal/pull/17376 +[#10083]: https://github.com/microsoft/terminal/issues/10083 +[#8639]: https://github.com/microsoft/terminal/issues/8639 + +[microsoft/DevHome/3005]: https://github.com/microsoft/DevHome/issues/3005 diff --git a/doc/specs/#1595 - Suggestions UI/dump-workflows.py b/doc/specs/#1595 - Suggestions UI/dump-workflows.py new file mode 100644 index 00000000000..789a2aaa416 --- /dev/null +++ b/doc/specs/#1595 - Suggestions UI/dump-workflows.py @@ -0,0 +1,45 @@ +import yaml +import json +import sys +import os + +def parse_yaml_files(tool, directory): + json_data = {} + json_data["name"] = f"{tool}..." + json_data["commands"] = [] + + for filename in os.listdir(directory): + if filename.endswith(".yaml") or filename.endswith(".yml"): + file_path = os.path.join(directory, filename) + with open(file_path, 'r', encoding="utf-8") as file: + try: + yaml_data = yaml.safe_load(file) + new_obj = {} + command = {} + command["input"] = yaml_data["command"] + command["action"] ="sendInput" + + new_obj["command"]=command + new_obj["name"] = yaml_data["name"] + + new_obj["description"] = yaml_data["description"] if "description" in yaml_data else "" + json_data["commands"].append(new_obj) + except yaml.YAMLError as e: + print(f"Error parsing {filename}: {e}") + sys.exit(-1) + return json_data + +def main(directory) -> int: + json_data = {} + json_data["actions"] = [] + + for tool_dir in os.listdir(directory): + # print(tool_dir) + json_data["actions"].append(parse_yaml_files(tool_dir, os.path.join(directory, tool_dir))) + print(json.dumps(json_data, indent=4)) + return 0 + +if __name__ == '__main__': + # Write this output to something like + # "%localappdata%\Microsoft\Windows Terminal\Fragments\warp-workflows\actions.json" + sys.exit(main("d:\\dev\\public\\workflows\\specs")) \ No newline at end of file diff --git a/doc/specs/#1595 - Suggestions UI/img/3121-sxn-menu-2023-000.gif b/doc/specs/#1595 - Suggestions UI/img/3121-sxn-menu-2023-000.gif new file mode 100644 index 00000000000..da5f0c4f663 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/3121-sxn-menu-2023-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/Copilot-in-cmdpal.png b/doc/specs/#1595 - Suggestions UI/img/Copilot-in-cmdpal.png new file mode 100644 index 00000000000..2fdef06059f Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/Copilot-in-cmdpal.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/GitHub-open-with.png b/doc/specs/#1595 - Suggestions UI/img/GitHub-open-with.png new file mode 100644 index 00000000000..cc6d484c8c4 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/GitHub-open-with.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/command-history-suggestions.gif b/doc/specs/#1595 - Suggestions UI/img/command-history-suggestions.gif new file mode 100644 index 00000000000..94e7be1a54e Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/command-history-suggestions.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/developers-already-do-this-000.png b/doc/specs/#1595 - Suggestions UI/img/developers-already-do-this-000.png new file mode 100644 index 00000000000..728d2ee9661 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/developers-already-do-this-000.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/inline-blocks-000.png b/doc/specs/#1595 - Suggestions UI/img/inline-blocks-000.png new file mode 100644 index 00000000000..2120143995b Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/inline-blocks-000.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/iterm2-CommandHistory.png b/doc/specs/#1595 - Suggestions UI/img/iterm2-CommandHistory.png new file mode 100644 index 00000000000..a5308b963b7 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/iterm2-CommandHistory.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/jupyter-notebooks-example.png b/doc/specs/#1595 - Suggestions UI/img/jupyter-notebooks-example.png new file mode 100644 index 00000000000..9b762bd45ef Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/jupyter-notebooks-example.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/mockup-000.png b/doc/specs/#1595 - Suggestions UI/img/mockup-000.png new file mode 100644 index 00000000000..3f160371756 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/mockup-000.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/save-command.gif b/doc/specs/#1595 - Suggestions UI/img/save-command.gif new file mode 100644 index 00000000000..b92e377cc7d Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/save-command.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/shell-autocomplete-jul-2022-000.gif b/doc/specs/#1595 - Suggestions UI/img/shell-autocomplete-jul-2022-000.gif new file mode 100644 index 00000000000..0a832691b02 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/shell-autocomplete-jul-2022-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/shell-completion-menu-2023-02-21.gif b/doc/specs/#1595 - Suggestions UI/img/shell-completion-menu-2023-02-21.gif new file mode 100644 index 00000000000..a8c32922e07 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/shell-completion-menu-2023-02-21.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/shell-completion-tooltip-000.png b/doc/specs/#1595 - Suggestions UI/img/shell-completion-tooltip-000.png new file mode 100644 index 00000000000..790adb58cbd Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/shell-completion-tooltip-000.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/tasks-from-cwd.gif b/doc/specs/#1595 - Suggestions UI/img/tasks-from-cwd.gif new file mode 100644 index 00000000000..2f9f7495bd7 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/tasks-from-cwd.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/tasks-suggestions.gif b/doc/specs/#1595 - Suggestions UI/img/tasks-suggestions.gif new file mode 100644 index 00000000000..3629dd5a7b7 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/tasks-suggestions.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/vscode-shell-autocomplete-000.gif b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-autocomplete-000.gif new file mode 100644 index 00000000000..93f76dc0d2e Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-autocomplete-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/vscode-shell-integration-gutter-mark.png b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-integration-gutter-mark.png new file mode 100644 index 00000000000..d73a5c7b14a Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-integration-gutter-mark.png differ diff --git a/doc/specs/#1595 - Suggestions UI/img/vscode-shell-suggestions.gif b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-suggestions.gif new file mode 100644 index 00000000000..93f76dc0d2e Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/vscode-shell-suggestions.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/vscode-tasks-000.gif b/doc/specs/#1595 - Suggestions UI/img/vscode-tasks-000.gif new file mode 100644 index 00000000000..b7ad4fe76c8 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/vscode-tasks-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/warp-workflows-000.gif b/doc/specs/#1595 - Suggestions UI/img/warp-workflows-000.gif new file mode 100644 index 00000000000..44763f08f65 Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/warp-workflows-000.gif differ diff --git a/doc/specs/#1595 - Suggestions UI/img/warp-workflows-001.gif b/doc/specs/#1595 - Suggestions UI/img/warp-workflows-001.gif new file mode 100644 index 00000000000..a770263df0d Binary files /dev/null and b/doc/specs/#1595 - Suggestions UI/img/warp-workflows-001.gif differ diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index 03570cf526d..3aa6e8b3128 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -48,7 +48,7 @@ TextBuffer::TextBuffer(til::size screenBufferSize, const TextAttribute defaultAttributes, const UINT cursorSize, const bool isActiveBuffer, - Microsoft::Console::Render::Renderer& renderer) : + Microsoft::Console::Render::Renderer* renderer) : _renderer{ renderer }, _currentAttributes{ defaultAttributes }, // This way every TextBuffer will start with a ""unique"" _lastMutationId @@ -373,38 +373,6 @@ TextBufferCellIterator TextBuffer::GetCellDataAt(const til::point at, const View return TextBufferCellIterator(*this, at, limit); } -//Routine Description: -// - Call before inserting a character into the buffer. -// - This will ensure a consistent double byte state (KAttrs line) within the text buffer -// - It will attempt to correct the buffer if we're inserting an unexpected double byte character type -// and it will pad out the buffer if we're going to split a double byte sequence across two rows. -//Arguments: -// - dbcsAttribute - Double byte information associated with the character about to be inserted into the buffer -//Return Value: -// - true if we successfully prepared the buffer and moved the cursor -// - false otherwise (out of memory) -void TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute) -{ - // Now compensate if we don't have enough space for the upcoming double byte sequence - // We only need to compensate for leading bytes - if (dbcsAttribute == DbcsAttribute::Leading) - { - const auto cursorPosition = GetCursor().GetPosition(); - const auto lineWidth = GetLineWidth(cursorPosition.y); - - // If we're about to lead on the last column in the row, we need to add a padding space - if (cursorPosition.x == lineWidth - 1) - { - // set that we're wrapping for double byte reasons - auto& row = GetMutableRowByOffset(cursorPosition.y); - row.SetDoubleBytePadded(true); - - // then move the cursor forward and onto the next row - IncrementCursor(); - } - } -} - // Given the character offset `position` in the `chars` string, this function returns the starting position of the next grapheme. // For instance, given a `chars` of L"x\uD83D\uDE42y" and a `position` of 1 it'll return 3. // GraphemePrev would do the exact inverse of this operation. @@ -740,144 +708,6 @@ OutputCellIterator TextBuffer::WriteLine(const OutputCellIterator givenIt, return newIt; } -//Routine Description: -// - Inserts one codepoint into the buffer at the current cursor position and advances the cursor as appropriate. -//Arguments: -// - chars - The codepoint to insert -// - dbcsAttribute - Double byte information associated with the codepoint -// - bAttr - Color data associated with the character -//Return Value: -// - true if we successfully inserted the character -// - false otherwise (out of memory) -void TextBuffer::InsertCharacter(const std::wstring_view chars, - const DbcsAttribute dbcsAttribute, - const TextAttribute attr) -{ - // Ensure consistent buffer state for double byte characters based on the character type we're about to insert - _PrepareForDoubleByteSequence(dbcsAttribute); - - // Get the current cursor position - const auto iRow = GetCursor().GetPosition().y; // row stored as logical position, not array position - const auto iCol = GetCursor().GetPosition().x; // column logical and array positions are equal. - - // Get the row associated with the given logical position - auto& Row = GetMutableRowByOffset(iRow); - - // Store character and double byte data - switch (dbcsAttribute) - { - case DbcsAttribute::Leading: - Row.ReplaceCharacters(iCol, 2, chars); - break; - case DbcsAttribute::Trailing: - Row.ReplaceCharacters(iCol - 1, 2, chars); - break; - default: - Row.ReplaceCharacters(iCol, 1, chars); - break; - } - - // Store color data - Row.SetAttrToEnd(iCol, attr); - IncrementCursor(); -} - -//Routine Description: -// - Inserts one ucs2 codepoint into the buffer at the current cursor position and advances the cursor as appropriate. -//Arguments: -// - wch - The codepoint to insert -// - dbcsAttribute - Double byte information associated with the codepoint -// - bAttr - Color data associated with the character -//Return Value: -// - true if we successfully inserted the character -// - false otherwise (out of memory) -void TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr) -{ - InsertCharacter({ &wch, 1 }, dbcsAttribute, attr); -} - -//Routine Description: -// - Finds the current row in the buffer (as indicated by the cursor position) -// and specifies that we have forced a line wrap on that row -//Arguments: -// - - Always sets to wrap -//Return Value: -// - -void TextBuffer::_SetWrapOnCurrentRow() -{ - _AdjustWrapOnCurrentRow(true); -} - -//Routine Description: -// - Finds the current row in the buffer (as indicated by the cursor position) -// and specifies whether or not it should have a line wrap flag. -//Arguments: -// - fSet - True if this row has a wrap. False otherwise. -//Return Value: -// - -void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) -{ - // The vertical position of the cursor represents the current row we're manipulating. - const auto uiCurrentRowOffset = GetCursor().GetPosition().y; - - // Set the wrap status as appropriate - GetMutableRowByOffset(uiCurrentRowOffset).SetWrapForced(fSet); -} - -//Routine Description: -// - Increments the cursor one position in the buffer as if text is being typed into the buffer. -// - NOTE: Will introduce a wrap marker if we run off the end of the current row -//Arguments: -// - -//Return Value: -// - true if we successfully moved the cursor. -// - false otherwise (out of memory) -void TextBuffer::IncrementCursor() -{ - // Cursor position is stored as logical array indices (starts at 0) for the window - // Buffer Size is specified as the "length" of the array. It would say 80 for valid values of 0-79. - // So subtract 1 from buffer size in each direction to find the index of the final column in the buffer - const auto iFinalColumnIndex = GetLineWidth(GetCursor().GetPosition().y) - 1; - - // Move the cursor one position to the right - GetCursor().IncrementXPosition(1); - - // If we've passed the final valid column... - if (GetCursor().GetPosition().x > iFinalColumnIndex) - { - // Then mark that we've been forced to wrap - _SetWrapOnCurrentRow(); - - // Then move the cursor to a new line - NewlineCursor(); - } -} - -//Routine Description: -// - Increments the cursor one line down in the buffer and to the beginning of the line -//Arguments: -// - -//Return Value: -// - true if we successfully moved the cursor. -void TextBuffer::NewlineCursor() -{ - const auto iFinalRowIndex = GetSize().BottomInclusive(); - - // Reset the cursor position to 0 and move down one line - GetCursor().SetXPosition(0); - GetCursor().IncrementYPosition(1); - - // If we've passed the final valid row... - if (GetCursor().GetPosition().y > iFinalRowIndex) - { - // Stay on the final logical/offset row of the buffer. - GetCursor().SetYPosition(iFinalRowIndex); - - // Instead increment the circular buffer to move us into the "oldest" row of the backing buffer - IncrementCircularBuffer(); - } -} - //Routine Description: // - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable. //Arguments: @@ -888,9 +718,9 @@ void TextBuffer::IncrementCircularBuffer(const TextAttribute& fillAttributes) { // FirstRow is at any given point in time the array index in the circular buffer that corresponds // to the logical position 0 in the window (cursor coordinates and all other coordinates). - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerFlush(true); + _renderer->TriggerFlush(true); } // Prune hyperlinks to delete obsolete references @@ -954,38 +784,6 @@ til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) co return coordEndOfText; } -// Routine Description: -// - Retrieves the position of the previous character relative to the current cursor position -// Arguments: -// - -// Return Value: -// - Coordinate position in screen coordinates of the character just before the cursor. -// - NOTE: Will return 0,0 if already in the top left corner -til::point TextBuffer::_GetPreviousFromCursor() const -{ - auto coordPosition = GetCursor().GetPosition(); - - // If we're not at the left edge, simply move the cursor to the left by one - if (coordPosition.x > 0) - { - coordPosition.x--; - } - else - { - // Otherwise, only if we're not on the top row (e.g. we don't move anywhere in the top left corner. there is no previous) - if (coordPosition.y > 0) - { - // move the cursor up one line - coordPosition.y--; - - // and to the right edge - coordPosition.x = GetLineWidth(coordPosition.y) - 1; - } - } - - return coordPosition; -} - const til::CoordType TextBuffer::GetFirstRowIndex() const noexcept { return _firstRow; @@ -1265,56 +1063,56 @@ bool TextBuffer::IsActiveBuffer() const noexcept return _isActiveBuffer; } -Microsoft::Console::Render::Renderer& TextBuffer::GetRenderer() noexcept +Microsoft::Console::Render::Renderer* TextBuffer::GetRenderer() noexcept { return _renderer; } void TextBuffer::NotifyPaintFrame() noexcept { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.NotifyPaintFrame(); + _renderer->NotifyPaintFrame(); } } void TextBuffer::TriggerRedraw(const Viewport& viewport) { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerRedraw(viewport); + _renderer->TriggerRedraw(viewport); } } void TextBuffer::TriggerRedrawAll() { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerRedrawAll(); + _renderer->TriggerRedrawAll(); } } void TextBuffer::TriggerScroll() { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerScroll(); + _renderer->TriggerScroll(); } } void TextBuffer::TriggerScroll(const til::point delta) { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerScroll(&delta); + _renderer->TriggerScroll(&delta); } } void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText) { - if (_isActiveBuffer) + if (_isActiveBuffer && _renderer) { - _renderer.TriggerNewTextNotification(newText); + _renderer->TriggerNewTextNotification(newText); } } diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 6f21ad41cb4..7d3a0074297 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -72,7 +72,7 @@ class TextBuffer final const TextAttribute defaultAttributes, const UINT cursorSize, const bool isActiveBuffer, - Microsoft::Console::Render::Renderer& renderer); + Microsoft::Console::Render::Renderer* renderer); TextBuffer(const TextBuffer&) = delete; TextBuffer(TextBuffer&&) = delete; @@ -121,11 +121,6 @@ class TextBuffer final const std::optional setWrap = std::nullopt, const std::optional limitRight = std::nullopt); - void InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttribute, const TextAttribute attr); - void InsertCharacter(const std::wstring_view chars, const DbcsAttribute dbcsAttribute, const TextAttribute attr); - void IncrementCursor(); - void NewlineCursor(); - // Scroll needs access to this to quickly rotate around the buffer. void IncrementCircularBuffer(const TextAttribute& fillAttributes = {}); @@ -166,7 +161,7 @@ class TextBuffer final void SetAsActiveBuffer(const bool isActiveBuffer) noexcept; bool IsActiveBuffer() const noexcept; - Microsoft::Console::Render::Renderer& GetRenderer() noexcept; + Microsoft::Console::Render::Renderer* GetRenderer() noexcept; void NotifyPaintFrame() noexcept; void TriggerRedraw(const Microsoft::Console::Types::Viewport& viewport); @@ -322,11 +317,6 @@ class TextBuffer final til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept; void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept; - til::point _GetPreviousFromCursor() const; - void _SetWrapOnCurrentRow(); - void _AdjustWrapOnCurrentRow(const bool fSet); - // Assist with maintaining proper buffer state for Double Byte character sequences - void _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute); void _ExpandTextRow(til::inclusive_rect& selectionRow) const; DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const; til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const; @@ -343,7 +333,7 @@ class TextBuffer final static void _AppendRTFText(std::string& contentBuilder, const std::wstring_view& text); - Microsoft::Console::Render::Renderer& _renderer; + Microsoft::Console::Render::Renderer* _renderer = nullptr; std::unordered_map _hyperlinkMap; std::unordered_map _hyperlinkCustomIdMap; diff --git a/src/buffer/out/ut_textbuffer/ReflowTests.cpp b/src/buffer/out/ut_textbuffer/ReflowTests.cpp index d1fad4c407f..191d7329b99 100644 --- a/src/buffer/out/ut_textbuffer/ReflowTests.cpp +++ b/src/buffer/out/ut_textbuffer/ReflowTests.cpp @@ -699,7 +699,7 @@ class ReflowTests static DummyRenderer renderer; static std::unique_ptr _textBufferFromTestBuffer(const TestBuffer& testBuffer) { - auto buffer = std::make_unique(testBuffer.size, TextAttribute{ 0x7 }, 0, false, renderer); + auto buffer = std::make_unique(testBuffer.size, TextAttribute{ 0x7 }, 0, false, &renderer); til::CoordType y = 0; for (const auto& testRow : testBuffer.rows) @@ -725,7 +725,7 @@ class ReflowTests static std::unique_ptr _textBufferByReflowingTextBuffer(TextBuffer& originalBuffer, const til::size newSize) { - auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, renderer); + auto buffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, &renderer); TextBuffer::Reflow(originalBuffer, *buffer); return buffer; } diff --git a/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp b/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp index 24d04a56ff8..be9e941c757 100644 --- a/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp +++ b/src/buffer/out/ut_textbuffer/UTextAdapterTests.cpp @@ -37,7 +37,7 @@ class UTextAdapterTests TEST_METHOD(Unicode) { DummyRenderer renderer; - TextBuffer buffer{ til::size{ 24, 1 }, TextAttribute{}, 0, false, renderer }; + TextBuffer buffer{ til::size{ 24, 1 }, TextAttribute{}, 0, false, &renderer }; RowWriteState state{ .text = L"abc ๐’ถ๐’ท๐’ธ abc ใƒใ‚ณใกใ‚ƒใ‚“", diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 92d0c153c8e..eb153ce1ed8 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -1711,7 +1711,9 @@ void Pane::_SetupChildCloseHandlers() IPaneContent Pane::_takePaneContent() { _closeRequestedRevoker.revoke(); - return std::move(_content); + // we cannot return std::move(_content) because we don't want _content to be null, + // since _content gets accessed even after Close is called + return _content; } // This method safely sets the content of the Pane. It'll ensure to revoke and @@ -1721,15 +1723,14 @@ void Pane::_setPaneContent(IPaneContent content) { // The IPaneContent::Close() implementation may be buggy and raise the CloseRequested event again. // _takePaneContent() avoids this as it revokes the event handler. - if (const auto c = _takePaneContent()) + if (_takePaneContent()) { - c.Close(); + _content.Close(); } - _content = std::move(content); - - if (_content) + if (content) { + _content = std::move(content); _closeRequestedRevoker = _content.CloseRequested(winrt::auto_revoke, [this](auto&&, auto&&) { Close(); }); } } diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index ceec02ed52a..146fb99b681 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1894,7 +1894,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto lock = _terminal->LockForWriting(); auto& renderSettings = _terminal->GetRenderSettings(); - renderSettings.ToggleBlinkRendition(*_renderer); + renderSettings.ToggleBlinkRendition(_renderer.get()); } void ControlCore::BlinkCursor() diff --git a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw index 54c6a4643b5..d618171e58b 100644 --- a/src/cascadia/TerminalControl/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/en-US/Resources.resw @@ -207,7 +207,8 @@ Please either install the missing font or choose another one. {0} is a file name - Unable to compile the specified pixel shader. + Pixel shader failed to compile: {0} + {0} is the error message generated by the compiler Renderer encountered an unexpected error: {0} @@ -223,7 +224,7 @@ Please either install the missing font or choose another one. Renderer encountered an unexpected error: {0:#010x} {1} - {Locked="{0:#010x}","{1}"} {0:#010x} is a placeholder for a Windows error code (e.g. 0x88985002). {1} is the corresponding message. + {Locked="{0:#010x}","{1}"} {0:#010x} is a placeholder for a Windows error code (e.g. 0x88985002). {1} is the corresponding message. {2} is the filename. Read-only mode is enabled. @@ -320,4 +321,4 @@ Please either install the missing font or choose another one. Suggested input: {0} {Locked="{0}"} {0} will be replaced with a string of input that is suggested for the user to input - + \ No newline at end of file diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 1fffe66dc50..bc045f36381 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1162,7 +1162,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderNotFound") }, parameter) }; break; case D2DERR_SHADER_COMPILE_FAILED: - message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }) }; + message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"PixelShaderCompileFailed") }, parameter) }; break; case DWRITE_E_NOFONT: message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorFontNotFound") }, parameter) }; @@ -1175,7 +1175,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation wchar_t buf[512]; const auto len = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, hr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &buf[0], ARRAYSIZE(buf), nullptr); const std::wstring_view msg{ &buf[0], len }; - message = winrt::hstring{ fmt::format(std::wstring_view{ RS_(L"RendererErrorOther") }, hr, msg) }; + std::wstring resourceString = RS_(L"RendererErrorOther").c_str(); + //conditional message construction + std::wstring partialMessage = fmt::format(std::wstring_view{ resourceString }, hr, msg); + if (!parameter.empty()) + { + fmt::format_to(std::back_inserter(partialMessage), LR"( "{0}")", parameter); + } + message = winrt::hstring{ partialMessage }; break; } } diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 05f9a681d84..c1b0436797d 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -47,9 +47,9 @@ void Terminal::Create(til::size viewportSize, til::CoordType scrollbackLines, Re Utils::ClampToShortMax(viewportSize.height + scrollbackLines, 1) }; const TextAttribute attr{}; const UINT cursorSize = 12; - _mainBuffer = std::make_unique(bufferSize, attr, cursorSize, true, renderer); + _mainBuffer = std::make_unique(bufferSize, attr, cursorSize, true, &renderer); - auto dispatch = std::make_unique(*this, renderer, _renderSettings, _terminalInput); + auto dispatch = std::make_unique(*this, &renderer, _renderSettings, _terminalInput); auto engine = std::make_unique(std::move(dispatch)); _stateMachine = std::make_unique(std::move(engine)); diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index 2ac6770cab4..a18db5349a4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -931,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - When enabled, the terminal draws custom glyphs for block element and box drawing characters instead of using the font. This feature is unavailable when using Direct2D as the Graphics API. + When enabled, the terminal draws custom glyphs for block element and box drawing characters instead of using the font. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index daaedf87d7a..d77a916ab80 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -117,7 +117,7 @@ SCREEN_INFORMATION::~SCREEN_INFORMATION() defaultAttributes, uiCursorSize, pScreen->IsActiveScreenBuffer(), - *ServiceLocator::LocateGlobals().pRender); + ServiceLocator::LocateGlobals().pRender); const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); pScreen->_textBuffer->GetCursor().SetType(gci.GetCursorType()); @@ -253,10 +253,9 @@ void SCREEN_INFORMATION::s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pSc { auto& g = ServiceLocator::LocateGlobals(); auto& gci = g.getConsoleInformation(); - auto& renderer = *g.pRender; auto& renderSettings = gci.GetRenderSettings(); auto& terminalInput = gci.GetActiveInputBuffer()->GetTerminalInput(); - auto adapter = std::make_unique(_api, renderer, renderSettings, terminalInput); + auto adapter = std::make_unique(_api, g.pRender, renderSettings, terminalInput); auto engine = std::make_unique(std::move(adapter)); // Note that at this point in the setup, we haven't determined if we're // in VtIo mode or not yet. We'll set the OutputStateMachine's diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index f70f5c6b8ec..d39be844de6 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -94,18 +94,10 @@ class TextBufferTests TEST_METHOD(TestCopyProperties); - TEST_METHOD(TestInsertCharacter); - - TEST_METHOD(TestIncrementCursor); - - TEST_METHOD(TestNewlineCursor); - void TestLastNonSpace(const til::CoordType cursorPosY); TEST_METHOD(TestGetLastNonSpaceCharacter); - TEST_METHOD(TestSetWrapOnCurrentRow); - TEST_METHOD(TestIncrementCircularBuffer); TEST_METHOD(TestMixedRgbAndLegacyForeground); @@ -145,7 +137,6 @@ class TextBufferTests TEST_METHOD(ResizeTraditionalHighUnicodeRowRemoval); TEST_METHOD(ResizeTraditionalHighUnicodeColumnRemoval); - TEST_METHOD(TestBurrito); TEST_METHOD(TestOverwriteChars); TEST_METHOD(TestReplace); TEST_METHOD(TestInsert); @@ -400,129 +391,6 @@ void TextBufferTests::TestCopyProperties() VERIFY_IS_TRUE(testTextBuffer->GetCursor().GetDelay()); } -void TextBufferTests::TestInsertCharacter() -{ - auto& textBuffer = GetTbi(); - - // get starting cursor position - const auto coordCursorBefore = textBuffer.GetCursor().GetPosition(); - - // Get current row from the buffer - auto& Row = textBuffer.GetRowByOffset(coordCursorBefore.y); - - // create some sample test data - const auto wch = L'Z'; - const std::wstring_view wchTest(&wch, 1); - const auto dbcsAttribute = DbcsAttribute::Leading; - const auto wAttrTest = BACKGROUND_INTENSITY | FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE; - auto TestAttributes = TextAttribute(wAttrTest); - - // ensure that the buffer didn't start with these fields - VERIFY_ARE_NOT_EQUAL(Row.GlyphAt(coordCursorBefore.x), wchTest); - VERIFY_ARE_NOT_EQUAL(Row.DbcsAttrAt(coordCursorBefore.x), dbcsAttribute); - - auto attr = Row.GetAttrByColumn(coordCursorBefore.x); - - VERIFY_ARE_NOT_EQUAL(attr, TestAttributes); - - // now apply the new data to the buffer - textBuffer.InsertCharacter(wchTest, dbcsAttribute, TestAttributes); - - // ensure that the buffer position where the cursor WAS contains the test items - VERIFY_ARE_EQUAL(Row.GlyphAt(coordCursorBefore.x), wchTest); - VERIFY_ARE_EQUAL(Row.DbcsAttrAt(coordCursorBefore.x), dbcsAttribute); - - attr = Row.GetAttrByColumn(coordCursorBefore.x); - VERIFY_ARE_EQUAL(attr, TestAttributes); - - // ensure that the cursor moved to a new position (X or Y or both have changed) - VERIFY_IS_TRUE((coordCursorBefore.x != textBuffer.GetCursor().GetPosition().x) || - (coordCursorBefore.y != textBuffer.GetCursor().GetPosition().y)); - // the proper advancement of the cursor (e.g. which position it goes to) is validated in other tests -} - -void TextBufferTests::TestIncrementCursor() -{ - auto& textBuffer = GetTbi(); - - // only checking X increments here - // Y increments are covered in the NewlineCursor test - - const auto sBufferWidth = textBuffer.GetSize().Width(); - - const auto sBufferHeight = textBuffer.GetSize().Height(); - VERIFY_IS_TRUE(sBufferWidth > 1 && sBufferHeight > 1); - - Log::Comment(L"Test normal case of moving once to the right within a single line"); - textBuffer.GetCursor().SetXPosition(0); - textBuffer.GetCursor().SetYPosition(0); - - auto coordCursorBefore = textBuffer.GetCursor().GetPosition(); - - textBuffer.IncrementCursor(); - - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 1); // X should advance by 1 - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y); // Y shouldn't have moved - - Log::Comment(L"Test line wrap case where cursor is on the right edge of the line"); - textBuffer.GetCursor().SetXPosition(sBufferWidth - 1); - textBuffer.GetCursor().SetYPosition(0); - - coordCursorBefore = textBuffer.GetCursor().GetPosition(); - - textBuffer.IncrementCursor(); - - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // position should be reset to the left edge when passing right edge - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y - 1, coordCursorBefore.y); // the cursor should be moved one row down from where it used to be -} - -void TextBufferTests::TestNewlineCursor() -{ - auto& textBuffer = GetTbi(); - - const auto sBufferHeight = textBuffer.GetSize().Height(); - - const auto sBufferWidth = textBuffer.GetSize().Width(); - // width and height are sufficiently large for upcoming math - VERIFY_IS_TRUE(sBufferWidth > 4 && sBufferHeight > 4); - - Log::Comment(L"Verify standard row increment from somewhere in the buffer"); - - // set cursor X position to non zero, any position in buffer - textBuffer.GetCursor().SetXPosition(3); - - // set cursor Y position to not-the-final row in the buffer - textBuffer.GetCursor().SetYPosition(3); - - auto coordCursorBefore = textBuffer.GetCursor().GetPosition(); - - // perform operation - textBuffer.NewlineCursor(); - - // verify - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // move to left edge of buffer - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y + 1); // move down one row - - Log::Comment(L"Verify increment when already on last row of buffer"); - - // X position still doesn't matter - textBuffer.GetCursor().SetXPosition(3); - - // Y position needs to be on the last row of the buffer - textBuffer.GetCursor().SetYPosition(sBufferHeight - 1); - - coordCursorBefore = textBuffer.GetCursor().GetPosition(); - - // perform operation - textBuffer.NewlineCursor(); - - // verify - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().x, 0); // move to left edge - VERIFY_ARE_EQUAL(textBuffer.GetCursor().GetPosition().y, coordCursorBefore.y); // cursor Y position should not have moved. stays on same logical final line of buffer - - // This is okay because the backing circular buffer changes, not the logical screen position (final visible line of the buffer) -} - void TextBufferTests::TestLastNonSpace(const til::CoordType cursorPosY) { auto& textBuffer = GetTbi(); @@ -568,37 +436,6 @@ void TextBufferTests::TestGetLastNonSpaceCharacter() TestLastNonSpace(14); } -void TextBufferTests::TestSetWrapOnCurrentRow() -{ - auto& textBuffer = GetTbi(); - - auto sCurrentRow = textBuffer.GetCursor().GetPosition().y; - - auto& Row = textBuffer.GetMutableRowByOffset(sCurrentRow); - - Log::Comment(L"Testing off to on"); - - // turn wrap status off first - Row.SetWrapForced(false); - - // trigger wrap - textBuffer._SetWrapOnCurrentRow(); - - // ensure this row was flipped - VERIFY_IS_TRUE(Row.WasWrapForced()); - - Log::Comment(L"Testing on stays on"); - - // make sure wrap status is on - Row.SetWrapForced(true); - - // trigger wrap - textBuffer._SetWrapOnCurrentRow(); - - // ensure row is still on - VERIFY_IS_TRUE(Row.WasWrapForced()); -} - void TextBufferTests::TestIncrementCircularBuffer() { auto& textBuffer = GetTbi(); @@ -1705,7 +1542,7 @@ void TextBufferTests::ResizeTraditional() const til::size smallSize = { 5, 5 }; const TextAttribute defaultAttr(0); - TextBuffer buffer(smallSize, defaultAttr, 12, false, _renderer); + TextBuffer buffer(smallSize, defaultAttr, 12, false, &_renderer); Log::Comment(L"Fill buffer with some data and do assorted resize operations."); @@ -1801,7 +1638,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Get a position inside the buffer const til::point pos{ 2, 1 }; @@ -1842,7 +1679,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Get a position inside the buffer const til::point pos{ 2, 1 }; @@ -1877,7 +1714,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Get a position inside the buffer in the bottom row const til::point pos{ 0, bufferSize.height - 1 }; @@ -1907,7 +1744,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Get a position inside the buffer in the last column (-2 as the inserted character is 2 columns wide). const til::point pos{ bufferSize.width - 2, 0 }; @@ -1929,33 +1766,12 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval() _buffer->ResizeTraditional(trimmedBufferSize); } -void TextBufferTests::TestBurrito() -{ - til::size bufferSize{ 80, 9001 }; - UINT cursorSize = 12; - TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); - - // This is the burrito emoji: ๐ŸŒฏ - // It's encoded in UTF-16, as needed by the buffer. - const auto burrito = L"\xD83C\xDF2F"; - OutputCellIterator burriter{ burrito }; - - auto afterFIter = _buffer->Write({ L"F" }); - _buffer->IncrementCursor(); - - auto afterBurritoIter = _buffer->Write(burriter); - _buffer->IncrementCursor(); - _buffer->IncrementCursor(); - VERIFY_IS_FALSE(afterBurritoIter); -} - void TextBufferTests::TestOverwriteChars() { til::size bufferSize{ 10, 3 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; + TextBuffer buffer{ bufferSize, attr, cursorSize, false, &_renderer }; auto& row = buffer.GetMutableRowByOffset(0); // scientist emoji U+1F9D1 U+200D U+1F52C @@ -2011,7 +1827,7 @@ void TextBufferTests::TestReplace() static constexpr til::size bufferSize{ 10, 3 }; static constexpr UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer }; + TextBuffer buffer{ bufferSize, attr, cursorSize, false, &_renderer }; #define complex L"\U0001F41B" @@ -2093,7 +1909,7 @@ void TextBufferTests::TestInsert() static constexpr TextAttribute attr1{ 0x11111111, 0x00000000 }; static constexpr TextAttribute attr2{ 0x22222222, 0x00000000 }; static constexpr TextAttribute attr3{ 0x33333333, 0x00000000 }; - TextBuffer buffer{ bufferSize, attr1, cursorSize, false, _renderer }; + TextBuffer buffer{ bufferSize, attr1, cursorSize, false, &_renderer }; struct Test { @@ -2232,7 +2048,7 @@ void TextBufferTests::GetWordBoundaries() til::size bufferSize{ 80, 9001 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Setup: Write lines of text to the buffer const std::vector text = { L"word other", @@ -2448,7 +2264,7 @@ void TextBufferTests::MoveByWord() til::size bufferSize{ 80, 9001 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Setup: Write lines of text to the buffer const std::vector text = { L"word other", @@ -2555,7 +2371,7 @@ void TextBufferTests::GetGlyphBoundaries() til::size bufferSize{ 10, 10 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // This is the burrito emoji: ๐ŸŒฏ // It's encoded in UTF-16, as needed by the buffer. @@ -2591,7 +2407,7 @@ void TextBufferTests::GetTextRects() til::size bufferSize{ 20, 50 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Setup: Write lines of text to the buffer const std::vector text = { L"0123456789", @@ -2671,7 +2487,7 @@ void TextBufferTests::GetPlainText() til::size bufferSize{ 10, 20 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Setup: Write lines of text to the buffer const std::vector bufferText = { L"12345", @@ -2759,7 +2575,7 @@ void TextBufferTests::GetPlainText() til::size bufferSize{ 5, 20 }; UINT cursorSize = 12; TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); // Setup: Write lines of text to the buffer const std::vector bufferText = { L"1234567", @@ -2888,7 +2704,7 @@ void TextBufferTests::HyperlinkTrim() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); static constexpr std::wstring_view url{ L"test.url" }; static constexpr std::wstring_view otherUrl{ L"other.url" }; @@ -2934,7 +2750,7 @@ void TextBufferTests::NoHyperlinkTrim() const til::size bufferSize{ 80, 10 }; const UINT cursorSize = 12; const TextAttribute attr{ 0x7f }; - auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, _renderer); + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, false, &_renderer); static constexpr std::wstring_view url{ L"test.url" }; static constexpr std::wstring_view customId{ L"CustomId" }; @@ -3086,7 +2902,7 @@ void TextBufferTests::ReflowPromptRegions() // After we resize, make sure to get the new textBuffers til::size newSize{ oldSize.Width() + dx, oldSize.Height() }; - auto newBuffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, _renderer); + auto newBuffer = std::make_unique(newSize, TextAttribute{ 0x7 }, 0, false, &_renderer); TextBuffer::Reflow(*tbi, *newBuffer); Log::Comment(L"========== Checking the host buffer state (after) =========="); diff --git a/src/inc/test/CommonState.hpp b/src/inc/test/CommonState.hpp index b6e8ad153be..4c002700469 100644 --- a/src/inc/test/CommonState.hpp +++ b/src/inc/test/CommonState.hpp @@ -188,7 +188,7 @@ class CommonState initialAttributes, uiCursorSize, true, - *g.pRender); + g.pRender); if (textBuffer.get() == nullptr) { m_hrTextBufferInfo = E_OUTOFMEMORY; diff --git a/src/renderer/atlas/colorbrewer.h b/src/inc/til/colorbrewer.h similarity index 79% rename from src/renderer/atlas/colorbrewer.h rename to src/inc/til/colorbrewer.h index be5cef6b8a6..5f22f972f86 100644 --- a/src/renderer/atlas/colorbrewer.h +++ b/src/inc/til/colorbrewer.h @@ -3,7 +3,7 @@ #pragma once -namespace Microsoft::Console::Render::Atlas::colorbrewer +namespace til::colorbrewer { // The following list of colors is only used as a debug aid and not part of the final product. // They're licensed under: @@ -22,7 +22,7 @@ namespace Microsoft::Console::Render::Atlas::colorbrewer // CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. // - inline constexpr u32 pastel1[]{ + inline constexpr uint32_t pastel1[]{ 0xfbb4ae, 0xb3cde3, 0xccebc5, @@ -33,4 +33,15 @@ namespace Microsoft::Console::Render::Atlas::colorbrewer 0xfddaec, 0xf2f2f2, }; + + inline constexpr uint32_t dark2[]{ + 0x1b9e77, + 0xd95f02, + 0x7570b3, + 0xe7298a, + 0x66a61e, + 0xe6ab02, + 0xa6761d, + 0x666666, + }; } diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 3591abe7905..ca665228345 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -465,6 +465,7 @@ void AtlasEngine::_present() return; } +#pragma warning(suppress : 4127) // conditional expression is constant if (!ATLAS_DEBUG_SHOW_DIRTY && !_p.s->target->disablePresent1 && memcmp(&dirtyRect, &fullRect, sizeof(RECT)) != 0) { params.DirtyRectsCount = 1; diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index cf7bd8a2bec..c8455cfdd92 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -7,7 +7,7 @@ #include #if ATLAS_DEBUG_SHOW_DIRTY -#include "colorbrewer.h" +#include #endif #if ATLAS_DEBUG_DUMP_RENDER_TARGET @@ -394,8 +394,14 @@ void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) THROW_IF_FAILED(target->GetBitmap(_builtinGlyphsBitmap.put())); _builtinGlyphsRenderTarget = target.query(); _builtinGlyphsBitmapCellCountU = cellCountU; - _builtinGlyphsRenderTargetActive = false; memset(&_builtinGlyphsReady[0], 0, sizeof(_builtinGlyphsReady)); + + _builtinGlyphsRenderTarget->BeginDraw(); + _builtinGlyphsRenderTargetActive = true; + + // The initial contents of the bitmap are undefined. + // -> We need to define them. :) + _builtinGlyphsRenderTarget->Clear(); } D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off) @@ -911,7 +917,7 @@ void BackendD2D::_debugShowDirty(const RenderingPayload& p) static_cast(rect.right), static_cast(rect.bottom), }; - const auto color = colorbrewer::pastel1[i] | 0x1f000000; + const auto color = til::colorbrewer::pastel1[i] | 0x1f000000; _fillRectangle(rectF, color); } } diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index b266d3ba092..9a83b75d233 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -15,9 +15,10 @@ #include "dwrite.h" #include "wic.h" #include "../../types/inc/ColorFix.hpp" +#include "../../types/inc/convert.hpp" #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_COLORIZE_GLYPH_ATLAS -#include "colorbrewer.h" +#include #endif TIL_FAST_MATH_BEGIN @@ -451,15 +452,21 @@ void BackendD3D::_recreateCustomShader(const RenderingPayload& p) { if (error) { - LOG_HR_MSG(hr, "%.*hs", static_cast(error->GetBufferSize()), static_cast(error->GetBufferPointer())); + if (p.warningCallback) + { + //to handle compile time errors + const std::string_view errMsgStrView{ static_cast(error->GetBufferPointer()), error->GetBufferSize() }; + const auto errMsgWstring = ConvertToW(CP_ACP, errMsgStrView); + p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, errMsgWstring); + } } else { - LOG_HR(hr); - } - if (p.warningCallback) - { - p.warningCallback(D2DERR_SHADER_COMPILE_FAILED, p.s->misc->customPixelShaderPath); + if (p.warningCallback) + { + //to handle errors such as file not found, path not found, access denied + p.warningCallback(hr, p.s->misc->customPixelShaderPath); + } } } @@ -2222,7 +2229,7 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p) if (rect.non_empty()) { _appendQuad() = { - .shadingType = ShadingType::Selection, + .shadingType = static_cast(ShadingType::Selection), .position = { static_cast(rect.left), static_cast(rect.top), @@ -2231,7 +2238,7 @@ void BackendD3D::_debugShowDirty(const RenderingPayload& p) static_cast(rect.right - rect.left), static_cast(rect.bottom - rect.top), }, - .color = colorbrewer::pastel1[i] | 0x1f000000, + .color = til::colorbrewer::pastel1[i] | 0x1f000000, }; } } diff --git a/src/renderer/atlas/atlas.vcxproj b/src/renderer/atlas/atlas.vcxproj index 694b850224d..37dac662d81 100644 --- a/src/renderer/atlas/atlas.vcxproj +++ b/src/renderer/atlas/atlas.vcxproj @@ -32,7 +32,6 @@ - @@ -102,4 +101,4 @@ $(SolutionDir)\oss\stb;$(OutDir)$(ProjectName);%(AdditionalIncludeDirectories) - + \ No newline at end of file diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index f885957796a..db9afca8dc4 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -259,7 +259,7 @@ COLORREF RenderSettings::GetAttributeUnderlineColor(const TextAttribute& attr) c // renderer if there are blinking cells currently in view. // Arguments: // - renderer: the renderer that will be redrawn. -void RenderSettings::ToggleBlinkRendition(Renderer& renderer) noexcept +void RenderSettings::ToggleBlinkRendition(Renderer* renderer) noexcept try { if (GetRenderMode(Mode::BlinkAllowed)) @@ -277,7 +277,10 @@ try // We reset the _blinkIsInUse flag before redrawing, so we can // get a fresh assessment of the current blink attribute usage. _blinkIsInUse = false; - renderer.TriggerRedrawAll(); + if (renderer) + { + renderer->TriggerRedrawAll(); + } } } } diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index c836bdde848..704f97d4f09 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -42,7 +42,7 @@ namespace Microsoft::Console::Render std::pair GetAttributeColors(const TextAttribute& attr) const noexcept; std::pair GetAttributeColorsWithAlpha(const TextAttribute& attr) const noexcept; COLORREF GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept; - void ToggleBlinkRendition(class Renderer& renderer) noexcept; + void ToggleBlinkRendition(class Renderer* renderer) noexcept; private: til::enumset _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright }; diff --git a/src/terminal/adapter/PageManager.cpp b/src/terminal/adapter/PageManager.cpp index 76b03faf779..9a0ae4de711 100644 --- a/src/terminal/adapter/PageManager.cpp +++ b/src/terminal/adapter/PageManager.cpp @@ -99,7 +99,7 @@ void Page::MoveViewportDown() noexcept _viewport.bottom++; } -PageManager::PageManager(ITerminalApi& api, Renderer& renderer) noexcept : +PageManager::PageManager(ITerminalApi& api, Renderer* renderer) noexcept : _api{ api }, _renderer{ renderer } { @@ -220,9 +220,9 @@ void PageManager::MoveTo(const til::CoordType pageNumber, const bool makeVisible } _activePageNumber = newPageNumber; - if (redrawRequired) + if (redrawRequired && _renderer) { - _renderer.TriggerRedrawAll(); + _renderer->TriggerRedrawAll(); } } diff --git a/src/terminal/adapter/PageManager.hpp b/src/terminal/adapter/PageManager.hpp index e625d24d142..652c98b83ca 100644 --- a/src/terminal/adapter/PageManager.hpp +++ b/src/terminal/adapter/PageManager.hpp @@ -46,7 +46,7 @@ namespace Microsoft::Console::VirtualTerminal using Renderer = Microsoft::Console::Render::Renderer; public: - PageManager(ITerminalApi& api, Renderer& renderer) noexcept; + PageManager(ITerminalApi& api, Renderer* renderer) noexcept; void Reset(); Page Get(const til::CoordType pageNumber) const; Page ActivePage() const; @@ -59,7 +59,7 @@ namespace Microsoft::Console::VirtualTerminal TextBuffer& _getBuffer(const til::CoordType pageNumber, const til::size pageSize) const; ITerminalApi& _api; - Renderer& _renderer; + Renderer* _renderer; til::CoordType _activePageNumber = 1; til::CoordType _visiblePageNumber = 1; static constexpr til::CoordType MAX_PAGES = 6; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 74ede69b53f..1a451941e09 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -16,7 +16,7 @@ using namespace Microsoft::Console::VirtualTerminal; static constexpr std::wstring_view whitespace{ L" " }; -AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) noexcept : +AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer* renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) noexcept : _api{ api }, _renderer{ renderer }, _renderSettings{ renderSettings }, @@ -1937,7 +1937,10 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con { return false; } - _renderer.TriggerRedrawAll(); + if (_renderer) + { + _renderer->TriggerRedrawAll(); + } return true; case DispatchTypes::ModeParams::DECOM_OriginMode: _modes.set(Mode::Origin, enable); @@ -3221,7 +3224,10 @@ bool AdaptDispatch::HardReset() TabSet(DispatchTypes::TabSetType::SetEvery8Columns); // Clear the soft font in the renderer and delete the font buffer. - _renderer.UpdateSoftFont({}, {}, false); + if (_renderer) + { + _renderer->UpdateSoftFont({}, {}, false); + } _fontBuffer = nullptr; // Reset internal modes to their initial state @@ -3501,18 +3507,20 @@ bool AdaptDispatch::SetColorTableEntry(const size_t tableIndex, const DWORD dwCo return false; } - // If we're updating the background color, we need to let the renderer - // know, since it may want to repaint the window background to match. - const auto backgroundIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground); - const auto backgroundChanged = (tableIndex == backgroundIndex); + if (_renderer) + { + // If we're updating the background color, we need to let the renderer + // know, since it may want to repaint the window background to match. + const auto backgroundIndex = _renderSettings.GetColorAliasIndex(ColorAlias::DefaultBackground); + const auto backgroundChanged = (tableIndex == backgroundIndex); - // Similarly for the frame color, the tab may need to be repainted. - const auto frameIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground); - const auto frameChanged = (tableIndex == frameIndex); + // Similarly for the frame color, the tab may need to be repainted. + const auto frameIndex = _renderSettings.GetColorAliasIndex(ColorAlias::FrameBackground); + const auto frameChanged = (tableIndex == frameIndex); + + _renderer->TriggerRedrawAll(backgroundChanged, frameChanged); + } - // Update the screen colors if we're not a pty - // No need to force a redraw in pty mode. - _renderer.TriggerRedrawAll(backgroundChanged, frameChanged); return true; } @@ -3566,14 +3574,19 @@ bool AdaptDispatch::AssignColor(const DispatchTypes::ColorItem item, const VTInt } // No need to force a redraw in pty mode. - const auto inPtyMode = _api.IsConsolePty(); - if (!inPtyMode) + if (_api.IsConsolePty()) + { + return false; + } + + if (_renderer) { const auto backgroundChanged = item == DispatchTypes::ColorItem::NormalText; const auto frameChanged = item == DispatchTypes::ColorItem::WindowFrame; - _renderer.TriggerRedrawAll(backgroundChanged, frameChanged); + _renderer->TriggerRedrawAll(backgroundChanged, frameChanged); } - return !inPtyMode; + + return true; } //Routine Description: @@ -3769,11 +3782,11 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string) bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) { const auto isConPty = _api.IsConsolePty(); - if (isConPty) + if (isConPty && _renderer) { // Flush the frame manually, to make sure marks end up on the right // line, like the alt buffer sequence. - _renderer.TriggerFlush(false); + _renderer->TriggerFlush(false); } if constexpr (!Feature_ScrollbarMarks::IsEnabled()) @@ -3813,11 +3826,11 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) { const auto isConPty = _api.IsConsolePty(); - if (isConPty) + if (isConPty && _renderer) { // Flush the frame manually, to make sure marks end up on the right // line, like the alt buffer sequence. - _renderer.TriggerFlush(false); + _renderer->TriggerFlush(false); } if constexpr (!Feature_ScrollbarMarks::IsEnabled()) @@ -3904,10 +3917,10 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) bool AdaptDispatch::DoVsCodeAction(const std::wstring_view string) { // This is not implemented in conhost. - if (_api.IsConsolePty()) + if (_api.IsConsolePty() && _renderer) { // Flush the frame manually to make sure this action happens at the right time. - _renderer.TriggerFlush(false); + _renderer->TriggerFlush(false); return false; } @@ -4048,10 +4061,13 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber, { _termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation()); } - const auto bitPattern = _fontBuffer->GetBitPattern(); - const auto cellSize = _fontBuffer->GetCellSize(); - const auto centeringHint = _fontBuffer->GetTextCenteringHint(); - _renderer.UpdateSoftFont(bitPattern, cellSize, centeringHint); + if (_renderer) + { + const auto bitPattern = _fontBuffer->GetBitPattern(); + const auto cellSize = _fontBuffer->GetCellSize(); + const auto centeringHint = _fontBuffer->GetTextCenteringHint(); + _renderer->UpdateSoftFont(bitPattern, cellSize, centeringHint); + } } return true; }; @@ -4912,9 +4928,9 @@ bool AdaptDispatch::PlaySounds(const VTParameters parameters) // If we're a conpty, we return false so the command will be passed on // to the connected terminal. But we need to flush the current frame // first, otherwise the visual output will lag behind the sound. - if (_api.IsConsolePty()) + if (_api.IsConsolePty() && _renderer) { - _renderer.TriggerFlush(false); + _renderer->TriggerFlush(false); return false; } @@ -4948,7 +4964,11 @@ ITermDispatch::StringHandler AdaptDispatch::_CreatePassthroughHandler() { // Before we pass through any more data, we need to flush the current frame // first, otherwise it can end up arriving out of sync. - _renderer.TriggerFlush(false); + if (_renderer) + { + _renderer->TriggerFlush(false); + } + // Then we need to flush the sequence introducer and parameters that have // already been parsed by the state machine. auto& stateMachine = _api.GetStateMachine(); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 76e32d0fdd5..7087932a497 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -36,7 +36,7 @@ namespace Microsoft::Console::VirtualTerminal using RenderSettings = Microsoft::Console::Render::RenderSettings; public: - AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) noexcept; + AdaptDispatch(ITerminalApi& api, Renderer* renderer, RenderSettings& renderSettings, TerminalInput& terminalInput) noexcept; void Print(const wchar_t wchPrintable) override; void PrintString(const std::wstring_view string) override; @@ -286,7 +286,7 @@ namespace Microsoft::Console::VirtualTerminal bool _initDefaultTabStops = true; ITerminalApi& _api; - Renderer& _renderer; + Renderer* _renderer; RenderSettings& _renderSettings; TerminalInput& _terminalInput; TerminalOutput _termOutput; diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index c0ff50e5748..415d22fe86c 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -250,7 +250,7 @@ class TestGetSet final : public ITerminalApi _setTextAttributesResult = TRUE; _returnResponseResult = TRUE; - _textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, _renderer); + _textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, &_renderer); // Viewport sitting in the "middle" of the buffer somewhere (so all sides have excess buffer around them) _viewport.top = 20; @@ -406,7 +406,7 @@ class AdapterTest _terminalInput = TerminalInput{}; auto& renderer = _testGetSet->_renderer; auto& renderSettings = renderer._renderSettings; - auto adapter = std::make_unique(*_testGetSet, renderer, renderSettings, _terminalInput); + auto adapter = std::make_unique(*_testGetSet, &renderer, renderSettings, _terminalInput); fSuccess = adapter.get() != nullptr; if (fSuccess) @@ -2448,7 +2448,7 @@ class AdapterTest Log::Comment(L"Starting test..."); til::inclusive_rect srTestMargins; - _testGetSet->_textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, _testGetSet->_renderer); + _testGetSet->_textBuffer = std::make_unique(til::size{ 100, 600 }, TextAttribute{}, 0, false, &_testGetSet->_renderer); _testGetSet->_viewport.right = 8; _testGetSet->_viewport.bottom = 8; auto sScreenHeight = _testGetSet->_viewport.bottom - _testGetSet->_viewport.top; diff --git a/src/tools/ConsoleBench/ConsoleBench.exe.manifest b/src/tools/ConsoleBench/ConsoleBench.exe.manifest new file mode 100644 index 00000000000..380e2e03a37 --- /dev/null +++ b/src/tools/ConsoleBench/ConsoleBench.exe.manifest @@ -0,0 +1,26 @@ + + + + + true + UTF-8 + + + + + + + + + + + + + diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj b/src/tools/ConsoleBench/ConsoleBench.vcxproj index 1e66412cc6b..5ea84a7c56f 100644 --- a/src/tools/ConsoleBench/ConsoleBench.vcxproj +++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj @@ -25,6 +25,9 @@ + + + false diff --git a/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters index 25a667f9907..ca5d01a8e55 100644 --- a/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters +++ b/src/tools/ConsoleBench/ConsoleBench.vcxproj.filters @@ -49,4 +49,9 @@ Source Files + + + Source Files + + \ No newline at end of file diff --git a/src/tools/ConsoleBench/arena.cpp b/src/tools/ConsoleBench/arena.cpp index d91986c7276..8c96c341b4c 100644 --- a/src/tools/ConsoleBench/arena.cpp +++ b/src/tools/ConsoleBench/arena.cpp @@ -5,7 +5,7 @@ using namespace mem; Arena::Arena(size_t bytes) { - m_alloc = static_cast(THROW_IF_NULL_ALLOC(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))); + m_alloc = static_cast(THROW_IF_NULL_ALLOC(VirtualAlloc(nullptr, bytes, MEM_RESERVE, PAGE_READWRITE))); } Arena::~Arena() @@ -41,8 +41,18 @@ void* Arena::_push_raw(size_t bytes, size_t alignment) { const auto mask = alignment - 1; const auto pos = (m_pos + mask) & ~mask; + const auto pos_new = pos + bytes; const auto ptr = m_alloc + pos; - m_pos = pos + bytes; + + if (pos_new > m_commit) + { + // Commit in 1MB chunks and pre-commit 1MiB in advance. + const auto commit_new = (pos_new + 0x1FFFFF) & ~0xFFFFF; + THROW_IF_NULL_ALLOC(VirtualAlloc(m_alloc + m_commit, commit_new - m_commit, MEM_COMMIT, PAGE_READWRITE)); + m_commit = commit_new; + } + + m_pos = pos_new; return ptr; } @@ -76,8 +86,8 @@ ScopedArena::~ScopedArena() static [[msvc::noinline]] std::array thread_arenas_init() { return { - Arena{ 64 * 1024 * 1024 }, - Arena{ 64 * 1024 * 1024 }, + Arena{ 1024 * 1024 * 1024 }, + Arena{ 1024 * 1024 * 1024 }, }; } @@ -166,7 +176,9 @@ std::wstring_view mem::format(Arena& arena, const wchar_t* fmt, va_list args) return {}; } + // Make space for a terminating \0 character. len++; + const auto buffer = arena.push_uninitialized(len); len = _vsnwprintf(buffer, len, fmt, args); diff --git a/src/tools/ConsoleBench/arena.h b/src/tools/ConsoleBench/arena.h index 1519f03377b..92409c32921 100644 --- a/src/tools/ConsoleBench/arena.h +++ b/src/tools/ConsoleBench/arena.h @@ -60,6 +60,7 @@ namespace mem void* _push_uninitialized(size_t bytes, size_t alignment = __STDCPP_DEFAULT_NEW_ALIGNMENT__); uint8_t* m_alloc = nullptr; + size_t m_commit = 0; size_t m_pos = 0; }; @@ -96,16 +97,32 @@ namespace mem } template - std::basic_string_view repeat_string(Arena& arena, std::basic_string_view in, size_t count) + auto repeat(Arena& arena, const T& in, size_t count) -> decltype(auto) { - const auto len = count * in.size(); - const auto buf = arena.push_uninitialized(len); - - for (size_t i = 0; i < count; ++i) + if constexpr (is_std_view::value) { - mem::copy(buf + i * in.size(), in.data(), in.size()); + const auto data = in.data(); + const auto size = in.size(); + const auto len = count * size; + const auto buf = arena.push_uninitialized(len); + + for (size_t i = 0; i < count; ++i) + { + mem::copy(buf + i * size, data, size); + } + + return T{ buf, len }; } + else + { + const auto buf = arena.push_uninitialized(count); + + for (size_t i = 0; i < count; ++i) + { + memcpy(buf + i, &in, sizeof(T)); + } - return { buf, len }; + return std::span{ buf, count }; + } } } diff --git a/src/tools/ConsoleBench/conhost.cpp b/src/tools/ConsoleBench/conhost.cpp index 95a0a3ea8a9..4db5352f99b 100644 --- a/src/tools/ConsoleBench/conhost.cpp +++ b/src/tools/ConsoleBench/conhost.cpp @@ -3,6 +3,7 @@ #include #include +#include #include "arena.h" @@ -46,12 +47,27 @@ static void conhostCopyToStringBuffer(USHORT& length, auto& buffer, const wchar_ ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path) { + const auto pathLen = wcslen(path); + const auto isDLL = pathLen > 4 && wcscmp(&path[pathLen - 4], L".dll") == 0; + const auto scratch = mem::get_scratch_arena(arena); - const auto server = conhostCreateHandle(nullptr, L"\\Device\\ConDrv\\Server", true, false); + auto server = conhostCreateHandle(nullptr, L"\\Device\\ConDrv\\Server", true, false); auto reference = conhostCreateHandle(server.get(), L"\\Reference", false, true); { - const auto cmd = format(scratch.arena, LR"("%s" --server 0x%zx)", path, server.get()); + const auto selfPath = scratch.arena.push_uninitialized(64 * 1024); + GetModuleFileNameW(nullptr, selfPath, 64 * 1024); + + std::wstring_view cmd; + + if (isDLL) + { + cmd = format(scratch.arena, LR"("%s" host %zx "%s")", selfPath, server.get(), path); + } + else + { + cmd = format(scratch.arena, LR"("%s" --server 0x%zx)", path, server.get()); + } uint8_t attrListBuffer[64]; @@ -154,6 +170,22 @@ ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path) }; } +// A continuation of spawn_conhost(). +void check_spawn_conhost_dll(int argc, const wchar_t* argv[]) +{ + if (argc == 4 && wcscmp(argv[1], L"host") == 0) + { + const auto serverHandle = reinterpret_cast(wcstoull(argv[2], nullptr, 16)); + const auto path = argv[3]; + + using Entrypoint = NTSTATUS(NTAPI*)(HANDLE); + const auto h = THROW_LAST_ERROR_IF_NULL(LoadLibraryExW(path, nullptr, 0)); + const auto f = THROW_LAST_ERROR_IF_NULL(reinterpret_cast(GetProcAddress(h, "ConsoleCreateIoThread"))); + THROW_IF_NTSTATUS_FAILED(f(serverHandle)); + ExitThread(S_OK); + } +} + HANDLE get_active_connection() { // (Not actually) FUN FACT! The handles don't mean anything and the cake is a lie! diff --git a/src/tools/ConsoleBench/conhost.h b/src/tools/ConsoleBench/conhost.h index e794eca555e..719a5bb87ad 100644 --- a/src/tools/ConsoleBench/conhost.h +++ b/src/tools/ConsoleBench/conhost.h @@ -16,5 +16,6 @@ struct ConhostHandle }; ConhostHandle spawn_conhost(mem::Arena& arena, const wchar_t* path); +void check_spawn_conhost_dll(int argc, const wchar_t* argv[]); HANDLE get_active_connection(); void set_active_connection(HANDLE connection); diff --git a/src/tools/ConsoleBench/main.cpp b/src/tools/ConsoleBench/main.cpp index 4e380474ebc..31b43e6b93d 100644 --- a/src/tools/ConsoleBench/main.cpp +++ b/src/tools/ConsoleBench/main.cpp @@ -7,26 +7,47 @@ #include "conhost.h" #include "utils.h" +#define ENABLE_TEST_OUTPUT_WRITE 1 +#define ENABLE_TEST_OUTPUT_SCROLL 1 +#define ENABLE_TEST_OUTPUT_FILL 1 +#define ENABLE_TEST_OUTPUT_READ 1 +#define ENABLE_TEST_INPUT 1 +#define ENABLE_TEST_CLIPBOARD 1 + using Measurements = std::span; using MeasurementsPerBenchmark = std::span; struct BenchmarkContext { - HWND hwnd; - HANDLE input; - HANDLE output; - int64_t time_limit; + bool wants_more() const; + void mark_beg(); + void mark_end(); + size_t rand(); + + HWND hwnd = nullptr; + HANDLE input = nullptr; + HANDLE output = nullptr; + mem::Arena& arena; std::string_view utf8_4Ki; std::string_view utf8_128Ki; std::wstring_view utf16_4Ki; std::wstring_view utf16_128Ki; + std::span attr_4Ki; + std::span char_4Ki; + std::span input_4Ki; + + Measurements m_measurements; + size_t m_measurements_off = 0; + int64_t m_time = 0; + int64_t m_time_limit = 0; + size_t m_rng_state = 0; }; struct Benchmark { const char* title; - void (*exec)(const BenchmarkContext& ctx, Measurements measurements); + void (*exec)(BenchmarkContext& ctx); }; struct AccumulatedResults @@ -37,158 +58,415 @@ struct AccumulatedResults MeasurementsPerBenchmark* measurments; }; -constexpr int32_t perf_delta(int64_t beg, int64_t end) -{ - return static_cast(end - beg); -} +static constexpr COORD s_buffer_size{ 120, 9001 }; +static constexpr COORD s_viewport_size{ 120, 30 }; -static constexpr Benchmark s_benchmarks[]{ +static constexpr Benchmark s_benchmarks[] = { +#if ENABLE_TEST_OUTPUT_WRITE Benchmark{ .title = "WriteConsoleA 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - const auto beg = query_perf_counter(); - WriteConsoleA(ctx.output, ctx.utf8_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); - - if (end >= ctx.time_limit) - { - break; - } + ctx.mark_beg(); + const auto res = WriteConsoleA(ctx.output, ctx.utf8_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ .title = "WriteConsoleW 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - const auto beg = query_perf_counter(); - WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), nullptr, nullptr); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); - - if (end >= ctx.time_limit) - { - break; - } + ctx.mark_beg(); + const auto res = WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ .title = "WriteConsoleA 128Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) { - const auto beg = query_perf_counter(); - WriteConsoleA(ctx.output, ctx.utf8_128Ki.data(), static_cast(ctx.utf8_128Ki.size()), nullptr, nullptr); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); + ctx.mark_beg(); + const auto res = WriteConsoleA(ctx.output, ctx.utf8_128Ki.data(), static_cast(ctx.utf8_128Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleW 128Ki", + .exec = [](BenchmarkContext& ctx) { + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; + + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleOutputAttribute(ctx.output, ctx.attr_4Ki.data(), static_cast(ctx.attr_4Ki.size()), pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; + + while (ctx.wants_more()) + { + ctx.mark_beg(); + const auto res = WriteConsoleOutputCharacterW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf16_4Ki.size()), pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, + Benchmark{ + .title = "WriteConsoleOutputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + static constexpr COORD size{ 64, 64 }; + static constexpr SMALL_RECT rect{ 0, 0, 63, 63 }; + + while (ctx.wants_more()) + { + auto written = rect; + + ctx.mark_beg(); + const auto res = WriteConsoleOutputW(ctx.output, ctx.char_4Ki.data(), size, pos, &written); + ctx.mark_end(); + debugAssert(res == TRUE); + } + }, + }, +#endif +#if ENABLE_TEST_OUTPUT_SCROLL + Benchmark{ + .title = "ScrollConsoleScreenBufferW 4Ki", + .exec = [](BenchmarkContext& ctx) { + for (int i = 0; i < 10; i++) + { + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + } - if (end >= ctx.time_limit) + static constexpr CHAR_INFO fill{ L' ', FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + static constexpr size_t w = 64; + static constexpr size_t h = 64; + + while (ctx.wants_more()) + { + auto r = ctx.rand(); + const auto srcLeft = (r >> 0) % (s_buffer_size.X - w); + const auto srcTop = (r >> 16) % (s_buffer_size.Y - h); + + size_t dstLeft; + size_t dstTop; + do { - break; - } + r = ctx.rand(); + dstLeft = (r >> 0) % (s_buffer_size.X - w); + dstTop = (r >> 16) % (s_buffer_size.Y - h); + } while (srcLeft == dstLeft && srcTop == dstTop); + + const SMALL_RECT scrollRect{ + .Left = static_cast(srcLeft), + .Top = static_cast(srcTop), + .Right = static_cast(srcLeft + w - 1), + .Bottom = static_cast(srcTop + h - 1), + }; + const COORD destOrigin{ + .X = static_cast(dstLeft), + .Y = static_cast(dstTop), + }; + + ctx.mark_beg(); + const auto res = ScrollConsoleScreenBufferW(ctx.output, &scrollRect, nullptr, destOrigin, &fill); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, Benchmark{ - .title = "WriteConsoleW 128Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - for (auto& d : measurements) + .title = "ScrollConsoleScreenBufferW vertical", + .exec = [](BenchmarkContext& ctx) { + for (int i = 0; i < 10; i++) { - const auto beg = query_perf_counter(); WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); + } - if (end >= ctx.time_limit) + static constexpr CHAR_INFO fill{ L' ', FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED }; + static constexpr size_t h = (4096 + s_buffer_size.X / 2) / s_buffer_size.X; + + while (ctx.wants_more()) + { + auto r = ctx.rand(); + const auto srcTop = r % (s_buffer_size.Y - h); + + size_t dstTop; + do { - break; - } + r = ctx.rand(); + dstTop = r % (s_buffer_size.Y - h); + } while (srcTop == dstTop); + + const SMALL_RECT scrollRect{ + .Left = 0, + .Top = static_cast(srcTop), + .Right = s_buffer_size.X - 1, + .Bottom = static_cast(srcTop + h - 1), + }; + const COORD destOrigin{ + .X = 0, + .Y = static_cast(dstTop), + }; + + ctx.mark_beg(); + const auto res = ScrollConsoleScreenBufferW(ctx.output, &scrollRect, nullptr, destOrigin, &fill); + ctx.mark_end(); + debugAssert(res == TRUE); } }, }, +#endif +#if ENABLE_TEST_OUTPUT_FILL Benchmark{ - .title = "Copy to clipboard 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); + .title = "FillConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; - for (auto& d : measurements) + while (ctx.wants_more()) { - SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF5 /* ID_CONSOLE_SELECTALL */, 0); + ctx.mark_beg(); + FillConsoleOutputAttribute(ctx.output, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED, 4096, pos, &written); + ctx.mark_end(); + debugAssert(written == 4096); + } + }, + }, + Benchmark{ + .title = "FillConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + DWORD written; - const auto beg = query_perf_counter(); - SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF0 /* ID_CONSOLE_COPY */, 0); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); + while (ctx.wants_more()) + { + ctx.mark_beg(); + FillConsoleOutputCharacterW(ctx.output, L'A', 4096, pos, &written); + ctx.mark_end(); + debugAssert(written == 4096); + } + }, + }, +#endif +#if ENABLE_TEST_OUTPUT_READ + Benchmark{ + .title = "ReadConsoleOutputAttribute 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(4096); + DWORD read; - if (end >= ctx.time_limit) - { - break; - } + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + ctx.mark_beg(); + ReadConsoleOutputAttribute(ctx.output, buf, 4096, pos, &read); + ctx.mark_end(); + debugAssert(read == 4096); } }, }, Benchmark{ - .title = "Paste from clipboard 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - set_clipboard(ctx.hwnd, ctx.utf16_4Ki); + .title = "ReadConsoleOutputCharacterW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(4096); + DWORD read; + + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + ctx.mark_beg(); + ReadConsoleOutputCharacterW(ctx.output, buf, 4096, pos, &read); + ctx.mark_end(); + debugAssert(read == 4096); + } + }, + }, + Benchmark{ + .title = "ReadConsoleOutputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + static constexpr COORD pos{ 0, 0 }; + static constexpr COORD size{ 64, 64 }; + static constexpr SMALL_RECT rect{ 0, 0, 63, 63 }; + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(size.X * size.Y); + + WriteConsoleW(ctx.output, ctx.utf16_128Ki.data(), static_cast(ctx.utf16_128Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + auto read = rect; + + ctx.mark_beg(); + ReadConsoleOutputW(ctx.output, buf, size, pos, &read); + ctx.mark_end(); + debugAssert(read.Right == 63 && read.Bottom == 63); + } + }, + }, +#endif +#if ENABLE_TEST_INPUT + Benchmark{ + .title = "WriteConsoleInputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + DWORD written; + FlushConsoleInputBuffer(ctx.input); - for (auto& d : measurements) + while (ctx.wants_more()) { - const auto beg = query_perf_counter(); - SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF1 /* ID_CONSOLE_PASTE */, 0); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); + ctx.mark_beg(); + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + ctx.mark_end(); + debugAssert(written == ctx.input_4Ki.size()); FlushConsoleInputBuffer(ctx.input); - - if (end >= ctx.time_limit) - { - break; - } } }, }, Benchmark{ - .title = "ReadConsoleInputW clipboard 4Ki", - .exec = [](const BenchmarkContext& ctx, Measurements measurements) { - static constexpr DWORD cap = 16 * 1024; + .title = "ReadConsoleInputW 4Ki", + .exec = [](BenchmarkContext& ctx) { + const auto scratch = mem::get_scratch_arena(ctx.arena); + const auto buf = scratch.arena.push_uninitialized(ctx.input_4Ki.size()); + DWORD written, read; + FlushConsoleInputBuffer(ctx.input); + + while (ctx.wants_more()) + { + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + debugAssert(written == ctx.input_4Ki.size()); + + ctx.mark_beg(); + ReadConsoleInputW(ctx.input, buf, static_cast(ctx.input_4Ki.size()), &read); + ctx.mark_end(); + debugAssert(read == ctx.input_4Ki.size()); + } + }, + }, + Benchmark{ + .title = "ReadConsoleW 4Ki", + .exec = [](BenchmarkContext& ctx) { const auto scratch = mem::get_scratch_arena(ctx.arena); - const auto buf = scratch.arena.push_uninitialized(cap); - DWORD read; + const auto cap = static_cast(ctx.input_4Ki.size()) * 4; + const auto buf = scratch.arena.push_uninitialized(cap); + DWORD written, read; + + FlushConsoleInputBuffer(ctx.input); + + while (ctx.wants_more()) + { + WriteConsoleInputW(ctx.input, ctx.input_4Ki.data(), static_cast(ctx.input_4Ki.size()), &written); + debugAssert(written == ctx.input_4Ki.size()); + ctx.mark_beg(); + ReadConsoleW(ctx.input, buf, cap, &read, nullptr); + debugAssert(read == ctx.input_4Ki.size()); + ctx.mark_end(); + } + }, + }, +#endif +#if ENABLE_TEST_CLIPBOARD + Benchmark{ + .title = "Clipboard copy 4Ki", + .exec = [](BenchmarkContext& ctx) { + WriteConsoleW(ctx.output, ctx.utf16_4Ki.data(), static_cast(ctx.utf8_4Ki.size()), nullptr, nullptr); + + while (ctx.wants_more()) + { + SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF5 /* ID_CONSOLE_SELECTALL */, 0); + + ctx.mark_beg(); + SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF0 /* ID_CONSOLE_COPY */, 0); + ctx.mark_end(); + } + }, + }, + Benchmark{ + .title = "Clipboard paste 4Ki", + .exec = [](BenchmarkContext& ctx) { set_clipboard(ctx.hwnd, ctx.utf16_4Ki); FlushConsoleInputBuffer(ctx.input); - for (auto& d : measurements) + while (ctx.wants_more()) { + ctx.mark_beg(); SendMessageW(ctx.hwnd, WM_SYSCOMMAND, 0xFFF1 /* ID_CONSOLE_PASTE */, 0); + ctx.mark_end(); - const auto beg = query_perf_counter(); - ReadConsoleInputW(ctx.input, buf, cap, &read); - debugAssert(read >= 1024 && read < cap); - const auto end = query_perf_counter(); - d = perf_delta(beg, end); - - if (end >= ctx.time_limit) - { - break; - } + FlushConsoleInputBuffer(ctx.input); } }, }, +#endif }; -static constexpr size_t s_benchmarks_count = _countof(s_benchmarks); -// Each of these strings is 128 columns. -static constexpr std::string_view payload_utf8{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor็œ ใ„ๅญ็Œซใฏใƒžใ‚ฐใƒญ็‹ฉใ‚Šใฎๅคขใ‚’่ฆ‹ใ‚‹" }; -static constexpr std::wstring_view payload_utf16{ L"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor็œ ใ„ๅญ็Œซใฏใƒžใ‚ฐใƒญ็‹ฉใ‚Šใฎๅคขใ‚’่ฆ‹ใ‚‹" }; +static constexpr size_t s_benchmarks_count = _countof(s_benchmarks); +static constexpr size_t s_samples_min = 20; +static constexpr size_t s_samples_max = 1000; + +// 128 characters and 124 columns. +static constexpr std::string_view s_payload_utf8{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna alฮ‘ฮ’ฮ“ฮ”ฮ•" }; +// 128 characters and 128 columns. +static constexpr std::wstring_view s_payload_utf16{ L"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.ฮ‘ฮ’ฮ“ฮ”ฮ•" }; + +static constexpr WORD s_payload_attr = FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED; +static constexpr CHAR_INFO s_payload_char{ + .Char = { .UnicodeChar = L'A' }, + .Attributes = s_payload_attr, +}; +static constexpr INPUT_RECORD s_payload_record{ + .EventType = KEY_EVENT, + .Event = { + .KeyEvent = { + .bKeyDown = TRUE, + .wRepeatCount = 1, + .wVirtualKeyCode = 'A', + .wVirtualScanCode = 0, + .uChar = 'A', + .dwControlKeyState = 0, + }, + }, +}; static bool print_warning(); static AccumulatedResults* prepare_results(mem::Arena& arena, std::span paths); @@ -196,6 +474,7 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const static void generate_html(mem::Arena& arena, const AccumulatedResults* results); int wmain(int argc, const wchar_t* argv[]) +try { if (argc < 2) { @@ -203,6 +482,8 @@ int wmain(int argc, const wchar_t* argv[]) return 1; } + check_spawn_conhost_dll(argc, argv); + const auto cp = GetConsoleCP(); const auto output_cp = GetConsoleOutputCP(); const auto restore_cp = wil::scope_exit([&]() { @@ -230,12 +511,29 @@ int wmain(int argc, const wchar_t* argv[]) { const auto title = results->trace_names[trace_idx]; print_format(scratch.arena, "\r\n# %.*s\r\n", title.size(), title.data()); + + // I found that waiting between tests fixes weird bugs when launching very old conhost versions. + if (trace_idx != 0) + { + Sleep(5000); + } + results->measurments[trace_idx] = run_benchmarks_for_path(scratch.arena, paths[trace_idx]); } generate_html(scratch.arena, results); return 0; } +catch (const wil::ResultException& e) +{ + printf("Exception: %08x\n", e.GetErrorCode()); + return 1; +} +catch (...) +{ + printf("Unknown exception\n"); + return 1; +} static bool print_warning() { @@ -284,7 +582,7 @@ static AccumulatedResults* prepare_results(mem::Arena& arena, std::span(9001); - memset(buf, '\n', 9001); - WriteFile(ctx.output, buf, 9001, nullptr, nullptr); + const auto buf = scratch.arena.push_uninitialized(s_buffer_size.Y); + memset(buf, '\n', s_buffer_size.Y); + WriteFile(ctx.output, buf, s_buffer_size.Y, nullptr, nullptr); } static std::span run_benchmarks_for_path(mem::Arena& arena, const wchar_t* path) @@ -364,7 +663,7 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const const auto parent_hwnd = GetConsoleWindow(); const auto freq = query_perf_freq(); - const auto handle = spawn_conhost(scratch.arena, path); + auto handle = spawn_conhost(scratch.arena, path); set_active_connection(handle.connection.get()); const auto print_with_parent_connection = [&](auto&&... args) { @@ -377,45 +676,56 @@ static std::span run_benchmarks_for_path(mem::Arena& arena, const .hwnd = GetConsoleWindow(), .input = GetStdHandle(STD_INPUT_HANDLE), .output = GetStdHandle(STD_OUTPUT_HANDLE), + .arena = scratch.arena, - .utf8_4Ki = mem::repeat_string(scratch.arena, payload_utf8, 4 * 1024 / 128), - .utf8_128Ki = mem::repeat_string(scratch.arena, payload_utf8, 128 * 1024 / 128), - .utf16_4Ki = mem::repeat_string(scratch.arena, payload_utf16, 4 * 1024 / 128), - .utf16_128Ki = mem::repeat_string(scratch.arena, payload_utf16, 128 * 1024 / 128), + .utf8_4Ki = mem::repeat(scratch.arena, s_payload_utf8, 4 * 1024 / s_payload_utf8.size()), + .utf8_128Ki = mem::repeat(scratch.arena, s_payload_utf8, 128 * 1024 / s_payload_utf8.size()), + .utf16_4Ki = mem::repeat(scratch.arena, s_payload_utf16, 4 * 1024 / s_payload_utf16.size()), + .utf16_128Ki = mem::repeat(scratch.arena, s_payload_utf16, 128 * 1024 / s_payload_utf16.size()), + .attr_4Ki = mem::repeat(scratch.arena, s_payload_attr, 4 * 1024), + .char_4Ki = mem::repeat(scratch.arena, s_payload_char, 4 * 1024), + .input_4Ki = mem::repeat(scratch.arena, s_payload_record, 4 * 1024), + + .m_measurements = scratch.arena.push_uninitialized_span(4 * 1024 * 1024), }; prepare_conhost(ctx, parent_hwnd); Sleep(1000); const auto results = arena.push_uninitialized_span(s_benchmarks_count); - for (auto& measurements : results) - { - measurements = arena.push_zeroed_span(2048); - } for (size_t bench_idx = 0; bench_idx < s_benchmarks_count; ++bench_idx) { const auto& bench = s_benchmarks[bench_idx]; - auto& measurements = results[bench_idx]; print_with_parent_connection("- %s", bench.title); - // Warmup for 0.1s. + // Warmup for 0.1s max. WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr); - ctx.time_limit = query_perf_counter() + freq / 10; - bench.exec(ctx, measurements); + ctx.m_measurements_off = 0; + ctx.m_time_limit = query_perf_counter() + freq / 10; + bench.exec(ctx); - // Actual run for 1s. + // Actual run for 3s max. WriteConsoleW(ctx.output, L"\033c", 2, nullptr, nullptr); - ctx.time_limit = query_perf_counter() + freq; - bench.exec(ctx, measurements); + ctx.m_measurements_off = 0; + ctx.m_time_limit = query_perf_counter() + freq * 3; + bench.exec(ctx); - // Trim off trailing 0s that resulted from the time_limit. - size_t len = measurements.size(); - for (; len > 0 && measurements[len - 1] == 0; --len) + const auto measurements = arena.push_uninitialized_span(std::min(ctx.m_measurements_off, s_samples_max)); + if (ctx.m_measurements_off <= s_samples_max) { + mem::copy(measurements.data(), ctx.m_measurements.data(), ctx.m_measurements_off); } - measurements = measurements.subspan(0, len); + else + { + const auto total = ctx.m_measurements_off; + for (size_t i = 0; i < s_samples_max; ++i) + { + measurements[i] = ctx.m_measurements[i * total / s_samples_max]; + } + } + results[bench_idx] = measurements; print_with_parent_connection(", done\r\n"); } @@ -463,7 +773,7 @@ static void generate_html(mem::Arena& arena, const AccumulatedResults* results) - +