Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/desktop/src/components/CommitMessageEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { WORKTREE_SERVICE } from '$lib/worktree/worktreeService.svelte';
import { inject } from '@gitbutler/core/context';
import { Button, TestId } from '@gitbutler/ui';
import { IME_COMPOSITION_HANDLER } from '@gitbutler/ui/utils/imeHandling';

import { tick } from 'svelte';

Expand Down Expand Up @@ -68,6 +69,7 @@

let composer = $state<ReturnType<typeof MessageEditor>>();
let titleInput = $state<HTMLTextAreaElement>();
const imeHandler = inject(IME_COMPOSITION_HANDLER);

const suggestionsHandler = new CommitSuggestions(aiService, uiState);
const diffInputArgs = $derived<DiffInputContextArgs>(
Expand Down Expand Up @@ -165,7 +167,8 @@
onchange={(value) => {
onChange?.({ title: value });
}}
onkeydown={async (e: KeyboardEvent) => {
oninput={imeHandler.handleInput()}
onkeydown={imeHandler.handleKeydown(async (e: KeyboardEvent) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
if (title.trim()) {
Expand All @@ -181,7 +184,7 @@
handleCancel();
}
e.stopPropagation();
}}
})}
/>
<MessageEditor
testId={TestId.CommitDrawerDescriptionInput}
Expand Down
10 changes: 6 additions & 4 deletions apps/desktop/src/components/ReviewCreation.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import { inject } from '@gitbutler/core/context';
import { persisted } from '@gitbutler/shared/persisted';
import { chipToasts, TestId } from '@gitbutler/ui';
import { IME_COMPOSITION_HANDLER } from '@gitbutler/ui/utils/imeHandling';
import { isDefined } from '@gitbutler/ui/utils/typeguards';
import { tick } from 'svelte';

Expand Down Expand Up @@ -91,6 +92,7 @@

let titleInput = $state<HTMLTextAreaElement | undefined>(undefined);
let messageEditor = $state<MessageEditor>();
const imeHandler = inject(IME_COMPOSITION_HANDLER);

// AI things
const aiGenEnabled = projectAiGenEnabled(projectId);
Expand Down Expand Up @@ -386,7 +388,7 @@
onchange={(value) => {
prTitle.set(value);
}}
onkeydown={(e: KeyboardEvent) => {
onkeydown={imeHandler.handleKeydown((e: KeyboardEvent) => {
if (e.key === 'Enter' || (e.key === 'Tab' && !e.shiftKey)) {
e.preventDefault();
messageEditor?.focus();
Expand All @@ -402,13 +404,13 @@
e.preventDefault();
onClose();
}
}}
})}
placeholder="PR title"
showCount={false}
oninput={(e: Event) => {
oninput={imeHandler.handleInput((e: Event) => {
const target = e.target as HTMLInputElement;
prTitle.set(target.value);
}}
})}
/>
<MessageEditor
forceSansFont
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/lib/bootstrap/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
EXTERNAL_LINK_SERVICE,
type ExternalLinkService
} from '@gitbutler/ui/utils/externalLinkService';
import { IMECompositionHandler, IME_COMPOSITION_HANDLER } from '@gitbutler/ui/utils/imeHandling';
import { PUBLIC_API_BASE_URL } from '$env/static/public';

export function initDependencies(args: {
Expand Down Expand Up @@ -260,6 +261,7 @@ export function initDependencies(args: {
// ============================================================================

const focusManager = new FocusManager();
const imeHandler = new IMECompositionHandler();
const reorderDropzoneFactory = new ReorderDropzoneFactory(stackService);
const shortcutService = new ShortcutService(backend);
const dragStateService = new DragStateService();
Expand Down Expand Up @@ -326,6 +328,7 @@ export function initDependencies(args: {
[HOOKS_SERVICE, hooksService],
[HTTP_CLIENT, httpClient],
[ID_SELECTION, idSelection],
[IME_COMPOSITION_HANDLER, imeHandler],
[IRC_CLIENT, ircClient],
[IRC_SERVICE, ircService],
[MODE_SERVICE, modeService],
Expand Down
87 changes: 87 additions & 0 deletions packages/ui/src/lib/utils/imeHandling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { InjectionToken } from '@gitbutler/core/context';

export const IME_COMPOSITION_HANDLER: InjectionToken<IMECompositionHandler> =
new InjectionToken<IMECompositionHandler>('IMECompositionHandler');

/**
* IME (Input Method Editor) handling utilities for text input components.
* This class provides a unified handler to manage IME composition state
* and prevent unintended keyboard shortcuts during text composition in
* Japanese, Chinese, Korean, and other languages that require input method editors.
*/
export class IMECompositionHandler {
private _isComposing = false;

get isComposing(): boolean {
return this._isComposing;
}

setComposing(composing: boolean): void {
this._isComposing = composing;
}

reset(): void {
this._isComposing = false;
}

/**
* Creates an input event handler that tracks IME composition state
*
* @param originalHandler - Optional original input handler to call
* @returns Input event handler function
*/
handleInput(originalHandler?: (e: Event) => void) {
return (event: Event) => {
if (event instanceof InputEvent) {
this.setComposing(event.isComposing);
}

originalHandler?.(event);
};
}

/**
* Creates a keydown event handler that blocks actions during IME composition
*
* @param originalHandler - Optional original keydown handler to call
* @param additionalBlockingKeys - Additional keys to block during composition
* @returns Keydown event handler function
*/
handleKeydown(
originalHandler?: (event: KeyboardEvent) => void,
additionalBlockingKeys: string[] = []
) {
return (event: KeyboardEvent) => {
if (
[...IME_BLOCKING_KEYS, ...additionalBlockingKeys].includes(event.key) &&
this.isComposing
) {
event.preventDefault();
event.stopPropagation();
this.reset();
return;
}

originalHandler?.(event);
};
}
}

/**
* Keys that should be blocked during IME composition to prevent unintended actions
*/
const IME_BLOCKING_KEYS = [
'Enter',
'Escape',
'Tab',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9'
] as const;
Loading