Skip to content

A minor TSF refactoring - #17067

Merged
Leonard Hecker (lhecker) merged 7 commits into
mainfrom
dev/lhecker/8000-conime
Apr 18, 2024
Merged

Leonard Hecker (lhecker) merged 7 commits into
mainfrom
dev/lhecker/8000-conime

Conversation

@lhecker

@lhecker Leonard Hecker (lhecker) commented Apr 16, 2024 •

Copy link
Copy Markdown
Member

Next in the popular series of minor refactorings:
Out with the old, in with the new!

This PR removes all of the existing TSF code, both for conhost and
Windows Terminal. conhost's TSF implementation was awful:
It allocated an entire text buffer per line of input.
Additionally, its implementation spanned a whopping 40 files and
almost 5000 lines of code. Windows Terminal's implementation was
absolutely fine in comparison, but it was user unfriendly due to
two reasons: Its usage of the CoreTextServices WinRT API indirectly
meant that it used a non-transitory TSF document, which is not the
right choice for a terminal. A TF_SS_TRANSITORY document (-context)
indicates to TSF that it cannot undo a previously completed composition
which is exactly what we need: Once composition has completed we send
the result to the shell and we cannot undo this later on.
The WinRT API does not allow us to use TF_SS_TRANSITORY and so it's
unsuitable for our application. Additionally, the implementation used
XAML to render the composition instead of being part of our text
renderer, which resulted in the text looking weird and hard to read.

The new implementation spans just 8 files and is ~1000 lines which
should make it significantly easier to maintain. The architecture is
not particularly great, but it's certainly better than what we had.
The implementation is almost entirely identical between both conhost
and Windows Terminal and thus they both also behave identical.
It fixes an uncountable number of subtle bugs in the conhost TSF
implementation, as it failed to check for status codes after calls.
It also adds several new features, like support for wavy underlines
(as used by the Japanese IME), dashed underlines (the default for
various languages now, like Vietnamese), colored underlines,
colored foreground/background controlled by the IME, and more!

I have tried to replicate the following issues and have a high
confidence that they're resolved now:
Closes #1304
Closes #3730
Closes #4052
Closes #5007 (as it is not applicable anymore)
Closes #5110
Closes #6186
Closes #6192
Closes #13805
Closes #14349
Closes #14407
Closes #16180

For the following issues I'm not entirely sure if it'll fix it,
but I suspect it's somewhat likely:
#13681
#16305
#16817

Lastly, there's one remaining bug that I don't know how to resolve.
However, that issue also plagues conhost and Windows Terminal
right now, so it's at least not a regression:

  • Press Win+. (emoji picker) and close it
  • Move the window around
  • Press Win+.

This will open the emoji picker at the old window location.
It also occurs when the cursor moves within the window.
While this is super annoying, I could not find a way to fix it.

Validation Steps Performed

  • See the above closed issues
  • Use Vietnamese Telex and type "xin choaf"
    Results in "xin chào" ✅
  • Use the MS Japanese IME and press Alt+`
    Toggles between the last 2 modes ✅
  • Use the MS Japanese IME, type "kyouhaishaheiku", and press Space
    • The text is converted, underlined and the first part is
      doubly underlined ✅
    • Left/Right moves between the 3 segments ✅
    • Home/End moves between start/end ✅
    • Esc puts a wavy line under the current segment ✅
  • Use the Korean IME, type "gksgks"
    This results in "한한" ✅
  • Use the Korean IME, type "gks", and press Right Ctrl
    Opens a popup which allows you to navigate with Arrow/Tab keys ✅

@microsoft-github-policy-service microsoft-github-policy-service Bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. Issue-Task It's a feature request, but it doesn't really need a major design. Area-i18n Internationalization issues (e.g.: non-US input handling doesn't work) Area-Input Related to input processing (key presses, mouse, etc.) Area-Rendering Text rendering, emoji, complex glyph & font-fallback issues Area-TerminalControl Issues pertaining to the terminal control (input, selection, keybindings, mouse interaction, etc.) Priority-1 A description (P1) Priority-2 A description (P2) Priority-3 A description (P3) Product-Conhost For issues in the Console codebase Product-Terminal The new Windows Terminal. labels Apr 16, 2024
@zadjii-msft

Copy link
Copy Markdown
Member

i dunno though, is fixing 11 issues in one PR enough to be considered "minor"? this is a trivial refactoring at best 😝

@miniksa

Copy link
Copy Markdown
Contributor

You're a champion, Leonard Hecker (@lhecker)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the PR is deleted files and deleting references to the deleted stuff, so that's nice.

The most complicated part is src/tsf/Implementation.cpp, but a good amount of the complexity there is that it's a lot of COM.

That said, looks great!

Comment on lines +187 to +190
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
};

state.text = text.substr(0, _pData->activeComposition.cursorPos);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
};
state.text = text.substr(0, _pData->activeComposition.cursorPos);
.columnLimit = buffer.GetRowByOffset(line.top).GetReadableColumnCount(),
.text = text.substr(0, _pData->activeComposition.cursorPos),
};

Curious, any particular reason you didn't initialize it here with columnLimit?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it so that the two ReplaceText() calls look somewhat symmetric. I felt like it's easier to read/understand that way.

// 3. Paint overlays that reside above the text buffer
_PaintOverlays(pEngine);

// 4. Paint Selection

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 4. Paint Selection
// 3. Paint Selection

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and update the other numbers below

@lhecker Leonard Hecker (lhecker) Apr 18, 2024 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not changing this code right now, because I'd like to remove it in the near-ish term and changing it now would only disturb the git blame until then IMHO.

Comment thread OpenConsole.sln Resolved
Comment thread src/interactivity/win32/windowproc.cpp Resolved
Comment thread src/interactivity/win32/windowproc.cpp Resolved
}

// Free all resources used by this thread
DeactivateTextServices();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any resources we need to free in the new code?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, yes. However, this code is currently effectively unreachable, because we TerminateProcess in the ServiceLocator on shutdown. A better location for shutting down TSF is in the destructor of Window but I figured it's fine if we just don't. After all, we also got the delete renderer calls commented out there.

Comment thread src/interactivity/win32/windowio.cpp Resolved
Comment thread src/tsf/Implementation.cpp Resolved
@lhecker
Leonard Hecker (lhecker) added this pull request to the merge queue Apr 18, 2024
Merged via the queue into main with commit 4e7b63c Apr 18, 2024
@lhecker
Leonard Hecker (lhecker) deleted the dev/lhecker/8000-conime branch April 18, 2024 18:18
加村有司 (YuujiKamura) pushed a commit to YuujiKamura/ghostty that referenced this pull request Mar 17, 2026
Document the design decisions inherited from Windows Terminal's TSF
rewrite by Leonard Hecker (microsoft/terminal#17067). The header now
explains why direct TSF was chosen over XAML TextBox IME, the
EditSessionProxy pattern, range walking with GUID_PROP_COMPOSING,
and callback-driven output architecture.

Closes #112

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jason (drvoss) added a commit to drvoss/terminal that referenced this pull request Mar 31, 2026
When an arrow key is pressed during an active Korean IME composition,
_KeyHandler was forwarding the key to the PTY immediately via
_TrySendKeyEvent. This moved the cursor before the async TSF edit
session had a chance to commit the finalized text, causing the
committed character to land at the wrong position.

The fix adds a guard that mirrors the existing behavior in the conhost
v1 key handler (src/interactivity/win32/windowio.cpp lines 361-364):
suppress all key events from being forwarded to the PTY while TSF has
an active composition. The IME re-injects navigation and confirmation
keys after the composition ends, at which point HasActiveComposition()
returns false and they are forwarded normally.

Historical note: this guard was not needed before PR microsoft#17067 (v1.22)
because the old TSF implementation used WinRT CoreTextServices via
XAML (TSFInputControl.xaml/.cpp). In that model the XAML framework
handled composition key events before they reached _KeyHandler, so
_KeyHandler never received them during composition. The new custom
Win32 TSF context introduced in microsoft#17067 no longer intercepts keys at
the XAML level, making _KeyHandler responsible for this check.

The bug was not visible in v1.22-v1.23 for Korean because the Korean
IME was excluded from the TSF path by TF_TMAE_UIELEMENTENABLEDONLY.
It became visible in v1.24 when PR microsoft#19738 removed that flag, causing
the Korean IME to activate through TSF for the first time.

Closes microsoft#20038
Dustin L. Howett (DHowett) pushed a commit that referenced this pull request Mar 31, 2026
Fix a regression introduced in v1.24 where pressing an arrow key during
Korean IME composition caused the committed character to be inserted at
the wrong cursor position.

Before #19738, the Korean IME activated through IMM32 (excluded from TSF
by `TF_TMAE_UIELEMENTENABLEDONLY`) and was not affected by
`TermControl::_KeyHandler` forwarding keys to the PTY during
composition. After #19738, the Korean IME activates through TSF, making
a missing composition guard in `_KeyHandler` visible as a bug.

The sequence of events that causes the bug:

1. User presses Left arrow during active Korean IME composition (e.g.
composing `가`).
2. `_KeyHandler` calls `_TrySendKeyEvent(VK_LEFT)` which enqueues
`\x1b[D` to the PTY input queue. The cursor moves.
3. TSF processes the key. The Korean IME sees the arrow and ends the
composition.
4. `OnEndComposition` schedules `_doCompositionUpdate` with
`TF_ES_ASYNC`.
5. The async session fires, reads finalized text `가`, calls
`HandleOutput("가")`.
6. PTY processes `[\x1b[D, "가"]`: cursor moves left first, then `가` is
inserted at the wrong (already-moved) position.

The fix adds a guard before `_TrySendKeyEvent`, which mirrors the
existing behavior in conhost (windowio.cpp). When TSF has an active
composition, key events are not converted into input. The Korean IME
re-injects navigation and confirmation keys after the composition ends,
at which point `HasActiveComposition()` returns false and they are
forwarded normally.

**Historical note:** This guard was not needed before PR #17067 (v1.22)
because the old implementation used WinRT `CoreTextServices` via XAML
(`TSFInputControl.xaml`). The XAML framework intercepted composition key
events before `_KeyHandler`. The new custom Win32 TSF context in #17067
no longer does this. The bug was latent from v1.22 but only became
visible for Korean in v1.24 when #19738 removed
`TF_TMAE_UIELEMENTENABLEDONLY`.

## Validation Steps Performed

1. Open Windows Terminal with Korean IME (Dubeolsik layout).
2. Type `rk` to begin composing `가` (composition active, syllable not
yet committed).
3. Press the Left arrow key.
4. Before fix: `가` is inserted one cell to the left of the intended
position.
5. After fix: `가` is inserted at the correct position, then cursor moves
left.

Also verified:
- Normal Korean text input (typing without arrow keys) still works
correctly.
- Arrow key navigation when no composition is active still works
correctly.
- English and other IME input is not affected.

Closes #20038
Refs #19738
Dustin L. Howett (DHowett) pushed a commit that referenced this pull request Mar 31, 2026
Fix a regression introduced in v1.24 where pressing an arrow key during
Korean IME composition caused the committed character to be inserted at
the wrong cursor position.

Before #19738, the Korean IME activated through IMM32 (excluded from TSF
by `TF_TMAE_UIELEMENTENABLEDONLY`) and was not affected by
`TermControl::_KeyHandler` forwarding keys to the PTY during
composition. After #19738, the Korean IME activates through TSF, making
a missing composition guard in `_KeyHandler` visible as a bug.

The sequence of events that causes the bug:

1. User presses Left arrow during active Korean IME composition (e.g.
composing `가`).
2. `_KeyHandler` calls `_TrySendKeyEvent(VK_LEFT)` which enqueues
`\x1b[D` to the PTY input queue. The cursor moves.
3. TSF processes the key. The Korean IME sees the arrow and ends the
composition.
4. `OnEndComposition` schedules `_doCompositionUpdate` with
`TF_ES_ASYNC`.
5. The async session fires, reads finalized text `가`, calls
`HandleOutput("가")`.
6. PTY processes `[\x1b[D, "가"]`: cursor moves left first, then `가` is
inserted at the wrong (already-moved) position.

The fix adds a guard before `_TrySendKeyEvent`, which mirrors the
existing behavior in conhost (windowio.cpp). When TSF has an active
composition, key events are not converted into input. The Korean IME
re-injects navigation and confirmation keys after the composition ends,
at which point `HasActiveComposition()` returns false and they are
forwarded normally.

**Historical note:** This guard was not needed before PR #17067 (v1.22)
because the old implementation used WinRT `CoreTextServices` via XAML
(`TSFInputControl.xaml`). The XAML framework intercepted composition key
events before `_KeyHandler`. The new custom Win32 TSF context in #17067
no longer does this. The bug was latent from v1.22 but only became
visible for Korean in v1.24 when #19738 removed
`TF_TMAE_UIELEMENTENABLEDONLY`.

## Validation Steps Performed

1. Open Windows Terminal with Korean IME (Dubeolsik layout).
2. Type `rk` to begin composing `가` (composition active, syllable not
yet committed).
3. Press the Left arrow key.
4. Before fix: `가` is inserted one cell to the left of the intended
position.
5. After fix: `가` is inserted at the correct position, then cursor moves
left.

Also verified:
- Normal Korean text input (typing without arrow keys) still works
correctly.
- Arrow key navigation when no composition is active still works
correctly.
- English and other IME input is not affected.

Closes #20038
Refs #19738

(cherry picked from commit 14f4271)
Service-Card-Id: PVTI_lADOAF3p4s4BQX0-zgo0JpM
Service-Version: 1.25
Dustin L. Howett (DHowett) pushed a commit that referenced this pull request Mar 31, 2026
Fix a regression introduced in v1.24 where pressing an arrow key during
Korean IME composition caused the committed character to be inserted at
the wrong cursor position.

Before #19738, the Korean IME activated through IMM32 (excluded from TSF
by `TF_TMAE_UIELEMENTENABLEDONLY`) and was not affected by
`TermControl::_KeyHandler` forwarding keys to the PTY during
composition. After #19738, the Korean IME activates through TSF, making
a missing composition guard in `_KeyHandler` visible as a bug.

The sequence of events that causes the bug:

1. User presses Left arrow during active Korean IME composition (e.g.
composing `가`).
2. `_KeyHandler` calls `_TrySendKeyEvent(VK_LEFT)` which enqueues
`\x1b[D` to the PTY input queue. The cursor moves.
3. TSF processes the key. The Korean IME sees the arrow and ends the
composition.
4. `OnEndComposition` schedules `_doCompositionUpdate` with
`TF_ES_ASYNC`.
5. The async session fires, reads finalized text `가`, calls
`HandleOutput("가")`.
6. PTY processes `[\x1b[D, "가"]`: cursor moves left first, then `가` is
inserted at the wrong (already-moved) position.

The fix adds a guard before `_TrySendKeyEvent`, which mirrors the
existing behavior in conhost (windowio.cpp). When TSF has an active
composition, key events are not converted into input. The Korean IME
re-injects navigation and confirmation keys after the composition ends,
at which point `HasActiveComposition()` returns false and they are
forwarded normally.

**Historical note:** This guard was not needed before PR #17067 (v1.22)
because the old implementation used WinRT `CoreTextServices` via XAML
(`TSFInputControl.xaml`). The XAML framework intercepted composition key
events before `_KeyHandler`. The new custom Win32 TSF context in #17067
no longer does this. The bug was latent from v1.22 but only became
visible for Korean in v1.24 when #19738 removed
`TF_TMAE_UIELEMENTENABLEDONLY`.

## Validation Steps Performed

1. Open Windows Terminal with Korean IME (Dubeolsik layout).
2. Type `rk` to begin composing `가` (composition active, syllable not
yet committed).
3. Press the Left arrow key.
4. Before fix: `가` is inserted one cell to the left of the intended
position.
5. After fix: `가` is inserted at the correct position, then cursor moves
left.

Also verified:
- Normal Korean text input (typing without arrow keys) still works
correctly.
- Arrow key navigation when no composition is active still works
correctly.
- English and other IME input is not affected.

Closes #20038
Refs #19738

(cherry picked from commit 14f4271)
Service-Card-Id: PVTI_lADOAF3p4s4BBcTlzgo0JpQ
Service-Version: 1.24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-i18n Internationalization issues (e.g.: non-US input handling doesn't work) Area-Input Related to input processing (key presses, mouse, etc.) Area-Rendering Text rendering, emoji, complex glyph & font-fallback issues Area-TerminalControl Issues pertaining to the terminal control (input, selection, keybindings, mouse interaction, etc.) Issue-Bug It either shouldn't be doing this or needs an investigation. Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. Issue-Task It's a feature request, but it doesn't really need a major design. Priority-1 A description (P1) Priority-2 A description (P2) Priority-3 A description (P3) Product-Conhost For issues in the Console codebase Product-Terminal The new Windows Terminal.

Projects

None yet

5 participants