Compare commits

..

12 Commits

Author SHA1 Message Date
Dustin L. Howett
ff5a1f5ae3 yeah, TSA's really jaggin' me 2026-04-17 14:46:56 -05:00
Dustin L. Howett
47a8bfee96 Use the small pool for the small jobs 2026-04-17 14:43:03 -05:00
Dustin L. Howett
7abe28069d Fix the publish dependency issue 2026-04-17 12:09:09 -05:00
Dustin L. Howett
5c3cb5ad69 Add optional publish step deps (copy to other templates) 2026-04-17 11:58:06 -05:00
Dustin L. Howett
c736fbac2e turn signing back on 2026-04-17 11:56:11 -05:00
Dustin L. Howett
9771a64ad5 nfci lower symbol expiry time 2026-04-17 11:55:05 -05:00
Dustin L. Howett
69064f3011 tidy 2026-04-17 11:53:32 -05:00
Dustin L. Howett
565fe8f9ff hax: try using local terrapin isntead 2026-04-16 16:14:00 -05:00
Dustin L. Howett
0e22d64221 hax: try terrapin! 2026-04-16 16:01:24 -05:00
Dustin L. Howett
7d965ee028 try to put templateContext in a parameter 2026-04-16 15:11:59 -05:00
Dustin L. Howett
3ccf958fa5 For now tie code signing to officiality 2026-04-16 14:56:57 -05:00
Dustin L. Howett
d05367991e add 1espt analogues for the build system 2026-04-16 14:54:07 -05:00
16 changed files with 502 additions and 203 deletions

View File

@@ -1,4 +1,5 @@
{
"codebaseName": "VSTS_Microsoft_OSGS_OpenConsole",
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
"areaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SHINE\\Terminal",

View File

@@ -0,0 +1,63 @@
trigger: none
pr: none
schedules:
- cron: "30 3 * * 2-6" # Run at 03:30 UTC Tuesday through Saturday (After the work day in Pacific, Mon-Fri)
displayName: "Nightly Terminal Build"
branches:
include:
- main
always: false # only run if there's code changes!
parameters:
- name: publishToAzure
displayName: "Deploy to **PUBLIC** Azure Storage"
type: boolean
default: false
- name: official
type: boolean
default: false
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
variables:
- template: templates-v2/variables-nuget-package-version.yml
parameters:
branding: Canary
extends:
template: templates-v2/pipeline-1espt-full-release-build.yml
parameters:
official: ${{parameters.official}}
branding: Canary
buildTerminal: true
pgoBuildMode: Optimize
codeSign: true
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)
tenantId: $(SigningTenantId)
akvName: $(SigningAKVName)
authCertName: $(SigningAuthCertName)
signCertName: $(SigningSignCertName)
useManagedIdentity: $(SigningUseManagedIdentity)
clientId: $(SigningOriginalClientId)
publishSymbolsToPublic: true
symbolExpiryTime: 1
symbolPublishingSubscription: $(SymbolPublishingServiceConnection)
symbolPublishingProject: $(SymbolPublishingProject)
${{ if eq(true, parameters.publishToAzure) }}:
extraPublishJobs:
- template: build/pipelines/templates-v2/job-deploy-to-azure-storage.yml@self
parameters:
pool:
name: SHINE-INT-S
os: windows
dependsOn: [PublishSymbols]
storagePublicRootURL: $(AppInstallerRootURL)
subscription: $(AzureSubscriptionName)
storageAccount: $(AzureStorageAccount)
storageContainer: $(AzureStorageContainer)
buildConfiguration: Release
buildPlatforms: [x64, x86, arm64]
environment: production-canary

View File

@@ -30,9 +30,13 @@ parameters:
- name: signingIdentity
type: object
default: {}
- name: outerTemplateContext
type: object
default: {}
jobs:
- job: ${{ parameters.jobName }}
templateContext: ${{ parameters.outerTemplateContext }}
${{ if ne(length(parameters.pool), 0) }}:
pool: ${{ parameters.pool }}
${{ if eq(parameters.codeSign, true) }}:

View File

@@ -74,9 +74,13 @@ parameters:
- name: afterBuildSteps
type: stepList
default: []
- name: outerTemplateContext
type: object
default: {}
jobs:
- job: ${{ parameters.jobName }}
templateContext: ${{ parameters.outerTemplateContext }}
${{ if ne(length(parameters.pool), 0) }}:
pool: ${{ parameters.pool }}
strategy:

View File

@@ -35,9 +35,13 @@ parameters:
- name: signingIdentity
type: object
default: {}
- name: outerTemplateContext
type: object
default: {}
jobs:
- job: ${{ parameters.jobName }}
templateContext: ${{ parameters.outerTemplateContext }}
${{ if ne(length(parameters.pool), 0) }}:
pool: ${{ parameters.pool }}
${{ if eq(parameters.codeSign, true) }}:

View File

@@ -30,9 +30,13 @@ parameters:
- name: signingIdentity
type: object
default: {}
- name: outerTemplateContext
type: object
default: {}
jobs:
- job: ${{ parameters.jobName }}
templateContext: ${{ parameters.outerTemplateContext }}
${{ if ne(length(parameters.pool), 0) }}:
pool: ${{ parameters.pool }}
${{ if eq(parameters.codeSign, true) }}:

View File

@@ -0,0 +1,233 @@
parameters:
- name: official
type: boolean
default: false
- name: branding
type: string
default: Release
values:
- Release
- Preview
- Canary
- Dev
- name: buildTerminal
type: boolean
default: true
- name: buildConPTY
type: boolean
default: false
- name: buildWPF
type: boolean
default: false
- name: pgoBuildMode
type: string
default: Optimize
values:
- Optimize
- Instrument
- None
- name: buildConfigurations
type: object
default:
- Release
- name: buildPlatforms
type: object
default:
- x64
- x86
- arm64
- name: codeSign
type: boolean
default: true
- name: terminalInternalPackageVersion
type: string
default: '0.0.8'
- name: publishSymbolsToPublic
type: boolean
default: true
- name: symbolExpiryTime
type: string
default: 36530 # This is the default from PublishSymbols@2
- name: symbolPublishingSubscription
type: string
- name: symbolPublishingProject
type: string
- name: extraPublishJobs
type: object
default: []
- name: signingIdentity
type: object
default: {}
resources:
repositories:
- repository: 1esPipelines
type: git
name: 1ESPipelineTemplates/1ESPipelineTemplates
ref: refs/tags/release
extends:
${{ if eq(parameters.official, true) }}:
template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines
${{ else }}:
template: v1/1ES.Unofficial.PipelineTemplate.yml@1esPipelines
parameters:
customBuildTags:
- 1ES.PT.ViaStartRight
pool:
name: SHINE-INT-L
os: windows
sdl:
tsa:
enabled: true
configFile: '$(Build.SourcesDirectory)\build\config\tsa.json'
binskim:
enabled: true
policheck:
enabled: false
severity: Note
baseline:
baselineFile: '$(Build.SourcesDirectory)\build\config\release.gdnbaselines'
suppressionSet: default
stages:
- stage: Build
displayName: Build
dependsOn: []
jobs:
- template: ./build/pipelines/templates-v2/job-build-project.yml@self
parameters:
outerTemplateContext:
outputs:
- output: pipelineArtifact
targetPath: $(JobOutputDirectory)
artifactName: $(JobOutputArtifactName)
publishArtifacts: false # Handled by 1ESPT
branding: ${{ parameters.branding }}
buildTerminal: ${{ parameters.buildTerminal }}
buildConPTY: ${{ parameters.buildConPTY }}
buildWPF: ${{ parameters.buildWPF }}
pgoBuildMode: ${{ parameters.pgoBuildMode }}
buildConfigurations: ${{ parameters.buildConfigurations }}
buildPlatforms: ${{ parameters.buildPlatforms }}
generateSbom: false # this is handled by 1ESPT
removeAllNonSignedFiles: true # appease the overlords
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
beforeBuildSteps:
- template: ./build/pipelines/templates-v2/steps-setup-versioning.yml@self
- template: ./build/pipelines/templates-v2/steps-install-terrapin.yml@self
- task: UniversalPackages@0
displayName: Download terminal-internal Universal Package
inputs:
feedListDownload: 2b3f8893-a6e8-411f-b197-a9e05576da48
packageListDownload: e82d490c-af86-4733-9dc4-07b772033204
versionListDownload: ${{ parameters.terminalInternalPackageVersion }}
- ${{ if eq(parameters.buildWPF, true) }}:
# Add an Any CPU build flavor for the WPF control bits
- template: ./build/pipelines/templates-v2/job-build-project.yml@self
parameters:
outerTemplateContext:
outputs:
- output: pipelineArtifact
targetPath: $(JobOutputDirectory)
artifactName: $(JobOutputArtifactName)
publishArtifacts: false # Handled by 1ESPT
jobName: BuildWPF
branding: ${{ parameters.branding }}
buildTerminal: false
buildWPFDotNetComponents: true
buildConfigurations: ${{ parameters.buildConfigurations }}
buildPlatforms:
- Any CPU
generateSbom: false # this is handled by 1ESPT
removeAllNonSignedFiles: true # appease the overlords
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
beforeBuildSteps:
- template: ./build/pipelines/templates-v2/steps-setup-versioning.yml@self
# WPF doesn't need the localizations or the universal package, but if it does... put them here.
- stage: Package
displayName: Package
dependsOn: [Build]
jobs:
- ${{ if eq(parameters.buildTerminal, true) }}:
- template: ./build/pipelines/templates-v2/job-merge-msix-into-bundle.yml@self
parameters:
pool:
name: SHINE-INT-S
os: windows
outerTemplateContext:
outputs:
- output: pipelineArtifact
targetPath: $(JobOutputDirectory)
artifactName: $(JobOutputArtifactName)
publishArtifacts: false # Handled by 1ESPT
jobName: Bundle
branding: ${{ parameters.branding }}
buildConfigurations: ${{ parameters.buildConfigurations }}
buildPlatforms: ${{ parameters.buildPlatforms }}
generateSbom: false # Handled by 1ESPT
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
- ${{ if eq(parameters.buildConPTY, true) }}:
- template: ./build/pipelines/templates-v2/job-package-conpty.yml@self
parameters:
pool:
name: SHINE-INT-S
os: windows
outerTemplateContext:
outputs:
- output: pipelineArtifact
targetPath: $(JobOutputDirectory)
artifactName: $(JobOutputArtifactName)
publishArtifacts: false # Handled by 1ESPT
buildConfigurations: ${{ parameters.buildConfigurations }}
buildPlatforms: ${{ parameters.buildPlatforms }}
generateSbom: false # this is handled by 1ESPT
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
- ${{ if eq(parameters.buildWPF, true) }}:
- template: ./build/pipelines/templates-v2/job-build-package-wpf.yml@self
parameters:
pool:
name: SHINE-INT-S
os: windows
outerTemplateContext:
outputs:
- output: pipelineArtifact
targetPath: $(JobOutputDirectory)
artifactName: $(JobOutputArtifactName)
publishArtifacts: false # Handled by 1ESPT
buildConfigurations: ${{ parameters.buildConfigurations }}
buildPlatforms: ${{ parameters.buildPlatforms }}
generateSbom: false # this is handled by 1ESPT
codeSign: ${{ parameters.codeSign }}
signingIdentity: ${{ parameters.signingIdentity }}
- stage: Publish
displayName: Publish
dependsOn:
- Build
- ${{ if or(parameters.buildTerminal, parameters.buildConPTY, parameters.buildWPF) }}:
- Package
jobs:
- template: ./build/pipelines/templates-v2/job-publish-symbols-using-symbolrequestprod-api.yml@self
parameters:
pool:
name: SHINE-INT-S
os: windows
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
symbolExpiryTime: ${{ parameters.symbolExpiryTime }}
subscription: ${{ parameters.symbolPublishingSubscription }}
symbolProject: ${{ parameters.symbolPublishingProject }}
- ${{ parameters.extraPublishJobs }}

View File

@@ -153,7 +153,10 @@ stages:
- stage: Publish
displayName: Publish
pool: ${{ parameters.pool }}
dependsOn: [Build, Package]
dependsOn:
- Build
- ${{ if or(parameters.buildTerminal, parameters.buildConPTY, parameters.buildWPF) }}:
- Package
jobs:
# We only support the vpack for Release builds that include Terminal
- ${{ if and(containsValue(parameters.buildConfigurations, 'Release'), parameters.buildTerminal, parameters.publishVpackToWindows) }}:

View File

@@ -279,7 +279,10 @@ extends:
- stage: Publish
displayName: Publish
dependsOn: [Build]
dependsOn:
- Build
- ${{ if or(parameters.buildTerminal, parameters.buildConPTY, parameters.buildWPF) }}:
- Package
jobs:
- template: ./build/pipelines/templates-v2/job-publish-symbols-using-symbolrequestprod-api.yml@self
parameters:

View File

@@ -0,0 +1,6 @@
steps:
- pwsh: |-
nuget install -source "https://pkgs.dev.azure.com/microsoft/_packaging/WindowsTerminal/nuget/v3/index.json" TerrapinRetrievalTool -Prerelease -OutputDirectory _trt
$TerrapinRetrievalToolPath = (Get-Item _trt\TerrapinRetrievalTool.*\win-x64\TerrapinRetrievalTool.exe).FullName
Write-Host "##vso[task.setvariable variable=X_VCPKG_ASSET_SOURCES]x-script,${TerrapinRetrievalToolPath} -b https://vcpkg.storage.devpackages.microsoft.io/artifacts/ -a true -u None -p {url} -s {sha512} -d {dst};x-block-origin"
displayName: Set up the Terrapin Retrieval Tool (vcpkg cache)

View File

@@ -1143,13 +1143,6 @@ til::CoordType ROW::GetTrailingColumnAtCharOffset(const ptrdiff_t offset) const
return _createCharToColumnMapper(offset).GetTrailingColumnAt(offset);
}
uint16_t ROW::GetCharOffset(til::CoordType col) const noexcept
{
const auto columns = GetReadableColumnCount();
const auto colBeg = clamp(col, 0, columns);
return _uncheckedCharOffset(gsl::narrow_cast<size_t>(colBeg));
}
DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept
{
const auto col = _clampedColumn(column);

View File

@@ -172,7 +172,6 @@ public:
std::wstring_view GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept;
til::CoordType GetLeadingColumnAtCharOffset(ptrdiff_t offset) const noexcept;
til::CoordType GetTrailingColumnAtCharOffset(ptrdiff_t offset) const noexcept;
uint16_t GetCharOffset(til::CoordType col) const noexcept;
DelimiterClass DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept;
auto AttrBegin() const noexcept { return _attr.begin(); }

View File

@@ -308,11 +308,6 @@ namespace TerminalAppLocalTests
// TerminalPage and not only create them successfully, but also create a
// tab using those settings successfully.
// - - - IMPORTANT - - -
// GH#14623: "closeOnExit": "never" is important for all test profiles. Without
// it, the spawned process exits immediately in the UAP test environment,
// and the default "automatic" close-on-exit behavior removes the
// tab/pane asynchronously, racing against test assertions.
static constexpr std::wstring_view settingsJson0{ LR"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
@@ -320,14 +315,12 @@ namespace TerminalAppLocalTests
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"closeOnExit": "never"
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
}
]
})" };
@@ -354,6 +347,10 @@ namespace TerminalAppLocalTests
void TabTests::TryDuplicateBadTab()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
// * Create a tab with a profile with GUID 1
// * Reload the settings so that GUID 1 is no longer in the list of profiles
// * Try calling _DuplicateFocusedTab on tab 1
@@ -368,14 +365,12 @@ namespace TerminalAppLocalTests
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"closeOnExit": "never"
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
}
]
})" };
@@ -387,8 +382,7 @@ namespace TerminalAppLocalTests
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
}
]
})" };
@@ -444,6 +438,10 @@ namespace TerminalAppLocalTests
void TabTests::TryDuplicateBadPane()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
// * Create a tab with a profile with GUID 1
// * Reload the settings so that GUID 1 is no longer in the list of profiles
// * Try calling _SplitPane(Duplicate) on tab 1
@@ -458,14 +456,12 @@ namespace TerminalAppLocalTests
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1,
"closeOnExit": "never"
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
}
]
})" };
@@ -477,8 +473,7 @@ namespace TerminalAppLocalTests
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
}
]
})" };
@@ -577,29 +572,25 @@ namespace TerminalAppLocalTests
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"tabTitle" : "Profile 0",
"historySize": 1,
"closeOnExit": "never"
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"tabTitle" : "Profile 1",
"historySize": 2,
"closeOnExit": "never"
"historySize": 2
},
{
"name" : "profile2",
"guid": "{6239a42c-3333-49a3-80bd-e8fdd045185c}",
"tabTitle" : "Profile 2",
"historySize": 3,
"closeOnExit": "never"
"historySize": 3
},
{
"name" : "profile3",
"guid": "{6239a42c-4444-49a3-80bd-e8fdd045185c}",
"tabTitle" : "Profile 3",
"historySize": 4,
"closeOnExit": "never"
"historySize": 4
}
],
"schemes":
@@ -702,6 +693,7 @@ namespace TerminalAppLocalTests
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
@@ -741,6 +733,10 @@ namespace TerminalAppLocalTests
void TabTests::MoveFocusFromZoomedPane()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
Log::Comment(L"Create a second pane");
@@ -786,6 +782,10 @@ namespace TerminalAppLocalTests
void TabTests::CloseZoomedPane()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
Log::Comment(L"Create a second pane");
@@ -841,6 +841,10 @@ namespace TerminalAppLocalTests
void TabTests::SwapPanes()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
Log::Comment(L"Setup 4 panes.");
@@ -1047,31 +1051,31 @@ namespace TerminalAppLocalTests
void TabTests::NextMRUTab()
{
// This is a test for GH#8025 - we want to make sure that MRU tab
// ordering works correctly and that in-order/disabled switching works.
//
// Note: We test MRU ordering directly rather than going through the
// command palette tab switcher, because the palette's anchor key
// handling auto-dismisses when no modifier keys are held (which we
// can't simulate in the test environment).
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
// This is a test for GH#8025 - we want to make sure that we can do both
// in-order and MRU tab traversal, using the tab switcher and with the
// tab switcher disabled.
auto page = _commonSetup();
Log::Comment(L"Create Tab[1]");
Log::Comment(L"Create a second tab");
TestOnUIThread([&page]() {
NewTerminalArgs newTerminalArgs{ 1 };
page->_OpenNewTab(newTerminalArgs);
});
VERIFY_ARE_EQUAL(2u, page->_tabs.Size());
Log::Comment(L"Create Tab[2]");
Log::Comment(L"Create a third tab");
TestOnUIThread([&page]() {
NewTerminalArgs newTerminalArgs{ 2 };
page->_OpenNewTab(newTerminalArgs);
});
VERIFY_ARE_EQUAL(3u, page->_tabs.Size());
Log::Comment(L"Create Tab[3]");
Log::Comment(L"Create a fourth tab");
TestOnUIThread([&page]() {
NewTerminalArgs newTerminalArgs{ 3 };
page->_OpenNewTab(newTerminalArgs);
@@ -1080,89 +1084,99 @@ namespace TerminalAppLocalTests
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify Tab[3] is focused");
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one");
});
Log::Comment(L"Select Tab[1]");
Log::Comment(L"Select the second tab");
TestOnUIThread([&page]() {
page->_SelectTab(1);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify Tab[1] is focused");
VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify the second tab is the focused one");
});
// MRU order should now be: Tab[1], Tab[3], Tab[2], Tab[0]
// Verify the MRU list directly.
Log::Comment(L"Verify MRU order: MRU[0]=Tab[1], MRU[1]=Tab[3]");
Log::Comment(L"Change the tab switch order to MRU switching");
TestOnUIThread([&page]() {
VERIFY_ARE_EQUAL(4u, page->_mruTabs.Size());
uint32_t mruIdx;
page->_tabs.IndexOf(page->_mruTabs.GetAt(0), mruIdx);
VERIFY_ARE_EQUAL(1u, mruIdx, L"MRU[0] should be Tab[1] (most recent)");
page->_tabs.IndexOf(page->_mruTabs.GetAt(1), mruIdx);
VERIFY_ARE_EQUAL(3u, mruIdx, L"MRU[1] should be Tab[3] (last tab added)");
page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::MostRecentlyUsed);
});
Log::Comment(L"Select MRU[1]=Tab[3] directly");
Log::Comment(L"Switch to the next MRU tab, which is the fourth tab");
TestOnUIThread([&page]() {
// The next MRU tab after Tab[1] is Tab[3]
uint32_t nextMruIdx;
page->_tabs.IndexOf(page->_mruTabs.GetAt(1), nextMruIdx);
page->_SelectTab(nextMruIdx);
page->_SelectNextTab(true, nullptr);
});
Log::Comment(L"Sleep to let events propagate");
Sleep(250);
TestOnUIThread([&page]() {
Log::Comment(L"Hide the command palette, to confirm the selection");
// If you don't do this, the palette will just stay open, and the
// next time we call _HandleNextTab, we'll continue traversing the
// MRU list, instead of just hoping one entry.
page->LoadCommandPalette().Visibility(Visibility::Collapsed);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify Tab[3] is focused");
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one");
});
Log::Comment(L"Select MRU[1]=Tab[1] directly");
Log::Comment(L"Switch to the next MRU tab, which is the second tab");
TestOnUIThread([&page]() {
uint32_t nextMruIdx;
page->_tabs.IndexOf(page->_mruTabs.GetAt(1), nextMruIdx);
page->_SelectTab(nextMruIdx);
page->_SelectNextTab(true, nullptr);
});
Log::Comment(L"Sleep to let events propagate");
Sleep(250);
TestOnUIThread([&page]() {
Log::Comment(L"Hide the command palette, to confirm the selection");
// If you don't do this, the palette will just stay open, and the
// next time we call _HandleNextTab, we'll continue traversing the
// MRU list, instead of just hoping one entry.
page->LoadCommandPalette().Visibility(Visibility::Collapsed);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify Tab[1] is focused");
VERIFY_ARE_EQUAL(1u, focusedIndex, L"Verify the second tab is the focused one");
});
Log::Comment(L"Change the tab switch order to in-order switching");
page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::InOrder);
Log::Comment(L"Switch to the next in-order tab, which is the third tab");
TestOnUIThread([&page]() {
page->_SelectNextTab(true, nullptr);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(2u, focusedIndex, L"Verify the third tab is the focused one");
});
// The Disabled tab switcher mode uses direct index-based switching
// without the command palette, so it works in the test environment.
Log::Comment(L"Change the tab switch order to not use the tab switcher (which is in-order always)");
page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::Disabled);
Log::Comment(L"Switch to the next in-order tab: Tab[2]");
Log::Comment(L"Switch to the next in-order tab, which is the fourth tab");
TestOnUIThread([&page]() {
page->_SelectNextTab(true, nullptr);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(2u, focusedIndex, L"Verify Tab[2] is focused");
});
Log::Comment(L"Switch to the next in-order tab: Tab[3]");
TestOnUIThread([&page]() {
page->_SelectNextTab(true, nullptr);
});
TestOnUIThread([&page]() {
auto focusedIndex = page->_GetFocusedTabIndex().value_or(-1);
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify Tab[3] is focused");
VERIFY_ARE_EQUAL(3u, focusedIndex, L"Verify the fourth tab is the focused one");
});
}
void TabTests::VerifyCommandPaletteTabSwitcherOrder()
{
// This is a test for GH#8188 - we want to make sure that the MRU
// ordering is correctly maintained as tabs are selected.
//
// Note: We verify MRU ordering directly rather than going through
// the command palette tab switcher, because the palette's anchor key
// handling auto-dismisses when no modifier keys are held (which we
// can't simulate in the test environment).
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
// This is a test for GH#8188 - we want to make sure that the order of tabs
// is preserved in the CommandPalette's TabSwitcher
auto page = _commonSetup();
@@ -1175,7 +1189,7 @@ namespace TerminalAppLocalTests
});
VERIFY_ARE_EQUAL(4u, page->_mruTabs.Size());
Log::Comment(L"give alphabetical names to all tabs");
Log::Comment(L"give alphabetical names to all switch tab actions");
TestOnUIThread([&page]() {
page->_GetTabImpl(page->_tabs.GetAt(0))->Title(L"a");
});
@@ -1197,14 +1211,18 @@ namespace TerminalAppLocalTests
VERIFY_ARE_EQUAL(L"c", page->_tabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"d", page->_tabs.GetAt(3).Title());
// MRU order after creating Tab[0]-Tab[3]: MRU[0]=Tab[3], MRU[3]=Tab[0]
VERIFY_ARE_EQUAL(L"d", page->_mruTabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"c", page->_mruTabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"b", page->_mruTabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title());
});
Log::Comment(L"Select Tab[0] through Tab[3] to establish MRU order");
Log::Comment(L"Change the tab switch order to MRU switching");
TestOnUIThread([&page]() {
page->_settings.GlobalSettings().TabSwitcherMode(TabSwitcherMode::MostRecentlyUsed);
});
Log::Comment(L"Select the tabs from 0 to 3");
RunOnUIThread([&page]() {
page->_UpdatedSelectedTab(page->_tabs.GetAt(0));
page->_UpdatedSelectedTab(page->_tabs.GetAt(1));
@@ -1212,31 +1230,47 @@ namespace TerminalAppLocalTests
page->_UpdatedSelectedTab(page->_tabs.GetAt(3));
});
Log::Comment(L"Verify MRU order: MRU[0]='d', MRU[1]='c', MRU[2]='b', MRU[3]='a'");
VERIFY_ARE_EQUAL(4u, page->_mruTabs.Size());
VERIFY_ARE_EQUAL(L"d", page->_mruTabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"c", page->_mruTabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"b", page->_mruTabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title());
Log::Comment(L"Select Tab[2]='c' (MRU[1] after 'd')");
TestOnUIThread([&page]() {
page->_SelectTab(2);
Log::Comment(L"Switch to the next MRU tab, which is the third tab");
RunOnUIThread([&page]() {
page->_SelectNextTab(true, nullptr);
// In the course of a single tick, the Command Palette will:
// * open
// * select the proper tab from the mru's list
// * raise an event for _filteredActionsView().SelectionChanged to
// immediately preview the new tab
// * raise a _SwitchToTabRequestedHandlers event
// * then dismiss itself, because we can't fake holing down an
// anchor key in the tests
});
Log::Comment(L"Verify MRU order updated: MRU[0]='c', MRU[1]='d', MRU[2]='b', MRU[3]='a'");
TestOnUIThread([&page]() {
VERIFY_ARE_EQUAL(L"c", page->_mruTabs.GetAt(0).Title());
VERIFY_ARE_EQUAL(L"d", page->_mruTabs.GetAt(1).Title());
VERIFY_ARE_EQUAL(L"b", page->_mruTabs.GetAt(2).Title());
VERIFY_ARE_EQUAL(L"a", page->_mruTabs.GetAt(3).Title());
});
const auto palette = winrt::get_self<winrt::TerminalApp::implementation::CommandPalette>(page->LoadCommandPalette());
VERIFY_ARE_EQUAL(winrt::TerminalApp::implementation::CommandPaletteMode::TabSwitchMode, palette->_currentMode, L"Verify we are in the tab switcher mode");
// At this point, the contents of the command palette's _mruTabs list is
// still the _old_ ordering (d, c, b, a). The ordering is only updated
// in TerminalPage::_SelectNextTab, but as we saw before, the palette
// will also dismiss itself immediately when that's called. So we can't
// really inspect the contents of the list in this test, unfortunately.
}
void TabTests::TestWindowRenameSuccessful()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
@@ -1269,6 +1303,7 @@ namespace TerminalAppLocalTests
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
auto page = _commonSetup();
@@ -1301,6 +1336,10 @@ namespace TerminalAppLocalTests
void TabTests::TestPreviewCommitScheme()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
Log::Comment(L"Preview a color scheme. Make sure it's applied, then committed accordingly");
auto page = _commonSetup();
@@ -1363,6 +1402,10 @@ namespace TerminalAppLocalTests
void TabTests::TestPreviewDismissScheme()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
Log::Comment(L"Preview a color scheme. Make sure it's applied, then dismissed accordingly");
auto page = _commonSetup();
@@ -1411,6 +1454,10 @@ namespace TerminalAppLocalTests
void TabTests::TestPreviewSchemeWhilePreviewing()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
Log::Comment(L"Preview a color scheme, then preview another scheme. ");
Log::Comment(L"Preview a color scheme. Make sure it's applied, then committed accordingly");
@@ -1478,6 +1525,10 @@ namespace TerminalAppLocalTests
void TabTests::TestClampSwitchToTab()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Ignore", L"True") // GH#19610 tracks re-enabling this test
END_TEST_METHOD_PROPERTIES()
Log::Comment(L"Test that switching to a tab index higher than the number of tabs just clamps to the last tab.");
auto page = _commonSetup();

View File

@@ -2906,6 +2906,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
else
{
// Do we ever get here (= uninitialized terminal)? If so: How?
assert(false);
return { 10, 10 };
}
}

View File

@@ -1061,7 +1061,38 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
ROW* rowBackup = nullptr;
if (row == compositionRow)
{
rowBackup = _PaintBufferOutputComposition(buffer, r, activeComposition);
auto& scratch = buffer.GetScratchpadRow();
scratch.CopyFrom(r);
rowBackup = &scratch;
std::wstring_view text{ activeComposition.text };
RowWriteState state{
.columnLimit = r.GetReadableColumnCount(),
.columnEnd = _compositionCache->absoluteOrigin.x,
};
size_t off = 0;
for (const auto& range : activeComposition.attributes)
{
const auto len = range.len;
auto attr = range.attr;
// Use the color at the cursor if TSF didn't specify any explicit color.
if (attr.GetBackground().IsDefault())
{
attr.SetBackground(_compositionCache->baseAttribute.GetBackground());
}
if (attr.GetForeground().IsDefault())
{
attr.SetForeground(_compositionCache->baseAttribute.GetForeground());
}
state.text = text.substr(off, len);
state.columnBegin = state.columnEnd;
const_cast<ROW&>(r).ReplaceText(state);
const_cast<ROW&>(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr);
off += len;
}
}
const auto restore = wil::scope_exit([&] {
if (rowBackup)
@@ -1101,107 +1132,6 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
}
}
ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition)
{
auto& scratch = buffer.GetScratchpadRow();
scratch.CopyFrom(r);
// *Overwrite* the original text with the active composition...
til::CoordType compositionEnd = 0;
{
std::wstring_view text{ activeComposition.text };
RowWriteState state{
.columnLimit = r.GetReadableColumnCount(),
.columnEnd = _compositionCache->absoluteOrigin.x,
};
size_t off = 0;
for (const auto& range : activeComposition.attributes)
{
const auto len = range.len;
auto attr = range.attr;
// Use the color at the cursor if TSF didn't specify any explicit color.
if (attr.GetBackground().IsDefault())
{
attr.SetBackground(_compositionCache->baseAttribute.GetBackground());
}
if (attr.GetForeground().IsDefault())
{
attr.SetForeground(_compositionCache->baseAttribute.GetForeground());
}
state.text = text.substr(off, len);
state.columnBegin = state.columnEnd;
const_cast<ROW&>(r).ReplaceText(state);
const_cast<ROW&>(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr);
off += len;
}
compositionEnd = state.columnEnd;
}
// The text we've overwritten may have been crucial to the user,
// so copy it back by absorbing available whitespace to the right
// and re-inserting the non-whitespace characters instead.
const auto compositionWidth = compositionEnd - _compositionCache->absoluteOrigin.x;
const auto colLimit = r.GetReadableColumnCount();
if (compositionWidth > 0 && compositionEnd < colLimit)
{
const auto text = scratch.GetText();
auto srcCol = _compositionCache->absoluteOrigin.x;
auto dstCol = compositionEnd;
auto remaining = compositionWidth;
size_t i = scratch.GetCharOffset(srcCol);
while (i < text.size() && dstCol < colLimit)
{
// Treat whitespace we encounter as a credit towards our composition width.
// This loop essentially absorbs the whitespace.
while (i < text.size() && til::at(text, i) == L' ' && remaining > 0)
{
remaining--;
srcCol++;
i++;
}
if (remaining <= 0)
{
break;
}
// Find the end of the non-whitespace span: Our span of text to insert.
auto spanEnd = i;
while (spanEnd < text.size() && til::at(text, spanEnd) != L' ')
{
spanEnd++;
}
// Copy the non-whitespace segment from the original text (scratch) back in.
RowCopyTextFromState state{
.source = scratch,
.columnBegin = dstCol,
.columnLimit = colLimit,
.sourceColumnBegin = srcCol,
.sourceColumnLimit = scratch.GetLeadingColumnAtCharOffset(spanEnd),
};
const_cast<ROW&>(r).CopyTextFrom(state);
const auto srcBeg = gsl::narrow_cast<uint16_t>(srcCol);
const auto srcEnd = gsl::narrow_cast<uint16_t>(state.sourceColumnEnd);
const auto attr = scratch.Attributes().slice(srcBeg, srcEnd);
const auto dstBeg = gsl::narrow_cast<uint16_t>(dstCol);
const auto dstEnd = gsl::narrow_cast<uint16_t>(dstCol + attr.size());
const_cast<ROW&>(r).Attributes().replace(dstBeg, dstEnd, attr);
dstCol = state.columnEnd;
srcCol = state.sourceColumnEnd;
i = spanEnd;
}
}
return &scratch;
}
static bool _IsAllSpaces(const std::wstring_view v)
{
// first non-space char is not found (is npos)

View File

@@ -121,7 +121,6 @@ namespace Microsoft::Console::Render
void _scheduleRenditionBlink();
[[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine);
void _PaintBufferOutput(_In_ IRenderEngine* const pEngine);
ROW* _PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition);
void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target);
void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget);
bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept;