How HALLELUJAH IM Handles the Composition Buffer and Original Buffer States
The HALLELUJAH IM engine maintains two distinct mutable string buffers—an original buffer for raw keystrokes and a composition buffer for finalized candidates—synchronizing them through the InputController class in src/InputController.mm before committing text to the client application.
The HALLELUJAH input method (dongyuwei/hallelujahim) implements a dual-buffer architecture to manage the complex lifecycle of text composition on macOS. By separating raw user input from converted candidates, the engine enables precise control over pre-edit display, deletion handling, and final text insertion while driving the candidate lookup mechanism.
Dual-Buffer Architecture
The input method maintains two separate NSMutableString properties in src/InputController.mm:
| Buffer | Purpose | Implementation Location |
|---|---|---|
Original Buffer (_originalBuffer) |
Holds raw keystrokes entered by the user to drive candidate lookup and spelling suggestions. | Accessor methods originalBuffer, setOriginalBuffer:, and originalBufferAppend: at lines 70-78. |
Composition Buffer (_composedBuffer) |
Contains the finalized text (selected candidate) ready for insertion into the client application. | Accessor methods composedBuffer, setComposedBuffer:, and appendToComposedBuffer: at lines 58-68. |
Buffer Lifecycle and State Management
Initialization via reset
The reset method clears both buffers and resets the insertion index when starting a new composition or after committing text:
- (void)reset {
[self setComposedBuffer:@""];
[self setOriginalBuffer:@""];
_insertionIndex = 0;
…
}
Source: src/InputController.mm lines 45-52.
Appending Input to the Original Buffer
When the user types alphabetic characters, onKeyEvent: appends the character to the original buffer and updates the candidate list:
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
[self originalBufferAppend:characters client:sender];
[sharedCandidates updateCandidates];
…
}
Source: src/InputController.mm lines 21-28.
Candidate Selection and Composition Updates
When a user selects a candidate, the engine copies the candidate text into the composition buffer via _updateComposedBuffer: before committing:
- (void)candidateSelected:(NSAttributedString *)candidateString {
[self _updateComposedBuffer:candidateString];
[self commitComposition:_currentClient];
}
Source: src/InputController.mm lines 32-36.
Committing Compositions
The commitComposition: method delivers the composition buffer to the client, falling back to the original buffer if the composition buffer is empty:
- (void)commitComposition:(id)sender {
NSString *text = [self composedBuffer];
if (text == nil || text.length == 0) {
text = [self originalBuffer];
}
…
[sender insertText:text replacementRange:NSMakeRange(NSNotFound, NSNotFound)];
[self reset];
}
Source: src/InputController.mm lines 12-30.
Pressing Space or Return triggers this commit path, while Escape invokes cancelComposition to discard the buffers and insert an empty string.
Handling Backspace and Synchronization
The deleteBackward: method keeps both buffers synchronized by removing the last character from the original buffer and mirroring the truncated string to the composition buffer:
NSMutableString *originalText = [self originalBuffer];
if (_insertionIndex > 0) {
--_insertionIndex;
NSString *convertedString = [originalText substringToIndex:originalText.length - 1];
[self setComposedBuffer:convertedString];
[self setOriginalBuffer:convertedString];
…
}
Source: src/InputController.mm lines 88-99.
Pre-edit Visualization
The showPreeditString: method constructs an attributed string that highlights the original buffer portion of the pre-edit text, sending it to the client via setMarkedText::
- (void)showPreeditString:(NSString *)input {
NSDictionary *attrs = [self markForStyle:kTSMHiliteSelectedRawText atRange:NSMakeRange(0, input.length)];
…
NSString *originalBuff = [NSString stringWithString:[self originalBuffer]];
if ([input.lowercaseString hasPrefix:originalBuff.lowercaseString]) {
attrString = [[NSAttributedString alloc]
initWithString:[NSString stringWithFormat:@"%@%@", originalBuff,
[input substringFromIndex:originalBuff.length]]
attributes:attrs];
} else {
…
}
[_currentClient setMarkedText:attrString …];
}
Source: src/InputController.mm lines 82-92.
Key Implementation Files
src/InputController.h: Declares the buffer properties (_originalBuffer,_composedBuffer), state variables, and public method signatures.src/InputController.mm: Implements the complete buffer lifecycle including keystroke handling, candidate UI management, and commit/cancel logic.src/ConversionEngine.mm: Generates candidate lists based on the current content of the original buffer.src/AnnotationWinController.m: Renders optional translation windows after candidate selection.
Summary
- The original buffer (
_originalBuffer) tracks raw user keystrokes usingoriginalBufferAppend:and drives candidate lookups via the conversion engine. - The composition buffer (
_composedBuffer) stores the finalized candidate text populated bysetComposedBuffer:and inserted viacommitComposition:. - Both buffers synchronize during
deleteBackward:operations and clear simultaneously viaresetwhen compositions end. - The engine provides visual feedback by highlighting the original buffer portion of pre-edit text through
showPreeditString:before final insertion.
Frequently Asked Questions
What is the difference between the original buffer and composition buffer in HALLELUJAH IM?
The original buffer stores raw keystrokes as the user types, functioning as the search key for candidate generation in ConversionEngine.mm. The composition buffer holds the finalized text—usually a selected candidate—that will be inserted into the application when the user commits the composition via commitComposition:.
How does HALLELUJAH IM handle the Escape key during composition?
When the user presses Escape, the cancelComposition method discards the current composition buffer, inserts an empty string into the client application, and calls reset to clear both buffers and reset the insertion index to zero, effectively canceling the current input session.
What happens when committing an empty composition buffer?
The commitComposition: method includes fallback logic that checks if the composition buffer is nil or empty. If so, it uses the original buffer content instead, ensuring that pressing Space or Return without selecting a candidate still inserts the typed romanization or raw input.
How are the buffers kept synchronized during backspace operations?
The deleteBackward: method in src/InputController.mm decrements the _insertionIndex, removes the last character from the original buffer, and immediately copies the truncated string to the composition buffer via setComposedBuffer:. This ensures both buffers reflect the current deletion state and remain consistent for the pre-edit display.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →