Skip to content

Commit f36dbba

Browse files
committed
feat: allow to wrap text selection in text composer
1 parent d87590e commit f36dbba

File tree

3 files changed

+113
-3
lines changed

3 files changed

+113
-3
lines changed

src/messageComposer/CustomDataManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
LocalMessage,
77
} from '..';
88
import type { MessageComposer } from './messageComposer';
9+
import type { DeepPartial } from '../types.utility';
910

1011
export type CustomDataManagerState = {
1112
message: CustomMessageData;
@@ -49,7 +50,7 @@ export class CustomDataManager {
4950
this.state.next(initState({ composer: this.composer, message }));
5051
};
5152

52-
setMessageData(data: Partial<CustomMessageData>) {
53+
setMessageData(data: DeepPartial<CustomMessageData>) {
5354
this.state.partialNext({
5455
message: {
5556
...this.state.getLatestValue().message,
@@ -58,7 +59,7 @@ export class CustomDataManager {
5859
});
5960
}
6061

61-
setCustomData(data: Partial<CustomMessageComposerData>) {
62+
setCustomData(data: DeepPartial<CustomMessageComposerData>) {
6263
this.state.partialNext({
6364
custom: {
6465
...this.state.getLatestValue().custom,

src/messageComposer/textComposer.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { TextComposerMiddlewareExecutor } from './middleware';
22
import { StateStore } from '../store';
33
import { logChatPromiseExecution } from '../utils';
4-
import type { TextComposerState, TextComposerSuggestion, TextSelection } from './types';
4+
import type {
5+
Suggestions,
6+
TextComposerState,
7+
TextComposerSuggestion,
8+
TextSelection,
9+
} from './types';
510
import type { MessageComposer } from './messageComposer';
611
import type { DraftMessage, LocalMessage, UserResponse } from '../types';
712

@@ -150,6 +155,11 @@ export class TextComposer {
150155
this.state.partialNext({ text });
151156
};
152157

158+
setSelection = (selection: TextSelection) => {
159+
if (!this.enabled) return;
160+
this.state.partialNext({ selection });
161+
};
162+
153163
insertText = ({ text, selection }: { text: string; selection?: TextSelection }) => {
154164
if (!this.enabled) return;
155165

@@ -184,6 +194,34 @@ export class TextComposer {
184194
});
185195
};
186196

197+
wrapSelection = ({
198+
head = '',
199+
selection,
200+
tail = '',
201+
}: {
202+
head?: string;
203+
selection?: TextSelection;
204+
tail?: string;
205+
}) => {
206+
if (!this.enabled) return;
207+
const currentSelection: TextSelection = selection ?? this.selection;
208+
const prependedText = this.text.slice(0, currentSelection.start);
209+
const selectedText = this.text.slice(currentSelection.start, currentSelection.end);
210+
const appendedText = this.text.slice(currentSelection.end);
211+
const finalSelection = {
212+
start: prependedText.length + head.length,
213+
end: prependedText.length + head.length + selectedText.length,
214+
};
215+
this.state.partialNext({
216+
text: [prependedText, head, selectedText, tail, appendedText].join(''),
217+
selection: finalSelection,
218+
});
219+
};
220+
221+
setSuggestions = (suggestions: Suggestions) => {
222+
this.state.partialNext({ suggestions });
223+
};
224+
187225
closeSuggestions = () => {
188226
const { suggestions } = this.state.getLatestValue();
189227
if (!suggestions) return;

test/unit/MessageComposer/textComposer.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,77 @@ describe('TextComposer', () => {
491491
});
492492
});
493493

494+
describe('wrapSelection', () => {
495+
const message: LocalMessage = {
496+
id: 'test-message',
497+
type: 'regular',
498+
text: 'Hello world',
499+
};
500+
501+
it('should wrap selection from both sides', () => {
502+
const selection = { start: 0, end: 5 };
503+
const {
504+
messageComposer: { textComposer },
505+
} = setup({ composition: message });
506+
textComposer.wrapSelection({ head: '**', tail: '**', selection });
507+
expect(textComposer.text).toBe('**Hello** world');
508+
expect(textComposer.selection).toEqual({ start: 2, end: 7 });
509+
});
510+
511+
it('should wrap selection from the head side', () => {
512+
const selection = { start: 0, end: 5 };
513+
const {
514+
messageComposer: { textComposer },
515+
} = setup({ composition: message });
516+
textComposer.wrapSelection({ head: '**', selection });
517+
expect(textComposer.text).toBe('**Hello world');
518+
expect(textComposer.selection).toEqual({ start: 2, end: 7 });
519+
});
520+
521+
it('should wrap selection from the tail side', () => {
522+
const selection = { start: 0, end: 5 };
523+
const {
524+
messageComposer: { textComposer },
525+
} = setup({ composition: message });
526+
textComposer.wrapSelection({ tail: '**', selection });
527+
expect(textComposer.text).toBe('Hello** world');
528+
expect(textComposer.selection).toEqual({ start: 0, end: 5 });
529+
});
530+
531+
it('should wrap cursor', () => {
532+
const selection = { start: 5, end: 5 };
533+
const {
534+
messageComposer: { textComposer },
535+
} = setup({ composition: message });
536+
textComposer.wrapSelection({ head: '**', tail: '**', selection });
537+
expect(textComposer.text).toBe('Hello**** world');
538+
expect(textComposer.selection).toEqual({ start: 7, end: 7 });
539+
});
540+
541+
it('should avoid changes if text composition is disabled', () => {
542+
const selection = { start: 5, end: 5 };
543+
const {
544+
messageComposer: { textComposer },
545+
} = setup({ composition: message, config: { enabled: false } });
546+
const initialSelection = textComposer.selection;
547+
textComposer.wrapSelection({ head: '**', tail: '**', selection });
548+
expect(textComposer.text).toBe(message.text);
549+
expect(selection).not.toEqual(initialSelection);
550+
expect(textComposer.selection).toEqual(initialSelection);
551+
});
552+
553+
it('should use current selection if custom not provided', () => {
554+
const initialSelection = { start: 2, end: 3 };
555+
const {
556+
messageComposer: { textComposer },
557+
} = setup({ composition: message });
558+
textComposer.setSelection(initialSelection);
559+
textComposer.wrapSelection({ head: '**', tail: '**' });
560+
expect(textComposer.text).toBe('He**l**lo world');
561+
expect(textComposer.selection).toEqual({ start: 4, end: 5 });
562+
});
563+
});
564+
494565
describe('closeSuggestions', () => {
495566
const message: LocalMessage = {
496567
id: 'test-message',

0 commit comments

Comments
 (0)